本人负责公司手机文件管理器模块的开发,经常跟MediaScanner打交道,本篇跟大家一起讨论一下MediaScanner的整体结构、MediaScanner的扫描流程、如何使用MediaScanner进行自定义扫描。
MediaScanner是Android系统Media的基础,系统启动之初,就扫描出Media文件供后续使用,有新媒体加入或者删除掉媒体文件,也需要更新相应的媒体库。Android的Music、Gallery等播放或呈现媒体文件的程序也都基于稳定的MediaScanner扫描媒体文件的结果,否则,会发现程序操作的Media文件的URI根本不是实际对应的Media文件,或者甚至来电铃声和闹铃等都会有问题。
一、MediaScanner的整体结构
在整个Android源代码中,MediaScanner整体结构的代码分布在两个地方:packages\providers\MediaProvider 和 frameworks\base\media\下,前者作为一个单独的package,用来接收扫描广播和操作contentProvider、调用扫描服务接口完成扫描;后者中包含了MediaScanner的 jni 和 java 文件,扫描的大部分工作都在这里。
首先来看一下MediaProvider这个package:
MediaProvider包括五个类:
- com.android.providers.media.MediaProvider
- com.android.providers.media.MediaScannerCursor
- com.android.providers.media.MediaScannerReceiver
- com.android.providers.media.MediaScannerService
- com.android.providers.media.MediaThumbRequest
此类继承ContentProvider,实现一个内容提供者。主要用于创建媒体库的数据库表。有自己创建过ContentProvider的同学相信都比较清楚的。
特别说明一下在MediaProvider中有个广播接收者,代码如下:
1: private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() {
2: @Override
3: public void onReceive(Context context, Intent intent) {
4: if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) {
5: // Remove the external volume and then notify all cursors backed by
6: // data on that volume
7: detachVolume(Uri.parse("content://media/external"));
8: sFolderArtMap.clear();
9: MiniThumbFile.reset();
10: }
11: }
12: };
此接收者是用来接收Sdcard卸载的广播。当Sdcard从手机中分离出来的时候,Sdcard中的媒体文件相对应的数据库将无法操作。
1: private void detachVolume(Uri uri) {
2: //判断是否是同一个进程
3: if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
4: throw new SecurityException(
5: "Opening and closing databases not allowed.");
6: }
7: //此方法只是操作Sdcard的媒体数据库,不支持手机内存的媒体数据库
8: String volume = uri.getPathSegments().get(0);
9: if (INTERNAL_VOLUME.equals(volume)) {
10: throw new UnsupportedOperationException(
11: "Deleting the internal volume is not allowed");
12: } else if (!EXTERNAL_VOLUME.equals(volume)) {
13: throw new IllegalArgumentException(
14: "There is no volume named " + volume);
15: }
16:
17: synchronized (mDatabases) {
18: DatabaseHelper database = mDatabases.get(volume);
19: if (database == null) return;
20:
21: try {
22: // touch the database file to show it is most recently used
23: File file = new File(database.getReadableDatabase().getPath());
24: file.setLastModified(System.currentTimeMillis());
25: } catch (SQLException e) {
26: Log.e(TAG, "Can't touch database file", e);
27: }
28: //移除数据库
29: mDatabases.remove(volume);
30: database.close();
31: }
32:
33: getContext().getContentResolver().notifyChange(uri, null);
34: if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume);
35: }
注意移除数据库并非删除数据库文件(*.db),mDatabases是一个HashMap<String,DatabaseHelper>,移除的含义是暂时无法操作,也可以说说是查询返回的数据都是空的。
2.MediaScannerCursor
一个自定义游标,用来查询媒体文件的扫描状态。主要有一个volume字段,用来区分是内置媒体数据库还是Sdcard的媒体数据库。
3.MediaScannerReceiver
此类实现广播接收者。接收到广播的时候对手机的媒体文件进行扫描。
1: public class MediaScannerReceiver extends BroadcastReceiver
2: {
3: private final static String TAG = "MediaScannerReceiver";
4:
5: @Override
6: public void onReceive(Context context, Intent intent) {
7: String action = intent.getAction();
8: Uri uri = intent.getData();
9: String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
10: //系统启动完毕
11: if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
12: // scan internal storage
13: scan(context, MediaProvider.INTERNAL_VOLUME);
14: } else {
15: if (uri.getScheme().equals("file")) {
16: // handle intents related to external storage
17: String path = uri.getPath();
18: if (action.equals(Intent.ACTION_MEDIA_MOUNTED/*Sdcard挂载广播*/) &&
19: externalStoragePath.equals(path)) {
20: scan(context, MediaProvider.EXTERNAL_VOLUME);
21: } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE/*单个文件扫描广播*/) &&
22: path != null && path.startsWith(externalStoragePath + "/")) {
23: scanFile(context, path);
24: }
25: }
26: }
27: }
扫描分为两种三种情况:
a,启动完毕扫面手机内存中的媒体文件
b.sdcard挂载完毕扫描扩展卡的媒体文件
c,扫描单个文件
应用实例:我们可以发送不同的广播让系统去扫描媒体文件。当需要扫描单个文件的时候需要设置一些参数,如下:
1: /**
2: * 扫描文件
3: *
4: * @param filePath 文件路径
5: * @author http://t.sina.com.cn/halzhang
6: */
7: public void scanOneFile(final String filePath) {
8: Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
9: Uri uri = Uri.parse("file://" + filePath);
10: intent.setData(uri);
11: sendBroadcast(intent);
12: }
接着看一下scan和scenFile两个方法:
1: private void scan(Context context, String volume/*内置卡或者外置卡*/) {
2: Bundle args = new Bundle();
3: args.putString("volume", volume);
4: context.startService(
5: new Intent(context, MediaScannerService.class).putExtras(args));
6: }
7:
8: private void scanFile(Context context, String path/*文件路径*/) {
9: Bundle args = new Bundle();
10: args.putString("filepath", path);
11: context.startService(
12: new Intent(context, MediaScannerService.class).putExtras(args));
13: }
两个方法都是启动MediaScannerService去扫描媒体文件的。
MSS实现了Runnable,所以必然的需要实现run方法了,代码如下:
1: public void run()
2: {
3: // reduce priority below other background threads to avoid interfering
4: // with other services at boot time.
5: Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
6: Process.THREAD_PRIORITY_LESS_FAVORABLE);
7: Looper.prepare();
8:
9: mServiceLooper = Looper.myLooper();
10: mServiceHandler = new ServiceHandler();
11:
12: Looper.loop();
13: }
接着看一下ServiceHandler的实现代码:
1: private final class ServiceHandler extends Handler
2: {
3: @Override
4: public void handleMessage(Message msg)
5: {
6: Bundle arguments = (Bundle) msg.obj;
7: //获取文件路径
8: String filePath = arguments.getString("filepath");
9:
10: try {
11: if (filePath != null) {
12: //文件路径不为空,则调用扫面当个文件的方法
13: IBinder binder = arguments.getIBinder("listener");
14: IMediaScannerListener listener =
15: (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
16: Uri uri = scanFile(filePath, arguments.getString("mimetype"));//扫描单个文件
17: if (listener != null) {
18: //执行扫描完成方法
19: listener.scanCompleted(filePath, uri);
20: }
21: } else {
22: //如果文件路径为空,则获取扫面手机内存或者sdcard
23: String volume = arguments.getString("volume");
24: String[] directories = null;
25: //内置卡
26: if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
27: // scan internal media storage
28: directories = new String[] {
29: Environment.getRootDirectory() + "/media",
30: };
31: }//外置卡
32: else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
33: // scan external storage
34: directories = new String[] {
35: Environment.getExternalStorageDirectory().getPath(),
36: };
37: }
38:
39: if (directories != null) {
40: if (Config.LOGD) Log.d(TAG, "start scanning volume " + volume);
41: //扫描
42: scan(directories, volume);
43: if (Config.LOGD) Log.d(TAG, "done scanning volume " + volume);
44: }
45: }
46: } catch (Exception e) {
47: Log.e(TAG, "Exception in handleMessage", e);
48: }
49:
50: stopSelf(msg.arg1);
51: }
52: };
在ServiceHandler中主要根据相关参数来调用不同的扫描方法。
那是在哪里调用ServiceHandler发送消息的呢?请看如下代码:
1: @Override
2: public void onCreate() {
3: PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
4: mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
5: //启用新线程,这样就可以避免阻塞,执行run,初始化成员变量loop和handler
6: Thread thr = new Thread(null, this, "MediaScannerService");
7: thr.start();
8: }
9:
10: @Override
11: public int onStartCommand(Intent intent, int flags, int startId) {
12: while (mServiceHandler == null) {
13: synchronized (this) {
14: try {
15: wait(100);
16: } catch (InterruptedException e) {
17: }
18: }
19: }
20:
21: if (intent == null) {
22: Log.e(TAG, "Intent is null in onStartCommand: ", new NullPointerException());
23: return Service.START_NOT_STICKY;
24: }
25:
26: Message msg = mServiceHandler.obtainMessage();
27: msg.arg1 = startId;
28: msg.obj = intent.getExtras();
29: //ServiceHandler发送消息
30: mServiceHandler.sendMessage(msg);
31:
32: // Try again later if we are killed before we can finish scanning.
33: return Service.START_REDELIVER_INTENT;
34: }
35:
36: @Override
37: public void onDestroy() {
38: // Make sure thread has started before telling it to quit.
39: while (mServiceLooper == null) {
40: synchronized (this) {
41: try {
42: wait(100);
43: } catch (InterruptedException e) {
44: }
45: }
46: }
47: mServiceLooper.quit();
48: }
最后,稍微看一下MSS里面扫描方面。主要是调用MediaScanner对媒体文件进行扫描分析的。至于MediaScanner的实现以后在分析。
1: private void openDatabase(String volumeName) {
2: try {
3: ContentValues values = new ContentValues();
4: values.put("name", volumeName);
5: getContentResolver().insert(Uri.parse("content://media/"), values);
6: } catch (IllegalArgumentException ex) {
7: Log.w(TAG, "failed to open media database");
8: }
9: }
10:
11: private void closeDatabase(String volumeName) {
12: try {
13: getContentResolver().delete(
14: Uri.parse("content://media/" + volumeName), null, null);
15: } catch (Exception e) {
16: Log.w(TAG, "failed to close media database " + volumeName + " exception: " + e);
17: }
18: }
19: //创建扫描器
20: private MediaScanner createMediaScanner() {
21: MediaScanner scanner = new MediaScanner(this);
22: Locale locale = getResources().getConfiguration().locale;
23: if (locale != null) {
24: String language = locale.getLanguage();
25: String country = locale.getCountry();
26: String localeString = null;
27: if (language != null) {
28: if (country != null) {
29: scanner.setLocale(language + "_" + country);
30: } else {
31: scanner.setLocale(language);
32: }
33: }
34: }
35:
36: return scanner;
37: }
38: //扫描目录
39: private void scan(String[] directories, String volumeName) {
40: // don't sleep while scanning
41: mWakeLock.acquire();
42:
43: ContentValues values = new ContentValues();
44: values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
45: Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
46:
47: Uri uri = Uri.parse("file://" + directories[0]);
48: sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
49:
50: try {
51: if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
52: openDatabase(volumeName);
53: }
54:
55: MediaScanner scanner = createMediaScanner();
56: scanner.scanDirectories(directories, volumeName);
57: } catch (Exception e) {
58: Log.e(TAG, "exception in MediaScanner.scan()", e);
59: }
60:
61: getContentResolver().delete(scanUri, null, null);
62:
63: sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
64: mWakeLock.release();
65: }
66: //扫描文件
67: private Uri scanFile(String path, String mimeType) {
68: String volumeName = MediaProvider.INTERNAL_VOLUME;
69: String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
70:
71: if (path.startsWith(externalStoragePath)) {
72: volumeName = MediaProvider.EXTERNAL_VOLUME;
73: openDatabase(volumeName);
74: }
75: MediaScanner scanner = createMediaScanner();
76: //扫描单个文件
77: return scanner.scanSingleFile(path, volumeName, mimeType);
78: }
在MediaProvider中还有一个类:MediaThumbRequest,用来创建预览图的,比如视频的预览图,图片的预览图,音频的专辑图片…这些图片的信息也是保存在数据库的,有兴趣的同学可以自己打开数据库看看里面的表。如下图:
二、MediaScanner的扫描流程
这里绘制了一张MediaScanner从接收广播开始,进行扫描工作的整个流程图:
MediaScannerReceiver 会在任何的 ACTION_BOOT_COMPLETED, ACTION_MEDIA_MOUNTED 或 ACTION_MEDIA_SCANNER_SCAN_FILE 意图( intent )发出的时候启动。因为解析媒体文件 的元数据或许会需要很长时间 ,所以MediaScannerReceiver 会启动MediaScannerService 。
MediaScannerService 调用一个公用类 MediaScanner 去处理真正的工作。MediaScannerReceiver 维持两种扫描目录:一种是内部卷(internal volume )指向$(ANDROID_ROOT)/media. 另一种是外部卷(external volume )指向 $(EXTERNAL_STORAGE).
扫描和解析工作位于 JAVA 层和 C++ 层。 JAVA 层是启动器。 MediaScanner 扫描所有目录,如下步骤:
1.JAVA 层初始化
在这一步骤中,它会根据目录是在内部卷还是外部卷打开不同的数据库。
2.Java 层预扫描
首先清除文件和播放列表的缓存条目。然后根据 MediaProvider 返回的请求结果生成新文件和播放列表缓存条目。
3.C++ 层处理目录
列举出所有文件和特定的所有子目录(如果子目录包含一个 .nomedia 隐藏文件,则不会被列举出来。)。被列举的文件是根据文件扩展来判断文件是否被支持。如果支持这种文件扩展, C++ 层就会回调到 JAVA 层扫描文件。这种扩展就会被扫描到 MediaFile.java 中列出。下面是支持的文件扩展列表。
到此为止,MediaProvider算是讲完了,下一篇跟大家一起学习一下frameworks\base\media\下的MediaScanner相关文件。