从共享存储访问媒体文件

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

照片选择器

作为使用媒体存储的替代方案,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。

以下代码段基于前面的代码段。此代码段显示了如何在将长歌曲存储到与MediaStore.Audio集合对应的目录时使用IS_PENDING标志。

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 设置文件共享。我们还建议将此工作流作为安全最佳实践

其他资源

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

示例

视频