Android Q之——分区存储
为了让用户更好地管理自己的文件并减少混乱,并且增强文件的安全性,以Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储)。此类应用只能看到本应用专有的目录(通过Context.getExternalFilesDir()访问)以及特定类型的媒体。除非您的应用需要访问存放在应用的专有目录以及 MediaStore之外的文件,否则最好使用分区存储。
分区存储对文件访问的影响
文件位置 | 所需权限 | 访问方法 (*) | 卸载应用时是否移除文件? |
---|---|---|---|
特定于应用的目录 | 无 | getExternalFilesDir() | 是 |
媒体集合(照片、视频、音频) | READ_EXTERNAL_STORAGE(仅当访问其他应用的文件时) | MediaStore | 否 |
下载内容(文档和电子书籍) | 无 | 存储访问框架 | 否 |
注:我们可以使用存储访问框架访问上表中显示的每一个位置,而无需请求任何权限。
文件访问权限
Android Q之前版本
Android Q之前,访问外部存储时,需要申请读或写的权限:READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE。应用获取到读或写权限之后,就可以自由访问相应的外部存储目录了。
注意,在Android 4.4(API 级别 19)之前,访问特定于应用的目录(应用的外部私有目录,位于:/Android/data/<app包名>/),是需要读或写权限的,但4.4之后的版本就不需要该权限了。
如果我们的应用支持4.4之前的版本,可以在AndroidManifest中声明:
<manifest ...>
<!-- If you need to modify files in external storage, request
WRITE_EXTERNAL_STORAGE instead. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
</manifest>
Android Q及之后版本
访问应用自己创建的文件
在Android Q中,应用对自己创建的文件始终拥有读/写权限,无论文件是否位于应用的专有目录内。
例如,应用可以查看外部存储设备内以下类型的文件,无需请求任何与存储相关的用户权限:
- 特定于应用的目录中的文件(使用 getExternalFilesDir() 访问)。
- 应用创建的照片、视频和音频片段(通过媒体库访问)。
访问其他应用创建的文件
Android Q中,若要访问其他应用创建的文件,则必须满足以下两个条件:
-
应用已获得 READ_EXTERNAL_STORAGE 权限。
-
这些文件位于以下其中一个明确定义的媒体集合中:
- 照片:存储在 MediaStore.Images 中。
- 视频:存储在 MediaStore.Video 中。
- 音频文件:存储在 MediaStore.Audio 中。
如果要访问其他文件及目录,包括“downloads”目录下的文件,应用必须使用存储访问框架,该框架允许用户选择特定文件。
如果应用尝试通过原始文件系统视图打开此目录之外的文件,则会发生错误:
- 在托管代码中,会发生 FileNotFoundException 错误。
- 在原生代码中,会发生 EPERM 内核错误。
访问媒体数据文件
分区存储会对访问媒体数据文件增加一些限制:
- 若应用未获得ACCESS_MEDIA_LOCATION权限,照片文件中的Exif元数据会被修改。一些照片在其Exif元数据中包含位置信息,以便用户查看照片的拍摄地点。但是,由于此位置信息属于敏感信息,如果应用使用了分区存储,默认情况下Android 10会对应用隐藏此信息。
- MediaStore.Files表格本身会经过过滤,仅显示照片、视频和音频文件。例如,表格中不显示PDF文件。
- 必须使用MediaStore在Java或Kotlin代码中访问媒体文件。
访问照片的位置信息示例
- 在应用的清单中请求ACCESS_MEDIA_LOCATION权限。
- 从MediaStore对象调用setRequireOriginal(),并传入照片的URI,如以下代码段中所示:
Uri photoUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
final double[] latLong;
// Get location data from the ExifInterface class.
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
double[] returnedLatLong = exifInterface.getLatLong();
// If lat/long is null, fall back to the coordinates (0, 0).
latLong = returnedLatLong != null ? returnedLatLong : new double[2];
// Don't reuse the stream associated with the instance of "ExifInterface".
stream.close();
} else {
// Failed to load the stream, so return the coordinates (0, 0).
latLong = new double[2];
}
唯一卷名称
面向Android Q(API 级别 29)或更高版本的应用可以访问系统分配给每个外部存储设备的唯一名称。此命名系统可帮助您高效地整理内容并将内容编入索引,还可让您控制新内容的存储位置。
- 主要共享存储设备始终名为:VOLUME_EXTERNAL_PRIMARY。
- 可以通过调用MediaStore.getExternalVolumeNames()来发现其他卷。
要查询、插入、更新或删除特定卷,请将卷名称传入 MediaStore API 中提供的任何 getContentUri() 方法,如以下代码段中所示:
// Assumes that the storage device of interest is the 2nd one
// that your app recognizes.
val volumeNames = MediaStore.getExternalVolumeNames(context)
val selectedVolumeName = volumeNames[1]
val collection = MediaStore.Audio.Media.getContentUri(selectedVolumeName)
// ... Use a ContentResolver to add items to the returned media collection.
另外,在旧版本的Android系统中,当存在多个不同的外部存储目录时(例如2个):
- Android4.4(API 级别 19)及之后版本,可以通过调用 getExternalFilesDirs() 来访问这两个位置,这会返回一个 File 数组,其中包含了每个存储位置的条目。数组中的第一个条目被视为主要外部存储,除非该位置已满或不可用,否则应该一律使用该位置。
- 如果您的应用支持 Android 4.3 及更低版本,则应使用支持库的静态方法ContextCompat.getExternalFilesDirs()。这始终会返回一个 File 数组,但如果设备搭载的是 Android 4.3及更低版本,数组中将仅包含主要外部存储的条目。(如果有第二个存储位置,您将无法在 Android 4.3 及更低版本上访问它。)
停用分区存储
如果我们的应用还想使用Android Q之前的存储策略,可以选择暂时停用“分区存储”。
我们可以使用2种方式来暂停分区存储:
- 以Android 9(API 级别 28)或更低版本为目标平台。
- 如果以Android Q或更高版本为目标平台,请在应用的清单文件中将requestLegacyExternalStorage的值设为true:
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
同样,要测试以Android 9 或更低版本为目标平台的应用在使用分区存储时的行为,您可以通过将requestLegacyExternalStorage的值设为false来选择启用该行为。
警告:未来,主要平台版本将要求所有应用都使用分区存储,无论应用的目标SDK级别是多少。因此,我们应该提前确保我们的应用能够使用分区存储。为此,请确保针对搭载Android 10(API 级别 29)及更高版本的设备启用了该行为。
我们在处理文件存储时,一定要考虑Android版本之间的差异,然后合理安排App的文件存储位置,避免使用不当导致的各种问题。