1.背景
Google于 2019年9月3日发布了Android10 release版本,为了更好的保护用户数据并限制设备冗余文件增加,Android 10版本变更了设备外部存储访问方式,外部存储新特性称为分区存储(Scoped Storage), 分区存储遵循以下三个原则对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件
文件更好的归属: 系统记录文件由哪个应用创建,应用不需要存储权限即可以访问应用自己创建文件
应用数据保护: 添加外部存储应用私有目录文件访问限制, 应用即使申请了存储权限也不能访问其他应用外部存储私有目录文件
用户数据保护: 添加pdf、office、doc等文件的访问限制,用户即使申请了存储权限也不能访问其他应用创建的pdf、office、doc等文件
2.Android存储目录
Android的存储目录被分为内部存储和外部存储两个部分,对于外部存储进行了重新设计,外部存储被分为应用私有目录以及共享目录两个部分。
①内部存储:
-
getFilesDir() - 应用内部存储 放在data/data/packagename/files/
-
getCacheDir() - 应用内部存储 放在data/data/packagename/cache/
主要用于存储应用的私密数据,用户在手机上是看不见这块区域的。在进行存储的过程中不需要存储权限,当应用被卸载后数据会被删除。应用内部存储目录访问方式与之前Android版本一致,可以通过file path获取资源
②外部存储的私有目录:
-
getExternalFilesDir - 放在外部存储Android/data/packagename/files/ 外部存储私有目录
- 应用卸载就会删除
- 5.0及以上不需要
WRITE_EXTERNAL_STORAGE
READ_EXTERNAL_STORAGE
- 不安全,别的应用可以写入数据到此目录
- Media扫描不出来,不会出现在相册
-
getExternalCacheDir - 存放临时缓存数据 放在外部存储Android/data/packagename/cache/
对应设置选项设置->应用->应用详情里面的"清除数据"与"清除缓存“选项"
应用外部存储私有目录访问方式与Android10之前版本一致,可以通过file path获取资源
**③外部存储共享目录:**外部存储共享目录 除了外部存储的私有目录以外的目录,都是共享目录。程序保存在共享目录中的数据,在应用被删除后,仍然保留。存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目录
-
getExternalStorageDirectory - 在sd卡目录mnt/sdcard,在Android10 以前版本中 可以通过file path获取资源,获取资源的时候需要获取
存储权限。卸载后保留创建的文件
-
在Android10 版本之后,共享目录文件需要通过MediaStore API或者Storage Access Framework方式访问
MediaStore API在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限
MediaStore API访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过
ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常;
MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过Storage Access Framework方式访问;
3.文件迁移
3.1文件迁移是将应用共享目录文件迁移到应用私有目录或者Android10要求的media集合目录
①针对只有应用自己访问并且应用卸载后允许删除的文件,需要迁移文件到应用私有目录文件,可以通过File path方式访问文件资源,降低适配成本
②允许其他应用访问,并且应用卸载后不允许删除的文件,文件需要存储在共享目录,应用可以选择是否进行目录整改,将文件迁移到Android10要求的media集合目录
3.2 文件访问兼容性适配
共享目录文件不能够通过File path方式读取,需要使用MediaStore API或者Storage Access Framework框架进行访问
4.Android10 以后使用MediaStore 进行文件的存储操作
MediaStore API 简介
系统会自动扫描外部存储,添加文件到系统已定义的Images、Videos、Audio files、Downloaded files集合中,Android 10通过MediaStore.Images、MediaStore.Video、MediaStore.Audio、MediaStore.Downloads 访问共享目录文件资源
媒体类型 | Uri | uri 定义的常量 | 默认创建目录 | 允许创建目录 | Mime Type |
---|---|---|---|---|---|
Image | content://media/external/images/media | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | Pictures | DCIM Pictures |
Pictures图片(image/*) |
Video | content://media/external/video/media | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | Movies | DCIM, Movies |
视频(video/*) |
Audio | content://media/external/audio/media | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | Music | Alarms, Music, Notifications, podcast Ringtones |
音频(audio/*) |
Download | content://media/external/downloads | MediaStore.Downloads.EXTERNAL_CONTENT_URI | Download | Download | NA |
File | content://media/external/ | MediaStore.Files.getContentUri(“external”) | Documents | Documents | file/* |
4.1 创建/保存文件
通过 ContentResolver.insert(Uri url,ContentValues values) 插入到对应的目录中,该方法会返回一个 Uri,通过对该 Uri 进行文件流的操作。
① insert()方法的第一个参数表示你要插入哪一个目录的Uri ,就是上表中定义的Uri常量。
② insert()方法的第二个参数,ContentValues ,首先构造一个 ContentValues 对象,
-
ContentValues 的 key 值可以通过
MediaStore.XXX.Media.YYY
获取到
XXX: 对应的媒体类型
YYY: 对应的字段常量 -
RELATIVE_PATH
注意RELATIVE_PATH需要targetVersion>=29 ,表示在公共媒体路径下创建子目录relativePath。例如上文我们将这个图片保存到了 Pictures/DemoPicture 文件夹下,如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下,例如,图片文件(mimeType = image/*)会被保存到 Pictures(Environment#DIRECTORY_PICTURES) 中,需要注意的是,不能将文件放置到不对应的顶级文件夹下,比如将一个 mimeType 为
audio/mpeg
放大 Pictures 这样的行为是不被允许的,也就是如果设置 MIME_TYPE = audia/* 并将 RELATIVE_PATH 设置为 Environment#DIRECTORY_PICTURES 这样是会 Throw IllegalArgumentException 的例如:
//创建一个相对路径在vido路径下面, key表示video路径下,value =Movies/ucamera 表示我在movies路径下面创建了一个ucamra子目录。允许创建的目录如上面的表格。 values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES +"/ucamera");
Environment 类中定义了一些我们常用的目录的字段;
public static String DIRECTORY_MUSIC = "Music";
public static String DIRECTORY_PODCASTS = "Podcasts";
public static String DIRECTORY_RINGTONES = "Ringtones";
public static String DIRECTORY_ALARMS = "Alarms";
public static String DIRECTORY_NOTIFICATIONS = "Notifications";
public static String DIRECTORY_PICTURES = "Pictures";
public static String DIRECTORY_MOVIES = "Movies";
public static String DIRECTORY_DOWNLOADS = "Download";
public static String DIRECTORY_DCIM = "DCIM";
public static String DIRECTORY_DOCUMENTS = "Documents";
public static String DIRECTORY_SCREENSHOTS = "Screenshots";
public static String DIRECTORY_AUDIOBOOKS = "Audiobooks";
创建返回Uri示例代码如下:
/***
*
* @param context 上下文
* @param videoName 视频文件名字带有后缀名如 :"123.mp4"
* @param mineType 视频格式类型如:"video/mp4"
* @param subDir 创建的子目录如:"/ucam"
* @return uri
*/
public Uri getSaveToGalleryVideoUri(Context context, String videoName, String mineType, String subDir) {
ContentValues values = new ContentValues();
//视频的名字
values.put(MediaStore.Video.Media.DISPLAY_NAME, videoName);
//视频类型;
values.put(MediaStore.Video.Media.MIME_TYPE, mineType);
//修改时间
values.put(MediaStore.Video.Media.DATE_MODIFIED, System.currentTimeMillis() / 1000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//大于等于10 的版本设置子目录
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + subDir);
}else{
//数据库表中列字段存储的是数据的绝对路径;android 10 之前,将绝对路径设置到这个key中;这个字段可能在后面的数据库查询可能会用到;
values.put(MediaStore.MediaColumns.DATA, getpath());
}
Uri mInsert = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
return mInsert;
}