处理外部存储中的媒体文件

MediaStore API 提供访问以下类型的媒体文件的接口:

MediaStore 还包含一个名为 MediaStore.Files 的集合,该集合提供访问所有类型的媒体文件的接口。

本指南介绍如何访问和共享通常存储在外部存储设备上的媒体文件。

访问文件

若要加载媒体文件,请从 ContentResolver 调用以下方法之一:

  • 对于单个媒体文件,请使用 openFileDescriptor()
  • 对于单个媒体文件的缩略图,请使用 loadThumbnail(),并传入要加载的缩略图的大小。
  • 对于媒体文件的集合,请使用 query()

以下代码段展示了如何访问媒体文件:

    val resolver = context.getContentResolver()

    // Open a specific media item.
    resolver.openFileDescriptor(item, mode).use { pfd ->
        // ...
    }

    // Load thumbnail of a specific media item.
    val mediaThumbnail = resolver.loadThumbnail(item, Size(640, 480), null)

    // Find all videos on a given storage device, including pending files.
    val collection = MediaStore.Video.Media.getContentUri(volumeName)
    val collectionWithPending = MediaStore.setIncludePending(collection)
    resolver.query(collectionWithPending, null, null, null).use { c ->
        // ...
    }

    // Publish a video onto an external storage device.
    val values = ContentValues().apply {
        put(MediaStore.Audio.Media.RELATIVE_PATH, "Video/My Videos")
        put(MediaStore.Audio.Media.DISPLAY_NAME, "My Video.mp4")
    }
    val item = resolver.insert(collection, values)
    

从原生代码访问

您可能会遇到您的应用需要在原生代码中使用特定媒体文件的情况,例如其他应用与您的应用共享的文件,或用户的媒体合集中的媒体文件。在这些情况下,请在基于 Java 或基于 Kotlin 的代码中找到相应媒体文件,然后将与此文件相关的文件描述符传递到原生代码。

以下代码段演示了如何将媒体对象的文件描述符传递到应用的原生代码:

Kotlin

    val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(BaseColumns._ID))
    val fileOpenMode = "r"
    val parcelFd = resolver.openFileDescriptor(uri, fileOpenMode)
    val fd = parcelFd?.detachFd()
    // Pass the integer value "fd" into your native code. Remember to call
    // close(2) on the file descriptor when you're done using it.
    

Java

    Uri contentUri = ContentUris.withAppendedId(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(Integer.parseInt(BaseColumns._ID)));
    String fileOpenMode = "r";
    ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);
    if (parcelFd != null) {
        int fd = parcelFd.detachFd();
        // Pass the integer value "fd" into your native code. Remember to call
        // close(2) on the file descriptor when you're done using it.
    }
    

要了解如何在原生代码中访问文件,请参阅 2018 年 Android 开发者峰会中的 Files for Miles 演讲。

内容查询中的列名称

如果您的应用代码使用列名称投影(例如 mime_type AS MimeType),请注意搭载 Android 10(API 级别 29)及更高版本的设备需要使用 MediaStore API 中定义的列名称。

如果应用中的依赖库需要未在 Android API 中定义的列名称(例如 MimeType),请在应用进程中使用 CursorWrapper 动态转换列名称。

为正在存储的媒体文件提供待处理状态

在搭载 Android 10(API 级别 29)及更高版本的设备上,您的应用可以通过使用 IS_PENDING 标记在媒体文件写入磁盘时获得对文件的独占访问权限。

以下代码段展示了在将图片存储到 MediaStore.Images 集合所对应的目录时如何使用 IS_PENDING 标记:

Kotlin

    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG")
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val resolver = context.getContentResolver()
    val collection = MediaStore.Images.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    val item = resolver.insert(collection, values)

    resolver.openFileDescriptor(item, "w", null).use { pfd ->
        // Write data into the pending image.
    }

    // Now that we're finished, release the "pending" status, and allow other apps
    // to view the image.
    values.clear()
    values.put(MediaStore.Images.Media.IS_PENDING, 0)
    resolver.update(item, values, null, null)
    

