上一章节【Android Framework系列】第16章 存储访问框架 (SAF) 主要分析了Android4.4引入的存储访问框架(SAF)
,本章节我们对Android10(Q)
的存储相关进行分析,了解下其限制存储方式。
Google
为了让用户更好地控制自己的文件,并限制文件混乱情况,Android Q
更改了App
访问设备存储空间的方式。
只要程序获得了READ_EXTERNAL_STORAGE
权限,就可以随意读取外部的存储公有目录;
只要程序获得了WRITE_EXTERNAL_STORAGE
权限,就可以随意在写入外部存储的公有目录上新建文件或文件夹。
于是Google在Android Q中提出了分区存储
,意在限制程序对外部存储中公有目录的使用。
分区存储对内部存储私有目录和外部存储私有目录都没有影响
当项目targetSdkVersion <= 28时,AndroidQ设备上默认使用兼容模式,存储方式跟Android Q
以前,App
访问Sdcard
一样,拥有完整的访问权限。
当项目targetSdkVersion > 28时,AndroidQ设备的App
只能直接访问App-specific
目录文件,没有权限访问App-specific
外的文件。访问其他目录,只能通过MediaStore
、SAF
、或者其他App提供ContentProvider
访问。
App
卸载后,不会删除SAF
、MediaStore
接口访问Filtered View App
,App-specific
目录只能自己直接访问App
卸载,数据会清除。Scoped Storage
对于App访问存储方式
、App数据存放
以及App间数据共享
,都产生很大影响。
具体适配参考官方文档
系统通过下列确定App运行模式:
App TargetSDK > 28
,默认Filtered View(沙箱模式)
App TargetSDK <= 28
,声明了READ_EXTERNAL_STORAGE或者WRITE_EXTERNAL_STORAGE权限,默认Legacy View(兼容模式)
- 系统应用可以申请
android.permission.WRITE_MEDIA_STORAGE
系统权限,同样拥有完整存储空间权限,可以访问所有文件,但是这个在CTS测试中,只有没有用户交互、可见的App,才能申请。具体参考《Android Bootcamp 2019 - Privacy Overview.pdf》
。
- App在下列条件都成立时
① 声明INSTALL_PACKAGES
、或者动态申请INSTALL_PACKAGES
权限
② 拥有WRITE_EXTERNAL_STORAGE权限
③ App拥有外置存储空间Read、Write权限
。
但是通过Environment.isExternalStorageLegacy
接口判断,返回不一定是Legacy View。
判断当前App运行什么模式,可以通过这个API判断:
Environment.isExternalStorageLegacy();
App启动Filtered View
后,只能直接访问自身App-specific目录
,所以Android Q
,提供了两种访问公共目录的方法:
MediaStore
提供了下列几种类型的访问Uri
,通过查找对应Uri数据,达到访问的目的。
下列每种类型又分为三种Uri
,Internal
、External
、可移动存储
:
Audio
- Internal: MediaStore.Audio.Media.INTERNAL_CONTENT_URI
content://media/internal/audio/media。- External: MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
content://media/external/audio/media。- 可移动存储: MediaStore.Audio.Media.getContentUri
content://media//audio/media。
Video
- Internal: MediaStore.Video.Media.INTERNAL_CONTENT_URI
content://media/internal/video/media。- External: MediaStore.Video.Media.EXTERNAL_CONTENT_URI
content://media/external/video/media。- 可移动存储: MediaStore.Video.Media.getContentUri
content://media//video/media。
Image
- Internal: MediaStore.Images.Media.INTERNAL_CONTENT_URI
content://media/internal/images/media。- External: MediaStore.Images.Media.EXTERNAL_CONTENT_URI
content://media/external/images/media。- 可移动存储: MediaStore.Images.Media.getContentUri
content://media//images/media。
File
- MediaStore. Files.Media.getContentUri
content://media//file。
Downloads
- Internal: MediaStore.Downloads.INTERNAL_CONTENT_URI
content://media/internal/downloads。- External: MediaStore.Downloads.EXTERNAL_CONTENT_URI
content://media/external/downloads。- 可移动存储: MediaStore.Downloads.getContentUri
content://media//downloads。
对于前面描述的Uri中,getContentUri
如何获取所有,可以通过下述方式:
for(String volume:MediaStore.getExternalVolumeNames(this)){
MediaStore.Audio.Media.getContentUri(volume);
}
MediaProvider
对于App存放到公共目录文件,通过ContentResolver insert
方法中Uri来确定,其中下表中
为相对路径,完整为:
content://media//
MediaStore
通过不同Uri
,为用户提供了增、删(如果通过File Uri无法删除文件,需要通过SAF接口)、改。
App对应的权限如下:
通过ContentResolver
,根据不同的Uri查询不同的内容:
try (Cursor c = getContentResolver()
.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null, null)) {
while (c.moveToNext()) {
Uri contentUri =
ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, c.getLong(0));
}
}
PS: MediaStore.Files
进行Query
时候,只会显示图片、视频跟音频文件。
通过ContentResolver query
接口,查找出来文件后如何读取,可以通过下面的方式:
ContentResolver openFileDescriptor
接口,选择对应的打开方式ParcelFileDescriptor
类型FD
。Thumbnail
,通过ContentResolver loadThumbnail
接口MediaProvider
返回指定大小的Thumbnail
。openFileDescriptor
返回ParcelFileDescriptor
ParcelFileDescriptor.detachFd()
读取FD
FD
传递给Native
层代码App
需要负责通过close
接口关闭FD
ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, file0penMode);
if (parcelFd != null) {
int fd = parcelFd.detachFd();
// Pass the integer value "fd" into your native code. Remember to call
// close(2) on the file descriptor when you're done using it.
}
如果需要新建文件存放到公共目录,需要通过ContentResolver insert
接口,使用不同的Uri
,选择存储到不同的目录。
如果需要修改多媒体文件,需要通过ContentResolver query
接口查找出来对应文件的Uri。
如果不是自己新建的文件,需要注意2.5.1.3 权限中描述,需要申请WRITE_EXTERNAL_STORAGE权限
或者catch RecoverableSecurityException
,弹框给用户选择。
通过下列接口,获取需要修改文件的FD
或者OutputStream
:
通过ContentResolver接口删除文件,Uri为query出来的Uri:
getContentResolver().delete(contentUri,null,null);
SAF
,即Storage Access Framework
,通过选择不同的DocumentsProvider
,提供给用户打开、浏览文件。我们上一章节可以回头看一下
Android默认提供了下列DocumentsProvider
:
MediaDocumentsProvider
、ExternalStorageProvider
、 DownloadStorageProvider
。
他们之间差异是:
这个图片上,有三个区域,分别是:
具体参考官方文档
大致方法如下:
DocumentsContract.deleteDocument(getContentResolver(),uri);
①获取OutputStream
getContentResolver().openOutputStream(uri);
②获取可写ParcelFileDescriptor
getContentResolver().openFileDescriptor(contentUri,"w");
getContentResolver().openFile (contentUri,"w",null);
具体Demo参考
访问App-specific
分为两种情况,第一是访问App自身App-specific目录
,第二是访问其他App目录文件
。
Android Q,App如果启动了Filtered View
,那么只能直接访问自己目录的文件:
App-specific目录内部多媒体文件:
App-specific
目录一样Media Scanner
不会扫描App-specific
里面的多媒体文件,如果需要扫描需要通过MediaScannerConnection.scanFile
添加到MediaProvider
数据库中ContentProvider
共享出去App是Filtered View,其他App无法直接访问当前App私有目录,需要通过下面方法:
DocumentsProvider
App自定义DocumentsProvider
需要做以下步骤:
a)指定DocumentsProvider
FileProvider具体使用参考
这边总结一下大概步骤:
App可以实现自定义ContentProvider
,尤其是内部文件共享
,但是不希望UI交互。
MediaStore
中,DATA即(_data)字段
,在Android Q
中开始废弃。读写文件需要通过openFileDescriptor
。
Android Q上,MediaStore
中添加了一个IS_PENDING Flag
,用于标记当前文件时Pending
状态。
其他App通过MediaStore
查询文件,如果没有设置setIncludePending
接口,查询不到设置为Pending状态的文件,这就给App专享访问此文件。在一些情况下使用,例如在下载的时候:下载中,文件是Pending状态下载完成,文件Pending状态置为0。
Android Q上,通过MediaStore
存储到公共目录的文件,除了2.5.1.2节Uri跟公共目录关系中规定的每一个存储空间的一级目录外,可以通过MediaColumns.RELATIVE_PATH来指定存储的次级目录,这个目录可以使多级,具体代码如下:
Android Q上, App如果需要访问图片上的Exif Metadata,需要做下列事情:
如果App在AndroidManifest.xml
中声明:android:hasFragileUserData="true"
卸载应用会有提示是否保留App数据:
Android Q上,App TargetSDK>=Q
默认是Filtered View
。App如果是Filtered View,会涉及到数据的迁移,不然会导致旧数据无法使用。可以从下面几方面着手数据迁移:
Legacy View
下才能拥有完整操作存储的权限SAF访问
,通过SAF选择目录文件,用户选择访问App文件。Images
、Video
、Audio
放到对应的公共目录,其他文件卸载后不删除文件可以放到Downloads
下面。在使用MediaStore
进行query
动作的时候,使用Projection
时,Column Name
要在MediaStore
中定义好的。
WRITE_MEDIA_STORAGE
是一个很大强大的权限,能够允许App获取访问所有存储设备的权限。访问所有存储设备的权限,这个应当只赋予Media Stack。
在Android系统中,规定了WRITE_MEDIA_STORAGE
能够获取media_rw
用户组:
App如果需要访问Media
或者外置存储设备
,可以通过MediaStore
或者Storage Access Framework(SAF)接口
。
好了,这里总结一下:
App TargetSDK > 28 即 Android10(Q)及以上
项目,Google限制了存储沙箱模式,在Android10(Q)以上的设备
建议使用私有目录data/data
,无法再直接访问外部SD卡存储目录
,如需要使用外部SD卡存储目录
则需要通过SAF
、MediaStore
接口访问,并且只能访问特定的外部SD卡存储目录
,如Downloads
、Documents
、Pictures
、DCIM
、Movies
、Music
、Ringtones
等。这些外部SD卡存储目录(公有目录)所有App都能访问,因此显得不太安全。App TargetSDK <= 28 即 Android10(Q)以下
项目,不受限制,有WRITE_MEDIA_STORAGE
,READ_MEDIA_STORAGE
权限即可。所以如果实在不想改动外部SD存储,那就将项目的targetSdk改成<=28。SAF
、MediaStore
接口访问外部SD卡存储特定目录
的方法,详细参考上面的说明或参考官方文档。这里有个使用MediaStore
方式访问外部SD卡存储特定目录
的Demo供小伙伴们参考,别忘记点Star喔