在应用中,将文件保存到本地,是很常见的 I/O 操作。而有的图片或者视频文件不单只是需要保存到本地,涉及到的不仅仅是一个 I/O 操作了,还需要考虑如何更新 MediaStore,这样才可以在系统相册中,看到它。
MediaStore的本质
MediaStore,本质上是 Android 维护的一个文件系统的数据库,它记录了当前磁盘上所有的文件索引,我们可以通过它,快速的查找当前系统的文件。
保存文件到MediaStore
MediaStore 刷新的时机是不一定的,也就是说,保存的一张图片文件,MediaStore 并不会立即刷新文件系统,将此文件索引记录下来。而系统本身是存在一些自动刷新 MediaStore 的时机,例如:重启手机。表现就是,当你保存了一张图片到本地文件夹中之后,通过文件管理器类的 App,可以在目录下找到这涨照片,但是在系统相册中,是无法立即看到它的,同时你想用诸如 微信、QQ 去分享这张图片的时候,也是找不到的。所以在我们保存图片文件之后,去触发系统刷新 MediaStore 就尤为重要了。
在Android10之前,在保存文件后,刷新系统 MediaStore 的方式有:
- 通过操作 MediaStore 类。
- 发送广播更新 MediaStore。
- 通过操作 MediaScannerConnection 类。
而在Android10提出了分区存储,只能通过操作 MediaStore 类来刷新系统MediaStore。
一、 通过操作MediaStore 类
保存图片
// 创建图片的索引
ContentValues values = new ContentValues();
String fileName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/ScreenShot");
}
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
ContentResolver contentResolver = mContext.getContentResolver();
// 将索引信息添加到数据表中,得到该条索引信息的uri
Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try {
OutputStream imageOutStream = contentResolver.openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
imageOutStream.close();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
contentResolver.update(uri, values, null, null);
}
return bitmap;
} catch (IOException e) {
e.printStackTrace();
}
如果你的应用程序需要执行一些非常耗时的操作,比如写入媒体文件,那么在文件被处理时对其进行独占访问是非常有用的。在运行Android 10或更高版本的设备上,你可以通过将IS_PENDING标志的值设置为1来获得这种独占访问。只有你的应用程序可以查看该文件,直到将IS_PENDING的值更改回0。
保存视频
private Uri insertVideoToMediaStore(String filePath, String fileName) {
ContentResolver resolver = getApplicationContext().getContentResolver();
// 拿到MediaStore.Video表的uri
Uri tableUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
// 创建视频索引
ContentValues values = new ContentValues();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 相对路径
values.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/ScreenRecord");
values.put(MediaStore.Video.Media.IS_PENDING, 1);
} else {
// 绝对路径
values.put(MediaStore.Video.Media.DATA, filePath);
MediaPlayer mp = MediaPlayer.create(this, Uri.parse(filePath));
int duration = mp.getDuration();
mp.release();
values.put(MediaStore.Video.Media.DURATION, duration);
}
values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
// 将索引信息插入到数据表中,获得视频的uri
Uri uri = resolver.insert(tableUri, values);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
copyVideoToMediaStore(uri, resolver, filePath);
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(uri, values, null, null);
}
return uri;
}
private void copyVideoToMediaStore(Uri uri, ContentResolver resolver, String filePath) {
try {
// android 10 及以上MediaStore无法访问getExternalFilesDir路径中存储的内容
// 需要将视频文件复制到MediaStore中
ParcelFileDescriptor fileDescriptor = resolver.openFileDescriptor(uri, "rw");
FileOutputStream fileOutputStream = new FileOutputStream(fileDescriptor.getFileDescriptor());
// Get the already saved video as fileinputstream from here
FileInputStream fileInputStream = new FileInputStream(filePath);
byte[] buf = new byte[8192];
int len;
while ((len = fileInputStream.read(buf)) > 0) {
fileOutputStream.write(buf, 0, len);
}
fileOutputStream.close();
fileInputStream.close();
fileDescriptor.close();
} catch (Exception e) {
mHandler.sendEmptyMessage(MSG_HIDE_PROGRESS_BAR);
e.printStackTrace();
}
}
二、发送广播
通过广播刷新 MediaStore 的方式非常的简单,只需要指定文件路径和 Action 就好了。
- 保存图片后通过广播刷新
Uri contentUri = Uri.fromFile(File(filePath))
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,contentUri)
mContext.sendBroadcast(mediaScanIntent)
正常情况下,它是没有问题的,不过假如你发现它不生效,就需要检查一下你文件的路径是否传递正确。
通过查看 MediaScannerReceiver 的源码,可以发现 onReceive() 方法中,针对 ACTION_MEDIA_SCANNER_SCAN_FILE 还有一个限制条件,那就是传递进去的文件绝对路径,必须是以 Environment.getExternalStorageDirectory() 方法的返回值开头。
三、 操作 MediaScannerConnection 类
主要是利用 MediaScannerConnection 类的 scanFile() 方法进行触发扫描。通过 scanFile() 方法,我们只需要指定一个待刷新的文件路径和对应的 MimeType 即可,它支持传递多个路径,也可就是支持批量扫描。注意这里的 MimeType 是一定要填写的,并且不能写通配符 / 或 null,否则会导致刷新失败,通常我们保存的是一个图片的话,只需要传递 image/jpeg 即可。最后一个参数, onScanCompletedListener 中可以监听我们扫描的结果。