Android 10(Api 29)新特性适配 - 分区存储

官方文档:https://developer.android.google.cn/preview/privacy/scoped-storage

问题描述

从Android 10开始应用将不可直接访问外部存储(/sdcard)文件,否则抛异常。

在AndroidQ上运行:

  • targetSdkVersion<Q,没影响;
  • targetSdkVersion>=Q,默认启用过滤视图,应用以外的文件需要通过存储访问框架SAFStorageAccessFramework)读写。

解决方法

停用过滤视图,使用旧版存储模式

1
2
3
4
5
6
<manifest ... >
<!-- This attribute is "false" by default on apps targeting Android Q. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>

将文件存储到过滤视图中,官方推荐。

1
2
// /Android/data/com.example.androidq/files/Documents
File dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
  • 优点:不用申请读写权限
  • 缺点:随应用卸载而删除

使用存储访问框架(SAF),由用户指定要读写的文件。

此功能Android 4.4(API: 19)就有,官方文档:https://developer.android.google.cn/guide/topics/providers/document-provider

获取用户指定的某个目录的读写权限

从Android 5.0(Api 21)开始就有,官方文档:https://developer.android.google.cn/about/versions/android-5.0#Storage

申请目录的访问权限

会打开系统的文件目录,由用户自己选择允许访问的目录,不用申请WRITE/READ_EXTERNAL_STORAGE权限。

1
2
3
4
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(intent, REQ_CODE);

执行上述代码后会出现类似如下图界面,点击‘允许访问“DuoKan”’按钮

Uri读写文件询问框

允许之后通过onActivityResult()intent.getData()得到该目录的Uri,通过Uri可获取子目录和文件。这种方式的缺点是应用重装后权限失效,即使保存这个Uri也没用。

1
2
3
4
5
6
Uri dirUri = intent.getData();
// 持久化;应用重装后权限失效,即使知道这个uri也没用
SPUtil.setValue(this, SP_DOC_KEY, dirUri.toString());
//重要:少这行代码手机重启后会失去权限
getContentResolver().takePersistableUriPermission(dirUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);

通过Uri读写文件

(1)创建文件

1
2
3
4
5
6
7
8
// 在mUri目录(‘DuoKan’目录)下创建'test.txt'文件
private void createFile() {
DocumentFile documentFile = DocumentFile.fromTreeUri(this, mUri);
DocumentFile file = documentFile.createFile("text/plain", "test.txt");
if (file != null && file.exists()) {
LogUtil.log(file.getName() + " created");
}
}

主要用到DocumentFile类,和File类的方法类似,有isFileisDirectoryexistslistFiles等方法

(2)删除文件

1
2
3
4
5
6
7
8
9
10
11
12
//删除"test.txt"
private void deleteFile() {
DocumentFile documentFile = DocumentFile.fromTreeUri(this, mUri);
// listFiles(),列出所有的子文件和文件夹
for (DocumentFile file : documentFile.listFiles()) {
if (file.isFile() && "test.txt".equals(file.getName())) {
boolean delete = file.delete();
LogUtil.log("deleteFile: " + delete);
break;
}
}
}

(3)写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void writeFile(Uri uri) {
try {
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
//这种方法会覆盖原来文件内容
OutputStreamWriter output =
new OutputStreamWriter(new FileOutputStream(pfd.getFileDescriptor()));
// 不能传uri.toString(),否则FileNotFoundException
// OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream(uri.toString(), true));
output.write("这是一段文件写入测试\n");
output.close();
LogUtil.log("写入成功。");
} catch (IOException e) {
LogUtil.log(e);
}
}

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :