SAF 存储访问框架

  • 使用 SAF 创建和读取文件

  • 资料来源:

    https://developer.android.com/guide/topics/providers/document-provider#virtual
    https://developer.android.com/training/basics/intents/result?hl=zh-cn

  • 更新

    1
    2
    20.06.25 初始化
    21.02.18 替换 startActivityForResult

导语

android Q 在 beta 中引入了文件沙盒,android 应用不再拥有内部存储的无限读写权限,结果 beta3 版本不兼容应用太多,暂缓实行,不过 Android R 里面还是来了.

客观上文件沙盒对用户隐私保护很有用,但是 google 给开发者留的坑有点大,如果适配的成本能降低很多,也不会有这么大的反对声音.

SAF 框架在 android 4.4 就已经引入了,可能是 R 以后直接访问用户文件的唯一方式.这里仅仅以 NowakeLock 为例演示如何读取和创建文件.

概览

SAF 实际上是 ContentProvider 的应用.系统或用户应用有提供 DocumentsProvider 的实现,应用可以通过 SAF 得到 uri 进而访问文件.不论这个文件是在本地还是云端.

所以使用 SAF 的过程也类似使用 ContentProvider

  • 发送 Intent,系统内的 DocumentsProvider 的实现会响应
  • 选择一个文件,拿到 uri
  • 在 onActivityResult 内对 uri 操作.读/写/创建等.

借助 Activity Results API 可以更加简化的实现文件访问.

具体参考 Getting a result from an activity (21.02.18)注意查看英文版本,中文版没有及时更新

这里仅更新 SAF 相关内容及代码

startActivityForResult

读文件

读文件对应的 Intent 有几种

  • ACTION_GET_CONTENT: 可以运行在 4.4 以下版本,如果只是导入文件,不需要长期的性的权限.
  • ACTION_OPEN_DOCUMENT: 仅在 4.4 以上可用,但是可以提供长期的持久性的访问权限.

示例是 Nowakelock 导入 json 备份文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private val readJson: Int = 42

private fun getJson() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
startActivityForResult(intent, readJson)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == readJson && resultCode == Activity.RESULT_OK) {
resultData?.data?.also { uri ->
//do something
}
}
}

写文件

创建文件过程

  • 发出 ACTION_CREATE_DOCUMENT 类型 Intent
  • 之后可以在 DocumentsProvider 的实现中选择保存位置,定义文件名,创建文件.
  • 在 onActivityResult 拿到 uri 再写入内容.

这里是 Nowakelock 创建备份文件的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private val saveJson: Int = 43

private fun createFile(mimeType: String, fileName: String) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, fileName)
}

startActivityForResult(intent, saveJson)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == saveJson && resultCode == Activity.RESULT_OK) {
resultData?.data?.also { uri ->
//do something
}
}
}

Activity Results API

读取文件

读取文件最后只是为了获取到文件的 uri,获取到到 uri 再开始读取文件.

1
2
3
4
5
6
private val restoreRAFR =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
viewModel.restore(uri)
}
// 这里没有限制可以传入的文件类型
restoreRAFR.launch("*/*")

写入文件

写入文件也是先创建文件,获取到新文件的 uri 再写入

1
2
3
4
5
6
private val backupRAFR =
registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
viewModel.backup(uri)
}

backupRAFR.launch("fileName")