Android13 Media 扫盘源码分析(一)

646 阅读3分钟

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);
        }
    }