为了提供更丰富的用户体验,许多应用允许用户提供和访问位于外部存储卷上的媒体。框架提供经过优化的媒体集合索引,称为媒体库,使应用可以更轻松地检索和更新这些媒体文件。即使应用已卸载,这些文件仍会保留在用户的设备上。
系统会自动扫描外部存储卷,并将媒体文件添加到以下明确定义的集合中:
- 图片(包括照片和屏幕截图)
- 存储在
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 或更高版本的设备不必要地请求存储相关权限。应用可以提供明确定义的媒体集合,包括MediaStore.Downloads
集合,而无需请求任何存储相关权限。例如,如果正在开发一款相机应用,无需请求存储相关权限,因为应用拥有将写入媒体库的图片。
如需访问由其他应用创建的文件,必须满足以下所有条件:
- 应用已获得
READ_EXTERNAL_STORAGE
权限。 - 这些文件位于以下其中一个明确定义的媒体集合中:
MediaStore.Images
MediaStore.Video
MediaStore.Audio
特别是,如果应用需要访问MediaStore.Downloads
集合中某个并非由其创建的文件,必须使用存储访问框架。
媒体位置权限
如果应用使用分区存储,需要在应用的清单中声明ACCESS_MEDIA_LOCATION
权限,然后在运行时请求此权限,应用才能从照片中检索未编辑的 Exif 元数据。
查询媒体集合
如需与媒体库抽象互动,使用从应用上下文中检索到的ContentResolver
对象。如需查找满足一组特定条件(例如时长为 5 分钟或更长时间)的媒体,使用类似于以下代码段中所示的类似 SQL 的选择语句:
// 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 中未定义的列名称,使用CursorWrapper
在应用的进程中动态转换列名称。
加载文件缩略图
如果应用显示多个媒体文件,并请求用户选择其中一个文件,加载文件的预览版本(或缩略图)会比加载文件本身效率更高。
如需加载给定媒体文件的缩略图,使用loadThumbnail()
并传入想加载的缩略图的大小,如以下代码段所示:
// Load thumbnail of a specific media item.
Bitmap thumbnail = getApplicationContext().getContentResolver().loadThumbnail(
contentUri, new Size(640, 480), null);
打开媒体文件
用于打开媒体文件的具体逻辑取决于媒体内容最佳表示形式是文件描述符还是文件流:
文件描述符
// 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();
}
文件流
// Open a specific media item using InputStream.
ContentResolver resolver = getApplicationContext().getContentResolver();
try (InputStream stream = resolver.openInputStream(content-uri)) {
// Perform operations on "stream".
}
访问媒体内容时的注意事项
存储卷
以 Android 10 或更高版本为目标平台的应用可以访问系统为每个外部存储卷分配的唯一名称。主要共享存储卷始终名为MediaStore.VOLUME_EXTERNAL_PRIMARY
。可以通过调用MediaStore.getExternalVolumeNames()
查看其他存储卷:
Set<String> volumeNames = MediaStore.getExternalVolumeNames(context);
String firstVolumeName = volumeNames.iterator().next();
照片中的位置信息
一些照片在其 Exif 元数据中包含位置信息,以便用户查看照片的拍摄地点。但是,由于此位置信息属于敏感信息,如果应用使用了分区存储,默认情况下 Android 10 会对应用隐藏此信息。
如果应用需要访问照片的位置信息,完成以下步骤:
- 在应用的清单中请求
ACCESS_MEDIA_LOCATION
权限。 - 通过调用
setRequireOriginal()
,从MediaStore
对象获取照片的确切字节,并传入照片的 URI,如以下代码段所示:
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];
}
媒体文件的应用归因
当以 Android 10 或更高版本为目标平台的应用启用了分区存储时,系统会将每个媒体文件归因于一个应用,这决定了应用在未请求任何存储权限时可以访问的文件。每个文件只能归因于一个应用。因此,如果应用创建的媒体文件存储在照片、视频或音频文件媒体集合中,应用便可以访问该文件。
但是,如果用户卸载并重新安装应用,应用必须请求READ_EXTERNAL_STORAGE
才能访问应用最初创建的文件。此权限请求是必需的,因为系统认为文件归因于以前安装的应用版本,而不是新安装的版本。
添加项目
如需将媒体项添加到现有集合,调用类似于以下内容的代码:
// Add a specific media item.
ContentResolver resolver = getApplicationContext().getContentResolver();
// Find all audio files on the primary external storage device.
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。以下代码段基于前面的代码段进行构建:
// 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.
Uri audioCollection = MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY);
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);
try (ParcelFileDescriptor pfd =
resolver.openFileDescriptor(songContentUri, "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(songContentUri, songDetails, null, null);
提供文件位置提示
当应用在搭载 Android 10 的设备上存储媒体时,系统默认按照类型对媒体进行整理。例如,新图片文件默认存放在Environment.DIRECTORY_PICTURES
目录中,该目录与MediaStore.Images
集合相对应。
如果应用知道应该存储文件的特定位置,例如名为 Pictures/MyVacationPictures 的相册,可以设置MediaColumns.RELATIVE_PATH
,为系统提供关于新写入文件存储位置的提示。
更新项目
如需更新应用拥有的媒体文件,运行类似于以下内容的代码:
// 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);
移除项目
如需从媒体库中移除应用不再需要的某个项目,使用类似于以下代码段所示的逻辑:
// 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);
参考资料:核心领域 > 应用数据和文件 > 保存到共享存储空间 > 媒体
访问共享存储空间中的媒体文件 | Android Developers