MediaScanner媒体扫描器顾名思义就是用于扫描手机内部多媒体文件的信息,并将这些信息存储到数据库中的模块。我们今天就针对MediaScanner这个模块进行分析学习一下:

普通文件的扫描流程

扫描请求的接收者 MediaScannerReceiver

MediaScannerReceiver 继承自BroadcastReceiver它是通过静态注册的方式进行注册的:

<receiver android:name="MediaScannerReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_UNMOUNTED" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
<data android:scheme="file" />
</intent-filter>
</receiver>

从上面的代码可以看出MediaScannerReceiver接受三种扫描请求广播:

  • Intent.ACTION_BOOT_COMPLETED:开机广播,在收到开机广播后会扫描内部和外部存储路径下的多媒体文件。
  • Intent.ACTION_MEDIA_MOUNTED:在某个外部存储器加载的时候。
  • Intent.ACTION_MEDIA_UNMOUNTED: 在某个外部存储器卸载的时候
  • Intent.ACTION_MEDIA_SCANNER_SCAN_FILE:在接受到对某个文件进行扫描的请求的时候调用scanFile方法进行扫描。

那每种广播到来的时候又会进行怎样的响应呢?

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
//扫描内部存储器中/system/media里面的内容,存放的是铃声等多媒体文件
scan(context, MediaProvider.INTERNAL_VOLUME);
//扫描手机的外部存储实际扫描的是/mnt/sdcard/
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
scan(context, MediaProvider.EXTERNAL_VOLUME);
//如果请求的是扫描路径,并且要扫描的属于外部存储路径则开始扫描
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {
scanFile(context, path);
}
}
}
}

我们看到整个扫描归结于scan和scanFile两个方法的调用。特别是scan方法既能够扫描外部存储又能扫描手机内存,唯一的区别是传入的参数不一样而已。我们接下来就来看下这部分的实现:

private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}

private void scanFile(Context context, String path) {
Bundle args = new Bundle();
args.putString("filepath", path);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}

很简单吧,就直接将路径传递给MediaScannerService就OK了,唯一的不同是携带数据的时候key不一样。

当调用startService后就会启动MediaScannerService,接下来就来看下MediaSannerService

扫描处理的后台处理者 MediaScannerService

在Scan和ScanFile启动首次MediaScannerService的时候MediaScannerService会调用onCreate方法创建这个服务,在onCreate中最重要的工作是启动一个线程用于扫描任务,因为Service运行在主线程中,如果直接在Service中执行扫描任务的话会阻塞主线程。

@Override
public void onCreate()
{
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
mExternalStoragePaths = storageManager.getVolumePaths();

// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block.
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
}

那么在扫描线程thr中又是做了哪些工作呢?从run方法中可以看出在thr线程中主要是创建了一个ServiceHandler。

public void run()
{
// reduce priority below other background threads to avoid interfering
// with other services at boot time.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();

mServiceLooper = Looper.myLooper();
mServiceHandler = new ServiceHandler();
Looper.loop();
}

在之后调用startService的时候,MediaScannerService类中的onStartCommand方法将会被调用。在onStartCommand方法中向mServiceHandler发送扫描请求。在传递给mServiceHandler的消息中包含了从Intent中获取的extra信息,凭借着这个信息可以区分当前的扫描请求是扫描整个valum还是单个文件。

@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
while (mServiceHandler == null) {
synchronized (this) {
try {
wait(100);
} catch (InterruptedException e) {
}
}
}

if (intent == null) {
Log.e(TAG, "Intent is null in onStartCommand: ",
new NullPointerException());
return Service.START_NOT_STICKY;
}

Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent.getExtras();
mServiceHandler.sendMessage(msg);

// Try again later if we are killed before we can finish scanning.
return Service.START_REDELIVER_INTENT;
}
private final class ServiceHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
Bundle arguments = (Bundle) msg.obj;
String filePath = arguments.getString("filepath");

try {
//这是扫描单个文件的情况
if (filePath != null) {
IBinder binder = arguments.getIBinder("listener");
IMediaScannerListener listener =
(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
Uri uri = null;
try {
//调用scanFile方法
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",
Environment.getOemDirectory() + "/media",
};
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
// scan external storage volumes
//这是扫描外部存储的情况
directories = mExternalStoragePaths;
}

if (directories != null) {
if (false) Log.d(TAG, "start scanning volume " + volume + ": "
+ Arrays.toString(directories));
//调用scan方法
scan(directories, volume);
if (false) Log.d(TAG, "done scanning volume " + volume);
}
}
} catch (Exception e) {
Log.e(TAG, "Exception in handleMessage", e);
}
//扫描结束后退出服务
stopSelf(msg.arg1);
}
};

