简介
关于应用常见的目录,参见Android Q 存储体系适配(一)。同时本人抛砖引玉,在github上面开源了一个库xStorage,用于适配Android Q 存储体系。在封装这个库的时候,最大的感想就是在Android Q 以后对文件的操作尽量使用Uri,可以极大的方便操作。
xStorage 的github路径为:https://github.com/XCodeKitBox/XStorage.git。欢迎各位吐槽评论。
背景介绍
Android 官方为了让用户更好地控制自己的文件,并限制应用随意创建目录导致文件混乱的情况,Android Q 修改了 APP 访问外部存储中文件的方法。外部存储的新特性被称为 Scoped Storage。
Android Q 仍然使用 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 作为面向用户的存储相关运行时权限,但现在即使获取了这些权限,访问外部存储也受到了限制。APP 需要这些运行时权限的情景发生了变化,且各种情况下外部存储对 APP 的可见性也发生了变化。
在 Android 10 的时候默认外部存储的新特性被称为 Scoped Storage。但是可以通过 android:requestLegacyExternalStorage="true"
关闭新特性。
内部存储
内部存储应该说是应用内部存储,在Android4.4以后此类目录的特点是:
- Android4.4 ~ Android 6.0 不需要申明权限,即可以读写文件
- Android6.0 ~ AndroidQ 不需要申明权限也不需要动态申请权限即可以读写文件。
- 此目录的读写权限只能是应用本身,不能提供给其他应用。
Android7.0以后与此目录相关的APIcontext.openFileOutput(fullName, mode)
。其中mode 只支持 MODE_PRIVATE,MODE_APPEND。如果传入 MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE,应用抛出异常,不能正常操作文件。
外部存储-应用专有目录
外部存储-应用专有目录,此目录的的特点:
- Android4.4 ~ Android 6.0 不需要申明权限,即可以读写文件。
- Android6.0 ~ AndroidQ 不需要申明权限也不需要动态申请权限即可以读写文件。
- Android10 以前其他应用申请了读写权限,也可以操作外部存储中应用专有目录的所有文件,即使此文件非本应用创建。
- Android10 不启用 Scoped Storage,则和第三点特点相同。
- Android10(启用 Scoped Storage),AndroidQ,应用只能操作应用自己存储在应用专又目录的文件,不能操作其他应用存储在外部存储中应用专有目录存储的文件。
- Android10 (启用Scoped Storage),Android Q应用传递给其他应用操作,(系统拍照,安装APK,分享)需要通过FileProvider将file://Uri 转为 content://Uri。
公共存储目录
公共存储目录,此目录的特点是:
- Android4.4 ~ Android 6.0 需要申明读写权限。
- Android6.0 ~ Android 10(不启用 Scoped Storage) 应用操作此目录,需要申明和动态申请读写权限。
- Android7.0 ~ Android 10(不启用Scoped Storage) 以后应该将多媒体目录传递给其他应用操作(系统拍照,安装APK,分享)需要通过FileProvider将file://Uri 转换为content://Uri
- Android10(启用 Scoped Storage),Android Q 应用操作操作公共存储目录只能通过MediaStore和SAF两个方式来访问,由于SAF的操作不是非常友好,推荐使用MediaStore进行操作。
MediaStore的特点:
- MediaStore 早在Android4.4时已经推出,由于操作繁琐,因此比较少使用。
- Android10(启用 Scoped Storage),Android 11 应用操作公共存储目录,可以不申明和动态申请读写权限,但是这样的话只能操作查询,删除,修改应该自身创建的文件。需要操作其他应用存储在公共存储目录的文件必须申明和动态申请读写权限。
- 通过MediaStore获取的文件操作Uri ,是content://Uri。
- 通过查看fileProvider的源码,确定fileProvider 在 Android10(启用 Scoped Storage),Android Q不能将file://uri 转换为content://uri
- 基于第3和第4点,在Android10 (启用 Scoped Storage),Android Q 可以将MediaStore 获取的Uri 传递给其他应用操作。
- MediaStore 在Android10(启用Scoped Storage),Android Q 时需要特别注意的是,在赋值给RELATIVE_PATH,文件和文件之间的文件分隔符不能多个,例如"/sdcard//a//b//b"和“/sdcard/a/b/b”是两个路径。在赋值给DISPLAY_NAME 文件分隔符会转换为下划线。
- 使用MediaStore insert接口时,在当前目录下存在同名文件,系统会给插入的文件改名。
- 使用MediaStore操作文件在Android 10(启动Scoped Storage)在Android Studio提供的虚拟机,华为荣耀手机上存在无法删除Document文件夹下的文件情况。Android 11 (Android Studio 提供的虚拟机)可以删除。因此怀疑是Android 10上的bug。
自定义目录
自定义目录的特点是:
- Android4.4 ~ Android6.0 应用申明读写权限,可以在外部存储上创建任意名称,任意层级的目录。
- Android6.0 ~ Android10 (不启用Scoped Storage) 应用申明读写权限,且动态申请权限,可以在外部存储上创建任意名称和任意层级的目录。
- Android10(启动Scoped)需要申明读写权限且动态申请权限。只能通过SAF来创建文件,操作文件。同时只对SAF创建的目录有读写权限,读写权限只对本应用生效,其他应用需要再次调SAF来赋予权限。
FileProvider
Android 7.0 以后应用不能通过file://Uri 将文件路径传递给其他应用操作,必须通过FileProvider将 file://Uri 转换为 content://Uri 。FileProvider提供了7个路径,具体如下:
/**
* 兼容 Android 7.0 ,通过 FileProvider 提供路径给系统应用调用
* 支持的路径有:(参见源码/parsePathStrategy)
* xml 配置 代码获取的路径 实际的路径
* 1. root-path File("/") /
* 2. files-path requireContext().filesDir.absolutePath /data/data/<applicationId>/files/
* 3. cache-path requireContext().cacheDir.absolutePath /data/data/<applicationId>/cache/
* 4. external-path Environment.getExternalStorageDirectory().absolutePath /sdcard/
* 5. external-files-path requireContext().getExternalFilesDir(null) /sdcard/Android/data/<applicationId>/files/
* 6. external-cache-path requireContext().externalCacheDir?.absolutePath /sdcard//Android/data/<applicationId>/cache/
* 7. external-media-path requireContext().externalMediaDirs[0] /sdcard/Android/media/<applicationId>/
*/
在兼容Android10(启动Scope Storage),Android Q 的情况下,root-path必须使用SAF访问。external-path 中的公共目录,可以通过MediaStore 转换为 content://Uri,其他自定义目录需要通过 SAF来访问。external-media-path 在Android 10 以后弃用。
不同版本的升级情况
情况1
一、应用targetSdkVersion <= 28
- 安装运行在设备版本SdkVersion = 29(Android10),SdkVersion = 30(Android11),使用传统存储机制。
- 应用升级在设备版本SdkVersion = 29(Android10),SdkVersion = 30(Android11),使用传统存储机制。
二、旧应用版本targetSdkVersion <= 28 升级到版本 targetSdkVersion = 29
- 安装运行在设备版本SdkVersion = 29(Android10) 使用分区存储机制。
- 应用升级在设备版本SdkVersion = 29(Android10)使用传统存储机制。
三、旧应用版本targetSdkVersion <= 28 升级到版本 targetSdkVersion = 30
- 安装运行在设备版本SdkVersion = 30(Android11) 使用分区存储机制。
- 应用升级在设备版本SdkVersion = 30(Android11)使用分区存储机制。原以存在文件/文件夹,有读写权限。
情况2
一、应用targetSdkVersion =29 requestLegacyExternalStorage=true
- 安装运行在设备版本SdkVersion = 29(Android10),SdkVersion = 30(Android11),使用传统存储机制。
- 应用升级在设备版本SdkVersion = 29(Android10),SdkVersion = 30(Android11),使用传统存储机制。
二、旧应用版本targetSdkVersion =29 requestLegacyExternalStorage=true 升级到版本 targetSdkVersion = 30
- 安装运行在设备版本SdkVersion = 30(Android11) 使用分区存储机制。
- 应用升级在设备版本SdkVersion = 30(Android11)使用分区存储机制。原以存在文件/文件夹,有读写权限
总结
以上是我兼容Android Q的 Scoped Storage的心得体会,希望能够和大家一起学习,一起进步。