从共享存储中访问媒体文件

为了提供更丰富的用户体验,许多应用允许用户贡献和访问外部存储卷上的媒体。框架提供了一个经过优化的媒体集合索引,称为媒体存储,允许用户更轻松地检索和更新这些媒体文件。即使您的应用被卸载,这些文件仍会保留在用户的设备上。

照片选择器

作为使用媒体存储的替代方案,Android 照片选择器工具为用户提供了一种安全、内置的方式来选择媒体文件,而无需授予您的应用访问其整个媒体库的权限。这仅在支持的设备上可用。有关更多信息,请参阅 照片选择器 指南。

媒体存储

要与媒体存储抽象进行交互,请使用从应用的上下文检索的 ContentResolver 对象

Kotlin

val projection = arrayOf(media-database-columns-to-retrieve)
val selection = sql-where-clause-with-placeholder-variables
val selectionArgs = values-of-placeholder-variables
val sortOrder = sql-order-by-clause

applicationContext.contentResolver.query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    while (cursor.moveToNext()) {
        // Use an ID column from the projection to get
        // a URI representing the media item itself.
    }
}

Java

String[] projection = new String[] {
        media-database-columns-to-retrieve
};
String selection = sql-where-clause-with-placeholder-variables;
String[] selectionArgs = new String[] {
        values-of-placeholder-variables
};
String sortOrder = sql-order-by-clause;

Cursor cursor = getApplicationContext().getContentResolver().query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
);

while (cursor.moveToNext()) {
    // Use an ID column from the projection to get
    // a URI representing the media item itself.
}

系统会自动扫描外部存储卷并将媒体文件添加到以下定义明确的集合中

  • 图像,包括照片和屏幕截图,存储在 DCIM/Pictures/ 目录中。系统将这些文件添加到 MediaStore.Images 表格中。
  • 视频,存储在 DCIM/Movies/Pictures/ 目录中。系统将这些文件添加到 MediaStore.Video 表格中。
  • 音频文件,存储在 Alarms/Audiobooks/Music/Notifications/Podcasts/Ringtones/ 目录中。此外,系统还会将 Music/Movies/ 目录中的音频播放列表以及 Recordings/ 目录中的语音记录识别为音频文件。系统将这些文件添加到 MediaStore.Audio 表格中。Recordings/ 目录在 Android 11(API 级别 30)及更低版本中不可用。
  • 下载的文件存储在Download/目录中。在运行 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在MediaStore.Downloads表中。此表在 Android 9(API 级别 28)及更低版本上不可用。

媒体库还包含一个名为MediaStore.Files的集合。其内容取决于您的应用是否使用作用域存储,作用域存储适用于面向 Android 10 或更高版本的应用。

  • 如果启用作用域存储,该集合仅显示您的应用创建的照片、视频和音频文件。大多数开发者不需要使用MediaStore.Files来查看其他应用的媒体文件,但如果您有特殊需求,可以声明READ_EXTERNAL_STORAGE权限。但是,我们建议您使用MediaStore API 来打开您的应用未创建的文件。
  • 如果作用域存储不可用或未使用,该集合将显示所有类型的媒体文件。

请求必要的权限

在对媒体文件执行操作之前,请确保您的应用已声明访问这些文件所需的权限。但是,请注意不要声明您的应用不需要或不使用的权限。

存储权限

您的应用是否需要访问存储的权限取决于它是否仅访问自己的媒体文件或其他应用创建的文件。

访问您自己的媒体文件

在运行 Android 10 或更高版本的设备上,您不需要存储相关的权限来访问和修改您的应用拥有的媒体文件,包括MediaStore.Downloads集合中的文件。例如,如果您正在开发一个相机应用,您不需要请求存储相关的权限来访问它拍摄的照片,因为您的应用拥有您写入媒体库的图像。

访问其他应用的媒体文件

要访问其他应用创建的媒体文件,您必须声明适当的存储相关的权限,并且这些文件必须位于以下媒体集合之一

只要可以从MediaStore.ImagesMediaStore.VideoMediaStore.Audio查询中查看文件,也可以使用MediaStore.Files查询查看它。

以下代码片段演示了如何声明适当的存储权限

<!-- Required only if your app needs to access images or photos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<!-- Required only if your app needs to access videos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- Required only if your app needs to access audio files
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="29" />

在旧版设备上运行的应用所需的额外权限

