Android13 扫盘逻辑分析
代码在源码目录为: packages\providers\MediaProvider\
一,扫盘起点
android.intent.action.MEDIA_MOUNTED插入U盘的广播
<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>
二,启动扫盘服务
启动扫盘服务 MediaService extends JobIntentService { }
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);
} else {
intent.setComponent(new ComponentName(context, MediaService.class));
MediaService.enqueueWork(context, intent);
}
}
}
三,MediaService中执行扫描起点
U盘插入ACTION为Intent.ACTION_MEDIA_MOUNTED,最终会走到onMediaMountedBroadcast
private static void onMediaMountedBroadcast(Context context, Intent intent)
throws IOException {
final StorageVolume volume = intent.getParcelableExtra(StorageVolume.EXTRA_STORAGE_VOLUME);
if (volume != null) {
// MediaVolume代表一个磁盘,信息有id,name等
MediaVolume mediaVolume = MediaVolume.fromStorageVolume(volume);
try (ContentProviderClient cpc = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
if (!((MediaProvider)cpc.getLocalContentProvider()).isVolumeAttached(mediaVolume)) {
// 扫描起点
onScanVolume(context, mediaVolume, REASON_MOUNTED);
} else {
Log.i(TAG, "Volume " + mediaVolume + " already attached");
}
}
} else {
Log.e(TAG, "Couldn't retrieve StorageVolume from intent");
}
}
四,扫描准备
外部磁盘扫描 provider.scanDirectory(volume.getPath(), reason);
//onScanVolume中部分逻辑
try (ContentProviderClient cpc = context.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY)) {
final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
provider.attachVolume(volume, /* validate */ true);
final ContentResolver resolver = ContentResolver.wrap(cpc.getLocalContentProvider());
ContentValues values = new ContentValues();
values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
Uri scanUri = resolver.insert(MediaStore.getMediaScannerUri(), values);
if (broadcastUri != null) {
// 开始扫描广播
context.sendBroadcastAsUser(
new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, broadcastUri), owner);
}
if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
for (File dir : FileUtils.getVolumeScanPaths(context, volumeName)) {
provider.scanDirectory(dir, reason);
}
} else {
// 外部磁盘扫描
provider.scanDirectory(volume.getPath(), reason);
}
resolver.delete(scanUri, null, null);
} finally {
if (broadcastUri != null) {
// 扫描结束广播
context.sendBroadcastAsUser(
new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, broadcastUri), owner);
}
}
五,执行扫描
MediaProvider.class中
public void scanDirectory(File file, int reason) {
// mMediaScanner 是 ModernMediaScanner
mMediaScanner.scanDirectory(file, reason);
}
以下内容在ModernMediaScanner中。 在scanDirectory方法中创建Scan,Scan构造器中有以下方法,代表当前卷轴。后续扫描,数据对比都是在当前卷中操作。
mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
run方法中调用runInternal(),主要执行三部操作。 第一步: walkFileTree,扫描文件,并执行插入操作。 第二步:更新当前卷轴媒体数据。 第三步:更新播放列表。
private void runInternal() {
final long startTime = SystemClock.elapsedRealtime();
// 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
if (mSingleFile && mScannedIds.size() == 1) {
// We can safely skip this step if the scan targeted a single
// file which we scanned above
} else {
// 第二步
reconcileAndClean();
}
// Third, resolve any playlists that we scanned
resolvePlaylists();
if (!mSingleFile) {
final long durationMillis = SystemClock.elapsedRealtime() - startTime;
Metrics.logScan(mVolumeName, mReason, mFileCount, durationMillis,
mInsertCount, mUpdateCount, mDeleteCount);
}
}
六,扫描文件
// 调用 Files.walkFileTree 遍历文件
private void walkFileTree() {
......
Files.walkFileTree(mRoot.toPath(), this);
......
}
上述方法会回调到 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 需要注意existingId,和插入更新有关。
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
......
long existingId = -1;
try (Cursor c = mResolver.query(mFilesUri, projection, queryArgs, mSignal)) {
if (c.moveToFirst()) {
// existingId 在 scanItemAudio创建ContentProviderOperation.Builder时使用 -1时执行插入,其他则是更新
existingId = c.getLong(0);
//记录id,在清除时会用到
mScannedIds.add(existingId);
// 下方还有挺多代码,不方便展示
.......
} finally {
Trace.endSection();
}
final ContentProviderOperation.Builder op;
Trace.beginSection("scanItem");
try {
// 根据existingId创建ContentProviderOperation.Builder
op = scanItem(existingId, realFile, attrs, actualMimeType, actualMediaType,
mVolumeName);
} finally {
Trace.endSection();
}
if (op != null) {
op.withValue(FileColumns._MODIFIER, FileColumns._MODIFIER_MEDIA_SCAN);
// 将ContentProviderOperation.Builder放入mPending
// mPending.add(op);
addPending(op.build());
// mPending大于32条,执行批量操作
maybeApplyPending();
}
}
七,更新媒体库数据
// 删除些不重要代码,保留核心逻辑
private void reconcileAndClean() {
//扫描时会将existingId值记录到scannedIds
final long[] scannedIds = mScannedIds.toArray();
Arrays.sort(scannedIds);
// 根据mFilesUri查找媒体库中的数据,并和scannedIds 进行对比,没有对比成功的是旧数据需要删除。
final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
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);
if (Arrays.binarySearch(scannedIds, id) < 0) {
//scannedIds保存的是本次操作的id,如果查询出来的媒体信息不在本次操作id中
final long dateExpire = c.getLong(2);
final boolean isPending = c.getInt(3) == 1;
if (isPending && dateExpire > System.currentTimeMillis() / 1000)
continue;
}
// 记录这些id
mUnknownIds.add(id);
}
}
} finally {
Trace.endSection();
}
// Third, clean all the unknown database entries found above
mSignal.throwIfCanceled();
Trace.beginSection("clean");
try {
for (int i = 0; i < mUnknownIds.size(); i++) {
final long id = mUnknownIds.get(i);
final Uri uri = MediaStore.Files.getContentUri(mVolumeName, id).buildUpon()
.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false")
.build();
// 删除没找到的id
addPending(ContentProviderOperation.newDelete(uri).build());
maybeApplyPending();
}
applyPending();
} finally {
if (mUnknownIds.size() > 0) {
String scanReason = "scan triggered by reason: " + translateReason(mReason);
Metrics.logDeletionPersistent(mVolumeName, scanReason, countPerMediaType);
}
Trace.endSection();
}
}
八,更新播放列表
表为:audio_playlists_map
标题五中的第三步resolvePlaylists()最后会调到MediaProvider.callInternal()方法。
case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
final LocalCallingIdentity token = clearLocalCallingIdentity();
final CallingIdentity providerToken = clearCallingIdentity();
try {
final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
resolvePlaylistMembers(playlistUri);
} finally {
restoreCallingIdentity(providerToken);
restoreLocalCallingIdentity(token);
}
return null;
}
private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
@NonNull SQLiteDatabase db) {
try {
// Refresh playlist members based on what we parse from disk
// 获取列表id
final long playlistId = ContentUris.parseId(playlistUri);
// 获取当前id下列表中的所有歌曲
final Map<String, Long> membersMap = getAllPlaylistMembers(playlistId);
// 根据列表id删除播放映射关系表中记录的信息
db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);
final Path playlistPath = queryForDataFile(playlistUri, null).toPath();
final Playlist playlist = new Playlist();
playlist.read(playlistPath.toFile());
final List<Path> members = playlist.asList();
for (int i = 0; i < members.size(); i++) {
try {
final Path audioPath = playlistPath.getParent().resolve(members.get(i));
// 更新或插入数据
final long audioId = queryForPlaylistMember(audioPath, membersMap);
final ContentValues values = new ContentValues();
values.put(Playlists.Members.PLAY_ORDER, i + 1);
values.put(Playlists.Members.PLAYLIST_ID, playlistId);
values.put(Playlists.Members.AUDIO_ID, audioId);
db.insert("audio_playlists_map", null, values);
} catch (IOException e) {
Log.w(TAG, "Failed to resolve playlist member", e);
}
}
} catch (IOException e) {
Log.w(TAG, "Failed to refresh playlist", e);
}
}