当前Android Q中启用了沙盒存储模式,限制了APP向SDcard中读写文件,当前谷歌尚未将该限制强制执行,可以通过设置属性启用旧存储特性。[原文]
沙盒模式下,每个APP在访问sdcard时会进入过滤视图,只能访问私有路径(Context.getExternalFilesDir())和公共存储空间(多媒体,MediaStore)。
官方文档给对应的解决方案,但是在我看来并不详尽,目前可以看到有以下三个解决方案:
文件位置 | 所需权限 | 访问方法 (*) | 卸载应用时是否移除文件? | 是否需要权限 |
---|---|---|---|---|
特定于应用的目录 | 无 | getExternalFilesDir() |
是 | 否 |
媒体集合 (照片、视频、音频) |
READ_EXTERNAL_STORAGE (仅当访问其他应用的文件时) |
MediaStore |
否 | 是 |
下载内容 (文档和 电子书籍) | 无 | 存储访问框架 (加载系统的文件选择器) | 否 | 否 |
除了第一种写在“/sdcard/Android/data/packageName/file”路径中可以正常使用InputStream&OutputStream读写文件,另外两种方法都无法直接对文件IO操作。
所以后续有向SDcard中写文件的需求优先向APP私有路径中写入。
但是,需要解决APP之间共享文件的场景,比如手机唯一标识配置文件(UUID啥的),这种需求场景需要对MediaStore和SAF存储访问框架做调研。
一、存储访问框架
访问存储框架是一个基于intent发起的文件检索UI,一般只能在View存在时调用系统的文件检索框架,在startActivityForResult() 的回调中收到文件对象。
/**
* Fires an intent to spin up the "file chooser" UI and select an image.
* 视图检索文件
*/
public void performFileSearch() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
但咱是写SDK的,不能在FileManager单例或者静态方法中读文件的话,对咱毫无意义
二、MediaStore
MediaStore是外部存储空间的公共媒体集合,存放的都是多媒体文件,在API >= 29后加入了download集合
-
照片:存储在
MediaStore.Images
中。 -
视频:存储在
MediaStore.Video
中。 -
音乐文件:存储在
MediaStore.Audio
中。 -
下载文件:存储在
MediaStore.Downloads
中。 -
所有文件:存储在
MediaStore.Files
中。
一般Android中通过手机数据库查询Uri来对MediaStore中的文件做查询,然后读取文件,下面以MediaStore.Files查询所有文件为例:
private void testFiles() {
ContentResolver contentResolver= this.getApplicationContext().getContentResolver();
String [] photoColumns=new String[]{
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.TITLE,
MediaStore.Files.FileColumns.MIME_TYPE,
MediaStore.Files.FileColumns.SIZE
};
Cursor cursor;
cursor=contentResolver.query
(MediaStore.Files.getContentUri("external"), photoColumns,
null,
null,
null);
while (cursor.moveToNext()) {
String _id=
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
String filePath=
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
String title=
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.TITLE));
String mime_type=
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE));
Log.d(TAG, "_id = " + _id);
Log.d(TAG, "title = " + title);
Log.d(TAG, "filePath = " + filePath);
Log.d(TAG, "mime_type = " + mime_type);
}
Log.d(TAG, "Finish Cursor Search");
}
通过上述代码可以在所有文件中找到指定文件,和绝对路径。同理,换成Image、Video、Audio、Downloads都一样,只是在数据库查询时把Uri替换掉。
PS: 但是查询有个致命伤,在开启沙盒模式时,只能在数据库中查询到Image、Video、Audio三个分类视图中的文件,那么我SDK向SDcard中写入的配置文件就不能在沙盒模式下通过数据库查询到。
只能获取到mime_type == {“image/”, “video/”, “audio/*”}这三类文件
上边代码中通过ContentResolver.query查询到文件详情,但如果需要修改文件内容,则使用ContentResolver.openFileDescriptor()获取单个文件描述符。配置文件拿不到,先不管这个文件读写了。
小结
目前只能先使用官方支持的规避方案:通过设置android:requestLegacyExternalStorage="false"属性来关闭APP的沙盒机制。
(我还会回来的)