如果您的应用在运行 Android 9 或更低版本的设备上使用,或者您的应用已暂时退出作用域存储,则必须请求READ_EXTERNAL_STORAGE权限来访问任何媒体文件。如果您想修改媒体文件,还必须请求WRITE_EXTERNAL_STORAGE权限。

访问其他应用的下载所需的文件存储访问框架

如果您的应用想要访问MediaStore.Downloads集合中您的应用未创建的文件,您必须使用文件存储访问框架。要详细了解如何使用此框架,请参阅访问共享存储中的文档和其他文件

媒体位置权限

如果您的应用面向 Android 10(API 级别 29)或更高版本,并且需要从照片中检索未删减的 EXIF 元数据,则需要在您的应用清单中声明ACCESS_MEDIA_LOCATION权限,然后在运行时请求此权限。

检查媒体库更新

为了更可靠地访问媒体文件,尤其是在您的应用缓存了媒体库的 URI 或数据的情况下,请检查媒体库版本是否与上次同步媒体数据时相比发生了变化。要执行此更新检查,请调用getVersion()。返回的版本是一个唯一字符串,每当媒体库发生重大更改时,它都会发生变化。如果返回的版本与上次同步的版本不同,请重新扫描并重新同步您的应用的媒体缓存。

在应用进程启动时完成此检查。无需每次查询媒体库时都检查版本。

不要假设有关版本号的任何实现细节。

查询媒体集合

要查找满足特定条件集的媒体(例如,时长 5 分钟或更长),请使用类似于以下代码片段中所示的 SQL 风格选择语句

Kotlin

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
data class Video(val uri: Uri,
    val name: String,
    val duration: Int,
    val size: Int
)
val videoList = mutableListOf<Video>()

val collection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Video.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL
        )
    } else {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    }

val projection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    // Cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
    val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val duration = cursor.getInt(durationColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList += Video(contentUri, name, duration, size)
    }
}

Java

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
class Video {
    private final Uri uri;
    private final String name;
    private final int duration;
    private final int size;

    public Video(Uri uri, String name, int duration, int size) {
        this.uri = uri;
        this.name = name;
        this.duration = duration;
        this.size = size;
    }
}
List<Video> videoList = new ArrayList<Video>();

Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
    collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}

String[] projection = new String[] {
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
        " >= ?";
String[] selectionArgs = new String[] {
    String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

try (Cursor cursor = getApplicationContext().getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    // Cache column indices.
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    int nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
    int durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
    int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        long id = cursor.getLong(idColumn);
        String name = cursor.getString(nameColumn);
        int duration = cursor.getInt(durationColumn);
        int size = cursor.getInt(sizeColumn);

        Uri contentUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList.add(new Video(contentUri, name, duration, size));
    }
}

在您的应用中执行此类查询时,请牢记以下几点

  • 在工作线程中调用query()方法。
  • 缓存列索引,这样您就不需要每次处理查询结果中的行时都调用getColumnIndexOrThrow()
  • 将 ID 附加到内容 URI,如本例所示。
  • 运行 Android 10 及更高版本的设备需要MediaStore API 中定义的列名。如果您的应用中的依赖库期望 API 中未定义的列名,例如"MimeType",请使用CursorWrapper在您的应用进程中动态翻译列名。

加载文件缩略图

如果您的应用显示多个媒体文件并要求用户选择其中一个文件,那么加载文件的预览版本(或缩略图)比加载文件本身更有效率。

要加载给定媒体文件的缩略图,请使用loadThumbnail()并传入要加载的缩略图的大小,如以下代码片段所示

Kotlin

// Load thumbnail of a specific media item.
val thumbnail: Bitmap =
        applicationContext.contentResolver.loadThumbnail(
        content-uri, Size(640, 480), null)

Java

// Load thumbnail of a specific media item.
Bitmap thumbnail =
        getApplicationContext().getContentResolver().loadThumbnail(
        content-uri, new Size(640, 480), null);

打开媒体文件

用于打开媒体文件的具体逻辑取决于媒体内容是否最好用文件描述符、文件流或直接文件路径表示。

文件描述符

要使用文件描述符打开媒体文件,请使用类似于以下代码片段中的逻辑

Kotlin

// Open a specific media item using ParcelFileDescriptor.
val resolver = applicationContext.contentResolver

// "rw" for read-and-write.
// "rwt" for truncating or overwriting existing file contents.
val readOnlyMode = "r"
resolver.openFileDescriptor(content-uri, readOnlyMode).use { pfd ->
    // Perform operations on "pfd".
}

Java

// Open a specific media item using ParcelFileDescriptor.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// "rw" for read-and-write.
// "rwt" for truncating or overwriting existing file contents.
String readOnlyMode = "r";
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(content-uri, readOnlyMode)) {
    // Perform operations on "pfd".
} catch (IOException e) {
    e.printStackTrace();
}

