MediaScanner学习笔记

平台:android2.3
场景:基于android2.3的HIFI项目,对于歌曲的专辑信息等无法识别,需要研究一下MediaScanner并添加修改相关内容。
时间:2012.7.6

下文大部分内容是摘抄老邓的第一本android书籍而来。

1.在MediaScannerReceiver的onReceive()函数中,接收三种请求,其对应的实际操作调用在MediaScannerService的工作线程的handleMessage():
(1).BOOT_COMPLETED,开始扫描internal区域,实际上就是扫描system/media目录。

                    if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
                        // scan internal media storage
                        directories = new String[] {
                                Environment.getRootDirectory() + "/media",
                        };

(2).MEDIA_MOUNTED,开始扫描external区域,android手机上即扫描mnt/sdcard目录,而在RK的平板方案上扫描两个路径:

                    else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                        // scan external storage
                        directories = new String[] {
                                Environment.getFlashStorageDirectory().getPath(),
                                Environment.getHostStorageDirectory().getPath(),
                                };
                    }

(3).ACTION_MEDIA_SCANNER_SCAN_FILE,默认为扫描SD卡上的一个文件,而在RK平板方案上做了添加,支持了小机器本身的flash部分。

2.在MediaScannerService中的scan()函数中,即为整个扫描的过程缩影。其将先发送广播ACTION_MEDIA_SCANNER_STARTED,然后调用createMediaScanner()创建一个MediaScanner对象,调用其scanDirectories()方法进行扫描和写入数据库的操作。最后再调用一次ACTION_MEDIA_SCANNER_FINISHED广播的发送,告知扫描结束。
值得注意的是,在createMediaScanner()中:

        MediaScanner scanner = new MediaScanner(this);
        Locale locale = getResources().getConfiguration().locale;
        if (locale != null) {
            String language = locale.getLanguage();
            String country = locale.getCountry();
            String localeString = null;
            if (language != null) {
                if (country != null) {
                    scanner.setLocale(language + "_" + country);
                } else {
                    scanner.setLocale(language);
                }
            }   
        }

将根据当前的系统设置的国家和语言来进行scanner的设置,其将涉及到数据库写入的编码格式!

3.MediaScan中的scanDirectories()函数:
(1).initialize()—获取各个数据类型的表地址(Uri),若是external上的媒体文件,还将支持播放列表,流派等内容。
(mAudioUri = Audio.Media.getContentUri(volumeName)是否每一个volumeName对应一个表?
目前的volumeName分为两种情况:internal和external,其确实是一个volumeName对应一个db!在平板方案上,因为flash部分对应的路径基本上都是mnt/sdcard的,因此internal.db对应就是system/media所扫描到的媒体文件信息,external.db对应着flash和SD卡上媒体信息)

(2).prescan()—-在扫描前,将此时数据库中的媒体文件路径和所属表的Uri信息去除并保存到mFileCache变量中。(为删除文件,更新数据库做准备)

(3).processDirectory()—在扫描过程中,若mFileCache中的文件依然存在,则将其中的mSeenInFileSystem变量设置为true,否则设为false。此函数即为扫描的关键函数,为JNI函数。

(4).postscan()—其将比较mFileCache中的各个文件的mSeenInFileSystem变量,进而删除不存在的文件在数据库中的相关信息。

4.在MediaScanner的构造函数中,将调用native_setup()。其中将调用createMediaScanner()创建JNI层的MediaScanner对象。将根据属性来判断使用哪一个MediaScanner库。
2.3之后,将使用google提供的StagefrightMediaScanner。而之前使用的是OpenCore。

