android5.1系统第一次开机扫描音频文件

一 、简介

Android系统在第一次开机时,会将保存在system/media/audio/ 目录下的音频文件读取出来,并且生成对应的URI地址,保存到SettingsProvider所在的数据库中(/data/data/com.android.providers.settings/databases/settings.db),所属表是system。
audio目录下的音频文件分为了alarms、notifications及ringtones等。分别对应了audio目录下的子目录。在这些子目录中,每个目录里都包含了大量的.ogg格式的音频文件,这些就是系统通知铃声等的音源文件。
注:除了扫描system/media/audio/目录之外,还会扫描内部或者外部存储,针对的目录是/music/和/podcasts/两个目录。

二、流程

Android系统关于media进程的源码在/packages/providers/MediaProvider/目录下,进程入口是MediaScannerReceiver.java这个类,这是个广播接收者,注册了android.intent.action.BOOT_COMPLETED的action,也就是开机广播。

MediaScannerReceiver.java的onReceive方法中会启动MediaScannerService这个服务,整个扫描的逻辑都是在这个服务里进行的调用。

@Override
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);
scan(context, MediaProvider.EXTERNAL_VOLUME);
}

}
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString(“volume”, volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
在MediaScannerService.java的onStartCommand方法中,会发送一个消息到它内部的handler(ServiceHandler)中,然后ServiceHandler会处理此消息,根据传进来的两个不同的参数,来分别封装成两个string数组,数组里包含的是要扫描的路径。从源码中可以看出,分别是[/system/media/ , oem/media/]和storage目录下的子目录。
本文主要追踪在system/media/目录下的音频文件的扫描过程,其他目录同此目录原理相同,几乎都走的相同代码。

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(); // 创建MediaScanner对象
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();
}
}
代码走到scan方法中,这里会创建MediaScanner对象,调用其scanDirectories的方法去扫描传参进去的路径。

public void scanDirectories(String[] directories, String volumeName) {
try {
long start = System.currentTimeMillis();
initialize(volumeName);
prescan(null, true); // 扫描前逻辑处理
long prescan = System.currentTimeMillis();
if (ENABLE_BULK_INSERTS) {
// create MediaInserter for bulk inserts
mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500);
}
for (int i = 0; i < directories.length; i++) {
processDirectory(directories[i], mClient); // 执行扫描的native方法
}
if (ENABLE_BULK_INSERTS) {
// flush remaining inserts
mMediaInserter.flushAll();
mMediaInserter = null;
}
long scan = System.currentTimeMillis();
postscan(directories); // 扫描结束,处理状态值。
long end = System.currentTimeMillis();
if (false) {
Log.d(TAG, ” prescan time: ” + (prescan - start) + “ms\n”);
Log.d(TAG, ” scan time: ” + (scan - prescan) + “ms\n”);
Log.d(TAG, “postscan time: ” + (end - scan) + “ms\n”);
Log.d(TAG, ” total time: ” + (end - start) + “ms\n”);
}
} catch (SQLException e) {
// this might happen if the SD card is removed while the media scanner is running
Log.e(TAG, “SQLException in MediaScanner.scan()”, e);
} catch (UnsupportedOperationException e) {
// this might happen if the SD card is removed while the media scanner is running
Log.e(TAG, “UnsupportedOperationException in MediaScanner.scan()”, e);
} catch (RemoteException e) {
Log.e(TAG, “RemoteException in MediaScanner.scan()”, e);
} finally {
releaseResources();
}
}
prescan方法,此方法会读取SettingsProvider的数据库,将数据查询后,根据返回的值进行判断,如果大于0,则说明已经对此数据库初始化过,就不再将扫描到的音频的URI写入(会将变量mWasEmptyPriorToScan赋值为false)。
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {

// 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; // 默认设置为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; // 如果为0,则中断循环
}
mWasEmptyPriorToScan = false; // 如果不为0 则将变量赋值为false

}
}
}
}
然后会遍历目录,调用processDirectory这个native方法,此篇博客不研究C++代码执行的逻辑,只做提示。processDirectory方法对应的JNI代码在/framework/base/media/jni/目录下的android_media_MediaScanner.cpp中的函数android_media_MediaScanner_processDirectory。
static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jobject client)
{
ALOGV(“processDirectory”);
MediaScanner *mp = getNativeScanner_l(env, thiz); // 初始化MediaScanner对象 (MediaScanner.cpp)
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;
}
MyMediaScannerClient myClient(env, client); // 初始化MyMediaScannerClient 类 ,
MediaScanResult result = mp->processDirectory(pathStr, myClient); // 调用MediaScanner.cpp 的processDirectory函数
if (result == MEDIA_SCAN_RESULT_ERROR) {
ALOGE(“An error occurred while scanning directory ‘%s’.”, pathStr);
}
env->ReleaseStringUTFChars(path, pathStr);
}
此函数会初始化MyMediaScannerClient(android_media_MediaScanner.cpp的class)对象,此类的构造函数中会获取
“android/media/MediaScannerClient”类的本地变量,并且获取其三个接口的methodID。
lass MyMediaScannerClient : public MediaScannerClient
{
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)), // 获取”android/media/MediaScannerClient” 的本地变量
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
ALOGV(“MyMediaScannerClient constructor”);
jclass mediaScannerClientInterface =
env->FindClass(kClassMediaScannerClient); // 获取 “android/media/MediaScannerClient” 类
if (mediaScannerClientInterface == NULL) {
ALOGE(“Class %s not found”, kClassMediaScannerClient);
} else {
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
“scanFile”,
“(Ljava/lang/String;JJZZ)V”); // 通过获取的类获取方法ID。
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
“handleStringTag”,
“(Ljava/lang/String;Ljava/lang/String;)V”);// 通过获取的类获取方法ID。
mSetMimeTypeMethodID = env->GetMethodID(
mediaScannerClientInterface,
“setMimeType”,
“(Ljava/lang/String;)V”);// 通过获取的类获取方法ID。
}
}
“android/media/MediaScannerClient”对应的其实是一个java接口,实现此java接口的类是MediaScanner的内部类MyMediaScannerClient,所以实际上获得的方法ID对应的也是MyMediaScannerClient的方法ID。

