管理分区外部存储访问

为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储)。此类应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用需要访问存放在应用的专有目录以及 MediaStore 之外的文件,否则最好使用分区存储。

下表总结了分区存储如何影响文件访问:

文件位置 所需权限 访问方法 (*) 卸载应用时是否移除文件?
特定于应用的目录 getExternalFilesDir()
媒体集合
(照片、视频、音频)
READ_EXTERNAL_STORAGE
(仅当
访问其他应用的文件时)
MediaStore
下载内容
(文档和
电子书籍)
存储访问框架
(加载系统的文件选择器)

*您可以使用存储访问框架访问上表中显示的每一个位置,而无需请求任何权限。

本页介绍了使用分区存储时您的应用可以访问的文件,以及如何更新应用,使其可以继续共享、访问和修改保存在外部存储设备上的其他文件。

访问文件所需的权限

使用分区存储的应用对自己创建的文件始终拥有读/写权限,无论文件是否位于应用的专有目录内。因此,如果您的应用仅保存和访问自己创建的文件,则无需请求获得 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限。

不过,若要访问其他应用创建的文件,则必须满足以下两个条件:

  1. 您的应用已获得 READ_EXTERNAL_STORAGE 权限。
  2. 这些文件位于以下其中一个明确定义的媒体集合中:

为了访问其他应用创建的任何其他文件(包括“downloads”目录下的文件),您的应用必须使用存储访问框架,该框架允许用户选择特定文件。

如果此类应用要访问外部存储设备的原始文件系统视图,则即使拥有 READ_EXTERNAL_STORAGE 权限,也只能访问应用专有的目录。如果应用尝试通过原始文件系统视图打开此目录之外的文件,则会发生错误:

媒体数据限制

分区存储会施加以下媒体数据限制:

该指南介绍了如何处理媒体文件,并提供了有关访问 MediaStore 内的单个文档和文档树的最佳做法。如果您的应用使用分区存储,则需要使用这些方法来访问媒体。

照片中的位置信息

一些照片在其 Exif 元数据中包含位置信息,以便用户查看照片的拍摄地点。但是,由于此位置信息属于敏感信息,如果应用使用了分区存储,默认情况下 Android 10 会对应用隐藏此信息。

如果您的应用需要访问照片的位置信息,请完成以下步骤:

  1. 在应用的清单中请求 ACCESS_MEDIA_LOCATION 权限。
  2. MediaStore 对象调用 setRequireOriginal(),并传入照片的 URI,如以下代码段中所示:

    Kotlin

        // Get location data from the ExifInterface class.
        val photoUri = MediaStore.setRequireOriginal(photoUri)
        contentResolver.openInputStream(photoUri).use { stream ->
            ExifInterface(stream).run {
                // If lat/long is null, fall back to the coordinates (0, 0).
                val latLong = ?: doubleArrayOf(0.0, 0.0)
            }
        }
        

    Java

        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];
        }
        

选择停用分区存储

在您的应用完全兼容分区存储之前,您可以根据应用的目标 SDK 级别或 requestLegacyExternalStorage 清单属性,暂时选择停用分区存储:

  • 以 Android 9(API 级别 28)或更低版本为目标平台。
  • 如果以 Android 10 或更高版本为目标平台,请在应用的清单文件中将 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 来选择启用该行为。