从上面代码中可以看出在mServiceHandler方法中,首先提取Intent中携带的信息,如果有携带关键字为filepath的数据则说明的是需要扫描的为文件,则调用scanFile方法对指定文件路径的文件进行扫描,如果携带的是关键字为volume的数据则说明当前扫描请求为扫描加载卷,调用的方法为scan方法。

接下来我们重点关注对存储器加载卷的扫描过程。对单个文件的扫描原理大致相同。

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);

//发送ACTION_MEDIA_SCANNER_STARTED广播通知系统其他部件扫描即将开始
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

try {
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
openDatabase(volumeName);
}
//创建MediaScanner开始扫描
MediaScanner scanner = createMediaScanner();
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();
}
}

于扫描过程是一个耗时的过程,因此在开始扫描之前必须获得电源锁,避免在扫描过程中手机休眠,在扫描结束后释放电源锁。在扫描开始之前会向系统发送一个Intent.ACTION_MEDIA_SCANNER_STARTED的广播,通知系统的其他部件扫描过程的开始,其他部件可以通过监听这个广播。来采取对应的处理,同样在扫描结束后会向系统发送一个Intent.ACTION_MEDIA_SCANNER_FINISHED广播。而扫描任务是通过创建一个MediaScanner对象,并调用scanDirectories方法来实现的。

在应用层转了一圈后终于进入frameworks层了,我们看下frameworks层的MediaScanner类
在MediaScanner类中首先加载media_jni库,并调用native_init这个native方法。

static {
System.loadLibrary("media_jni");
native_init();
}
public MediaScanner(Context c) {
native_setup();
mContext = c;
mPackageName = c.getPackageName();
mBitmapOptions.inSampleSize = 1;
mBitmapOptions.inJustDecodeBounds = true;

setDefaultRingtoneFileNames();

mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath();
mExternalIsEmulated = Environment.isExternalStorageEmulated();
//mClient.testGenreNameConverter();
}

在MediaScanner的构造方法中最重要的是调用了native_setup()方法,native_setup也是一个native方法。
接下来我们看下native_init和native_setup这两个native方法。

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
jclass clazz = env->FindClass(kClassMediaScanner);
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}
}

native_init负责获取MediaScanner中的mNativeContext变量并保存到fields.context
native_setup负责创建一个MediaScanner的子类StagefrightMediaScanner并将native_init获取到的上下文传给StagefrightMediaScanner对象。

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
MediaScanner *mp = new StagefrightMediaScanner;
if (mp == NULL) {
jniThrowException(env, kRunTimeException, "Out of memory");
return;
}
env->SetLongField(thiz, fields.context, (jlong)mp);
}

接着我们看下scanDirectories方法:

public void scanDirectories(String[] directories, String volumeName) {

//.......
//1.扫描前预处理
initialize(volumeName);
//2. 预扫描
prescan(null, true);
//.......
if (ENABLE_BULK_INSERTS) {
// create MediaInserter for bulk inserts
mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500);
}

for (int i = 0; i < directories.length; i++) {
//3.扫描某个目录
processDirectory(directories[i], mClient);
}

if (ENABLE_BULK_INSERTS) {
// flush remaining inserts
mMediaInserter.flushAll();
mMediaInserter = null;
}
//4. 扫描后处理
postscan(directories);
//.......
}

上面我们过滤掉所有的无关代码,我们看到整个扫描过程分成如下四个部分:

  • initialize 扫描前预处理
  • prescan 预扫描
  • processDirectory扫描某个目录
  • postscan扫描后处理
  1. 扫描前预处理:
    在initialize中主要初始化各种Uri地址:包括音频,视频,图像,图像缩略图,视频缩略图,文件等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;
    }
    }
  2. 预扫描处理:
    预扫描处理主要完成如下工作:
  • 建立mPlayLists,因为在主扫描过程中对于播放列表是放到后面处理的,所以要新建一个mPlayLists用于存储播放列表文件记录,在beginFile扫到播放列表的时候需要先将其存储到mPlayLists中,等到postscan阶段对其进行统一处理。
  • 扫描数据库并查找数据库中的每一项的本地文件是否存在,这里每次只扫描1000条数据,避免因为加载太多数据而出现卡顿现象,需要注意的是这里并不需要删除真实的文件这是因为如果文件确实消失了则不需要删除,如果是因为在扫描期间文件系统被卸载而后又挂载,这时候删除操作也是不需要的。如果当前项所对应的本地文件不存在,则看下是否是播放列表文件,如果不是播放列表文件则从数据库中删除。如果是播放列表类型则不删除,因为这有可能是由于播放列表信息被用户修改导致的。用户可以在播放器中删除它们。
  • 在清空缺乏实际文件的数据项后重新查询图片数目,音频,视频的数目
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
Cursor c = null;
String where = null;
String[] selectionArgs = null;
//建立mPlayLists
if (mPlayLists == null) {
mPlayLists = new ArrayList<FileEntry>();
} else {
mPlayLists.clear();
}
//通过上述处理mPlayLists将会被清空,
if (filePath != null) {
// query for only one file
where = MediaStore.Files.FileColumns._ID + ">?" +" AND " + Files.FileColumns.DATA + "=?";
selectionArgs = new String[] { "", filePath };
} else {
where = MediaStore.Files.FileColumns._ID + ">?";
selectionArgs = new String[] { "" };
}

