我们先来初步了解一下MediaScannerService,它在AndroidManifest.xml文件里的相关信息如下:
【packages/providers/mediaprovider/AndroidManifest.xml】
MediaScannerService本身继承于Service,而且还实现了Runnable接口。其定义截选如下:【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】
public class MediaScannerService extends Service implements Runnable
{
private static final String TAG = "MediaScannerService";
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private PowerManager.WakeLock mWakeLock;
private String[] mExternalStoragePaths;
. . . . . .
private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub()
. . . . . .
. . . . . .
}
MediaScannerService的onCreate()函数如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】
@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();
// 启动最重要的工作线程,该线程也是个消息泵线程
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
}
可以看到,onCreate()里会启动最重要的工作线程,该线程也是个消息泵线程。每当用户需要扫描媒体文件时,基本上都是在向这个消息泵里发送Message,并在处理Message时完成真正的scan动作。请注意,创建Thread时传入的第二个参数就是MediaScannerService自身,也就是说线程的主要行为其实就是MediaScannerService的run()函数,该函数的代码如下:
public void run()
{
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();
mServiceLooper = Looper.myLooper(); // 消息looper
mServiceHandler = new ServiceHandler(); // 发送消息的handler
Looper.loop();
}
后续就是通过上面那个mServiceHandler向消息队列发送Message的。
比较常见的向消息泵发送Message的做法是调用startService(),并在MediaScannerService的onStartCommand()函数里sendMessage()。比如,和MediaScannerService配套提供的MediaScannerReceiver,当它收到类似ACTION_BOOT_COMPLETED这样的系统广播时,就会调用自己的scan()或scanFile()函数。而scan()函数的代码如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerReceiver.java】
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
context.startService( new Intent(context, MediaScannerService.class).putExtras(args));
}
startService()动作会导致走到service的onStartCommand(),并进一步发送消息,其函数截选如下:
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
. . . . . .
. . . . . .
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 IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() {
public void requestScanFile(String path, String mimeType, IMediaScannerListener listener)
{
Bundle args = new Bundle();
args.putString("filepath", path);
args.putString("mimetype", mimeType);
if (listener != null) {
args.putIBinder("listener", listener.asBinder());
}
startService(new Intent(MediaScannerService.this,
MediaScannerService.class).putExtras(args));
}
public void scanFile(String path, String mimeType) {
requestScanFile(path, mimeType, null);
}
};
说到底还是在调用startService()。
private final class ServiceHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
Bundle arguments = (Bundle) msg.obj;
String filePath = arguments.getString("filepath");
. . . . . .
if (filePath != null) {
. . . . . .
uri = scanFile(filePath, arguments.getString("mimetype"));
. . . . . .
} else {
. . . . . .
scan(directories, volume);
. . . . . .
}
. . . . . .
stopSelf(msg.arg1);
}
};
此时调用的scanFile()或scan()函数才是实际进行扫描动作的地方。扫描动作中主要借助的是辅助类MediaScanner,这个类非常重要,它是打通Java层和C++层的关键,扫描动作最终会调用到MediaScanner的某个native函数,于是程序流程开始走到C++层。
现在我们已经了解了,要发起扫描动作,大体上只有两种方式:
1)用广播来发起扫描动作;
2)绑定服务来发起扫描动作;
下面我们细说一下这两种方式。
扫描服务的配套receiver是MediaScannerReceiver,它在AndroidManifest.xml里的描述如下:
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
scan(context, MediaProvider.INTERNAL_VOLUME); // INTERNAL_VOLUME = "internal"
scan(context, MediaProvider.EXTERNAL_VOLUME); // EXTERNAL_VOLUME = "external"
} else {
if (uri.getScheme().equals("file")) {
// handle intents related to external storage
. . . . . .
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);
}
}
}
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode,
CancellationSignal signal)
throws FileNotFoundException
{
. . . . . .
// When finished writing, kick off media scanner
return ParcelFileDescriptor.open(file, pfdMode, mHandler,
new OnCloseListener() {
@Override
public void onClose(IOException e) {
final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
getContext().sendBroadcast(intent); // 用广播来发起扫描动作
}
});
. . . . . .
}
除了利用类似ACTION_MEDIA_SCANNER_SCAN_FILE这样的广播,系统中还有一种办法可以发起扫描动作,那就是先利用bindService机制得到的IMediaScannerService代理接口,而后再通过调用该接口的requestScanFile()或scanFile(),同样可以向MediaScannerService发出扫描语义。
不过,我们一般并不直白地去bindService,而是通过一种封装好的辅助类:MediaScannerConnection。该类的定义截选如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】
public class MediaScannerConnection implements ServiceConnection {
private static final String TAG = "MediaScannerConnection";
private Context mContext;
private MediaScannerConnectionClient mClient;
private IMediaScannerService mService;
private boolean mConnected; // true if connect() has been called since last disconnect()
private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub()
. . . . . .
请注意那个mService成员,它就是为了绑定service而设计的。
动态形式scanFile()的代码截选:
public void scanFile(String path, String mimeType) {
. . . . . .
mService.requestScanFile(path, mimeType, mListener);
. . . . . .
}
private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() {
public void scanCompleted(String path, Uri uri) {
MediaScannerConnectionClient client = mClient;
if (client != null) {
client.onScanCompleted(path, uri);
}
}
};
它是个简单的binder实体。每当MediaScannerService扫描完所指定的一个文件后,就会回调到该实体的scanCompleted()。此时一般会经由client.onScanCompleted()一句间接调用下一次scanFile()的动作,从而使扫描多个文件的动作连贯起来。
静态形式scanFile()的代码截选:
public static void scanFile(Context context, String[] paths, String[] mimeTypes,
OnScanCompletedListener callback) {
ClientProxy client = new ClientProxy(paths, mimeTypes, callback);
MediaScannerConnection connection = new MediaScannerConnection(context, client);
client.mConnection = connection;
connection.connect(); // 内部主要是bindService动作
}
public interface MediaScannerConnectionClient extends OnScanCompletedListener {
public void onMediaScannerConnected();
public void onScanCompleted(String path, Uri uri);
}
public void onMediaScannerConnected() {
scanNextPath();
}
public void onScanCompleted(String path, Uri uri) {
if (mClient != null) {
mClient.onScanCompleted(path, uri);
}
scanNextPath();
}
可以看到一旦连接建立成功或者某个文件扫描完毕,就会调用scanNextPath(),进一步扫描接下来的内容,直到把调用静态scanFile()时传入的paths数组遍历完毕。
void scanNextPath() {
if (mNextPath >= mPaths.length) {
mConnection.disconnect();
return;
}
String mimeType = mMimeTypes != null ? mMimeTypes[mNextPath] : null;
mConnection.scanFile(mPaths[mNextPath], mimeType);
mNextPath++;
}
public void connect() {
synchronized (this) {
if (!mConnected) {
Intent intent = new Intent(IMediaScannerService.class.getName());
intent.setComponent( new ComponentName("com.android.providers.media",
"com.android.providers.media.MediaScannerService"));
mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
mConnected = true;
}
}
}
因为bindService()动作本身是异步的,初始时mService的值还是null,所以我们不能直接在这里执行类似mService.requestScanFile()这样的操作。我们必须等到bind动作成功完成,系统回调到MediaScannerConnection的onServiceConnected(),才会给mService赋值:
public void onServiceConnected(ComponentName className, IBinder service) {
. . . . . .
synchronized (this) {
mService = IMediaScannerService.Stub.asInterface(service);
if (mService != null && mClient != null) {
mClient.onMediaScannerConnected();
}
}
}
如果bind动作是成功的,而且用户在构造MediaScannerConnection对象时传入了client参数。那么此时就会回调mClient的onMediaScannerConnected()函数。
private static final class ScannerClient implements MediaScannerConnectionClient {
String mPath = null;
MediaScannerConnection mScannerConnection;
SQLiteDatabase mDb;
public ScannerClient(Context context, SQLiteDatabase db, String path) {
mDb = db;
mPath = path;
mScannerConnection = new MediaScannerConnection(context, this);
mScannerConnection.connect();
}
@Override
public void onMediaScannerConnected() {
. . . . . .
}
@Override
public void onScanCompleted(String path, Uri uri) {
}
}
发出ACTION_MEDIA_SCANNER_SCAN_FILE广播的地方:
发起方 |
相关代码位置 |
说明 |
ExternalStorageProvider | openDocument()注册OnCloseListener的地方 | |
ComposeMessageActivity | MMS里copyPart()函数中 | saveRingtone()、 copyMedia()中都会调用copyPart()。 |
DownloadProvider | openFile()注册OnCloseListener的地方 | |
EmlAttachmentProvider | copyAttachment(),将附件拷到外部下载目录(一般是SD卡)时 | provider在update()中处理ATTACHMENT的地方 |
SoundRecorder | addToMediaDB() | 录制sample后,要添加进多媒体数据库 |
利用MediaScannerConnection的地方:
发起方 |
相关代码位置 |
说明 |
AttachmentUtilities | saveAttachment() | 代码截选见下文 |
BeamTransferManager | processFiles() | NFC方面, finishTransfer()、handleMessage()处理MSG_NEXT_TRANSFER_TIMER时,都会调用processFiles()。 |
BluetoothOppService | MediaScannerNotifier | 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的MediaScannerNotifier |
CalendarDebugActivity | doInBackground() | DumpDbTask的doInBackground(),将数据库文件存成calendar.db.zip之后,调用MediaScannerConnection.scanFile() |
DownloadScanner | DownloadScanner | 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的DownloadScanner |
FmRecorder | addRecordingToDatabase() | MediaScannerConnection.scanFile(context, new String[] { mRecordFile.getPath() }, null, null); |
IngestService | ScannerClient | 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的ScannerClient |
MediaProvider | ScannerClient | 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的ScannerClient |
VCardService | CustomeMediaScannerConnectionClient | 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的CustomeMediaScannerConnectionClient |
我们举一个实际的例子。在Email模块中,如果附件存入了外部存储器,那么就有必要扫描一次媒体文件了,这样才能够立即将相关文件体现到Gallery、Music中。所以在saveAttachment()函数里,就会调用MediaScannerConnection.scanFile():
【packages/apps/email/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java】
public static void saveAttachment(Context context, InputStream in, Attachment attachment) {
. . . . . .
ContentResolver resolver = context.getContentResolver();
if (attachment.mUiDestination == UIProvider.AttachmentDestination.CACHE) {
. . . . . .
} else if (Utility.isExternalStorageMounted()) {
. . . . . .
File file = Utility.createUniqueFile(downloads, attachment.mFileName);
size = copyFile(in, new FileOutputStream(file));
String absolutePath = file.getAbsolutePath();
// 尽管下载管理器会扫描媒体文件,但只会在用户运行download APP并点击相关按钮后,
// 才会进行扫描。所以,我们自己运行一下media scanner,以便把附件立即添加进gallery / music。
MediaScannerConnection.scanFile(context, new String[] {absolutePath},
null, null);
. . . . . .
DownloadManager dm = (DownloadManager)
context.getSystemService(Context.DOWNLOAD_SERVICE);
long id = dm.addCompletedDownload(attachment.mFileName,
attachment.mFileName,
false /* do not use media scanner */,
mimeType, absolutePath, size,
true /* show notification */);
contentUri = dm.getUriForDownloadedFile(id).toString();
. . . . . .
} else {
. . . . . .
throw new IOException();
}
. . . . . .
context.getContentResolver().update(uri, cv, null, null);
}
前文介绍MediaScannerService的消息泵线程时已经说过,最终ServiceHandler的handleMessage()会调用scanFile()或scan()来完成扫描。现在我们来看看scanFile()、scan()的细节。
MediaScannerService的scanFile()定义如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】
private Uri scanFile(String path, String mimeType) {
String volumeName = MediaProvider.EXTERNAL_VOLUME;
openDatabase(volumeName);
MediaScanner scanner = createMediaScanner();
try {
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;
}
}
可以看到,scanFile()函数内部借助了辅助类MediaScanner,调用了该类的scanSingleFile()。这个MediaScanner才是重头戏,它的scanSingleFile()代码截选如下:
public Uri scanSingleFile(String path, String volumeName, String mimeType) {
. . . . . .
initialize(volumeName);
prescan(path, true);
File file = new File(path);
. . . . . .
// 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));
. . . . . .
}
借助了mClient.doScanFile()。
private final MyMediaScannerClient mClient = new MyMediaScannerClient();
MyMediaScannerClient类的doScanFile()的代码截选如下:
public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
. . . . . .
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);
. . . . . .
if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
if (noMedia) {
result = endFile(entry, false, false, false, false, false);
} else {
. . . . . .
. . . . . .
// we only extract metadata for audio and video files
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
if (isimage) {
processImageFile(path);
}
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
}
. . . . . .
return result;
}
因为MyMediaScannerClient是MediaScanner的内嵌类,所以它可以直接调用MediaScanner的processFile()。
与scanFile()动作类似,MediaScannerService中扫描目录的动作是scan():
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】
private void scan(String[] directories, String volumeName) {
. . . . . .
values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
. . . . . .
MediaScanner scanner = createMediaScanner();
scanner.scanDirectories(directories, volumeName);
. . . . . .
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
. . . . . .
}
同样是借助了辅助类MediaScanner,调用了该类的scanDirectories()。
public void scanDirectories(String[] directories, String volumeName) {
. . . . . .
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient);
}
. . . . . .
}
顾名思义,MediaScanner就是个“媒体文件扫描器”。它必须打通java层次和C++层次。请大家注意它的两个native函数:native_init()和native_setup(),以及两个重要成员变量:一个是上文刚刚提到的mClient成员,另一个是mNativeContext。
MediaScanner的相关代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】
public class MediaScanner
{
static {
System.loadLibrary("media_jni");
native_init(); // 将java层和c++层联系起来
}
. . . . . .
private long mNativeContext;
. . . . . .
public MediaScanner(Context c) {
native_setup();
. . . . . .
}
. . . . . .
// 一开始就具有明确的mClient对象
private final MyMediaScannerClient mClient = new MyMediaScannerClient();
. . . . . .
}
MediaScanner类加载之时,就会同时加载动态链接库“media_jni”,并调用native_init()将java层和c++层联系起来。而且MediaScanner对象一开始就具有明确的mClient对象,类型为MyMediaScannerClient。
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->SetLongField(thiz, fields.context, (jlong)mp);
}
最后一句env->SetLongField()其实就是在为java层MediaScanner的mNativeContext域赋值。
不管是扫描文件,还是扫描目录,总之MediaScannerService已经把工作委托给MediaScanner的scanSingleFile()和scanDirectories()了,而这两个函数到头来都是调用MediaScanner自己的native函数,即processFile()和processDirectory()。其声明如下:
【frameworks/base/media/java/android/media/MediaScanner.java】
private native void processDirectory(String path, MediaScannerClient client);
private native void processFile(String path, String mimeType, MediaScannerClient client);
static void android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path,
jstring mimeType, jobject client)
{
. . . . . .
MediaScanner *mp = getNativeScanner_l(env, thiz);
. . . . . .
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); // 构造一个临时的myClient
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
if (result == MEDIA_SCAN_RESULT_ERROR) {
ALOGE("An error occurred while scanning file '%s'.", pathStr);
}
. . . . . .
}
注意这里构造了一个局部的(C++层次)MyMediaScannerClient对象,构造myClient时传入的client参数来自于Java层调用processFile()时传入的那个(Java层次)MyMediaScannerClient对象。这个对象会记录在C++层MyMediaScannerClient的mClient域中,这个在前面的示意图中已有表示。
static void android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
. . . . . .
MediaScanner *mp = getNativeScanner_l(env, thiz);
. . . . . .
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processDirectory(pathStr, myClient);
. . . . . .
}
android_media_MediaScanner_processFile()函数中的那个mp是经由下面这句得到的:
MediaScanner *mp = getNativeScanner_l(env, thiz);
它指向的其实就是StagefrightMediaScanner,所以这里调用的processFile就是:
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;
}
主要行为在processFileInternal()里:
MediaScanResult StagefrightMediaScanner::processFileInternal(
const char *path, const char * /* mimeType */,
MediaScannerClient &client) {
const char *extension = strrchr(path, '.');
. . . . . .
if (!FileHasAcceptableExtension(extension)) {
return MEDIA_SCAN_RESULT_SKIPPED;
}
if (!strcasecmp(extension, ".mid")
|| !strcasecmp(extension, ".smf")
|| !strcasecmp(extension, ".imy")
. . . . . .
return HandleMIDI(path, &client);
}
sp mRetriever(new MediaMetadataRetriever);
int fd = open(path, O_RDONLY | O_LARGEFILE);
. . . . . .
status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
close(fd);
. . . . . .
const char *value;
if ((value = mRetriever->extractMetadata(
METADATA_KEY_MIMETYPE)) != NULL) {
status = client.setMimeType(value);
. . . . . .
}
struct KeyMap {
const char *tag;
int key;
};
static const KeyMap kKeyMap[] = {
{ "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },
{ "discnumber", METADATA_KEY_DISC_NUMBER },
{ "album", METADATA_KEY_ALBUM },
{ "artist", METADATA_KEY_ARTIST },
. . . . . .
};
static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);
for (size_t i = 0; i < kNumEntries; ++i) {
const char *value;
if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
status = client.addStringTag(kKeyMap[i].tag, value);
. . . . . .
}
}
return MEDIA_SCAN_RESULT_OK;
}
调用FileHasAcceptableExtension()函数,看看文件的扩展名是不是属于多媒体文件扩展名,合适的扩展名有:
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】
static bool FileHasAcceptableExtension(const char *extension) {
static const char *kValidExtensions[] = {
".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac",
".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota",
".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf",
".avi", ".mpeg", ".mpg", ".awb", ".mpga"
};
. . . . . .
}
如果扩展名不合适,则直接return MEDIA_SCAN_RESULT_SKIPPED。
看看文件是不是midi文件,如果是midi文件,则以HandleMIDI()来处理。
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】
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);
}
从HandleMIDI()的代码看,要解析并提取midi文件的元数据,需要用到一种EAS引擎,利用EAS_ParseMetaData()解析出时长信息。并调用MyMediaScannerClient的addStringTag()。
如果是其他支持的多媒体文件,则利用工具类MediaMetadataRetriever来获取文件的元数据,并将得到的元数据传递给MyMediaScannerClient。其实MediaMetadataRetriever内部是利用系统服务“media.player”来解析多媒体文件的,这个系统服务对应的代理接口是IMediaPlayerService,它有个成员函数createMetadataRetriever()可以用于获取IMediaMetadataRetriever接口,而后就可以调用该接口的setDataSource()和extractMetadata()了。
processFileInternal()里主要通过两个函数,向Java层的MyMediaScannerClient传递数据,一个是setMimeType(),另一个是addStringTag()。以C++层的setMimeType()为例,其代码如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】
virtual status_t setMimeType(const char* mimeType)
{
ALOGV("setMimeType: %s", mimeType);
jstring mimeTypeStr;
if ((mimeTypeStr = mEnv->NewStringUTF(mimeType)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
mEnv->CallVoidMethod(mClient, mSetMimeTypeMethodID, mimeTypeStr);
mEnv->DeleteLocalRef(mimeTypeStr);
return checkAndClearExceptionFromCallback(mEnv, "setMimeType");
}
基本上只是通过JNI技术,调用到Java层的setMimeType()而已。
按理说,和processFile()类似,processDirectory()最终对应的代码也应该在StagefrightMediaScanner里,但是StagefrightMediaScanner并没有编写这个函数,又因为StagefrightMediaScanner继承于MediaScanner(C++层次),所以实际上使用的是MediaScanner的ProcessDirectory()
【frameworks/av/media/libmedia/MediaScanner.cpp】
MediaScanResult MediaScanner::processDirectory(
const char *path, MediaScannerClient &client) {
int pathLength = strlen(path);
. . . . . .
char* pathBuffer = (char *)malloc(PATH_MAX + 1);
. . . . . .
strcpy(pathBuffer, path);
. . . . . .
client.setLocale(locale());
MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
free(pathBuffer);
return result;
}
MediaScanResult MediaScanner::doProcessDirectory(char *path, int pathRemaining,
MediaScannerClient &client, bool noMedia) {
char* fileSpot = path + strlen(path);
struct dirent* entry;
if (shouldSkipDirectory(path)) {
. . . . . .
return MEDIA_SCAN_RESULT_OK;
}
// Treat all files as non-media in directories that contain a ".nomedia" file
if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
strcpy(fileSpot, ".nomedia");
if (access(path, F_OK) == 0) {
ALOGV("found .nomedia, setting noMedia flag");
noMedia = true;
}
. . . . . .
}
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;
}
bool MediaScanner::shouldSkipDirectory(char *path) {
if (path && mSkipList && mSkipIndex) {
int len = strlen(path);
int idx = 0;
int startPos = 0;
while (mSkipIndex[idx] != -1) {
if ((len == mSkipIndex[idx])
&& (strncmp(path, &mSkipList[startPos], len) == 0)) {
return true;
}
startPos += mSkipIndex[idx] + 1; // extra char for the delimiter
idx++;
}
}
return false;
}
其实就是比对一下“需要扫描的目录”是否存在于mSkipList列表中。这个列表的内容其实来自于“testing.mediascanner.skiplist”属性,该属性可以记录若干目录名,目录名之间以逗号分隔。在C++层的MediaScanner构造函数中,会调用loadSkipList()来读取这个属性,解析属性中记录的所有目录名并写入mSkipList列表。
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;
. . . . . .
int type = entry->d_type;
. . . . . .
if (type == DT_DIR) { // 普通目录
. . . . . .
if (stat(path, &statbuf) == 0) {
status_t status = client.scanFile(path, statbuf.st_mtime, 0,
true /*isDirectory*/, childNoMedia);
. . . . . .
}
// and now process its contents
strcat(fileSpot, "/");
MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
client, childNoMedia);
. . . . . .
} else if (type == DT_REG) { // 普通文件
stat(path, &statbuf);
status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
false /*isDirectory*/, noMedia);
. . . . . .
}
return MEDIA_SCAN_RESULT_OK;
}
不管当前处理的入口类型是“目录”还是“文件”,最终都是依靠client的scanFile()来处理,只不过前者倒数第二个参数(isDirectory)为true,后者为false而已。
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
. . . . . .
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}
@Override
public void scanFile(String path, long lastModified, long fileSize,
boolean isDirectory, boolean noMedia) {
doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}
调用到doScanFile()函数。
站在Java层次来看,不管是扫描具体的文件,还是扫描一个目录,最终都会走到Java层MyMediaScannerClient的doScanFile()。在前文我们已经列出过这个函数的代码,为了说明问题,这里再列一下其中的重要句子:
【frameworks/base/media/java/android/media/MediaScanner.java】
public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
. . . . . .
FileEntry entry = beginFile(path, mimeType, lastModified,
fileSize, isDirectory, noMedia);
. . . . . .
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
if (isimage) {
processImageFile(path);
}
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
. . . . . .
return result;
}
本小节着重看一下其中和MediaProvider相关的beginFile()和endFile()。
private static class FileEntry {
long mRowId;
String mPath;
long mLastModified;
int mFormat;
boolean mLastModifiedChanged;
FileEntry(long rowId, String path, long lastModified, int format) {
mRowId = rowId;
mPath = path;
mLastModified = lastModified;
mFormat = format;
mLastModifiedChanged = false;
}
. . . . . .
}
FileEntry的几个成员变量,其实体现了查表时的若干列的值。
public FileEntry beginFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean noMedia) {
. . . . . .
FileEntry entry = makeEntryFor(path); // 从MediaProvider中查出该文件或目录对应的入口
. . . . . .
if (entry == null || wasModified) {
if (wasModified) {
entry.mLastModified = lastModified;
} else {
// 如果前面没查到FileEntry,就在这里new一个新的FileEntry
entry = new FileEntry(0, path, lastModified,
(isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
}
entry.mLastModifiedChanged = true;
}
. . . . . .
return entry;
}
其中调用的makeEntryFor()内部就会查询MediaProvider:
FileEntry makeEntryFor(String path) {
String where;
String[] selectionArgs;
Cursor c = null;
try {
where = Files.FileColumns.DATA + "=?";
selectionArgs = new String[] { path };
c = mMediaProvider.query(mPackageName, mFilesUriNoNotify,
FILES_PRESCAN_PROJECTION,
where, selectionArgs, null, null);
if (c.moveToFirst()) {
long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
return new FileEntry(rowId, path, lastModified, format);
}
} catch (RemoteException e) {
} finally {
if (c != null) {
c.close();
}
}
return null;
}
查询语句中用的FILES_PRESCAN_PROJECTION的定义如下:
private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
Files.FileColumns._ID, // 0
Files.FileColumns.DATA, // 1
Files.FileColumns.FORMAT, // 2
Files.FileColumns.DATE_MODIFIED, // 3
};
看到了吗,特意要去查一下MediaProvider中记录的待查文件的最后修改日期。能查到就返回一个FileEntry,如果查询时出现异常就返回null。beginFile()的lastModified参数可以理解为是从文件系统里拿到的待查文件的最后修改日期,它应该是最准确的。而MediaProvider里记录的信息则有可能“较老”。beginFile()内部通过比对这两个“最后修改日期”,就可以知道该文件是不是真的改动了。如果的确改动了,就要把FileEntry里的mLastModified调整成最新数据。
result = mMediaProvider.insert(mPackageName, tableUri, values);
而当mRowId为非0值时,则会考虑调用:
mMediaProvider.update(mPackageName, result, values, null, null);
这就是改变MediaProvider中相关信息的最核心句子啦。
private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
throws RemoteException {
. . . . . .
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);
}
. . . . . .
long rowId = entry.mRowId;
if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
. . . . . .
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) {
. . . . . .
}
. . . . . .
if (rowId == 0) {
. . . . . .
// 扫描的是新文件,insert记录。如果是目录的话,必须比它所含有的所有文件更早插入记录,
// 所以在批量插入时,就需要有更高的优先权。如果是文件的话,而且我们现在就需要其对应
// 的rowId,那么应该立即进行插入,此时不过多考虑批量插入。
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 {
. . . . . .
mMediaProvider.update(mPackageName, result, values, null, null);
}
. . . . . .
return result;
}
除了直接调用mMediaProvider.insert()向MediaProvider中写入数据,函数中还有一种方式是经由inserter对象,其类型为MediaInserter。
private void flush(Uri tableUri, List list) throws RemoteException {
if (!list.isEmpty()) {
ContentValues[] valuesArray = new ContentValues[list.size()];
valuesArray = list.toArray(valuesArray);
mProvider.bulkInsert(mPackageName, tableUri, valuesArray);
list.clear();
}
}
写了这么多,终于看到MediaScannerService是如何更新MediaProvider的了。当然,里面还有大量的细节,本文就不展开来讲了,要不然相信大家头壳都得炸掉。那么就先写这么多了。