准备好了c++回调java的函数后,执行mp->processDirectory,processDirectory是MediiaScanner.cpp 的函数

MediaScanResult MediaScanner::processDirectory(
const char *path, MediaScannerClient &client) {

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) {
// place to copy file or directory name

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;

}
MediaScanResult MediaScanner::doProcessDirectoryEntry(
char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
struct dirent* entry, char* fileSpot) {

if (type == DT_DIR) {
    bool childNoMedia = noMedia;
    // set noMedia flag on directories with a name that starts with '.'
    // for example, the Mac ".Trashes" directory
    if (name[0] == '.')
        childNoMedia = true;
    // report the directory to the client
    if (stat(path, &statbuf) == 0) {
        status_t status = client.scanFile(path, statbuf.st_mtime, 0,
                true /*isDirectory*/, childNoMedia);  // 回调到java层
        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) {
    stat(path, &statbuf);
    status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
            false /*isDirectory*/, noMedia); // 回调到java层
    if (status) {
        return MEDIA_SCAN_RESULT_ERROR;
    }
}
return MEDIA_SCAN_RESULT_OK;

}
从上述代码可知,通过doProcessDirectoryEntry函数来对目录进行读取,并做相应的校验,并且调用client.scanFile函数回调到java层。从上述代码可知,这里的client就是MyMediaScannerClient 初始化的myClient对象,因为其继承了MediaScannerClient类。MediaScannerClient类在mediascanner.h文件中,只是提供了一些虚函数,这里不做详解。
追踪到MyMediaScannerClient的scanFile函数。
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
ALOGV(“scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)”,
path, lastModified, fileSize, isDirectory);
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia); // 关键点,调用了mScanFileMethodID 这个方法ID对应的java方法。也就是MediaScanner.java内部 // 类MyMediaScannerClient的scanFile方法,
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, “scanFile”);
}
代码追踪到这里,算是回到java层中。
@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);
}
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); // 封装成FileEntry对象
// 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); // 根据传递进来的路径进行判断是ringtones还是notifications
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); // 判断类型,这个在beginFile方法中确定了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) {
processFile(path, mimeType, this);
}
if (isimage) {
processImageFile(path);
}
result = endFile(entry, ringtones, notifications, alarms, music, podcasts); // 最终处理File
}
}
} 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;
}
这里调用了最后的代码,endFile方法,此方法最终处理递归过来的音频文件。

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
throws RemoteException {
// update database

Uri result = null;
boolean needToSetSettings = false;
boolean needToCustomizeMmsNotification = false;
boolean needToCustomizeRingtone2 = false;
boolean needToCustomizeRingtone3 = 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.
Log.i(TAG, “endFile: mWasEmptyPriorToScan = ” + mWasEmptyPriorToScan);
if (mWasEmptyPriorToScan) { // 这个变量就是在prescan方法中进行赋值的,如果不是第一次开机,settingsProvider数据库中已经有值了,就不会走下面的代码
if (notifications && !mDefaultNotificationSet) {
needToSetSettings = toSetSettings(entry.mPath, mDefaultNotificationFilename); // 判断是否是默认通知音频,如果是就写入到数据库中。此文件名是从system/build.prop文件中读取到的。源码配置在build/target/product/full_base.mk 文件中
} else if (ringtones && !mDefaultRingtoneSet) {
needToSetSettings = toSetSettings(entry.mPath, mDefaultRingtoneFilename); // 铃声
} else if (alarms && !mDefaultAlarmSet) {
needToSetSettings = toSetSettings(entry.mPath, mDefaultAlarmAlertFilename); // alarm
}
if (isSoundCustomized()) {
if (notifications && !mDefaultMmsNotificationSet) {
needToCustomizeMmsNotification = toSetSettings(entry.mPath,
mDefaultMmsNotificationFilename);
} else if (ringtones) {
if (!mDefaultRingtone2Set) {
needToCustomizeRingtone2 = toSetSettings(entry.mPath,
mDefaultRingtone2Filename);
}
if (!mDefaultRingtone3Set) {
needToCustomizeRingtone3 = toSetSettings(entry.mPath,
mDefaultRingtone3Filename);
}
}
}
}
// 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
|| needToCustomizeMmsNotification
|| needToCustomizeRingtone2
|| needToCustomizeRingtone3) {
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) { // 是否需要写入到settingsProvider数据库中
if (notifications) { // 判断是否是通知铃声
setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
Log.i(TAG, “endFile: notifications = ” + notifications + ” tableUri = ” + tableUri);
mDefaultNotificationSet = true;
} else if (ringtones) { // 铃声
// memorize default system ringtone persistently
setSettingIfNotSet(Settings.System.DEFAULT_RINGTONE, tableUri, rowId);
// set default ringtone uri for at least three slots
// irrespective of how many sim cards are actually supported
setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
setSettingIfNotSet(Settings.System.RINGTONE_2, tableUri, rowId);
setSettingIfNotSet(Settings.System.RINGTONE_3, tableUri, rowId);
Log.i(TAG, “endFile: ringtones = ” + ringtones + ” tableUri = ” + tableUri);
if (TelephonyManager.getDefault().isMultiSimEnabled()) {
int phoneCount = TelephonyManager.getDefault().getPhoneCount();
for (int i = PhoneConstants.SUB3+1; i < phoneCount; i++) {
// Set the default setting to the given URI for multi SIMs
setSettingIfNotSet((Settings.System.RINGTONE + “_” + (i+1)), tableUri, rowId);
}
}
mDefaultRingtoneSet = true;
} else if (alarms) {
setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
Log.i(TAG, “endFile: alarms = ” + alarms + ” tableUri = ” + tableUri);
mDefaultAlarmSet = true;
}
}
if (isSoundCustomized()) {
if (notifications && needToCustomizeMmsNotification) {
overrideSetting(Settings.System.MMS_NOTIFICATION_SOUND, tableUri, rowId);
mDefaultMmsNotificationSet = true;
} else if (ringtones) {
int phoneCount = TelephonyManager.getDefault().getPhoneCount();
if (phoneCount == 2 && needToCustomizeRingtone2) {
overrideSetting(Settings.System.RINGTONE_2, tableUri, rowId);
mDefaultRingtone2Set = true;
}
if (phoneCount == 3) {
if (needToCustomizeRingtone2) {
overrideSetting(Settings.System.RINGTONE_2, tableUri, rowId);
mDefaultRingtone2Set = true;
}
if (needToCustomizeRingtone3) {
overrideSetting(Settings.System.RINGTONE_3, tableUri, rowId);
mDefaultRingtone3Set = true;
}
}
}
}
return result;
}
到这里源码就追踪完毕了,最终的结果,是将读取到的默认音频文件对应的URI地址写入到SettingsProvider的数据库(settings.db)中的system表里,对应的名称分别是alarm_alert、notification_sound和ringtone等。

三、总结

本篇博客的目的主要是记录这两天在研究开机初始化时,media进程执行的一些操作。方便以后进行回顾查询。

你可能感兴趣的:(android-system,源码分析,android,media)