// 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).
//在这里告诉provider不要删除那些通过扫描发现不存在的文件,
为的是避免在扫描的时候文件系统卸载了,但是在以后的时候又挂载上了。
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) {
//如果为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 = Os.access(path, android.system.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();
}
}

通过预处理保证数据库的文件记录与本地的实际文件将一一对应。紧接着就进入了扫描阶段在scanDirectories方法中将会对每个需要扫描的目录调用processDirectory(directories[i], mClient);方法。processDirectory是一个nativie方法。
在JNI层的android_media_MediaScanner_processDirectory将会被调用,在该方法中将会调用native层的MediaScanner.cpp的processDirectory方法。

static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
ALOGV("processDirectory");
//获得Native层的媒体扫描器
MediaScanner *mp = getNativeScanner_l(env, thiz);
//.......
//这里新建一个MyMediaScannerClient对象然后将其作为参数传递到processDirectory中
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processDirectory(pathStr, myClient);
//............
}
MediaScanResult MediaScanner::processDirectory(
const char *path, MediaScannerClient &client) {
//..........
client.setLocale(locale());
////调用doProcessDirectory处理
MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
//.....
return result;
}

我们继续看下native层MediaScannner是如何完成扫描工作的:在MediaScannner中的processDirectory 方法中会对每个扫描路径调用一次processDirectory方法,这里的path为外部存储的或者内部存储的路径

MediaScanResult MediaScanner::doProcessDirectory(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {

//.......
DIR* dir = opendir(path);
//...........
MediaScanResult result = MEDIA_SCAN_RESULT_OK;
while ((entry = readdir(dir))) {
if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot) == MEDIA_SCAN_RESULT_ERROR) {
result = MEDIA_SCAN_RESULT_ERROR;
break;
}
}
closedir(dir);
return result;
}

doProcessDirectory这个方法会对每个主路径下的子文件调用doProcessDirectoryEntry进行扫描。
在开始扫描之前需事先判断当前的文件类型,并根据不同的文件类型进行不同的处理:

  • 如果当前文件类型为目录则直接调用client.scanFile方法,将扫描的工作交给MyMediaScannerClient,紧接着调用doProcessDirectory对该目录下的子文件再进行递归扫描。
  • 如果当前文件类型为普通文件类型则直接调用client.scanFile方法,但是与文件类型为目录不同的是当文件类型为普通文件类型的时候调用client.scanFile的时候传入isDirectory参数为false。
MediaScanResult MediaScanner::doProcessDirectoryEntry(char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,struct dirent* entry, char* fileSpot) {
struct stat statbuf;
const char* name = entry->d_name;
if (type == DT_DIR) {
// report the directory to the client
//如果是目录类型
if (stat(path, &statbuf) == 0) {
//调用client.scanFile扫描目录:这里的isDirectory = true
status_t status = client.scanFile(path, statbuf.st_mtime, 0,true /*isDirectory*/, childNoMedia);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
// and now process its contents
//在这个地方开始扫描该目录的子文件
strcat(fileSpot, "/");
MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1, client, childNoMedia);
if (result == MEDIA_SCAN_RESULT_ERROR) {
return MEDIA_SCAN_RESULT_ERROR;
}
} else if (type == DT_REG) {
//如果是普通文件 这里的isDirectory = false
stat(path, &statbuf);
status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,false /*isDirectory*/, noMedia);
if (status) {
return MEDIA_SCAN_RESULT_ERROR;
}
}
return MEDIA_SCAN_RESULT_OK;
}

不论怎样这里都是调用的client.scanFile只不过,传入的isDirectory不一样罢了。大家还记得client为MyMediaScannerClient吧,我们就来看下这个方法:

virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
//......
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
//......
}

在上述方法中主要调用的是mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,fileSize, isDirectory, noMedia);这是调用哪个方法呢?要解决这个问题必须找到mScanFileMethodID

MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
jclass mediaScannerClientInterface =
env->FindClass(kClassMediaScannerClient);
// static const char* const kClassMediaScannerClient =
"android/media/MediaScannerClient";
if (mediaScannerClientInterface == NULL) {
ALOGE("Class %s not found", kClassMediaScannerClient);
} else {
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(
mediaScannerClientInterface,
"setMimeType",
"(Ljava/lang/String;)V");
}
}

从上面可以看出MyMediaScannerClient实例中的scanFile方法实际上是调用位于MediaScanner.java文件中的MyMediaScannerClient的ScanFile方法:

@Override
public void scanFile(String path, long lastModified, long fileSize,
boolean isDirectory, boolean noMedia) {
// This is the callback funtion from native codes.
// Log.v(TAG, "scanFile: "+path);
doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}

ScanFile方法很简单就直接调用doScanFile方法。在doScanFile方法中将会进入下一个重要的阶段—获取音频视频文件的TAG数据。但是在介绍这个重要阶段之前还是先介绍下beginFile。

public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {

//调用beginFile
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);

if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
if (noMedia) {

} 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) {
path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
.getAbsolutePath();
}

//如果为音频或者视频文件抽取元数据
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
//如果是图像则调用processImageFile
if (isimage) {
processImageFile(path);
}
//调用endFile
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
}
return result;
}

beginFile故名思议就是在扫描前进行的一些处理:

  • 如果当前扫描的文件是普通文件则确定当前普通文件的文件类型。
  • 使用当前path参数到数据库中进行查询,这时候有两种情况,该文件的记录已经存在数据库中了,这时候使用path在数据库中找到对应的文件信息,并使用这些信息来创建一个FileEntry,如果当前的文件是新添加的文件,则在数据中并没有关于这个文件的信息,则new出一个新的FileEntry:
    entry = new FileEntry(0, path, lastModified,(isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
  • 如果当前文件是播放列表类型则将其先存储到mPlayLists中,在主扫描进程中先不做处理。
  • 清空元数据,为接下来的扫描做准备。
    public FileEntry beginFile(String path, String mimeType, long lastModified,long fileSize, boolean isDirectory, boolean noMedia) {
    mMimeType = mimeType;
    mFileType = 0;
    mFileSize = fileSize;
    mIsDrm = false;

    if (!isDirectory) {
    if (!noMedia && isNoMediaFile(path)) {
    noMedia = true;
    }
    mNoMedia = noMedia;

    // try mimeType first, if it is specified
    if (mimeType != null) {
    mFileType = MediaFile.getFileTypeForMimeType(mimeType);
    }
    // if mimeType was not specified, compute file type based on file extension.
    if (mFileType == 0) {
    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
    if (mediaFileType != null) {
    mFileType = mediaFileType.fileType;
    if (mMimeType == null) {
    mMimeType = mediaFileType.mimeType;
    }
    }
    }
    if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
    mFileType = getFileTypeFromDrm(path);
    }
    }
    //使用path查询数据库,在数据库中找到对应的文件信息,
    并使用这些信息来创建一个FileEntry
    FileEntry entry = makeEntryFor(path);
    // add some slack to avoid a rounding error
    long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
    boolean wasModified = delta > 1 || delta < -1;
    if (entry == null || wasModified) {
    if (wasModified) {
    entry.mLastModified = lastModified;
    } else {
    entry = new FileEntry(0, path, lastModified,
    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
    }
    entry.mLastModifiedChanged = true;
    }
    //如果当前类型为播放列表类型
    if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
    //将数据存储到队列中在后续的部分处理,不在主扫描过程中处理
    mPlayLists.add(entry);
    // we don't process playlists in the main scan, so return null
    return null;
    }
    // clear all the metadata
    mArtist = null;
    mAlbumArtist = null;
    mAlbum = null;
    mTitle = null;
    mComposer = null;
    mGenre = null;
    mTrack = 0;
    mYear = 0;
    mDuration = 0;
    mPath = path;
    mLastModified = lastModified;
    mWriter = null;
    mCompilation = 0;
    mWidth = 0;
    mHeight = 0;
    return entry;
    }

如果当前文件不是音频视频或者图像文件则直接在doScanFile中调用endFile将信息存储到数据库中。我们看下endFile这个方法。
在endFile方法中做了如下工作:

  • 如果歌手信息不存在则使用专辑作者作为歌手信息.
  • 接着调用toValues方法将文件的基本信息添加到ContentValues中
  • 对toValues方法添加的基本信息进行修正。
  • 如果是新添加的音频文件则将传入的ringtones,notifications,alarms,music,podcasts添加到ContentValues中,如果是图片则从ExifInterface中获取图片的长度宽度,经纬度,时区信息。
  • 如果当前扫描的文件是新增的则将当前的ContentValues插入到数据库中,如果当前的文件的记录在数据库中本来就有的则调用update对数据库原先存在的项进行更新。
  • 更新Setting上的相关设置.
private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
throws RemoteException {
// update database
// use album artist if artist is missing
if (mArtist == null || mArtist.length() == 0) {
mArtist = mAlbumArtist;
}
ContentValues values = toValues();
String title = values.getAsString(MediaStore.MediaColumns.TITLE);
if (title == null || TextUtils.isEmpty(title.trim())) {
title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
values.put(MediaStore.MediaColumns.TITLE, title);
}
String album = values.getAsString(Audio.Media.ALBUM);
if (MediaStore.UNKNOWN_STRING.equals(album)) {
album = values.getAsString(MediaStore.MediaColumns.DATA);
// extract last path segment before file name
int lastSlash = album.lastIndexOf('/');
if (lastSlash >= 0) {
int previousSlash = 0;
while (true) {
int idx = album.indexOf('/', previousSlash + 1);
if (idx < 0 || idx >= lastSlash) {
break;
}
previousSlash = idx;
}
if (previousSlash != 0) {
album = album.substring(previousSlash + 1, lastSlash);
values.put(Audio.Media.ALBUM, album);
}
}
}
long rowId = entry.mRowId;
if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
// Only set these for new entries. For existing entries, they
// may have been modified later, and we want to keep the current
// values so that custom ringtones still show up in the ringtone
// picker.
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 && !mNoMedia) {
ExifInterface exif = null;
try {
exif = new ExifInterface(entry.mPath);
} catch (IOException ex) {
// exif is null
}
if (exif != null) {
float[] latlng = new float[2];
if (exif.getLatLong(latlng)) {
values.put(Images.Media.LATITUDE, latlng[0]);
values.put(Images.Media.LONGITUDE, latlng[1]);
}

long time = exif.getGpsDateTime();
if (time != -1) {
values.put(Images.Media.DATE_TAKEN, time);
} else {
// If no time zone information is available, we should consider using
// EXIF local time as taken time if the difference between file time
// and EXIF local time is not less than 1 Day, otherwise MediaProvider
// will use file time as taken time.
time = exif.getDateTime();
if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
values.put(Images.Media.DATE_TAKEN, time);
}
}

int orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION, -1);
if (orientation != -1) {
// We only recognize a subset of orientation tag values.
int degree;
switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
default:
degree = 0;
break;
}
values.put(Images.Media.ORIENTATION, degree);
}
}
}

