1.MediaReceiver
扫描的功能集中在MediaProvider中,源码位置:packages/providers/MediaProvider
其中的packages/providers/MediaProvider/AndroidManifest.xml:
<receiver android:name="com.android.providers.media.MediaReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<action android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
<data android:scheme="package" />
</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_SCANNER_SCAN_FILE" />
<data android:scheme="file" />
</intent-filter>
</receiver>
可以看到有一个对应的MediaReceiver,它是负责接受外部的指令消息,以便发起相关盘符的扫描任务。常见的Action如android.intent.action.BOOT_COMPLETED开机启动完成、android.intent.action.MEDIA_MOUNTED磁盘挂载成功等,都会触发MediaReceiver的消息接收。代码如下:
public class MediaReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
// Register our idle maintenance service
IdleService.scheduleIdlePass(context);
StableUriIdleMaintenanceService.scheduleIdlePass(context);
} else {
// All other operations are heavier-weight, so redirect them through
// service to ensure they have breathing room to finish
intent.setComponent(new ComponentName(context, MediaService.class));
MediaService.enqueueWork(context, intent);
}
}
}
2.MediaService
这里调用了MediaService的enqueueWork方法,去添加一个扫描任务,并在恰当的时候异步执行。
MeidaService继承自JobIntentService,它是Android8.0之后专门用来处理后台任务的服务。只需要执行enqueueWork方法,就可以把当前的任务Job加入到JobIntentService内部的任务队列里面,任务队列里又是使用的JobScheduler来调度任务,会在主线程任务不繁忙的时候,去调度并执行这个任务。如下:
// core/core/src/main/java/androidx/core/app/JobIntentService.java
//8.0的WorkEnqueuer.enqueueWork()
@Override
void enqueueWork(Intent work) {
if (DEBUG) Log.d(TAG, "Enqueueing work: " + work);
mJobScheduler.enqueue(mJobInfo, new JobWorkItem(work));
}
并且JobIntentService内部在执行任务的时候,是使用的线程池去执行的异步任务,不会阻塞主线程。针对于扫描文件这种耗时且耗性能的任务,使用此方式可以很好避免整机出现的性能瓶颈问题。关键代码如下:
// core/core/src/main/java/androidx/core/app/JobIntentService.java
@SuppressWarnings({"deprecation", "ObjectToString"}) /* AsyncTask */
void ensureProcessorRunningLocked(boolean reportStarted) {
if (mCurProcessor == null) {
mCurProcessor = new CommandProcessor();
if (mCompatWorkEnqueuer != null && reportStarted) {
mCompatWorkEnqueuer.serviceProcessingStarted();
}
if (DEBUG) Log.d(TAG, "Starting processor: " + mCurProcessor);
//使用线程池去执行异步任务
mCurProcessor.executeOnExecutor(android.os.AsyncTask.THREAD_POOL_EXECUTOR);
}
}
同时,MediaService会实现JobIntentService的onHandleWork方法,当JobIntentService内部的JobScheduler调度器在执行到本次任务的时候,就会回调到这个方法里面,去执行具体的任务。这一点,有点类似于Handler切换线程的机制。
//packages/providers/MediaProvider/src/com/android/providers/media/MediaService.java
@Override
protected void onHandleWork(Intent intent) {
switch (intent.getAction()) {
case Intent.ACTION_LOCALE_CHANGED: {
onLocaleChanged();
break;
}
......
case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: {
onScanFile(this, intent.getData());
break;
}
case Intent.ACTION_MEDIA_MOUNTED: {
onMediaMountedBroadcast(this, intent);
break;
}
case ACTION_SCAN_VOLUME: {
final MediaVolume volume = intent.getParcelableExtra(EXTRA_MEDIAVOLUME);
int reason = intent.getIntExtra(EXTRA_SCAN_REASON, REASON_DEMAND);
onScanVolume(this, volume, reason);
break;
}
}
}
可以看到针对于不同的消息类型,有不同的处理方式,我们这里以Intent.ACTION_MEDIA_MOUNTED消息类型为例,表示当磁盘挂载上之后的盘符扫描。
// packages/providers/MediaProvider/src/com/android/providers/media/MediaService.java
private static void onMediaMountedBroadcast(Context context, Intent intent)
throws IOException {
onScanVolume(context, mediaVolume, REASON_MOUNTED);
}
public static void onScanVolume(Context context, MediaVolume volume, int reason)
throws IOException {
......
provider.scanDirectory(volume.getPath(), reason);
......
}
3.MediaProvider以及ModernMediaScanner
在MediaService里面的onScanVolume会调用到MediaProvider的scanDirectory方法:
// packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public void scanDirectory(@NonNull File dir, @ScanReason int reason) {
mMediaScanner.scanDirectory(dir, reason);
}
调用到MediaScanner的scanDirectory方法,而MediaScanner是一个接口,它的实现类是ModernMediaScanner.java,看它内部实现扫描的逻辑:
// packages/providers/MediaProvider/src/com/android/providers/media/scan/ModernMediaScanner.java
@Override
public void scanDirectory(@NonNull File file, @ScanReason int reason) {
try (Scan scan = new Scan(file, reason)) {
scan.run();
} catch (FileNotFoundException e) {
Log.e(TAG, "Couldn't find directory to scan", e);
}
}
实例化了一个Scan对象,并执行了run方法。
Scan类是一个定义在ModernMediaScanner的内部类,它实现了Runnable接口,也实现了FileVisitor接口,如下:
private class Scan implements Runnable, FileVisitor<Path>, AutoCloseable {
@Override
public void run() {
runInternal();
}
private void runInternal() {
// First, scan everything that should be visible under requested location, tracking scanned IDs along the way
// 遍历文件树
walkFileTree();
// Second, reconcile all items known in the database against all the items we scanned above
//更新多媒体数据库,删除过时的文件信息
reconcileAndClean();
// Third, resolve any playlists that we scanned
//更新播放列表数据
resolvePlaylists();
}
}
具体的扫描过程分为3个步骤,第一步是递归扫描整个文件数,遍历文件树里面的每一个文件;第二步是更新媒体数据库,删除不存在的数据项;第三步是更新播放列表的数据。
4.文件扫描----文件的遍历
文件的遍历,使用到了Java提供的API walkFileTree,它会递归遍历指定盘符下的所有文件,每扫描到一个文件,就会通过FileVisitor接口的visitFile回调方法返回扫描的文件结果:
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
// 获取文件的类型
int actualMediaType = mediaTypeFromMimeType(
realFile, actualMimeType, FileColumns.MEDIA_TYPE_NONE);
//查询媒体库是否存在当前文件
try (Cursor c = mResolver.query(mFilesUri, projection, queryArgs, mSignal)) {
if (c.moveToFirst()) {
// 缓存存在的文件id,在防止在后面更新媒体库的时候被删除
mScannedIds.add(existingId);
final boolean sameMetadata =
hasSameMetadata(attrs, realFile, isPendingFromFuse, c);
final boolean sameMediaType = actualMediaType == mediaType;
// 文件没有改变,则继续下一个文件的遍历
if (sameMetadata && sameMediaType) {
if (LOGV) Log.v(TAG, "Skipping unchanged " + file);
return FileVisitResult.CONTINUE;
}
}
//如果媒体库不存在此文件,则是新加入的文件,则获取出该文件的详细信息
op = scanItem(existingId, realFile, attrs, actualMimeType, actualMediaType,
mVolumeName);
//缓存待执行数据库插入操作的数据项
addPending(op.build());
//是否达到批量操作的数量
maybeApplyPending();
}
以上代码是对扫描到的文件做的逻辑处理,首先会获取文件的mediatype类型,然后再根据文件的路径去媒体数据库查找,看是否之前已经存在了该数据项。如果从MediaProvider中查找到了相同的文件信息,会去和现在扫描到的文件做比较,查看它的mime类型和mediatype类型是否一样,是一样的说明就没有变,那么就会继续执行下一个文件的扫描;如果媒体库没有查到该文件已经存在,或者查到的文件和当前文件不一样,那么就会重新获取该文件的详细信息,并且放入pending的集合中,当pending的集合达到32个之后,就会执行批量入库的操作:
private void maybeApplyPending() {
if (mPending.size() > BATCH_SIZE) {
applyPending();
}
}
private void applyPending() {
//使用applyBatch来执行批量的数据操作
ContentProviderResult[] results = mResolver.applyBatch(AUTHORITY, mPending);
//把入库后的文件id缓存,防止后面被删除
mScannedIds.add(id);
mPending.clear();
}
5.文件扫描----数据的清理
完成所有文件的遍历之后,还需要对MediaProvider数据库里过时的数据做清除,如下:
private void reconcileAndClean() {
final long[] scannedIds = mScannedIds.toArray();
// 获得需要删除的数据id集合
addUnknownIdsAndGetMediaTypeCount(queryArgs, scannedIds);
for (int i = 0; i < mUnknownIds.size(); i++) {
final Uri uri = MediaStore.Files.getContentUri(mVolumeName, id).buildUpon()
.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false")
.build();
//添加批量删除的集合
addPending(ContentProviderOperation.newDelete(uri).build());
//执行批量操作
maybeApplyPending();
}
}
这里最关键一步就是得到需要删除的数据集合。首先会把MediaProvider里面的多媒体数据全部查询出来,然后对游标获取的数据做逐个对比,使用Arrays.binarySearch函数,去查看数据库中读取的id是否在本次扫描缓存的id集合中,如果不存在,那么返回的值就会小于0,就把这个数据库中读取出来的id放入mUnknownIds集合中,即这个id需要在后续从MediaProvider数据库中被删除。如下:
private int[] addUnknownIdsAndGetMediaTypeCount(Bundle queryArgs, long[] scannedIds) {
//从mediaprovider中读取全部数据
try (Cursor c = mResolver.query(mFilesUri,
new String[]{FileColumns._ID, FileColumns.MEDIA_TYPE, FileColumns.DATE_EXPIRES,
FileColumns.IS_PENDING}, queryArgs, mSignal)) {
while (c.moveToNext()) {
final long id = c.getLong(0);
// 比较此id是否在盘符扫描结果的集合中
if (Arrays.binarySearch(scannedIds, id) < 0) {
//加入被删除的集合中,后续执行批量的删除操作
mUnknownIds.add(id);
}
}
}
}
6.文件扫描----播放列表更新
扫描过程的第三步就是更新媒体播放列表。首先会根据扫描的盘符去获取到对应的Uri信息,然后查询到该Uri是否在数据库中存在播放列表的数据,然后就是对该播放列表的做清除,然后重新插入新扫描的数据,如下:
//packages/providers/MediaProvider/src/com/android/providers/media/scan/ModernMediaScanner.java
private void resolvePlaylists() {
final Uri playlistsUri = MediaStore.Audio.Playlists.getContentUri(mVolumeName);
MediaStore.resolvePlaylistMembers(mResolver,
ContentUris.withAppendedId(playlistsUri, id));
}
会调用到MediaProvider里面的callInternal方法里:
//packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private Bundle callInternal(String method, String arg, Bundle extras) {
case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
return getResultForResolvePlaylistMembers(extras);
}
}
private void resolvePlaylistMembers(@NonNull Uri playlistUri) {
final DatabaseHelper helper;
try {
helper = getDatabaseForUri(playlistUri);
} catch (VolumeNotFoundException e) {
throw e.rethrowAsIllegalArgumentException();
}
helper.runWithTransaction((db) -> {
resolvePlaylistMembersInternal(playlistUri, db);
return null;
});
}
这里就会调用到数据库去做删除和插入的操作:
private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
@NonNull SQLiteDatabase db) {
//删除表里对应的playlist的内容
db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);
//逐个添加播放列表的数据
for (int i = 0; i < members.size(); i++) {
db.insert("audio_playlists_map", null, values);
}
}
至此,一个针对于特定盘符的扫描任务就完成了,新的数据也保存在了MediaProvider中,提供给其他第三方的应用使用。