Android系统中有一个媒体库,这个大家应该有所了解,平时在开发过程中如果不涉及媒体文件(图片、音频、视频)这块则很少接触到。有些时候我们在本地添加一张图片,但是在相册中却无法搜索到,这里主要原因就是没有通知系统媒体库刷新导致的。本篇我们就探讨下Android上媒体库的这些事。
为什么通知媒体库后,媒体库里就能找到了呢?兴许你还会遇到一种情况,就是明明相册里可以发现这张图片,可是到图片的具体路径下却找不到这张图片。到此应该会猜测到是不是媒体库和本地相册都持有一份媒体文件信息呢?基本上猜到了八九不离十了,其实媒体库就是一个数据库,专门管理媒体文件的相关信息,例如图片信息,缩略图等。
多媒体文件管理
Android多媒体文件扫描管理简单来说,有以下四部分内容:
- 通知:MediaScannerReceiver
- 扫描:MediaScannerService
- 存储:MediaProvider
- 查询:MediaStore
MediaScannerReceiver
MediaScannerReceiver主要用于接受扫描通知,然后启动MediaScannerService进行扫描操作。
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final Uri uri = intent.getData();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
// Scan internal only.
scan(context, MediaProvider.INTERNAL_VOLUME);
} else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
scanTranslatable(context);
} else {
if (uri.getScheme().equals("file")) {
// handle intents related to external storage
String path = uri.getPath();
String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
try {
path = new File(path).getCanonicalPath();
} catch (IOException e) {
Log.e(TAG, "couldn't canonicalize " + path);
return;
}
if (path.startsWith(legacyPath)) {
path = externalStoragePath + path.substring(legacyPath.length());
}
Log.d(TAG, "action: " + action + " path: " + path);
if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
// scan whenever any volume is mounted
scan(context, MediaProvider.EXTERNAL_VOLUME);
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {
scanFile(context, path);
}
}
}
。。。
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
private void scanFile(Context context, String path) {
Bundle args = new Bundle();
args.putString("filepath", path);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
private void scanTranslatable(Context context) {
final Bundle args = new Bundle();
args.putBoolean(MediaStore.RETRANSLATE_CALL, true);
context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
}
从onReceive方法中可以看出,MediaScannerReceiver执行scan的时机有四种:
- 启动完毕,扫描内部存储和外部存储
- 本地语言切换
- sdcard挂载完毕,扫描外部存储
- 扫描单个文件
MediaScannerService
MediaScannerService主要负责媒体文件的扫描过程,因此是耗时的,其内部的scan()方法是扫描核心。
private void scan(String[] directories, String volumeName) {
...
try {
...
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
try {
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
openDatabase(volumeName);
}
try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
scanner.scanDirectories(directories);
}
} catch (Exception e) {
Log.e(TAG, "exception in MediaScanner.scan()", e);
}
...
} finally {
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
}
}
核心内容也很简单,最终调用MediaScanner的scanDirectories()执行文件扫描。其中MediaScanner是一个专门用于媒体文件扫描的类,其scanDirectories()内部核心就是采用ContentProviderClient来对MediaProvider进行数据的更新或删除操作。
MediaProvider
我们都知道Android有四大组件,Activity、Service、BroadcastReceiver、ContentProvider。MediaProvider就是Android系统中的一个数据库,类似的还有TelephonyProvider、CalendarProvider、ContactsProvider,这些数据库的源码都在/packages/providers/目录下。
其中MediaProvider又称多媒体数据库,保存了手机上存储的所有媒体文件的信息。这个数据库存放在/data/data/com.android.providers.media/databases当中,里面有两个数据库:internal.db和external.db,internal.db存放的是系统分区的文件信息,开发者是没法通过接口获得其中的信息的,而external.db存放的则是我们用户能看到的存储区的文件信息,即包含了手机内置存储,还包含了SD卡。
MediaStore
MediaStore主要用于提供内部或外部存储中所有可用的媒体文件的各种信息,我们后边都需要借助此类来进行媒体库的操作(添加,删除等)。
媒体库信息查询
上边我们知道MediaStore是专门用于存放多媒体信息的,通过ContentResolver即可对数据库进行操作。
MediaStore中的资源有四类:
- MediaStore.Files
- MediaStore.Audio
- MediaStore.Images
- MediaStore.Video
这些内部类中都又包含了Media,Thumbnails和相应的MediaColumns,分别提供了媒体信息,缩略信息和操作字段。
查询
媒体库是通过ContentResolver来进行查询的,其核心扫描方法如下:
public final @Nullable Cursor query(@RequiresPermission.Read
@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder
@Nullable CancellationSignal cancellationSignal)
开发过程中可能根据不同情况调用此方法的重载方法,下边我们就解析一下这个方法的相关入参。
Uri uri
Uri uri = MediaStore.Video.Media.INTERNAL_CONTENT_URI;
Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
uri表示数据资源路径,总共有两种,分别对应内部存储和外部存储。
需要注意的是,MediaStore.Files没有EXTERNAL_CONTENT_URI,所以只能用getContentUri()自行获取
MediaStore.Images.Files.getContentUri("external")
以MediaStore.Images.Media为例,其URI有三种写法:
Uri uri1 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri uri2 = MediaStore.Images.Media.getContentUri("external");
Uri uri3 = Uri.parse("content://media/external/images/media");
String[] projection
String[] mediaColumns = {
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DATA,
MediaStore.Video.Media.SIZE,
MediaStore.Video.Media.DATE_MODIFIED,
MediaStore.Video.Media.DURATION};
用于指定查询后返回给用户的媒体信息,当然你可以理解为对应媒体数据库中相关字段。
String selection 和 String[] selectionArgs
String selection = MediaStore.Video.Media.MIME_TYPE + "=? or "
+ MediaStore.Video.Media.MIME_TYPE + "=? or "
+ MediaStore.Video.Media.MIME_TYPE + "=? or "
+ MediaStore.Video.Media.MIME_TYPE + "=? or "
+ MediaStore.Video.Media.MIME_TYPE + "=?"
String[] selectionArgs = new String[]{"video/mp4", "video/avi", "video/quicktime", "video/webm", "video/x-ms-wmv"}
定制化查询条件,这两个必须结合使用,前者表示条件语句,后者表示对应的条件参数。
String sortOrder
查询的排序方式
CancellationSignal cancellationSignal
取消正在进行的操作的信号,如果没有则为空。如果操作被取消,那么在执行查询时将抛出
OperationCanceledException
异常
举栗
1.利用MediaStore.Files,查询所有类型的文件:
/**
* 获取所有文件
**/
public static List<FileEntity> getFilesByType(Context context) {
List<FileEntity> files = new ArrayList<>();
// 扫描files文件库
Cursor c = null;
try {
mContentResolver = context.getContentResolver();
c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, null, null, null);
int columnIndexOrThrow_ID = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID);
int columnIndexOrThrow_MIME_TYPE = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE);
int columnIndexOrThrow_DATA = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA);
int columnIndexOrThrow_SIZE = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE);
// 更改时间
int columnIndexOrThrow_DATE_MODIFIED = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED);
int tempId = 0;
while (c.moveToNext()) {
String path = c.getString(columnIndexOrThrow_DATA);
String minType = c.getString(columnIndexOrThrow_MIME_TYPE);
LogUtil.d("FileManager", "path:" + path);
int position_do = path.lastIndexOf(".");
if (position_do == -1) {
continue;
}
int position_x = path.lastIndexOf(File.separator);
if (position_x == -1) {
continue;
}
String displayName = path.substring(position_x + 1, path.length());
long size = c.getLong(columnIndexOrThrow_SIZE);
long modified_date = c.getLong(columnIndexOrThrow_DATE_MODIFIED);
File file = new File(path);
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(file.lastModified()));
FileEntity info = new FileEntity();
info.setName(displayName);
info.setPath(path);
info.setSize(ShowLongFileSzie(size));
info.setId((tempId++) + "");
info.setTime(time);
files.add(info);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (c != null) {
c.close();
}
}
return files;
}
2.指定获取文件字段
String[] columns = new String[]{MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns
.SIZE, MediaStore.Files.FileColumns.DATE_MODIFIED, MediaStore.Files.FileColumns.DATA};
c = mContentResolver.query(MediaStore.Files.getContentUri("external"), columns, null, null, null);
3.根据文件夹的名称查询
//查找文件夹ScreenRecord下的文件
c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, MediaStore.Video.Media.BUCKET_DISPLAY_NAME+"=?", "ScreenRecord", null);
4.查询指定类型的文件
String select = "(" + MediaStore.Files.FileColumns.DATA + " LIKE '%.doc'" + " or " + MediaStore.Files.FileColumns.DATA + " LIKE '%.docx'" + ")";
c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, select , null, null);
5.指定排序类型,如根据id倒序查询
c = mContentResolver.query(MediaStore.Files.getContentUri("external"), null, null, null, MediaStore.Files.FileColumns._ID+"DESC");
刷新媒体库
媒体库刷新方法
刷新媒体库常用的有如下几种方式:
- 通过ContentProvider操作媒体数据库。
- 发送广播更新MediaStore。
- 通过操作MediaScannerConnection类。
通过ContentProvider操作媒体数据库
ContentValues values = new ContentValues(4);
values.put(MediaStore.Video.Media.TITLE, "");
values.put(MediaStore.Video.Media.MIME_TYPE, minetype);
values.put(MediaStore.Video.Media.DATA, path);
values.put(MediaStore.Video.Media.DURATION, duration_int);
context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
和上边所讲的媒体库信息查询一样,直接对数据库操作。需要注意的是这种方式不能和其他刷新媒体库方式公用,有可能同时存入两张一模一样的文件。
发送广播更新MediaStore进行刷新
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(new File(filePath)));
context.sendBroadcast(intent);
在Android4.4之前,是可以通过
ACTION_MEDIA_MOUNTED
广播,来通知系统刷新MediaStore的,4.4后系统封闭这种方式,取而代之的是
ACTION_MEDIA_SCANNER_SCAN_FILE
,建议单个文件扫描插入。
通过操作MediaScannerConnection类进行刷新
Android4.0系统API中多了一个更新媒体库的方法——MediaScannerConnection,这也是我们比较推荐的。MediaScannerConnection有一个静态方法scanFile(),可直接操作此方法完成媒体库刷新操作。并且可对其刷新完成后回调更新。
public static void insert(Context context, String[] paths, String[] types) {
MediaScannerConnection.scanFile(
context,
paths,
types,
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
LogUtils.i(TAG, "insert onScanCompleted path " + path + " uri " + uri);
}
});
}
当然你也可以实现MediaScannerConnection.MediaScannerConnectionClient来进行扫描,在构造方法中执行connect(),在onScanCompleted()方法中执行disconnect()关闭链接,在onMediaScannerConnected()中执行scanFile()进行扫描。
媒体文件添加后刷新
通常我们在图片或者音视频添加后,在需要更新的地方执行刷新媒体库操作才能在媒体库中看到。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
new MediaScanner(context, file);
} else {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
context.sendBroadcast(intent);
}
媒体文件删除后刷新
有时候我们需要删除本地图片同时又希望刷新一下媒体库,让媒体库中去除此图片,以上边刷新媒体库的方式大多都是insert模式,那我们只能直接操作数据库了。需要注意file.delete()后不可立即将file置为null;
if (!file.exists()) {
String filePath = file.getAbsolutePath();
if (filePath.endsWith(".mp4")) {
context.getContentResolver().delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",
null);
} else if (filePath.endsWith(".jpg") || filePath.endsWith(".png") || filePath.endsWith(".bmp")) {
context.getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",
null);
}
return;
}
一个项目中使用的例子:
public class MediaScanner implements MediaScannerConnection.MediaScannerConnectionClient {
private static final String TAG = MediaScanner.class.getSimpleName();
/**
* 刷新媒体库
*
* @param context
* @param file
*/
public static void refresh(Context context, File file) {
if (context == null || file == null) {
return;
}
//如果图片不存在,删除媒体库中记录
if (!file.exists()) {
String filePath = file.getAbsolutePath();
if (filePath.endsWith(".mp4")) {
context.getContentResolver().delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",
null);
} else if (filePath.endsWith(".jpg") || filePath.endsWith(".png") || filePath.endsWith(".bmp")) {
context.getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
MediaStore.Audio.Media.DATA + "= \"" + filePath + "\"",
null);
}
return;
}
//4.0以上的系统使用MediaScanner更新
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
new MediaScanner(context, file);
} else {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
context.sendBroadcast(intent);
}
}
private File mFile;
private MediaScannerConnection mMsc;
private MediaScanner(Context context, File file) {
this.mFile = file;
this.mMsc = new MediaScannerConnection(context, this);
mMsc.connect();
}
@Override
public void onMediaScannerConnected() {
mMsc.scanFile(mFile.getAbsolutePath(), null);
}
@Override
public void onScanCompleted(String path, Uri uri) {
mMsc.disconnect();
}
}
刷新过滤
有时候,我们有一些目录下的媒体文件,并不想让MediaStore扫描到,例如在SDCard上缓存的图片、图标等,这些我们都不想出现在系统相册内。怎么办呢?
很简单,文件夹中新建一个.nomedia的空文件,会屏蔽掉系统默认的媒体库扫描。带有该文件的文件夹只能通过文件遍历的方式进行扫描。
总结
以上是有关媒体库开发过程中的知识点,系统媒体库是维护了一个有关媒体文件的数据库,在开发过程中只有在需要相册内的图片或者音视频更新时才需要刷新媒体库,这点个人建议适量使用不可滥用,否则有可能会造成媒体库文件泛滥,或者媒体库中有相应文件的预览,本地却不存在此文件的bug。
参考
- https://developer.android.com/guide/topics/data/data-storage.html
- https://blog.csdn.net/yann02/article/details/92844364
- https://juejin.im/post/5ae0541df265da0b9d77e45a
- https://zhuanlan.zhihu.com/p/46533159
- https://www.bbsmax.com/A/amd0omej5g/