Uri tableUri = mFilesUri;
MediaInserter inserter = mMediaInserter;
if (!mNoMedia) {
if (MediaFile.isVideoFileType(mFileType)) {
tableUri = mVideoUri;
} else if (MediaFile.isImageFileType(mFileType)) {
tableUri = mImagesUri;
} else if (MediaFile.isAudioFileType(mFileType)) {
tableUri = mAudioUri;
}
}
Uri result = null;
boolean needToSetSettings = false;
if (rowId == 0) {
if (mMtpObjectHandle != 0) {
values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
}
if (tableUri == mFilesUri) {
int format = entry.mFormat;
if (format == 0) {
format = MediaFile.getFormatCode(entry.mPath, mMimeType);
}
values.put(Files.FileColumns.FORMAT, format);
}
// Setting a flag in order not to use bulk insert for the file related with
// notifications, ringtones, and alarms, because the rowId of the inserted file is
// needed.
if (mWasEmptyPriorToScan) {
if (notifications && !mDefaultNotificationSet) {
if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
needToSetSettings = true;
}
} else if (ringtones && !mDefaultRingtoneSet) {
if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
needToSetSettings = true;
}
} else if (alarms && !mDefaultAlarmSet) {
if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
needToSetSettings = true;
}
}
}

// New file, insert it.
// Directories need to be inserted before the files they contain, so they
// get priority when bulk inserting.
// If the rowId of the inserted file is needed, it gets inserted immediately,
// bypassing the bulk inserter.
if (inserter == null || needToSetSettings) {
if (inserter != null) {
inserter.flushAll();
}
result = mMediaProvider.insert(mPackageName, tableUri, values);
} else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
inserter.insertwithPriority(tableUri, values);
} else {
inserter.insert(tableUri, values);
}