5.在native的MediaScanner.cpp子类中的processDirectory()函数中,将使用native的Client对象(其通过内部的mClient保存java层的MyMSC对象)的setLocale()方法(此方法设置的参数,是从java层调用下来的),此处亦与保存媒体信息到数据库的编码格式有关。
然后将调用
(native)doProcessDirectory()(在这个扫描过程中,将调用递归调用来将传入的path单个文件路径化!同时将判断单个文件的path对应的文件是否是被支持的扫描类型,是否存在:

else if (fileMatchesExtension(path, extensions)) {
                struct stat statbuf;
                stat(path, &statbuf);
                if (statbuf.st_size > 0) {
                    client.scanFile(path, statbuf.st_mtime, statbuf.st_size);
                }
                if (exceptionCheck && exceptionCheck(exceptionEnv)) goto failure;
            }

当都满足时,才将调用client.scanFile继续扫描!

(native)client.scanFile()(此处的client对象为JNI中继承native的MediaScannerClient的MyMediaScannerClient的对象)
—->
(java)mClient.scanFile()(mClient为java层MediaScan中的内部类MyMediaScannerClient的对象,此处扫描的path为单个文件的路径)

6.在MyMediaScannerClient对象的scanFile()函数中:
(1).beginFile()—将保存在mFileCache中的对应文件信息的mSeenInFileSystem设为true。如果这个文件之前没有在mFileCache中保存,则会创建一个新项添加到mFileCache中。同时,它还会根据传入的lastModified值做一些处理,以判断此文件是否在前后两次扫描的这个时间段内被修改,若修改,则需要重新扫描。
—其控制着是否需要进行文件扫描的判断条件。

(2).

                    if (!MediaFile.isImageFileType(mFileType)) {
                        processFile(path, mimeType, this);
                    }

如果不是图片,则调用processFile()进行扫描,图片不需要扫描即可处理。

(3).endFile()—扫描完后,需要把新的信息更新到数据库。

7.在native的processFile()函数中,依然还有对用的调用到client.beginFile()和client.endFile()。
此处将调用基类MediaScannerClient.cpp中的方法。
beginFile()—构造了两个字符串数组提供给后面的字符编码相关使用:
mNames = new StringArray;
mValues = new StringArray;
endFile()—根据client设置的区域信息来对mValues中的字符做语言转换。转换后每一个value都会调用handleStringTag()做后续处理。
在processFile完成扫描后,将调用client.addStringTag()进行信息的添加,其实现在MediaScannerClient.cpp中。其中:

       if (nonAscii) {
            // save the strings for later so they can be used for native encoding detection
            mNames->push_back(name);
            mValues->push_back(value);
            return true;
        }

若name和value的编码不是ASCII,则保存到mNames和mValues中,等到endFile函数的时候集中做字符集的转换。

8.native的handleStringTag将通过jni调用到MediaScan.java中的handleStringTag()函数,将信息保存到MyMSC对应的成员变量中。

9.系统封装了一个MediaScannerConnection类提供使用。使得可以直接调用scanFile进行指定的扫描。

10.关于添加专辑封面。
针对单个文件的扫描完成后,系统将在MediaScanner.java中调用mMediaProvider.insert()进行数据库的插入工作。此时将引发对专辑封面信息的解析和写入操作:
(1).MediaProvider::insertInternal()====>
(2).MediaProvider::getKeyIdForName()====>
(3).MediaProvider::makeThumbAsync()—注释为We just inserted a new album. Now create an album art thumbnail for it。====>
(4).MediaProvider::makeThumbInternal()
—关键函数!将调用getCompressedAlbumArt()获取到byte数组,然后对数据进行压缩操作,再进行写入到对应的flash位置上。
====>
(5).MediaProvider::getCompressedAlbumArt()
—注释为Extract compressed image data from the audio file itself or, if that fails,look for a file “AlbumArt.jpg” in the containing directory.将调用MediaScanner对象的extractAlbumArt()函数。====>
(6).android_media_MediaScanner::extractAlbumArt()—调用native层MediaScanner子类的extractAlbumArt()方法,返回一个char数组。====>
(7).StagefrightMediaScanner::extractAlbumArt()—sp mem = mRetriever->extractAlbumArt()通过mRetriever调用各个类型对应的方法去获取封面的byte数据。

注意,在native调用的时候,保存byte数据,将返回的byte数组的头4个字节用于保存了byte数组的大小。

11.关于拔出SD卡的过程?媒体数据库如何变化?
在RK-Android2.3平台上,当拔出SD卡后,会正常更新掉SD卡中保存在媒体数据库中的记录。
但在MediaProvider中mUnmountReceiver的onReceiver()函数中并未见到什么特殊的处理。那SD卡对应的记录是如何被清除掉的?
—在MountService.java中,当SD卡拔出时,从commandlistener返回的code为VoldResponseCode.VolumeBadRemoval,RK在其中做了一定特殊的处理:

                if(!cooked[2].contains("flash")) {

                    in = new Intent(Intent.ACTION_MEDIA_MOUNTED,
                                 Uri.parse("file://" + Environment.getFlashStorageDirectory().toString()));
                    in.putExtra("read-only", false);
                    Slog.d(TAG,"Bad-remove,Sending internal storage mounted event for mediascanner");
                    mContext.sendBroadcast(in);
                }

若是真实的SD卡拔出了,发出一个 ACTION_MEDIA_MOUNTED的广播,直接用ACTION_MEDIA_MOUNTED广播去驱动MediaScanner进行一次扫描。
——这也就是为什么SD卡的对应记录被清除掉了。那为什么可以这样做?或者说这样做影响大么?
必须可以这样做。因为再次发送ACTION_MEDIA_MOUNTED去驱动扫描,在一般情况下,其消耗并不大。因为此处是一个拔卡的过程,在媒体库扫描的preScan过程中,会对本次文件与上次文件记录先进行简单的存在比对。而不需要去真实的扫描每个文件的信息,因此对数据库的操作也就是对SD卡对应的记录的删除操作,与真实需求一致,唯一耗费的是preScan的对比过程(消耗不大,一般在2s以内)。

而在4.0的系统中,即使在RK的方案中,我们也不会再看到MountService.java中如此般处理。
——因为4.0中直接把拔SD卡后对对应数据记录的处理放到了MediaProvider中直接处理:mUnmountReceiver的onReceiver()承担了这个光荣的任务。
不过依然还是与
VoldResponseCode.VolumeBadRemoval
相关,与ACTION_MEDIA_EJECT广播相关,以它来驱动onReceiver操作:

                                String where = FileColumns.STORAGE_ID + "=?";
                                String[] whereArgs = new String[] { Integer.toString(storage.getStorageId()) };
                                db.update("files", values, where, whereArgs);
                                // now delete the records db.delete("files", where, whereArgs);

FileColumns.STORAGE_ID值为storage_id,上面代码操作的是files这个table。关键是storage这个变量,其在MountService的广播中传递过来的值,包含了storage_id信息。

12.扫描目录和扫描单个文件的区别
扫描目录其本质是对指定目录下的所有的单个文件进行扫描,这就是他们的共同处—-都将对每个文件进行扫描。
此处的实现是在java层的MyMediaScannerClient类的doScanFile()函数中。扫描单个文件,将直接从java到java的调用,在scanSingleFile()函数中就直接调用了mClient.doScanFile()进行扫描(在其之前也会先prescan(),但不需要postscan()。此处prescan的作用是?)。
而扫描目录,需要到native层进行对目录的遍历,然后确定每个单个文件的存在后,再从native到java来调用doScanFile()进行最终的目录下的每个单个文件的扫描!
另外在扫描目录的时,将在MediaProvider中发出ACTION_MEDIA_SCANNER_STARTED和ACTION_MEDIA_SCANNER_FINISHED的广播。

13.关于添加对指定目录的扫描,亦可以此来单独更新某个删除的文件,而不引起整个路径的扫描。
MediaProvider封装出来的接口,默认只支持对单个指定文件的扫描以及就是接收BOOT_COMPLETED对system/media扫描(扫描到的信息保存在internal.db中)和接收MEDIA_MOUNTED对flash和SD卡扫描(扫描到的信息保存在external.db中,这是平板方案中的一贯性做法,即flash占用这mnt/sdcard路径)。
若项目中需要添加对指定目录的扫描,也是很容易扩展的—因为MediaScanner本身的扫描函数就是支持任意目录的path的,只是在MediaProvider封装出来的时候给写死了路径。我们只需要稍微的修改一下MediaProvider即可。
全志A13的4.0方案中,即在原始的ACTION_MEDIA_SCANNER_SCAN_FILE中做了扩展,支持了扫描指定目录:
(1).在MediaScannerReceiver::onReceiver()扩展我们的广播接收,并调用scan()
(2).在scan()中启动MediaScannerService的时候,传入我们指定的扫描目录的path
(3).在MediaScannerService中ServiceHandler::ServiceHandler()中的处理中,directories变量赋值为传入的path—-注意!scan(directories, volume)中的volume保持为MediaProvider.EXTERNAL_VOLUME。

你可能感兴趣的:(MediaScanner学习笔记)