Android多媒体文件扫描流程
2016-02-01 17:18
495 查看
插入u盘后,系统会发送广播android.intent.action.MEDIA_MOUNTED
位于
packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java
MediaScannerService可以接收开机广播和u盘插入广播,收到广播后执行scan方法,将文件或文件夹路径存储到Bundle,并启动MediaScannerService,我们看MediaScannerService中ServiceHandler的代码
可以看到代码会调用scan或者scanFile扫描
下一步进入到MediaScanner.java,文件在
下面是扫描单个文件的代码
下面是真正的扫描实现
在路径frameworks/base/media/java/android/media/MediaFile.java
可以找到音视频扫描所支持的类型。
其中有一段代码
WMA音频格式是Google的专利,所以想支持扫描wma格式,可以去grep “AUDIO_DECODER_WMA” -rn .
在frameworks/av/media/libmedia/MediaProfiles.cpp中
在板级中device/XXX/XXXX/config/media_profiles.xml
将false改为true即可。
我们看一下英文说明
大意就是,processFile会根据文件类型调用不用的方法,并由MyMediaScannerClient.handleStringTag回调
这段native方法可以在下面的文件目录找到
mp->processFile在路径
通过grep搜索发现,方法定义在文件frameworks/av/media/libmedia/MediaScanner.h
可以看到这是一个虚函数
虚函数定义如下:如果父类的函数(方法)根本没有必要或者无法实现,完全要依赖子类去实现的话,可以把此函数(方法)设为virtual 函数名=0 我们把这样的函数(方法)称为纯虚函数如果一个类包含了纯虚函数,称此类为抽象类。
我们接着往下跟,找到真正实现的地方
回到前一个jni
这里面有实例化mediascanner的地方
马上就能找到实例化的地方了,打开
android支持哪些音视频格式类型在FileHasAcceptableExtension(extension)函数中定义的
如果想增加某种音频或者视频格式,需要做的就是两步
1、在kValidExtensions[] 数组中添加后缀名。如.dat
2、frameworks/base/media/java/android/media/MediaFile.java中添加public static final int FILE_TYPE_ABC = 数字;
addFileType(“DAT”, FILE_TYPE_DAT, “video/dat”);
android-4.4可以在相应的路径中mm 生成的frameworks.jar
android-5.1不能mm,必须make systemimage 重新烧录。
位于
packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java
MediaScannerService可以接收开机广播和u盘插入广播,收到广播后执行scan方法,将文件或文件夹路径存储到Bundle,并启动MediaScannerService,我们看MediaScannerService中ServiceHandler的代码
private final class ServiceHandler extends Handler { @Override public void handleMessage(Message msg) { Bundle arguments = (Bundle) msg.obj; String filePath = arguments.getString("filepath");//单个文件 String path = arguments.getString("path");//文件目录 try { if (filePath != null) { IBinder binder = arguments.getIBinder("listener"); IMediaScannerListener listener = (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder)); Uri uri = null; try { uri = scanFile(filePath, arguments.getString("mimetype"));//扫描单个文件 } catch (Exception e) { Log.e(TAG, "Exception scanning file", e); } if (listener != null) { listener.scanCompleted(filePath, uri); } } else { String volume = arguments.getString("volume"); String[] directories = null; if (MediaProvider.INTERNAL_VOLUME.equals(volume)) { // scan internal media storage directories = new String[] { Environment.getRootDirectory() + "/media", }; } else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) { // scan external storage volumes if (path == null) { directories = mExternalStoragePaths; } else if (path.startsWith("/mnt/usb_storage")) { directories = new String[] {path};//{"/mnt/usb_storage"}; } else { directories = new String[] {path}; } } if (directories != null) { if (true) Log.d(TAG, "start scanning volume " + volume + ": " + Arrays.toString(directories)); scan(directories, volume);//扫描文件列表 if (true) Log.d(TAG, "done scanning volume " + volume); } } } catch (Exception e) { Log.e(TAG, "Exception in handleMessage", e); } stopSelf(msg.arg1); } };
可以看到代码会调用scan或者scanFile扫描
private void scan(String[] directories, String volumeName) { Uri uri = Uri.parse("file://" + directories[0]); // don't sleep while scanning mWakeLock.acquire(); try { ContentValues values = new ContentValues(); values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName); Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values); sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri)); try { if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) { openDatabase(volumeName); } MediaScanner scanner = createMediaScanner(); if(directories!=null){ scanner.scanDirectories(directories, volumeName); } } catch (Exception e) { Log.e(TAG, "exception in MediaScanner.scan()", e); } getContentResolver().delete(scanUri, null, null); } finally { sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri)); mWakeLock.release(); } }
private Uri scanFile(String path, String mimeType) { String volumeName = MediaProvider.EXTERNAL_VOLUME; openDatabase(volumeName); MediaScanner scanner = createMediaScanner(); try { // make sure the file path is in canonical form String canonicalPath = new File(path).getCanonicalPath(); return scanner.scanSingleFile(canonicalPath, volumeName, mimeType); } catch (Exception e) { Log.e(TAG, "bad path " + path + " in scanFile()", e); return null; } }
下一步进入到MediaScanner.java,文件在
frameworks/base/media/java/android/media/MediaScanner.java
下面是扫描单个文件的代码
// this function is used to scan a single file public Uri scanSingleFile(String path, String volumeName, String mimeType) { try { initialize(volumeName); prescan(path, true); File file = new File(path); if (!file.exists()) { return null; } // lastModified is in milliseconds on Files. long lastModifiedSeconds = file.lastModified() / 1000; // always scan the file, so we can return the content://media Uri for existing files return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), false, true, MediaScanner.isNoMediaPath(path)); } catch (RemoteException e) { Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); return null; } } //初始化默认的uri private void initialize(String volumeName) { mMediaProvider = mContext.getContentResolver().acquireProvider("media"); mAudioUri = Audio.Media.getContentUri(volumeName); mVideoUri = Video.Media.getContentUri(volumeName); mImagesUri = Images.Media.getContentUri(volumeName); mThumbsUri = Images.Thumbnails.getContentUri(volumeName); mFilesUri = Files.getContentUri(volumeName); mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); if (!volumeName.equals("internal")) { // we only support playlists on external media mProcessPlaylists = true; mProcessGenres = true; mPlaylistsUri = Playlists.getContentUri(volumeName); mCaseInsensitivePaths = true; } } //扫描预处理,暂时不知道做什么用 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { Cursor c = null; String where = null; String[] selectionArgs = null; if (mPlayLists == null) { mPlayLists = new ArrayList<FileEntry>(); } else { mPlayLists.clear(); } if (filePath != null) { // query for only one file where = MediaStore.Files.FileColumns._ID + ">?" + " AND " + Files.FileColumns.DATA + " like '"+filePath+"%' "; selectionArgs = new String[] { "" }; } else { where = MediaStore.Files.FileColumns._ID + ">?"; selectionArgs = new String[] { "" }; } if (DEBUG_MEDIASCANNER) Log.d(TAG,"-----------enter prescan,filePath = "+filePath+"where = "+where); // Tell the provider to not delete the file. // If the file is truly gone the delete is unnecessary, and we want to avoid // accidentally deleting files that are really there (this may happen if the // filesystem is mounted and unmounted while the scanner is running). Uri.Builder builder = mFilesUri.buildUpon(); builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, mPackageName, builder.build()); // Build the list of files from the content provider try { if (prescanFiles) { // First read existing files from the files table. // Because we'll be deleting entries for missing files as we go, // we need to query the database in small batches, to avoid problems // with CursorWindow positioning. long lastId = Long.MIN_VALUE; Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build(); mWasEmptyPriorToScan = true; while (true) { selectionArgs[0] = "" + lastId; if (c != null) { c.close(); c = null; } c = mMediaProvider.query(mPackageName, limitUri, FILES_PRESCAN_PROJECTION, where, selectionArgs, MediaStore.Files.FileColumns._ID, null); if (c == null) { break; } int num = c.getCount(); if (num == 0) { break; } mWasEmptyPriorToScan = false; while (c.moveToNext()) { long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); lastId = rowId; // Only consider entries with absolute path names. // This allows storing URIs in the database without the // media scanner removing them. if (path != null && path.startsWith("/")) { boolean exists = false; try { exists = Libcore.os.access(path, libcore.io.OsConstants.F_OK); } catch (ErrnoException e1) { } if (!exists && !MtpConstants.isAbstractObject(format)) { // do not delete missing playlists, since they may have been // modified by the user. // The user can delete them in the media player instead. // instead, clear the path and lastModified fields in the row 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(); mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, parent, null); } } } } } } } } finally { if (c != null) { c.close(); } deleter.flush(); } // compute original size of images mOriginalCount = 0; c = mMediaProvider.query(mPackageName, mImagesUri, ID_PROJECTION, null, null, null, null); if (c != null) { mOriginalCount = c.getCount(); c.close(); } }
下面是真正的扫描实现
public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { Uri result = null; // long t1 = System.currentTimeMillis(); try { FileEntry entry = beginFile(path, mimeType, lastModified, fileSize, isDirectory, noMedia); if (path.startsWith("/mnt/usb_storage")) mIsUsbStorage =true; else mIsUsbStorage =false; // if this file was just inserted via mtp, set the rowid to zero // (even though it already exists in the database), to trigger // the correct code path for updating its entry if (mMtpObjectHandle != 0) { entry.mRowId = 0; } // rescan for metadata if file was modified since last scan if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { if (noMedia) { result = endFile(entry, false, false, false, false, false); } else { String lowpath = path.toLowerCase(Locale.ROOT); boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || (!ringtones && !notifications && !alarms && !podcasts); //判断是否是音视频格式,来决定下面是否对其扫描。 boolean isaudio = MediaFile.isAudioFileType(mFileType); boolean isvideo = MediaFile.isVideoFileType(mFileType); boolean isimage = MediaFile.isImageFileType(mFileType); if (isaudio || isvideo || isimage) { if (mExternalIsEmulated && path.startsWith(mExternalStoragePath)) { // try to rewrite the path to bypass the sd card fuse layer String directPath = Environment.getMediaStorageDirectory() + path.substring(mExternalStoragePath.length()); File f = new File(directPath); if (f.exists()) { path = directPath; } } } // we only extract metadata for audio and video files if (isaudio /*|| isvideo */) { //jni方法调用,真正的扫描开始 processFile(path, mimeType, this); } //if (isimage) { // processImageFile(path); //} result = endFile(entry, ringtones, notifications, alarms, music, podcasts); } } } catch (RemoteException e) { Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); } // long t2 = System.currentTimeMillis(); // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); return result; }
在路径frameworks/base/media/java/android/media/MediaFile.java
可以找到音视频扫描所支持的类型。
public class MediaFile { 39 40 // Audio file types 41 public static final int FILE_TYPE_MP3 = 1; 42 public static final int FILE_TYPE_M4A = 2; 43 public static final int FILE_TYPE_WAV = 3; 44 public static final int FILE_TYPE_AMR = 4; 45 public static final int FILE_TYPE_AWB = 5; .......................... static { 205 addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg", MtpConstants.FORMAT_MP3); 206 addFileType("MP3", FILE_TYPE_MP3, "audio/lume", MtpConstants.FORMAT_MP3); 207 addFileType("MPGA", FILE_TYPE_MP3, "audio/mpeg", MtpConstants.FORMAT_MP3); 208 addFileType("M4A", FILE_TYPE_M4A, "audio/mp4", MtpConstants.FORMAT_MPEG); ............................
其中有一段代码
180 private static boolean isWMAEnabled() { 181 List<AudioDecoder> decoders = DecoderCapabilities.getAudioDecoders(); 182 int count = decoders.size(); 183 for (int i = 0; i < count; i++) { 184 AudioDecoder decoder = decoders.get(i); //当下面条件成立才行 185 if (decoder == AudioDecoder.AUDIO_DECODER_WMA) { 186 return true; 187 } 188 } 189 return false; 190 } ....................... 212 if (isWMAEnabled()) { ">213 addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma", MtpConstants.FORMAT_WMA); 214 }
WMA音频格式是Google的专利,所以想支持扫描wma格式,可以去grep “AUDIO_DECODER_WMA” -rn .
在frameworks/av/media/libmedia/MediaProfiles.cpp中
const char *defaultXmlFile = "/etc/media_profiles.xml"
在板级中device/XXX/XXXX/config/media_profiles.xml
<AudioDecoderCap name="wma" enabled="false"/>
将false改为true即可。
我们看一下英文说明
MediaScanner.processFile(). * - MediaScanner.processFile() calls one of several methods, depending on the type of the * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA. * - each of these methods gets metadata key/value pairs from the file, and repeatedly * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java * counterparts in this file.
大意就是,processFile会根据文件类型调用不用的方法,并由MyMediaScannerClient.handleStringTag回调
这段native方法可以在下面的文件目录找到
frameworks/base/media/jni/android_media_MediaScanner.cpp
static void android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client) { ALOGV("processFile"); // Lock already hold by processDirectory MediaScanner *mp = getNativeScanner_l(env, thiz); if (mp == NULL) { jniThrowException(env, kRunTimeException, "No scanner available"); return; } if (path == NULL) { jniThrowException(env, kIllegalArgumentException, NULL); return; } const char *pathStr = env->GetStringUTFChars(path, NULL); if (pathStr == NULL) { // Out of memory return; } const char *mimeTypeStr = (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL); if (mimeType && mimeTypeStr == NULL) { // Out of memory // ReleaseStringUTFChars can be called with an exception pending. env->ReleaseStringUTFChars(path, pathStr); return; } MyMediaScannerClient myClient(env, client); MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient); if (result == MEDIA_SCAN_RESULT_ERROR) { ALOGE("An error occurred while scanning file '%s'.", pathStr); } env->ReleaseStringUTFChars(path, pathStr); if (mimeType) { env->ReleaseStringUTFChars(mimeType, mimeTypeStr); } }
mp->processFile在路径
通过grep搜索发现,方法定义在文件frameworks/av/media/libmedia/MediaScanner.h
可以看到这是一个虚函数
virtual MediaScanResult processFile( const char *path, const char *mimeType, MediaScannerClient &client) = 0;
虚函数定义如下:如果父类的函数(方法)根本没有必要或者无法实现,完全要依赖子类去实现的话,可以把此函数(方法)设为virtual 函数名=0 我们把这样的函数(方法)称为纯虚函数如果一个类包含了纯虚函数,称此类为抽象类。
我们接着往下跟,找到真正实现的地方
回到前一个jni
frameworks/base/media/jni/android_media_MediaScanner.cpp
这里面有实例化mediascanner的地方
static void android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz) { ALOGV("native_setup"); MediaScanner *mp = new StagefrightMediaScanner; if (mp == NULL) { jniThrowException(env, kRunTimeException, "Out of memory"); return; } env->SetIntField(thiz, fields.context, (int)mp); }
马上就能找到实例化的地方了,打开
frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp
MediaScanResult StagefrightMediaScanner::processFile( const char *path, const char *mimeType, MediaScannerClient &client) { ALOGV("processFile '%s'.", path); client.setLocale(locale()); client.beginFile(); MediaScanResult result = processFileInternal(path, mimeType, client); client.endFile(); return result; } MediaScanResult StagefrightMediaScanner::processFileInternal( const char *path, const char *mimeType, MediaScannerClient &client) { const char *extension = strrchr(path, '.'); if (!extension) { return MEDIA_SCAN_RESULT_SKIPPED; } //过滤支持的类型 if (!FileHasAcceptableExtension(extension)) { return MEDIA_SCAN_RESULT_SKIPPED; } //根据扩展名调用不同的解析方法 if (!strcasecmp(extension, ".mid") || !strcasecmp(extension, ".smf") || !strcasecmp(extension, ".imy") || !strcasecmp(extension, ".midi") || !strcasecmp(extension, ".xmf") || !strcasecmp(extension, ".rtttl") || !strcasecmp(extension, ".rtx") || !strcasecmp(extension, ".ota") || !strcasecmp(extension, ".mxmf")) { return HandleMIDI(path, &client); } //parse ape audio file if (!strcasecmp(extension, ".ape")) { return parseAPE(path, client); } sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever); int fd = open(path, O_RDONLY | O_LARGEFILE); status_t status; if (fd < 0) { // couldn't open it locally, maybe the media server can? status = mRetriever->setDataSource(path); } else { status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL); close(fd); } if (status) { return MEDIA_SCAN_RESULT_ERROR; } const char *value; if ((value = mRetriever->extractMetadata( METADATA_KEY_MIMETYPE)) != NULL) { status = client.setMimeType(value); if (status) { return MEDIA_SCAN_RESULT_ERROR; } }
android支持哪些音视频格式类型在FileHasAcceptableExtension(extension)函数中定义的
static bool FileHasAcceptableExtension(const char *extension) { 41 static const char *kValidExtensions[] = { 42 ".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2", 43 ".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac", 44 ".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota", 2> 45 ".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf", .> 46 ".avi", ".mpeg", ".mpg", ".awb", ".mpga", ".wmv", ".flv", ".rm", ".rmvb", ".mov", ".vob", ".m4v", ".f4v", ".amv", ".dat" 47 }; 48 static const size_t kNumValidExtensions = 49 sizeof(kValidExtensions) / sizeof(kValidExtensions[0]); }> 50 51 for (size_t i = 0; i < kNumValidExtensions; ++i) { 52 if (!strcasecmp(extension, kValidExtensions[i])) { 53 return true; 54 } 55 } 56 57 return false; 58 }
如果想增加某种音频或者视频格式,需要做的就是两步
1、在kValidExtensions[] 数组中添加后缀名。如.dat
2、frameworks/base/media/java/android/media/MediaFile.java中添加public static final int FILE_TYPE_ABC = 数字;
addFileType(“DAT”, FILE_TYPE_DAT, “video/dat”);
android-4.4可以在相应的路径中mm 生成的frameworks.jar
android-5.1不能mm,必须make systemimage 重新烧录。
相关文章推荐
- Android之WebRTC介绍
- Android的ClassLoader、DexLoader和插件化
- Android简单的修剪图片 上传图片
- Android按返回键退出程序但不销毁,程序后台运行,同QQ退出处理方式
- 【Android基础学习】Android权限
- 获取Android控件的宽和高
- Android Toast 解析以及减少“无意义的”toast
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- 【OpenSource】【Android】Android 开源项目
- Android Dialog 列表的创建
- android 窗口的使用
- Android Fragment是什么
- AS不能发布release版本的解决方案
- 自学android 坑2