访问共享存储空间中的媒体文件

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

如需与媒体库抽象互动,请使用从应用上下文中检索到的 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/ 目录中的音频播放列表中。系统将这些文件添加到 MediaStore.Audio 表格中。
  • 下载的文件,存储在 Download/ 目录中。在搭载 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在 MediaStore.Downloads 表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。

媒体库还包含一个名为 MediaStore.Files 的集合。其内容取决于您的应用是否使用分区存储(适用于以 Android 10 或更高版本为目标平台的应用):

  • 如果启用了分区存储,集合只会显示您的应用创建的照片、视频和音频文件。
  • 如果分区存储不可用或未使用,集合将显示所有类型的媒体文件。

请求必要权限

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

存储权限

用于访问应用内媒体文件的权限模型取决于您的应用是否使用分区存储(适用于以 Android 10 或更高版本为目标平台的应用)。

已启用分区存储

如果您的应用使用分区存储,则应仅针对搭载 Android 9(API 级别 28)或更低版本的设备请求存储相关权限。您可以通过将 android:maxSdkVersion 属性添加到应用清单文件中的权限声明来应用此条件:

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

请勿为搭载 Android 10 或更高版本的设备不必要地请求存储相关权限。您的应用可以提供明确定义的媒体集合,包括 MediaStore.Downloads 集合,而无需请求任何存储相关权限。例如,如果您正在开发一款相机应用,您无需请求存储相关权限,因为您的应用拥有您将写入媒体库的图片

如需访问由其他应用创建的文件,必须满足以下所有条件:

特别是,如果应用需要访问 MediaStore.Downloads 集合中某个并非由其创建的文件,您必须使用存储访问框架。如需详细了解如何使用此框架,请参阅有关如何访问文档和其他文件的指南。

无法使用分区存储

如果应用在搭载 Android 9 或更低版本的设备上使用,或者应用使用了存储兼容性功能,您必须请求 READ_EXTERNAL_STORAGE 权限才能访问媒体文件。如果要修改媒体文件,您还必须请求 WRITE_EXTERNAL_STORAGE 权限。

媒体位置权限

如果应用使用分区存储,您需要在应用的清单中声明 ACCESS_MEDIA_LOCATION 权限,然后在运行时请求此权限,应用才能从照片中检索未编辑的 Exif 元数据。

查询媒体集合

如需查找满足一组特定条件(例如时长为 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 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(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    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>();

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(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    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 10 或更高版本为目标平台的应用可以访问系统为每个外部存储卷分配的唯一名称。此命名系统可帮助您高效地整理内容并将内容编入索引,还可让您控制新媒体文件的存储位置。

主要共享存储卷始终名为 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();

照片中的位置信息

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

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

  1. 在应用的清单中请求 ACCESS_MEDIA_LOCATION 权限。
  2. 通过调用 setRequireOriginal(),从 MediaStore 对象获取照片的确切字节,并传入照片的 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];
    }
    

媒体共享

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

如需共享媒体文件,请按照内容提供程序创建指南中的建议使用 content:// URI。

使用原始文件路径的内容访问

如果您没有任何与存储空间相关的权限,您可以访问应用专属目录中的文件,并可使用 File API 访问归因于您的应用的媒体文件

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

如需在搭载 Android 10 的设备上访问共享存储空间中的其他文件,建议您在应用的清单文件中将 requestLegacyExternalStorage 设置为 true 以停用分区存储。

从原生代码访问内容

您可能会遇到您的应用需要在原生代码中使用特定媒体文件的情况,例如其他应用与您的应用共享的文件,或用户的媒体合集中的媒体文件。

您必须先执行以下操作,然后应用才能使用 fopen() 等原生文件方法读取媒体文件:

  1. 在应用的清单文件中将 requestLegacyExternalStorage 设置为 true
  2. 请求 READ_EXTERNAL_STORAGE 权限。

如果您需要写入这些媒体文件,请将与文件相关联的文件描述符从基于 Java 或基于 Koltin 的代码传入原生代码。以下代码段演示了如何将媒体对象的文件描述符传入应用的原生代码:

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.
}

如需详细了解如何在原生代码中访问文件,请参阅 2018 年 Android 开发者峰会中的 Files for Miles 演讲(从 15:20 开始)。

媒体文件的应用归因

当以 Android 10 或更高版本为目标平台的应用启用了分区存储时,系统会将每个媒体文件归因于一个应用,这决定了应用在未请求任何存储权限时可以访问的文件。每个文件只能归因于一个应用。因此,如果您的应用创建的媒体文件存储在照片、视频或音频文件媒体集合中,应用便可以访问该文件。

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

添加项目

如需将媒体项添加到现有集合,请调用类似于以下内容的代码:

Kotlin

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

// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY)

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

// Keeps a handle to the new song's URI in case we 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.
// On API <= 28, use VOLUME_EXTERNAL instead.
Uri audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY);

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

// Keeps a handle to the new song's URI in case we 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 shouldn'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.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media
        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

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)

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

// Now that we're finished, release the "pending" status, and allow other apps
// to 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 shouldn'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.
// On API <= 28, use VOLUME_EXTERNAL instead.
Uri audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY);

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

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

try (ParcelableFileDescriptor pfd =
        resolver.openFileDescriptor(longSongContentUri, "w", null)) {
    // Write data into the pending audio file.
}

// Now that we're finished, release the "pending" status, and allow other apps
// to play the audio track.
songDetails.clear();
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0);
resolver.update(longSongContentUri, 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 we 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 we 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);

如果分区存储不可用或未启用,上述代码段中所示的过程也适用于不属于您的应用的文件。

更新其他应用的媒体文件

如果您的应用使用分区存储,它通常无法更新其他应用存放到媒体库中的媒体文件。

不过,您仍可通过捕获平台抛出的 RecoverableSecurityException 来征得用户同意修改文件。然后,您可以请求用户授予您的应用对此特定内容的写入权限,如以下代码段所示:

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    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 {
    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);
    }
}

每当您的应用需要修改并非由其创建的媒体文件时,都请完成此过程。

如果您的应用有其他用例未包含在分区存储范围内,请提交功能请求使用平台提供的应用兼容性功能

移除项目

如需从媒体库中移除您的应用不再需要的某个项目,请使用类似于以下代码段所示的逻辑:

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,如更新媒体项部分所述。

如果您的应用有其他用例未包含在分区存储范围内,请提交功能请求使用平台提供的应用兼容性功能

要求替代媒体库的用例

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

管理媒体文件组

媒体创建应用通常使用目录层次结构来管理文件组。如需在您的应用中提供此功能,请按照有关如何存储和访问文档及其他文件的指南中所述,使用 ACTION_OPEN_DOCUMENT_TREE intent 操作。

使用其他类型的文件

如果您的应用使用的文档和文件并非专门包含媒体内容,例如使用 EPUB 或 PDF 文件扩展名的文件,请按照有关如何存储和访问文档及其他文件的指南中所述,使用 ACTION_OPEN_DOCUMENT intent 操作。

在配套应用中共享文件

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

其他资源

如需详细了解如何存储和访问媒体,请参阅以下资源。

示例

视频