文件流

要使用文件流打开媒体文件,请使用类似于以下代码片段中的逻辑

Kotlin

// Open a specific media item using InputStream.
val resolver = applicationContext.contentResolver
resolver.openInputStream(content-uri).use { stream ->
    // Perform operations on "stream".
}

Java

// Open a specific media item using InputStream.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();
try (InputStream stream = resolver.openInputStream(content-uri)) {
    // Perform operations on "stream".
}

直接文件路径

为了帮助您的应用更顺畅地与第三方媒体库协作,Android 11(API 级别 30)及更高版本允许您使用除MediaStore API 之外的 API 来访问共享存储中的媒体文件。您可以改用以下 API 之一直接访问媒体文件

  • File API
  • 本机库,例如fopen()

如果您没有任何存储相关的权限,您可以使用File API 访问特定于应用的目录中的文件,以及归属于您的应用的媒体文件

如果您的应用尝试使用File API 访问文件,但没有必要的权限,则会发生FileNotFoundException

要访问在运行 Android 10(API 级别 29)的设备上的共享存储中的其他文件,我们建议您暂时退出作用域存储,方法是在您的应用的清单文件中将requestLegacyExternalStorage设置为true。要在 Android 10 上使用本机文件方法访问媒体文件,您还必须请求READ_EXTERNAL_STORAGE权限。

访问媒体内容时的注意事项

在访问媒体内容时,请牢记以下部分中讨论的注意事项。

缓存数据

如果您的应用缓存了媒体库的 URI 或数据,请定期检查媒体库的更新。此检查可以让您的应用端缓存数据与系统端提供者数据保持同步。

性能

当您使用直接文件路径对媒体文件执行顺序读取时,其性能与MediaStore API 的性能相当。

但是,当您使用直接文件路径对媒体文件执行随机读写时,该过程的速度可能会慢至两倍。在这种情况下,我们建议您改用MediaStore API。

DATA 列

当您访问现有媒体文件时,您可以在逻辑中使用DATA列的值。这是因为此值具有有效的文件路径。但是,不要假设该文件始终可用。做好处理可能发生的任何基于文件的 I/O 错误的准备。

另一方面,要创建或更新媒体文件,不要使用DATA列的值。而是使用DISPLAY_NAMERELATIVE_PATH列的值。

存储卷

面向 Android 10 或更高版本的应用可以访问系统分配给每个外部存储卷的唯一名称。此命名系统可以帮助您高效地组织和索引内容,并且它可以让您控制新媒体文件的存储位置。

以下卷尤其有用,请牢记

  • VOLUME_EXTERNAL卷提供了对设备上所有共享存储卷的视图。您可以读取此合成卷的内容,但无法修改其内容。
  • VOLUME_EXTERNAL_PRIMARY 卷表示设备上的主要共享存储卷。您可以读取和修改此卷的内容。

您可以通过调用 MediaStore.getExternalVolumeNames() 来发现其他卷。

Kotlin

val volumeNames: Set<String> = MediaStore.getExternalVolumeNames(context)
val firstVolumeName = volumeNames.iterator().next()

Java

Set<String> volumeNames = MediaStore.getExternalVolumeNames(context);
String firstVolumeName = volumeNames.iterator().next();

媒体捕获位置

一些照片和视频在其元数据中包含位置信息,显示了拍摄照片或录制视频的位置。

您在应用中访问此位置信息的方式取决于您是否需要访问照片或视频的位置信息。

照片

如果您的应用使用 作用域存储,系统默认情况下会隐藏位置信息。要访问此信息,请完成以下步骤

  1. 在应用的清单中请求 ACCESS_MEDIA_LOCATION 权限。
  2. 从您的 MediaStore 对象中,通过调用 setRequireOriginal() 并传入照片的 URI 来获取照片的准确字节,如以下代码片段所示

    Kotlin

    val photoUri: Uri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex)
    )
    
    // Get location data using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    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 = 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 using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    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];
    }
    

视频

要访问视频元数据中的位置信息,请使用 MediaMetadataRetriever 类,如以下代码片段所示。您的应用无需请求任何其他权限即可使用此类。

Kotlin

val retriever = MediaMetadataRetriever()
val context = applicationContext