Java

    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG");
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    values.put(MediaStore.Images.Media.IS_PENDING, 1);

    ContentResolver resolver = context.getContentResolver();
    Uri collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
    Uri item = resolver.insert(collection, values);

    try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null)) {
        // Write data into the pending image.
    } catch (IOException e) {
        e.printStackTrace();
    }

    // Now that we're finished, release the "pending" status, and allow other apps
    // to view the image.
    values.clear();
    values.put(MediaStore.Images.Media.IS_PENDING, 0);
    resolver.update(item, values, null, null);
    

更新其他应用的媒体文件

如果您的应用使用分区存储,它通常无法更新其他应用存放到媒体存储中的媒体文件。不过,仍然可以通过捕获平台抛出的 RecoverableSecurityException 来征得用户同意以修改文件。然后,您可以请求用户授予您的应用对此特定内容的写入权限,如以下代码段所示:

Kotlin

    try {
        // ...
    } catch (rse: RecoverableSecurityException) {
        val requestAccessIntentSender = rse.userAction.actionIntent.intentSender

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null)
    }
    

Java

    try {
        // ...
    } catch (RecoverableSecurityException rse) {
        IntentSender requestAccessIntentSender = rse.getUserAction()
                .getActionIntent().getIntentSender();

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null);
    }
    

提供关于文件存储位置的提示

当应用在搭载 Android 10(API 级别 29)的设备上存放媒体文件时,系统默认会按照媒体文件的类型进行存放。例如,新图片文件默认存放在 Environment.DIRECTORY_PICTURES 目录下,该目录对应于 MediaStore.Images 集合。

如果您的应用知道应该将文件存放在哪个具体位置(例如 Pictures/MyVacationPictures),您可以设置 MediaColumns.RELATIVE_PATH 来提示系统将新写入的文件存放在何处。同样,您也可以通过更改 MediaColumns.RELATIVE_PATHMediaColumns.DISPLAY_NAME,在调用 update() 期间移动磁盘上的文件。

常见用例

本节将介绍如何实现与媒体文件相关的几个常见用例:

分享媒体文件

某些应用允许用户彼此分享媒体文件。例如,用户可以通过社交媒体应用与朋友分享照片和视频。

要访问用户想要分享的媒体文件,请使用有关如何访问文件和如何使用唯一名称访问卷的章节中介绍的流程。

如果您提供了一组配套应用(例如短信应用和个人资料应用),请使用 content:// URI 来设置文件分享。我们还建议将此工作流程作为一项安全最佳做法

使用文档

某些应用将文档用作存储单元,用户可以在其中输入可能要与同伴分享或要导入其他文档的数据。例如,用户打开企业办公文档或打开另存为 EPUB 文件的图书。

在这些情况下,请通过调用 ACTION_OPEN_DOCUMENT intent 来支持用户选择要打开的文件,此 intent 会打开系统的文件选择器应用。要仅显示应用支持的文件类型,请在 intent 中添加 Intent.EXTRA_MIME_TYPES extra。

GitHub 上的 ActionOpenDocument 示例说明了如何在征得用户同意后使用 ACTION_OPEN_DOCUMENT 打开文件。

管理文件组

文件管理和媒体创建应用通常在目录层次结构中管理文件组。这些应用可以调用 ACTION_OPEN_DOCUMENT_TREE intent,以允许用户授予对整个目录树的访问权限。此类应用可以编辑所选目录及其子目录中的任何文件。

使用该接口,用户可以通过 DocumentsProvider 实例访问文件,而任何在本地受支持或基于云的解决方案都支持该实例。 LX: locally-backed 的翻译 FMR 而任何基于本地或云端的解决方案都支持该实例

GitHub 上的 ActionOpenDocumentTree 示例说明了如何在征得用户同意后使用 ACTION_OPEN_DOCUMENT_TREE 打开目录树。