if (result != null) {
rowId = ContentUris.parseId(result);
entry.mRowId = rowId;
}
} else {
// updated file
result = ContentUris.withAppendedId(tableUri, rowId);
// path should never change, and we want to avoid replacing mixed cased paths
// with squashed lower case paths
values.remove(MediaStore.MediaColumns.DATA);

int mediaType = 0;
if (!MediaScanner.isNoMediaPath(entry.mPath)) {
int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
if (MediaFile.isAudioFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_AUDIO;
} else if (MediaFile.isVideoFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_VIDEO;
} else if (MediaFile.isImageFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_IMAGE;
} else if (MediaFile.isPlayListFileType(fileType)) {
mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
}
values.put(FileColumns.MEDIA_TYPE, mediaType);
}
mMediaProvider.update(mPackageName, result, values, null, null);
}

if(needToSetSettings) {
if (notifications) {
setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
mDefaultNotificationSet = true;
} else if (ringtones) {
setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
mDefaultRingtoneSet = true;
} else if (alarms) {
setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
mDefaultAlarmSet = true;
}
}

return result;
}

到目前为止processDirectory方法的整个流程已经介绍完毕,如果是非视频音频图像文件则已经结束了整个扫描的过程,但是我们上面介绍的时候还忽略了播放列表文件。所以在下面的部分介绍播放列表文件,音频,视频,图像文件的处理。

播放列表的处理:

我们在介绍beginFile方法的时候说到在主扫描进程中并不对扫描到的播放列表进行处理,只是将其放到mPlayLists中。那么这些播放列表是什么时候处理呢?又将如何处理这些播放列表?在MediaScanner中播放列表文件的处理位于postscan方法中,这时候我们已经知道了存储器中有哪些多媒体文件。在processPlayLists方法中只处理最新的和上次扫描以后修改后的播放列表文件,通过获取播放列表的类型,再根据播放列表的类型调用不同的方法处理播放列表文件。

private void postscan(String[] directories) throws RemoteException {

// handle playlists last, after we know what media files are on the storage.
//如果当前扫描路径为外部存储路径则,则处理播放列表
if (mProcessPlaylists) {
//只处理最新的和上次扫描以后修改后的播放列表文件,
//获得最后的名字以及最后的修改时间,
//并根据播放列表的类型调用不同的方法处理播放列表文件
processPlayLists();
}

if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
pruneDeadThumbnailFiles();

// allow GC to clean up
mPlayLists = null;
mMediaProvider = null;
}

processPlayLists方法中首先获取全部的音频文件,并使用全部的音频文件作为参数,传入processPlayList,这个参数在后续将会介绍,其实之所以传入全部音频文件是用于获取每个播放列表的Playlist Member(播放列表成员歌曲)

private void processPlayLists() throws RemoteException {
Iterator<FileEntry> iterator = mPlayLists.iterator();
Cursor fileList = null;
try {
// use the files uri and projection because we need the format column,
// but restrict the query to just audio files
/获取全部的音频文件
fileList = mMediaProvider.query(mPackageName, mFilesUri, FILES_PRESCAN_PROJECTION,
"media_type=2", null, null, null);
while (iterator.hasNext()) {
FileEntry entry = iterator.next();
// only process playlist files if they are new or have been modified since the last scan
// 只处理最新的和上次扫描以后修改后的播放列表文件
if (entry.mLastModifiedChanged) {
processPlayList(entry, fileList);
}
}
} catch (RemoteException e1) {
} finally {
if (fileList != null) {
fileList.close();
}
}
}

我们看下对每个播放列表是怎么处理的,在处理前必须确保有一个播放列表的名字,首先通过查找MediaStore.Audio.Playlists.NAME对应的值,看下是否存在,如果不存在则从MediaStore.MediaColumns.TITLE中获取,如果还是没有值,就再尝试从文件名中获取。如果当前的播放列表是新增的则往数据库中插入新的播放列表信息,并获取播放列表成员的Uri,如果当前播放列表存在的话就更新其内容并且删除原来的member Uri。最后获取播放列表文件的类型,根据不同的文件类型调用不同的方法调用不同的方法处理。