// Find the videos that are stored on a device by querying the video collection.
val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    while (cursor.moveToNext()) {
        val id = cursor.getLong(idColumn)
        val videoUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )
        extractVideoLocationInfo(videoUri)
    }
}

private fun extractVideoLocationInfo(videoUri: Uri) {
    try {
        retriever.setDataSource(context, videoUri)
    } catch (e: RuntimeException) {
        Log.e(APP_TAG, "Cannot retrieve video file", e)
    }
    // Metadata uses a standardized format.
    val locationMetadata: String? =
            retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
}

Java

MediaMetadataRetriever retriever = new MediaMetadataRetriever();
Context context = getApplicationContext();

// Find the videos that are stored on a device by querying the video collection.
try (Cursor cursor = context.getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    while (cursor.moveToNext()) {
        long id = cursor.getLong(idColumn);
        Uri videoUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
        extractVideoLocationInfo(videoUri);
    }
}

private void extractVideoLocationInfo(Uri videoUri) {
    try {
        retriever.setDataSource(context, videoUri);
    } catch (RuntimeException e) {
        Log.e(APP_TAG, "Cannot retrieve video file", e);
    }
    // Metadata uses a standardized format.
    String locationMetadata = retriever.extractMetadata(
            MediaMetadataRetriever.METADATA_KEY_LOCATION);
}

分享

某些应用允许用户相互分享媒体文件。例如,社交媒体应用允许用户与朋友分享照片和视频。

要分享媒体文件,请使用 content:// URI,如 创建内容提供者指南 中所建议的那样。

媒体文件的应用归属

当针对 Android 10 或更高版本的应用启用了 作用域存储 时,系统会将应用归属到每个媒体文件,这决定了当应用未请求任何存储权限时,应用可以访问哪些文件。每个文件只能归属于一个应用。因此,如果您的应用创建了存储在照片、视频或音频文件媒体集中中的媒体文件,您的应用就可以访问该文件。

但是,如果用户卸载并重新安装您的应用,则您必须请求 READ_EXTERNAL_STORAGE 才能访问应用最初创建的文件。此权限请求是必需的,因为系统认为该文件属于以前安装的应用版本,而不是新安装的版本。

添加项目

要将媒体项目添加到现有集合中,请使用类似于以下代码的代码。此代码片段访问运行 Android 10 或更高版本的设备上的 VOLUME_EXTERNAL_PRIMARY 卷。这是因为,在这些设备上,您只能修改卷的内容,前提是它是主卷,如 存储卷 部分所述。

Kotlin

// Add a specific media item.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

// Publish a new song.
val newSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Song.mp3")
}

// Keep a handle to the new song's URI in case you need to modify it
// later.
val myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails)

Java

// Add a specific media item.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

// Publish a new song.
ContentValues newSongDetails = new ContentValues();
newSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Song.mp3");

// Keep a handle to the new song's URI in case you need to modify it
// later.
Uri myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails);

切换媒体文件的待处理状态

如果您的应用执行可能需要较长时间的操作,例如写入媒体文件,那么在处理过程中拥有对文件的独占访问权限非常有用。在运行 Android 10 或更高版本的设备上,您的应用可以通过将 IS_PENDING 标志的值设置为 1 来获得这种独占访问权限。只有您的应用才能查看该文件,直到您的应用将 IS_PENDING 的值更改回 0 为止。

以下代码片段建立在前面的代码片段的基础上。此代码片段演示了在存储长歌曲时,如何使用 IS_PENDING 标志,以便将歌曲存储到与 MediaStore.Audio 集合相对应的目录中

Kotlin

// Add a media item that other apps don't see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

val songDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Workout Playlist.mp3")
    put(MediaStore.Audio.Media.IS_PENDING, 1)
}

val songContentUri = resolver.insert(audioCollection, songDetails)

// "w" for write.
resolver.openFileDescriptor(songContentUri, "w", null).use { pfd ->
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)

Java

// Add a media item that other apps don't see until the item is
// fully written to the media store.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

ContentValues songDetails = new ContentValues();
songDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Workout Playlist.mp3");
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 1);

Uri songContentUri = resolver
        .insert(audioCollection, songDetails);

// "w" for write.
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(songContentUri, "w", null)) {
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear();
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0);
resolver.update(songContentUri, songDetails, null, null);

提示文件位置

当您的应用将媒体存储在运行 Android 10 的设备上时,默认情况下,媒体会根据其类型进行组织。例如,默认情况下,新的图像文件将放置在 Environment.DIRECTORY_PICTURES 目录中,该目录对应于 MediaStore.Images 集合。

