在上一篇中分析到,由MediaScannerService.java中的scanFile()方法或者scan()方法调用scanSingleFile()方法或者scanDirectories()方法来进入MediaScanner.java开始进行扫描操作的。
先来看MediaScanner.java的构造方法:
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();
}
1、加载media_jni.so库,使用native_init()和native_setup()这两个jni方法执行一些初始化设置操作,不做具体分析;
2、调用方法setDefaultRingtoneFileNames()设置默认铃声文件名称,读取设置在/build/target/product/full_base.mk文件中默认铃声名;
private void setDefaultRingtoneFileNames() {
mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
+ Settings.System.RINGTONE);
mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
+ Settings.System.NOTIFICATION_SOUND);
mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
+ Settings.System.ALARM_ALERT);
}
/build/target/product/full_base.mk文件:
PRODUCT_PROPERTY_OVERRIDES := \
ro.config.ringtone=Ring_Synth_04.ogg \
ro.config.notification_sound=pixiedust.ogg
3、读取外部存储设备的路径;
至此,我们知道在MediaScanner.java的构造方法中,主要是用来加载.so文件、调用本地jni方法、读取设置的默认铃声名称、实例化成员变量等;
接下来来看MediaScanner.java中的真正开始文件扫描的地方scanSingleFile()方法或者scanDirectories()方法,一个是用来扫描单个文件另一个是用来扫描目录的。我们直接来看扫描目录的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);
}
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();
}
}
1、initialize()方法;见名知意执行初始化操作
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;
}
}
(1)、获取MediaProvider对象;
(2)、初始化不同类型数据的Uri,供之后根据不同的表进行插值;
(3)、如果是外部存储,则可以获得播放列表的Uri和支持显示播放列表、音乐的流派等内容;
2、prescan()方法,扫描预处理;主要是一些查询MediaProvider数据库的一些操作,mFileCache保存了扫描前所有媒体文件的信息,这些信息是从数据库中查询得来的,也就是旧有的信息。
3、processDirectory()方法,开始扫描目录;这个方法是个native方法,用来扫描文件,参数directories[i]是传入的路径数组,mClient是MyMediaScannerClient的实例。在c文件中进过一系列的调用最终会回调内部类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);
}
直接调用
public Uri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia)
参数包括文件路径、文件类型、最后修改时间、文件大小、是否是目录、是否强制扫描等等,其中的scanAlways用于控制是否强制扫描,有时候一些文件在前后两次扫描过程中没有发生变化,这时候MS可以不处理这些文件。如果scanAlways为true,则这些没有变化的文件也要扫描。
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 (entry == null) {
return null;
}
// 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;
}
if (entry.mPath != null) {
if (((!mDefaultNotificationSet &&
doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename))
|| (!mDefaultRingtoneSet &&
doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename))
|| (!mDefaultAlarmSet &&
doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) {
Log.w(TAG, "forcing rescan of " + entry.mPath +
"since ringtone setting didn't finish");
scanAlways = true;
} else if (isSystemSoundWithMetadata(entry.mPath)
&& !Build.FINGERPRINT.equals(sLastInternalScanFingerprint)) {
// file is located on the system partition where the date cannot be trusted:
// rescan if the build fingerprint has changed since the last scan.
Log.i(TAG, "forcing rescan of " + entry.mPath
+ " since build fingerprint changed");
scanAlways = true;
}
}
// 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) {
path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
.getAbsolutePath();
}
// 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);
}
}
} 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;
}
3.1、首先调用
beginFile()方法;由于篇幅,不贴代码,简要分析代码。这个方法主要是根据传入的各项文件参数去解析构造
FileEntry 的实例;
3.2、判断文件是否被插入到via mtp,则将其rowId设为0,用来以正确的路径来更新数据库的条目;再判断文件的路径是否满足特定的要求如:默认来电铃声、默认通知铃声等等或者是一些系统媒体文件。如果满足的话就需要将scanAlways置为true使得后续控制强制扫描。
3.3、判断以判断这个文件是否在前后扫描的这个时间段内被修改,如果有修改且这个文件是属于媒体文件,则需要重新扫描;
3.4、调用endFile()方法更新数据库。这个方法简单分析如下:
// 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);
}
}
}
3.4.1、根据扫描的文件构建插入数据库的ContentValues ,这个对象包含媒体文件的专辑、作曲、文件名、文件内容、文件类型等等,如果被扫描的文件中这些值有缺失,使用MediaStore.MediaColumns.DATA来构建相应的缺失值。
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);
}
3.4.2、判断文件类型是否是音频文件、rowID为0且未被插入到mtp中;
else if ((mFileType == MediaFile.FILE_TYPE_JPEG
|| MediaFile.isRawImageFileType(mFileType)) && !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);
}
}
3.4.3、判断文件类型是否是图片文件;根据图片的exif信息来解析构建ContentValues,包含拍照经纬度、拍照时间、照片朝向等等;
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;
}
}
3.4.4、根据文件类型判断所要插入的数据库表的url;
boolean needToSetSettings = false;
// 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 (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;
}
}
3.4.5、设置一个flag避免批量插入与notifications, ringtones, and alarms相关的文件,因为这些文件在插入的时候需要rowID关键字;
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);
}
// 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(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(result, values, null, null);
}
3.4.6、如果rowID为0,则执行mMediaProvider插入操作;否则执行更新数据库的操作;rowID为0一般表示需要重新进行插入,以确保其值是正确的!
if(needToSetSettings) {
if (notifications) {
setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
mDefaultNotificationSet = true;
} else if (ringtones) {
setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
mDefaultRingtoneSet = true;
} else if (alarms) {
setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
mDefaultAlarmSet = true;
}
}
3.4.7、根据3.4.5设置的needToSetSettings的这个flag判断是否属于notifications, ringtones, and alarms等类型的文件,并根据相应的类型进行判断是否执行插入Settings.System的数据库的操作。
完成文件的扫描。
4、postscan()方法:
private void postscan(final 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.clear();
}
根据扫描的结果来处理
playlists 。并且只处理在最后一次扫描中认定为新文件或者被修改的文件;