想发起media扫描一般我们有两种方式:
1.发送广播
我们先不讲原理,先来看看Android源码中是如何发起一次扫描的
private void updateMediaStore(String path) {
Intent intent_mount = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent_mount.setData(Uri.fromFile(new File(path)));
context.sendBroadcast(intent_mount);
}
上述代码是DownloaderImpl.java中的一个方法,作用就是发起一次扫描以更新媒体信息,ACTION_MEDIA_SCANNER_SCAN_FILE表示发起一次文件的扫描。下一行在Intent中放入待扫描文件的uri。
可以看到通过广播方式发起一次扫描的步骤很简单,可以总结为:1.新建Intent,指定action;2.向intent中传入待扫描文件的uri;3.发送广播
了解了发起扫描的步骤,自然我们会疑惑:那发送什么ACTION的广播才会发起扫描呢?如果我要扫描的不是一个文件而是一个文件夹及其内部的文件呢?难道除了手动发起扫描其他时刻就不会进行扫描了吗?这些问题都可以在媒体扫描模块的广播接收器MediaScannerRecevier中得到答案,下面我们一起来分析一下MediaScannerReceiver的代码。
在MediaScannerRecevier(下面简称MSR)中最重要的自然就是onReceiver方法,通过阅读源码发现onReceive方法首先取出存在intent中的action和待扫描文件的URI,并且根据action不同采取不同的操作,具体如下:
1.ACTION_BOOT_COMPLETED : 在接收到开机完成广播之后对内部卷和外部卷进行扫描,通过调用
scan(context,MediaProvider.INTERNAL_VOLUME);
scan(context, MediaProvider.EXTERNAL_VOLUME);
这两个方法,开启MediaScannerService服务,以下简称MSS,并传入需要扫描的volume进行扫描。具体的扫描过程很复杂,我将会在下一篇blog中进行讲解
2.ACTION_MEDIA_MOUNTED: 在接收到外部存储设备挂载的广播也会通过启动MSS发起扫描,内部卷外部卷均会被扫描
3.ACTION_MEDIA_SCANNER_SCAN_FILE : 发起单个文件的扫描请求,也会通过启动MSS来进行扫描,一般是其他程序发出该请求,比如新下载了一个文件,这时候DownLoadManager应该向MediaScannerService发起扫描该文件的请求
4.ACTION_MEDIA_SCANNER_SCAN_FOLDER: 发起扫描文件夹请求,也是通过启动MSS来进行扫描,一般是其他程序发出该请求,比如通过MTP传入了一个文件夹,这时候MTPService应该向MediaScannerService发起扫描该文件的请求
可见MSR的功能就是接收各种扫描的请求,从而通过启动MSS来进行扫描,真正进行扫描的工作主要是由MSS及其相关模块来进行
@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 both internal and external storage
scan(context, MediaProvider.INTERNAL_VOLUME);
scan(context, MediaProvider.EXTERNAL_VOLUME);
} 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
String internalStoragePath = MediaProvider.INTERNAL_STORAGE_DIRECTORY.getPath();
if (internalStoragePath.equals(path)) {
scan(context, MediaProvider.INTERNAL_VOLUME);
}
// MIUI MOD
// else if (externalStoragePath.equals(path)) {
else if (isExternalStoragePath(context, path)) {
// END
if (android.os.Environment.isExternalStorageRemovable()) {
// XXX
// 解决在sdcard unmount情况下扫描, 导致media记录混乱的bug, 重现步骤:
// (1) sdcard(mount), MediaProvider的EXTERNAL_VOLUME指向external-30363399.db
// (30363399任意八位十六进制串)
// (2) sdcard(unmount), 执行MediaProvider.onCreate(),
// MediaProvider的EXTERNAL_VOLUME指向external-ffffffff.db
// (3) sdcard(mount), 如果没有修改MediaProvider的EXTERNAL_VOLUME, 其指向仍然是external-ffffffff.db,
// 导致mount前后media记录不一致
// 解决方案: 在mount时, 使MeidaProvider的EXTERNAL_VOLUME重新指向external-30363399.db
ContentValues values = new ContentValues();
values.put("name", MediaProvider.EXTERNAL_VOLUME);
context.getContentResolver().insert(Uri.parse(MediaProvider.CONTENT_AUTHORITY), values);
}
// scan whenever any volume is mounted
scan(context, MediaProvider.EXTERNAL_VOLUME);
}
// MIUI MOD: START
// } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
// path != null && path.startsWith(externalStoragePath + "/")) {
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
startsWithExternalStoragePath(context, path)) {
// END
scanFile(context, path);
}
// MIUI ADD: START
else if (MiuiIntent.ACTION_MEDIA_SCANNER_SCAN_FOLDER.equals(action) &&
startsWithExternalStoragePath(context, path)) {
scanFolder(context, path);
}
// END
}
}
2.通过MediaScannerConnection
同样我们也先不要陷入原理分析,先来看看怎么通过使用MediaScannerConnection来发起扫描,通过MediaScannerConnection来发起扫描相较于发送广播的方式稍显复杂,但是其实逻辑上却更加清晰
private static final class ScannerClient implements MediaScannerConnectionClient {
String mPath = null;
MediaScannerConnection mScannerConnection;
SQLiteDatabase mDb;
public ScannerClient(Context context, SQLiteDatabase db, String path) {
mDb = db;
mPath = path;
mScannerConnection = new MediaScannerConnection(context, this);
mScannerConnection.connect();
}
@Override
public void onMediaScannerConnected() {
Cursor c = mDb.query("files", openFileColumns,
"_data >= ? AND _data < ?",
new String[] { mPath + "/", mPath + "0"},
null, null, null);
try {
while (c.moveToNext()) {
String d = c.getString(0);
File f = new File(d);
if (f.isFile()) {
mScannerConnection.scanFile(d, null);
}
}
mScannerConnection.disconnect();
} finally {
IoUtils.closeQuietly(c);
}
}
@Override
public void onScanCompleted(String path, Uri uri) {
}
}
上面这一段代码是MediaProvider中在删除了.nomedia(该文件会屏蔽其子目录的媒体扫描)之后,重新发起对其子目录进行扫描时使用的一个内部类。
这段代码乍一看感觉摸不到头脑,到处都是回调,感觉逻辑上很混乱,下面我们就来分析一下这其中的逻辑,我们先看一下MediaScannerConnection这个类,这个类实际上就是实现了ServiceConnection,ServiceConnection大家都很熟悉了,在bindService的时候我们需要实现这个接口,也就是实现其中的onServiceConnected和onServiceDisconnected方法。并且作为bingservice的参数传入。
但是我们看上面的使用的代码,并没有看到bindService,这是因为MediaScannerConnection是ServiceConnection的高级实现,在MediaScannerConnection中,connect方法封装了bindSevice相关操作。
如果仔细观察会发现,MediaScannerConnection的构造函数中需要传入一个MediaScannerConnectionClient对象,其实主要的功能也是由这个对象来实现的。
所以总结一下:一般的调用顺序为 MediaScannerConnection::connect –> MediaScannerConnection::onServiceConnected –> MediaScannerConnectionClient::onMediaScannerConnected(这个方法一般要自己写,因为这个方法做了主要的操作,但最终都是会去扫描) –> MediaScannerConnection::scanFile –> IMediaScannerService::requestScanFile(在MSS匿名内部中实现) –> 启动MSS发起扫描
大致的调用顺序就是这样,所以我们一般会重写MediaScannerConnectionClient类,主要是为了重写其中的onMediaScannerConnected方法!
常见的启动MSS的方式就是以上这两种,这两种方式启动的结果会稍有不同,如果是扫描文件夹的话最好是使用广播的方式,如果是像上面那样连续扫描多个文件可以使用MediaScannerConnection,以便面发送过多广播。具体的不同我们将会在讨论MSS的时候进行讨论.