如果您的应用知道可以存储文件的特定位置,例如名为 Pictures/MyVacationPictures 的相册,您可以将 MediaColumns.RELATIVE_PATH 设置为向系统提供提示,以指示将新写入的文件存储在何处。

更新项目

要更新应用拥有的媒体文件,请使用类似于以下代码的代码

Kotlin

// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver

// When performing a single item update, prefer using the ID.
val selection = "${MediaStore.Audio.Media._ID} = ?"

// By using selection + args you protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())

// Update an existing song.
val updatedSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}

// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs)

Java

// Updates an existing media item.
long mediaId = // MediaStore.Audio.Media._ID of item to update.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// When performing a single item update, prefer using the ID.
String selection = MediaStore.Audio.Media._ID + " = ?";

// By using selection + args you protect against improper escaping of
// values. Here, "song" is an in-memory object that caches the song's
// information.
String[] selectionArgs = new String[] { getId().toString() };

// Update an existing song.
ContentValues updatedSongDetails = new ContentValues();
updatedSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Favorite Song.mp3");

// Use the individual song's URI to represent the collection that's
// updated.
int numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs);

如果作用域存储不可用或未启用,则前面代码片段中所示的过程也适用于应用不拥有的文件。

在本地代码中更新

如果您需要使用本地库写入媒体文件,请将文件的关联文件描述符从基于 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(contentUri, 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(contentUri, 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.
}

更新其他应用的媒体文件

如果您的应用使用 作用域存储,则通常无法更新其他应用贡献到媒体存储中的媒体文件。

但是,您可以通过捕获平台抛出的 RecoverableSecurityException 来获得用户修改文件的同意。然后,您可以请求用户授予您的应用对该特定项目的写入访问权限,如以下代码片段所示

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    // "w" for write.
    contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
        setGrayscaleFilter(it)
    }
} catch (securityException: SecurityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        val intentSender =
            recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, image-request-code,
                    null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}

Java

try {
    // "w" for write.
    ParcelFileDescriptor imageFd = getContentResolver()
            .openFileDescriptor(image-content-uri, "w");
    setGrayscaleFilter(imageFd);
} catch (SecurityException securityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        RecoverableSecurityException recoverableSecurityException;
        if (securityException instanceof RecoverableSecurityException) {
            recoverableSecurityException =
                    (RecoverableSecurityException)securityException;
        } else {
            throw new RuntimeException(
                    securityException.getMessage(), securityException);
        }
        IntentSender intentSender =recoverableSecurityException.getUserAction()
                .getActionIntent().getIntentSender();
        startIntentSenderForResult(intentSender, image-request-code,
                null, 0, 0, 0, null);
    } else {
        throw new RuntimeException(
                securityException.getMessage(), securityException);
    }
}

每次应用需要修改未创建的媒体文件时,都要完成此过程。

或者,如果您的应用在 Android 11 或更高版本上运行,则您可以允许用户授予您的应用对一组媒体文件的写入访问权限。使用 createWriteRequest() 方法,如有关如何 管理媒体文件组 的部分所述。

如果您的应用有作用域存储未涵盖的其他用例,请 提交功能请求暂时退出作用域存储

删除项目

要从媒体存储中删除应用不再需要的项目,请使用与以下代码片段中所示逻辑类似的逻辑

Kotlin

// Remove a specific media item.
val resolver = applicationContext.contentResolver

// URI of the image to remove.
val imageUri = "..."

// WHERE clause.
val selection = "..."
val selectionArgs = "..."

// Perform the actual removal.
val numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs)

Java

// Remove a specific media item.
ContentResolver resolver = getApplicationContext()
        getContentResolver();

// URI of the image to remove.
Uri imageUri = "...";

// WHERE clause.
String selection = "...";
String[] selectionArgs = "...";

// Perform the actual removal.
int numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs);

如果作用域存储不可用或未启用,则可以使用前面的代码片段删除其他应用拥有的文件。但是,如果启用了作用域存储,则需要为应用想要删除的每个文件捕获 RecoverableSecurityException,如有关 更新媒体项目 的部分所述。

如果您的应用在 Android 11 或更高版本上运行,则您可以允许用户选择要删除的一组媒体文件。使用 createTrashRequest() 方法或 createDeleteRequest() 方法,如有关如何 管理媒体文件组 的部分所述。

如果您的应用有作用域存储未涵盖的其他用例,请 提交功能请求暂时退出作用域存储