private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {

String path = entry.mPath;
ContentValues values = new ContentValues();
int lastSlash = path.lastIndexOf('/');
if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
Uri uri, membersUri;
//从FileEnty中获取rowId
long rowId = entry.mRowId;
// 确保有一个播放列表名字,首先查看MediaStore.Audio.Playlists.NAME
// 再查看MediaStore.MediaColumns.TITLE 如果还是为空则获取最后的.后面的字符
String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
if (name == null) {
name = values.getAsString(MediaStore.MediaColumns.TITLE);
if (name == null) {
// 从文件中获取播放列表名字
int lastDot = path.lastIndexOf('.');
name = (lastDot < 0 ? path.substring(lastSlash + 1):
path.substring(lastSlash + 1, lastDot));
}
}
//将播放列表的名字和修改时间添加到数据库中
values.put(MediaStore.Audio.Playlists.NAME, name);
values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
if (rowId == 0) {
//如果不存在的话插入新的播放列表信息
values.put(MediaStore.Audio.Playlists.DATA, path);
//这时候value包含NAME DATA DATE_MODIFIED
uri = mMediaProvider.insert(mPackageName, mPlaylistsUri, values);
rowId = ContentUris.parseId(uri);
//获得插入的行id
//获得播放列表成员的Uri
membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
} else {
//如果存在的话就更新并且删除原来的member Uri
uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
mMediaProvider.update(mPackageName, uri, values, null, null);
membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
mMediaProvider.delete(mPackageName, membersUri, null, null);
}
//获取播放列表的目录
String playListDirectory = path.substring(0, lastSlash + 1);
//获取播放列表的文件类型
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
//根据不同的文件类型调用不同的方法调用不同的方法处理
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);
}
}

如果是M3U播放列表:则调用processM3uPlayList方法处理:
在processM3uPlayList方法打开播放列表文件,并读取播放列表内容,将读取到的播放列表成员记录添加到mPlsylist队列中调用processCachedPlaylist处理。

private void processM3uPlayList(String path, String playListDirectory, Uri uri,
ContentValues values, Cursor fileList) {
BufferedReader reader = null;
try {
File f = new File(path);
//判断播放列表文件是否存在,如果存在则调用cachePlaylistEntry
//读取后添加到mPlsylist队列中调用processCachedPlaylist处理
if (f.exists()) {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(f)),
8192);
String line = reader.readLine();
//清除播放列表缓冲列表
mPlaylistEntries.clear();
while (line != null) {
// 忽略注释行
if (line.length() > 0 && line.charAt(0) != '#') {
//如果不是注释行,则表示为播放列表信息,则将其处理后添加到mPlsylist队列中
cachePlaylistEntry(line, playListDirectory);
}
line = reader.readLine();
}
processCachedPlaylist(fileList, values, uri);
}
} catch (IOException e) {
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
}
}
}

如果是PLS格式的播放列表则将包含“File = ”开头的行,则将等号后面的内容读取出来(这些就是播放列表成员的记录)并存储到mPlsylist中,并调用processCachedPlaylist进行处理。

private void processPlsPlayList(String path, String playListDirectory, Uri uri,
ContentValues values, Cursor fileList) {
BufferedReader reader = null;
try {
File f = new File(path);
if (f.exists()) {
reader = new BufferedReader(
new InputStreamReader(new FileInputStream(f)), 8192);
String line = reader.readLine();
mPlaylistEntries.clear();
while (line != null) {
//如果是File开头的行则表示是PLS格式的播放列表信息
if (line.startsWith("File")) {
int equals = line.indexOf('=');
if (equals > 0) {
//将=后面的内容添加到mPlsylist中,调用processCachedPlaylist进行处理
cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
}
}
line = reader.readLine();
}
processCachedPlaylist(fileList, values, uri);
}
} catch (IOException e) {
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {

}
}
}

如果是WPL格式的播放列表文件则使用Xml解析器对其进行解析。同样最后也是调用processCachedPlaylist对其中的播放列表进行处理。

private void processWplPlayList(String path, String playListDirectory, Uri uri,
ContentValues values, Cursor fileList) {
Xlog.i(TAG,"MediaScanner#processWplPlayList()");
FileInputStream fis = null;
try {
File f = new File(path);
if (f.exists()) {
fis = new FileInputStream(f);
mPlaylistEntries.clear();
Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
new WplHandler(playListDirectory, uri, fileList).getContentHandler());
processCachedPlaylist(fileList, values, uri);
}
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException e) {
Xlog.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
}
}
}

cachePlaylistEntry负责讲从播放列表文件中读取到的播放列表行进行处理后添加到mPlaylistEntries中。

