目录
前言
上一篇《MediaProvider源码分析》分析到,正在对多媒体的扫描是在MediaScanner中,因此进入就进一步分析多媒体扫描逻辑。
涉及代码目录
- #java
- frameworks\base\media\java\android\media\MediaScanner.java
- #cpp
- frameworks\av\media\libmedia\MediaScanner.cpp
- frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp
- #jni
- frameworks\base\media\jni\android_media_MediaScanner.cpp
正文
接上文,扫描涉及到MediaScanner
- #1先初始化MediaScanner
- MediaScanner scanner = new MediaScanner(this, volumeName)
- #2 扫描目录或扫描指定文件
- scanDirectories(String[] directories)
- scanSingleFile(String path, String mimeType)
因此,我们就针对上面其中一个进入跟踪,这里一scanDirectories()为例。
这里涉及JNI,这个推荐看完大致流程可以重新学习一下,会C和Java的调用很用帮助。
- 1. processDirectory(String path, MediaScannerClient client);//遍历文件
- 2. processFile(String path, String mimeType, MediaScannerClient client);//解析音视频文件
- 3. setLocale(String locale);//语言切换
- 4. extractAlbumArt(FileDescriptor fd);//专辑图
- 5. native_init();
- 6. native_setup();
- 7. native_finalize();
MediaScanner()
先初始化MediaScanner,先看看做了哪些操作。
- //volumeName是MediaProvider.EXTERNAL_VOLUME或MediaProvider.INTERNAL_VOLUME
- public MediaScanner(Context c, String volumeName) {
- //native 初始化
- native_setup();
- //获取mMediaProvider用于操作数据库
- mMediaProvider = mContext.getContentResolver()
- .acquireContentProviderClient(MediaStore.AUTHORITY);
- //mProcessPlaylists后续需要,从这里看就是external才需要处理播放列表
- if (!volumeName.equals("internal")) {
- mProcessPlaylists = true;
- mProcessGenres = true;
- mPlaylistsUri = Playlists.getContentUri(volumeName);
- } else {
- mProcessPlaylists = false;
- mProcessGenres = false;
- mPlaylistsUri = null;
- }
- //检查是否有忘记关的资源
- mCloseGuard.open("close");
- }
在初始化MediaScanner时会提前加载media_jni库。
- static {
- System.loadLibrary("media_jni");
- native_init();
- }
scanDirectories()
- public void scanDirectories(String[] directories) {
- //扫描前预准备[先判断数据库中文件是否存在,不存在就删除]
- prescan(null, true);
- if (ENABLE_BULK_INSERTS) {//true
- mMediaInserter = new MediaInserter(mMediaProvider, 500);
- }
- //directories可能[/storage/emulated/0, /storage/udisk0]
- for (int i = 0; i < directories.length; i++) {
- // native 函数,调用它来对目标文件夹进行扫描
- processDirectory(directories[i], mClient);
- }
- if (ENABLE_BULK_INSERTS) {
- mMediaInserter.flushAll();
- mMediaInserter = null;
- }
- // 扫描后处理 主要是processPlayLists处理
- postscan(directories);
- }
这里才开始扫描磁盘中的文件。
prescan()
- private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
- //这里传入的filePath=null,prescanFiles= true
- //查询数据库,判断数据是否存在,如果不存在就删除
- try {
- if (prescanFiles) {
- Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
- while (true) {
- selectionArgs[0] = "" + lastId;
- if (c != null) {
- c.close();
- c = null;
- }
- c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
- where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
- if (c == null) {
- break;
- }
- int num = c.getCount();
- //遍历数据,上面已经限制了1000条
- while (c.moveToNext()) {
- //判断文件或文件夹是否符合条件,并判断是否存在,如果不存在就从数据库中删除。
- if (path != null && path.startsWith("/")) {
- boolean exists = false;
- try {
- exists = Os.access(path, android.system.OsConstants.F_OK);
- } catch (ErrnoException e1) {
- }
- //如果文件不存在,就删除
- if (!exists && !MtpConstants.isAbstractObject(format)) {
- MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
- int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
- if (!MediaFile.isPlayListFileType(fileType)) {
- deleter.delete(rowId);
- if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
- deleter.flush();
- String parent = new File(path).getParent();
- Log.d(TAG, "prescan parent: "+ parent );
- mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
- }
- }
- }
- }
- }
- }
- }
- }
- finally {
- if (c != null) {
- c.close();
- }
- deleter.flush();
- }
- }
这种提前扫描可以缩短扫描时间,因为重新解析ID3是比较耗时间的。
processDirectory()
调用了native层方法。传入的directories[i]
- [/storage/emulated/0, /storage/udisk0]
并传入了mClient
- private final MyMediaScannerClient mClient = new MyMediaScannerClient();
MyMediaScannerClient是MediaScanner中的内部类,native层扫描后会通过scanFile()回调上来处理。
scanFile()直接调用的是doScanFile()
- public Uri doScanFile(String path, String mimeType, long lastModified,
- long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
- Uri result = null;
- try {
- //为了后续和MediaProvider打交道,准备一个FileEntry
- FileEntry entry = beginFile(path, mimeType, lastModified,
- fileSize, isDirectory, noMedia);
- //略
- if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
- //不是多媒体文件
- if (noMedia) {
- //endFile()用于插入数据和刷新数据
- result = endFile(entry, false, false, false, false, false);
- } else {
- //略
- //音频或视频
- if (isaudio || isvideo) {
- mScanSuccess = processFile(path, mimeType, this);
- }
- //图片
- if (isimage) {
- mScanSuccess = processImageFile(path);
- }
- //endFile()用于插入数据和刷新数据
- result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
- }
- }
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
- }
- return result;
- }
这里做了三件事,封装FileEntry,处理音视频图片,插入和刷新数据库
beginFile()
- public FileEntry beginFile(String path, String mimeType, long lastModified,
- long fileSize, boolean isDirectory, boolean noMedia) {
- //略
- if (!isDirectory) {
- //是否多媒体文件?
- if (!noMedia && isNoMediaFile(path)) {
- noMedia = true;
- }
- mNoMedia = noMedia;
- // try mimeType first, if it is specified
- //mimeType是否指定,如果是就直接查询文件类型,一般为null
- if (mimeType != null) {
- mFileType = MediaFile.getFileTypeForMimeType(mimeType);
- }
- // 默认是0,当时如果上面制定了,就不等于0
- if (mFileType == 0) {
- //MediaFile中添加了音视频文件支持后缀。
- MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
- if (mediaFileType != null) {
- mFileType = mediaFileType.fileType;
- if (mMimeType == null) {
- mMimeType = mediaFileType.mimeType;
- }
- }
- }
- //是否启动了DRM数字版权管理
- if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
- mFileType = getFileTypeFromDrm(path);
- }
- }
- //从MediaProvider中查出该文件或目录对应的FileEntry
- FileEntry entry = makeEntryFor(path);
- if (entry == null || wasModified) {
- //是否被更新过
- if (wasModified) {
- entry.mLastModified = lastModified;
- } else {
- //entry= null时会走这里,也就是没有查询到,new一个新的FileEntry
- entry = new FileEntry(0, path, lastModified,
- (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
- }
- entry.mLastModifiedChanged = true;
- }
- //MediaScanner构造函数中时,当volumeName=external时mProcessPlaylists=true
- //如果是多媒体,且外置磁盘,都会走这里
- if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
- mPlayLists.add(entry);
- return null;
- }
- //略
- return entry;
- }
对于音视频支持的文件后缀,可以看MediaFile.java
- frameworks\base\media\java\android\media\MediaFile.java
endFile()
主要是插入和更新数据,需要注意的,这里会更新判断进行更新电话铃声,通知音和闹钟铃声。
- private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
- boolean alarms, boolean music, boolean podcasts)
- throws RemoteException {
- //对一些null的数据进行一定赋值
- //比如没有获取到歌曲名,可以用文件名替代。
- //略
- long rowId = entry.mRowId;
- if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
- //音频
- values.put(Audio.Media.IS_RINGTONE, ringtones);
- values.put(Audio.Media.IS_NOTIFICATION, notifications);
- values.put(Audio.Media.IS_ALARM, alarms);
- values.put(Audio.Media.IS_MUSIC, music);
- values.put(Audio.Media.IS_PODCAST, podcasts);
- } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
- || mFileType == MediaFile.FILE_TYPE_HEIF
- || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
- //图片
- }
- //rowid == 0 表示数据库中不存在,就需要进行插入数据
- if (rowId == 0) {
- //略
- if (inserter == null || needToSetSettings) {
- if (inserter != null) {
- inserter.flushAll();
- }
- result = mMediaProvider.insert(tableUri, values);
- } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
- inserter.insertwithPriority(tableUri, values);
- } else {
- inserter.insert(tableUri, values);
- }
- } else {
- //由于rowid不为0,也就是数据库中存在数据,因此进行update
- //略
- mMediaProvider.update(result, values, null, null);
- }
- //是否需要设置 通知一下,铃声 闹钟铃声等
- if(needToSetSettings) {
- if (notifications) {
- setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
- mDefaultNotificationSet = true;
- } else if (ringtones) {
- setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
- mDefaultRingtoneSet = true;
- } else if (alarms) {
- setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
- mDefaultAlarmSet = true;
- }
- }
- return result;
- }
setRingtoneIfNotSet()
- private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) {
- ContentResolver cr = mContext.getContentResolver();
- String existingSettingValue = Settings.System.getString(cr, settingName);
- if (TextUtils.isEmpty(existingSettingValue)) {
- final Uri settingUri = Settings.System.getUriFor(settingName);
- final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId);
- //设置铃声
- RingtoneManager.setActualDefaultRingtoneUri(mContext,
- RingtoneManager.getDefaultType(settingUri), ringtoneUri);
- }
- Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1);
- }
postscan()
- private void postscan(final String[] directories) throws RemoteException {
- //上面MediaScanner()中有当external时mProcessPlaylists = true
- if (mProcessPlaylists) {
- processPlayLists();
- }
- mPlayLists.clear();
- }
除了mProcessPlaylists,还需要满足isPlayListFileType(),才会放入mPlayLists
- //主要针对如下几种格式
- public static final int FILE_TYPE_M3U = 41;
- public static final int FILE_TYPE_PLS = 42;
- public static final int FILE_TYPE_WPL = 43;
- public static final int FILE_TYPE_HTTPLIVE = 44;
- private static final int FIRST_PLAYLIST_FILE_TYPE = FILE_TYPE_M3U;
- private static final int LAST_PLAYLIST_FILE_TYPE = FILE_TYPE_HTTPLIVE;
- public static boolean isPlayListFileType(int fileType) {
- return (fileType >= FIRST_PLAYLIST_FILE_TYPE &&
- fileType <= LAST_PLAYLIST_FILE_TYPE);
- }
看看processPlayLists()
- private void processPlayLists() throws RemoteException {
- //mPlayLists在beginFile()中
- //当mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)为true时添加
- Iterator<FileEntry> iterator = mPlayLists.iterator();
- Cursor fileList = null;
- try {
- fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
- "media_type=2", null, null, null);
- while (iterator.hasNext()) {
- FileEntry entry = iterator.next();
- if (entry.mLastModifiedChanged) {
- processPlayList(entry, fileList);
- }
- }
- } catch (RemoteException e1) {
- } finally {
- if (fileList != null) {
- fileList.close();
- }
- }
- }
真正干活的是processPlayList(FileEntry entry, Cursor fileList)
- private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
- long rowId = entry.mRowId;
- //略
- //rowid =0 表示数据库没有,insert;反之update
- if (rowId == 0) {
- values.put(MediaStore.Audio.Playlists.DATA, path);
- uri = mMediaProvider.insert(mPlaylistsUri, values);
- rowId = ContentUris.parseId(uri);
- membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
- } else {
- uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
- mMediaProvider.update(uri, values, null, null);
- // delete members of existing playlist
- membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
- mMediaProvider.delete(membersUri, null, null);
- }
- String playListDirectory = path.substring(0, lastSlash + 1);
- MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
- int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
- //针对FILE_TYPE_M3U,FILE_TYPE_PLS和FILE_TYPE_WPL再处理。
- if (fileType == MediaFile.FILE_TYPE_M3U) {
- processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
- } else if (fileType == MediaFile.FILE_TYPE_PLS) {
- processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
- } else if (fileType == MediaFile.FILE_TYPE_WPL) {
- processWplPlayList(path, playListDirectory, membersUri, values, fileList);
- }
- }
android_media_MediaScanner.cpp
上面介绍过MediaScanner初始化是就会加重JNI并初始化,同时部分方法还是native实现的。
MediaScanner.java中设计的native方法有如下几个:
- private native void processDirectory(String path, MediaScannerClient client);
- private native boolean processFile(String path, String mimeType, MediaScannerClient client);
- private native void setLocale(String locale);
- public native byte[] extractAlbumArt(FileDescriptor fd);
- private static native final void native_init();
- private native final void native_setup();
- private native final void native_finalize();
native_init()
部分代码删除
- static void android_media_MediaScanner_native_init(JNIEnv *env){
- //获取上下文context
- fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
- }
native_setup()
- static void android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz){
- // mp 是StagefrightMediaScanner的对象,后面需要使用
- MediaScanner *mp = new StagefrightMediaScanner;
- env->SetLongField(thiz, fields.context, (jlong)mp);
- }
等setLocale(),进行一定的初始化。
接上面我们Java层调用了
- for (int i = 0; i < directories.length; i++) {
- processDirectory(directories[i], mClient);
- }
native层
- static void android_media_MediaScanner_processDirectory(
- JNIEnv *env, jobject thiz, jstring path, jobject client){
- //mp就是native_setup创建StagefrightMediaScanner的对象
- MediaScanner *mp = getNativeScanner_l(env, thiz);
- //创建native层的MyMediaScannerClient,后面需要回调数据给Java层
- MyMediaScannerClient myClient(env, client);
- //调用StagefrightMediaScanner.processDirectory()
- MediaScanResult result = mp->processDirectory(pathStr, myClient);
- }
由于StagefrightMediaScanner类没有重写,但父类有实现,因此就跳到MediaScanner.cpp中。
MyMediaScannerClient
继承于MediaScannerClient
- class MediaScannerClient{
- public:
- MediaScannerClient();
- virtual ~MediaScannerClient();
- void setLocale(const char* locale);
- void beginFile();
- status_t addStringTag(const char* name, const char* value);
- void endFile();
- virtual status_t scanFile(const char* path, long long lastModified,
- long long fileSize, bool isDirectory, bool noMedia) = 0;
- virtual status_t handleStringTag(const char* name, const char* value) = 0;
- virtual status_t setMimeType(const char* mimeType) = 0;
- };
setLocale在MediaScannerClient.cpp有实现。至于其他的都是空的方法。
MyMediaScannerClient中实现的只有scanFile(),handleStringTag()和setMimeType(),这三个会回调到Java层的MyMediaScannerClient中。
- class MyMediaScannerClient : public MediaScannerClient
- {
- public:
- virtual status_t scanFile(const char* path, long long lastModified,
- long long fileSize, bool isDirectory, bool noMedia){
- //略
- //此处的mClient是java层的MyMediaScannerClient,调用的也是java层的scanFile方法
- mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
- fileSize, isDirectory, noMedia);
- mEnv->DeleteLocalRef(pathStr);
- return checkAndClearExceptionFromCallback(mEnv, "scanFile");
- }
- virtual status_t handleStringTag(const char* name, const char* value){
- //略
- return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
- }
- virtual status_t setMimeType(const char* mimeType){
- //略
- return checkAndClearExceptionFromCallback(mEnv, "setMimeType");
- }
- };
StagefrightMediaScanner.cpp
StagefrightMediaScanner的父类是MediaScanner,可以看StagefrightMediaScanner.h中的定义。
主要实现了解析音视频文件processFile()方法和extractAlbumArt()音频专辑图,解析媒体相关的信息,并通过MediaScannerClient回到给MediaScanner.java。
- MediaScanResult StagefrightMediaScanner::processFile(
- const char *path, const char *mimeType,
- MediaScannerClient &client) {
- //调用native层的MyMediaScannerClient对象进行local信息,语言设置
- client.setLocale(locale());
- //空方法,没有作用,下面的endFile也是一样
- client.beginFile();
- //具体的方法是调用processFileInternal实现的
- MediaScanResult result = processFileInternal(path, mimeType, client);
- //失败时
- if (mimeType == NULL && result != MEDIA_SCAN_RESULT_OK) {
- client.setMimeType("application/octet-stream");
- }
- client.endFile();
- return result;
- }
- MediaScanResult StagefrightMediaScanner::processFileInternal(
- const char *path, const char * /* mimeType */,
- MediaScannerClient &client) {
- //获取文件的元数据【ID3相关信息】
- sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
- const char *value;
- if ((value = mRetriever->extractMetadata(
- METADATA_KEY_MIMETYPE)) != NULL) {
- // 关键函数setMimeType
- //mimeType回调java中的MyMediaScannerClient
- status = client.setMimeType(value);
- }
- for (size_t i = 0; i < kNumEntries; ++i) {
- const char *value;
- if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
- // 关键函数addStringTag。
- //真正调用handleStringTag(name, value)会回调java中的MyMediaScannerClient的handleStringTag()
- status = client.addStringTag(kKeyMap[i].tag, value);
- }
- }
- return MEDIA_SCAN_RESULT_OK;
- }
通过JNI,Java跟C通信,C也反馈信息给Java。具体看android_media_MediaScanner.cpp
PS: 好烦,JNI如何让C和Java相互调用又忘了,下次重新整理一下。
MediaScanner.cpp
MediaScanner是StagefrightMediaScanner父类,android_media_MediaScanner.cpp调初始化的是StagefrightMediaScanner,但由于StagefrightMediaScanner只重写部分父类方法,因此没有重写的调用的还是父类的方法。
主要设计下面两个方法。
setLocale()
processDirectory()
主要是第二个方法,最终也是通过
- status_t status = client.scanFile(path, statbuf.st_mtime, 0,true, childNoMedia);
更上面一样,client也是MediaScannerClient对象,最后也就是返回到Java层的MyMediaScannerClient中的scanFile()方法。
PS:MediaScannerClient是MyMediaScannerClient父类。
上面的MediaScanner流程都大概走完了。对于JNI,准备重新学习一下并记录一下,都忘记了。
参考文章
《》
《》
《