检测媒体文件的更新

您的应用可能需要识别与以前的时间点相比,应用添加或修改的包含媒体文件的存储卷。要最可靠地检测这些更改,请将感兴趣的存储卷传递到 getGeneration() 中。只要媒体存储版本没有更改,此方法的返回值就会随着时间的推移单调递增。

特别是,getGeneration() 比媒体列中的日期(例如 DATE_ADDEDDATE_MODIFIED)更稳健。这是因为,当应用调用 setLastModified() 或用户更改系统时钟时,这些媒体列值会发生更改。

管理媒体文件组

在 Android 11 和更高版本上,您可以要求用户选择一组媒体文件,然后在一个操作中更新这些媒体文件。这些方法在设备之间提供更好的一致性,并且这些方法使用户更容易管理其媒体集合。

提供这种“批量更新”功能的方法包括以下方法

createWriteRequest()
请求用户授予您的应用对指定媒体文件组的写入访问权限。
createFavoriteRequest()
请求用户将指定的媒体文件标记为设备上其“收藏夹”媒体的一部分。任何对该文件具有读取访问权限的应用都可以看到用户已将该文件标记为“收藏夹”。
createTrashRequest()

请求用户将指定的媒体文件放入设备的垃圾箱中。垃圾箱中的项目将在系统定义的时间段后永久删除。

createDeleteRequest()

请求用户立即永久删除指定的媒体文件,而不先将其放入垃圾箱。

调用任何这些方法后,系统会构建一个 PendingIntent 对象。在您的应用调用此意图后,用户会看到一个对话框,请求他们同意您的应用更新或删除指定的媒体文件。

例如,以下是构建 createWriteRequest() 调用的方法

Kotlin

val urisToModify = /* A collection of content URIs to modify. */
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)

Java

List<Uri> urisToModify = /* A collection of content URIs to modify. */
PendingIntent editPendingIntent = MediaStore.createWriteRequest(contentResolver,
                  urisToModify);

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.getIntentSender(),
    EDIT_REQUEST_CODE, null, 0, 0, 0);

评估用户的响应。如果用户提供了同意,请继续进行媒体操作。否则,请向用户解释应用为什么需要该权限

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    ...
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}

Java

@Override
protected void onActivityResult(int requestCode, int resultCode,
                   @Nullable Intent data) {
    ...
    if (requestCode == EDIT_REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            /* Edit request granted; proceed. */
        } else {
            /* Edit request not granted; explain to the user. */
        }
    }
}

您可以对 createFavoriteRequest()createTrashRequest()createDeleteRequest() 使用相同的通用模式。

媒体管理权限

用户可能信任某个特定应用程序来执行媒体管理,例如频繁编辑媒体文件。如果您的应用针对 Android 11 或更高版本,并且不是设备的默认图库应用,则必须在每次应用尝试修改或删除文件时向用户显示确认对话框。

如果您的应用程序针对 Android 12(API 级别 31)或更高版本,您可以请求用户授予您的应用程序访问“媒体管理”特殊权限。此权限允许您的应用程序执行以下操作,而无需提示用户进行每个文件操作。

要执行此操作,请完成以下步骤。

  1. 在您的应用程序的清单文件中声明 MANAGE_MEDIA 权限和 READ_EXTERNAL_STORAGE 权限。

    要在不显示确认对话框的情况下调用 createWriteRequest(),请声明 ACCESS_MEDIA_LOCATION 权限。

  2. 在您的应用中,向用户展示一个 UI,解释为什么他们可能希望授予您的应用媒体管理访问权限。

  3. 调用 ACTION_REQUEST_MANAGE_MEDIA 意图操作。这会将用户带到系统设置中的“媒体管理应用”屏幕。在这里,用户可以授予应用程序特殊访问权限。

需要媒体存储库替代方案的用例

如果您的应用程序主要执行以下角色之一,请考虑使用 MediaStore API 的替代方案。

使用其他类型的文件

如果您的应用程序使用不完全包含媒体内容的文档和文件,例如使用 EPUB 或 PDF 文件扩展名的文件,请使用 ACTION_OPEN_DOCUMENT 意图操作,如“存储和访问文档和其他文件”指南中所述。

配套应用程序中的文件共享

在您提供一套配套应用程序(例如消息应用程序和个人资料应用程序)的情况下,请使用 content:// URI 设置文件共享。我们还建议将此工作流程作为 安全最佳实践

其他资源

有关如何存储和访问媒体的更多信息,请参阅以下资源。

示例

视频