private void cachePlaylistEntry(String line, String playListDirectory) {
PlaylistEntry entry = new PlaylistEntry();
// 去除尾部的空白符
int entryLength = line.length();
while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
if (entryLength < 3) return;//如果小于三个字符则不合法
if (entryLength < line.length()) line = line.substring(0, entryLength);
//如果entryLength小于line.length()表示尾部有空格则删除尾部的空白符

//当前路径是否是绝对路径?
char ch1 = line.charAt(0);
boolean fullPath = (ch1 == '/' ||
(Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
// 如果是一个相对路径,则通过和playListDirectory进行合并构成全路径
if (!fullPath)
line = playListDirectory + line;
entry.path = line;
// 将全路径保存到mPlaylistEntries
mPlaylistEntries.add(entry);
}

processCachedPlaylist方法中将数据库中全部音频文件数据与存储在mPlaylistEntries的当前播放列表的成员歌曲记录进行匹配,匹配过程是调用matchEntries方法来完成的,它会使用全部音频文件与mPlaylistEntries中未匹配的项进行对比,如果路径相同则认为找到匹配的,如果不完全一致就通过对比路径的各个部分,寻找最匹配的那个。 如果mPlaylistEntries中全部项都匹配了,则返回true。最后将mPlaylistEntries中的成员作为当前播放列表的playlist member添加到数据库中。

private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
fileList.moveToPosition(-1);
while (fileList.moveToNext()) {
long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
// mPlaylistEntries中全部项都匹配了
if (matchEntries(rowId, data)) {
break;
}
}
int len = mPlaylistEntries.size();
int index = 0;
for (int i = 0; i < len; i++) {
PlaylistEntry entry = mPlaylistEntries.get(i);
if (entry.bestmatchlevel > 0) {
try {
//将mPlaylistEntries中的成员作为当前播放列表的playlist member
//添加到数据库中。
values.clear();
values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER,
Integer.valueOf(index));
values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID,
Long.valueOf(entry.bestmatchid));
mMediaProvider.insert(mPackageName, playlistUri, values);
index++;
} catch (RemoteException e) {
return;
}
}
}
mPlaylistEntries.clear();
}
private boolean matchEntries(long rowId, String data) {

int len = mPlaylistEntries.size();
boolean done = true;
for (int i = 0; i < len; i++) {
PlaylistEntry entry = mPlaylistEntries.get(i);
//如果entry.bestmatchlevel == Integer.MAX_VALUE 则表示已经被匹配了
//这种情况下将当前的与mPlaylistEntries的进行比较
if (entry.bestmatchlevel == Integer.MAX_VALUE) {
continue;
}
//开始比较的时候先将done置为false
done = false;
//data字段已经匹配不需要进行路径匹配了
if (data.equalsIgnoreCase(entry.path)) {
entry.bestmatchid = rowId;
entry.bestmatchlevel = Integer.MAX_VALUE;
continue;
}
//通过比对路径的每个字段寻找最比配的那个播放列表
int matchLength = matchPaths(data, entry.path);
if (matchLength > entry.bestmatchlevel) {
entry.bestmatchid = rowId;
entry.bestmatchlevel = matchLength;
}
}
return done;
}

总结以下,播放列表的处理是将播放列表文件进行解析,然后将播放列表文件中的成员添加到一个容器中,然后对容器进行遍历,并在数据库中对其进行匹配,如果匹配则添加到Playlist Member中

缩略图处理:

在MediaScanner中对缩略图处理也是位于Postscan方法中。在pruneDeadThumbnailFiles方法中先获取DCIM/thumbnails目录下的所有文件列表,在这个文件夹下存放的是所有的缩略图,而后再查看数据库中的视频和图像的缩略图信息,如果在数据库中存在该记录则表示该缩略图有用,从existingFiles中移除,到最后existingFiles中剩余的就是那些数据库中不需要的缩略图了,这些不需要的缩略图可以从DCIM/.thumbnails中删除,最后删除Mini 缩略图文件。

private void pruneDeadThumbnailFiles() {
HashSet<String> existingFiles = new HashSet<String>();
String directory = "/sdcard/DCIM/.thumbnails";
String [] files = (new File(directory)).list();
Cursor c = null;
if (files == null)
files = new String[0];

for (int i = 0; i < files.length; i++) {
String fullPathString = directory + "/" + files[i];
existingFiles.add(fullPathString);
}

try {
c = mMediaProvider.query(
mPackageName,
mThumbsUri,
new String [] { "_data" },
null,
null,
null, null);
Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
if (c != null && c.moveToFirst()) {
do {
String fullPathString = c.getString(0);
existingFiles.remove(fullPathString);
} while (c.moveToNext());
}

for (String fileToDelete : existingFiles) {
if (false)
Log.v(TAG, "fileToDelete is " + fileToDelete);
try {
(new File(fileToDelete)).delete();
} catch (SecurityException ex) {
}
}

Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
} catch (RemoteException e) {
// We will soon be killed...
} finally {
if (c != null) {
c.close();
}
}
}
音频视频文件帧信息的获取

这部分比较复杂我们将放在下一篇博客中介绍

Contents
  1. 1. 普通文件的扫描流程
    1. 1.1. 扫描请求的接收者 MediaScannerReceiver
    2. 1.2. 扫描处理的后台处理者 MediaScannerService
  2. 2. 播放列表的处理:
  3. 3. 缩略图处理:
    1. 3.1. 音频视频文件帧信息的获取