Android多媒体扫描机制分析(一)

想发起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的时候进行讨论.

你可能感兴趣的:(android)