Android MediaProvider

本文以 Android 9.0 爲準

Android 系統提供了對多媒體的統一處理機制,通過一套良好的框架實現了多媒體信息的掃描、存儲、讀取。用戶可以基於這套框架非常方便的對多媒體信息進行處理,這套框架主要包含了三部分:

  • MediaScannerReceiver:多媒體掃描廣播接收者,繼承 BroadcastReceiver,主要響應APP發送的廣播命令,並開啓 MediaScannerService 執行掃描工作。
  • MediaScannerService:多媒體掃描服務,繼承 Service,主要是處理 APP 發送的請求,要用到 Framework 中的 MediaScanner 來共同完成具體掃描工作,並獲取媒體文件的 metadata,最後將數據寫入或刪除 MediaProvider 提供的數據庫中。
  • MediaProvider:多媒體內容提供者,繼承 ContentProvider,主要是負責操作數據庫,並提供給別的程序 insert、query、delete、update 等操作。

本文就從上面三部分作爲入口,分析它們是如何工作的,如何對設備上的多媒體進行掃描,如何將多媒體信息進行存儲,用戶如何讀取、修改多媒體信息?


1. 如何調用 MediaScannerService?

1.1 MediaScannerActivity

我們可以從 Android 自帶的 Dev Tools 中的 MediaScannerActivity 入手,看看它是如何掃描多媒體。
/development/apps/Development/src/com/android/development/MediaScannerActivity.java

package com.android.development;

public class MediaScannerActivity extends Activity
{
    private TextView mTitle;

    /** Called when the activity is first created or resumed. */
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        setContentView(R.layout.media_scanner_activity);

        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_STARTED);
        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
        intentFilter.addDataScheme("file");
        registerReceiver(mReceiver, intentFilter);

        mTitle = (TextView) findViewById(R.id.title);
    }

    /** Called when the activity going into the background or being destroyed. */
    @Override
    public void onDestroy() {
        unregisterReceiver(mReceiver);
        super.onDestroy();
    }

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
                mTitle.setText("Media Scanner started scanning " + intent.getData().getPath());
            }
            else if (intent.getAction().equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
                mTitle.setText("Media Scanner finished scanning " + intent.getData().getPath());
            }
        }
    };

    public void startScan(View v) {
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"
                + Environment.getExternalStorageDirectory())));

        mTitle.setText("Sent ACTION_MEDIA_MOUNTED to trigger the Media Scanner.");
    }
}

主要做了兩件事:

  • 註冊掃描開始和結束的廣播,用來展示掃描狀態;
  • 在點擊事件中,發送了 ACTION_MEDIA_MOUNTED 廣播。

那麼系統肯定存在一個的接收者,在收到 ACTION_MEDIA_MOUNTED 後進行掃描,這就是 MediaScannerReceiver。

1.2 MediaScannerReceiver

首先關注 AndroidManifest,對接受的廣播一目瞭然。
/packages/providers/MediaProvider/AndroidManifest.xml

<receiver android:name="MediaScannerReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.LOCALE_CHANGED" />
    </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_UNMOUNTED" />
        <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>

/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerReceiver.java

package com.android.providers.media;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;

import java.io.File;
import java.io.IOException;

public class MediaScannerReceiver extends BroadcastReceiver {
    private final static String TAG = "MediaScannerReceiver";

    @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(context, MediaProvider.INTERNAL_VOLUME);
        } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
            // 處理系統語言變換
            scanTranslatable(context);
        } else {
            if (uri.getScheme().equals("file")) {
                // 處理外部存儲
                String path = uri.getPath();
                String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
                String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();

                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    Log.e(TAG, "couldn't canonicalize " + path);
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                }

                Log.d(TAG, "action: " + action + " path: " + path);
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                    // 每當掛載外部存儲時
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    // 掃描單個文件,並且路徑是在外部存儲路徑下
                    scanFile(context, path);
                }
            }
        }
    }

    // 掃描內部或者外部存儲,根據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));
    }

    // 掃描單個文件,不可以是文件夾
    private void scanFile(Context context, String path) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }

    // 掃描可轉換語言的多媒體
    private void scanTranslatable(Context context) {
        final Bundle args = new Bundle();
        args.putBoolean(MediaStore.RETRANSLATE_CALL, true);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }
}

再對比下 Android 6.0 中 MediaScannerReceiver 源碼:

package com.android.providers.media;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.IOException;

public class MediaScannerReceiver extends BroadcastReceiver {
    private final static String TAG = "MediaScannerReceiver";

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

        } else {
            if (uri.getScheme().equals("file")) {
                // handle intents related to external storage
                String path = uri.getPath();
                String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
                String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();

                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    Log.e(TAG, "couldn't canonicalize " + path);
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                }

                Log.d(TAG, "action: " + action + " path: " + path);
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                    // scan whenever any volume is mounted
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    scanFile(context, path);
                }
            }
        }
    }

    private void scan(Context context, String volume) {
        Bundle args = new Bundle();
        args.putString("volume", volume);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }

    private void scanFile(Context context, String path) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }
}

掃描的時機爲以下幾點:

  1. Intent.ACTION_BOOT_COMPLETED.equals(action)
    6.0 中接到設備重啓的廣播,對 Internal 和 External 掃描,而 9.0 中只對 Internal 掃描。
  2. Intent.ACTION_LOCALE_CHANGED.equals(action)
    9.0 相比 6.0 增加了系統語言發生改變時的廣播,用於進行掃描可以轉換語言的多媒體。
  3. uri.getScheme().equals("file")
    6.0 和 9.0 處理的一致,都是先過濾 scheme 爲 "file" 的 Intent,再通過下面兩個 action 對 External 進行掃描:
    • Intent.ACTION_MEDIA_MOUNTED.equals(action)
      插入外部存儲時掃描 scan()。
    • Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) && path != null && path.startsWith(externalStoragePath + "/")
      掃描外部存儲中的單個文件 scanFile()。

注意:不支持掃描外部存儲中的文件夾,需要遍歷文件夾中文件,使用掃描單個文件的方式。


2. MediaScannerService 如何工作?

MediaScannerService 繼承 Service,並實現 Runnable 工作線程。通過 ServiceHandler 這個 Handler 把主線程需要大量計算的工作放到工作線程中。
/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java

2.1 在 onCreate() 中啓動工作線程:

@Override
public void onCreate() {
    // 獲取電源鎖,防止掃描過程中休眠
    PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    // 獲取外部存儲掃描路徑
    StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
    mExternalStoragePaths = storageManager.getVolumePaths();

    // Start up the thread running the service.  Note that we create a
    // separate thread because the service normally runs in the process's
    // main thread, which we don't want to block.
    Thread thr = new Thread(null, this, "MediaScannerService"); // 啓動最重要的工作線程,該線程也是個消息泵線程
    thr.start();
}

可以看到,onCreate() 裏會啓動最重要的工作線程,該線程也是個消息泵線程。每當用戶需要掃描媒體文件時,基本上都是在向這個消息泵裏發送 Message,並在處理 Message 時完成真正的 scan 動作。請注意,創建 Thread 時傳入的第二個參數就是 MediaScannerService 自身,也就是說線程的主要行爲其實就是 MediaScannerService 的 run() 方法,該方法的代碼如下:

@Override
public void run() {
    // reduce priority below other background threads to avoid interfering
    // with other services at boot time.
    // 設置進程優先級,媒體掃描比較費時,防止 CPU 一直被 MediaScannerService 佔用
    // 這會導致用戶感覺系統變得很慢
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
            Process.THREAD_PRIORITY_LESS_FAVORABLE);
    Looper.prepare();

    mServiceLooper = Looper.myLooper(); // 消息looper
    mServiceHandler = new ServiceHandler(); // 發送消息的handler

    Looper.loop();
}

後續就是通過上面那個 mServiceHandler 向消息隊列發送 Message 的。

2.2 向工作線程發送 Message

比較常見的向消息泵發送 Message 的做法是調用 startService(),並在 MediaScannerService 的 onStartCommand() 方法裏 sendMessage()。比如,和 MediaScannerService 配套提供的 MediaScannerReceiver,當它收到類似 ACTION_BOOT_COMPLETED 這樣的系統廣播時,就會調用自己的 scan() 或 scanFile() 方法,裏面的 startService() 動作會導致走到 service 的 onStartCommand(),並進一步發送消息,其方法截選如下:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    ...
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent.getExtras();
    mServiceHandler.sendMessage(msg); // 發送消息

    // Try again later if we are killed before we can finish scanning.
    return Service.START_REDELIVER_INTENT;
}

另外一種比較常見的發送 Message 的做法是先直接或間接 bindService(),綁定成功後會得到一個 IMediaScannerService 接口,而後外界再通過該接口向 MediaScannerService 發起命令,請求其掃描特定文件或目錄。

IMediaScannerService 接口只提供了兩個接口方法:

  • void requestScanFile(String path, String mimeType, in IMediaScannerListener listener);
  • void scanFile(String path, String mimeType);

處理這兩種請求的實體是服務內部的 mBinder 對象,參考代碼如下:

private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() {
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener) {
        if (false) {
            Log.d(TAG, "IMediaScannerService.scanFile: " + path + " mimeType: " + mimeType);
        }
        Bundle args = new Bundle();
        args.putString("filepath", path);
        args.putString("mimetype", mimeType);
        if (listener != null) {
            args.putIBinder("listener", listener.asBinder());
        }
        startService(new Intent(MediaScannerService.this,
                MediaScannerService.class).putExtras(args));
    }

    public void scanFile(String path, String mimeType) {
        requestScanFile(path, mimeType, null);
    }
};

說到底還是在調用 startService()。

具體處理消息泵線程裏的消息時,執行的是 ServiceHandler 的 handleMessage() 方法:

private final class ServiceHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        Bundle arguments = (Bundle) msg.obj;
        String filePath = arguments.getString("filepath");

        try {
            if (filePath != null) {
                // 掃描單個文件
                ...
                try {
                    uri = scanFile(filePath, arguments.getString("mimetype"));
                } catch (Exception e) {
                    Log.e(TAG, "Exception scanning file", e);
                }
                ...
            } else if (arguments.getBoolean(MediaStore.RETRANSLATE_CALL)) {
                // 切換語言
                ContentProviderClient mediaProvider = getBaseContext().getContentResolver()
                    .acquireContentProviderClient(MediaStore.AUTHORITY);
                mediaProvider.call(MediaStore.RETRANSLATE_CALL, null, null);
            } else {
                // 掃描內部或外部
                String volume = arguments.getString("volume");
                String[] directories = null;

                if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
                    // 如果是掃描內部存儲,實際掃描目錄爲
                    directories = new String[] {
                            Environment.getRootDirectory() + "/media",
                            Environment.getOemDirectory() + "/media",
                            Environment.getProductDirectory() + "/media",
                }
                else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                    // 如果是掃描外部存儲,實際掃描目錄爲
                    if (getSystemService(UserManager.class).isDemoUser()) {
                        directories = ArrayUtils.appendElement(String.class,
                                mExternalStoragePaths,
                                Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
                    } else {
                        directories = mExternalStoragePaths;
                    }
                }
                // 調用 scan 函數開展文件夾掃描工作
                if (directories != null) {
                    scan(directories, volume);
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Exception in handleMessage", e);
        }

        stopSelf(msg.arg1); // 掃描結束,MediaScannerService完成本次使命,可以stop自身了
    }
}

MediaScannerService中 的 scanFile() 方法,用來掃描單個文件:

private Uri scanFile(String path, String mimeType) {
    String volumeName = MediaProvider.EXTERNAL_VOLUME;

    try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
        // make sure the file path is in canonical form
        String canonicalPath = new File(path).getCanonicalPath();
        return scanner.scanSingleFile(canonicalPath, mimeType);
    } catch (Exception e) {
        Log.e(TAG, "bad path " + path + " in scanFile()", e);
        return null;
    }
}

MediaScannerService 中的 scan() 方法,用來掃描內部或外部存儲的路徑:

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);
        // 通過 insert 這個特殊的 uri,讓 MeidaProvider 做一些準備工作
        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);
            }
            // 創建媒體掃描器,並調用 scanDirectories 掃描目標文件夾
            try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                scanner.scanDirectories(directories);
            }
        } catch (Exception e) {
            Log.e(TAG, "exception in MediaScanner.scan()", e);
        }
        // 通過 delete 這個 uri,讓 MeidaProvider 做一些清理工作
        getContentResolver().delete(scanUri, null, null);

    } finally {
        // 發送結束掃描的廣播
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
        mWakeLock.release();
    }
}

上面的代碼中,比較複雜的是 MediaScannerService 和 MediaProvider 的交互。MediaScannerService 經常使用一些特殊 Uri 做數據庫操作,而 MediaProvider 針對這些 Uri 會走一些特殊的處理,例如打開數據庫文件等,後面在 MediaProvider 中會重點說到,我們還先回到掃描的邏輯。

scanFile() 或 scan() 纔是實際進行掃描的地方,掃描動作中主要藉助的是 MediaScanner,它是打通 Java 層和 C++ 層的關鍵,掃描動作最終會調用到 MediaScanner的某個 native 函數,於是程序流程開始走到 C++ 層。

現在,我們可以畫一張示意圖:


3. MediaScanner 如何工作?

顧名思義,MediaScanner 就是個“媒體文件掃描器”。它必須打通 Java 層和 C++ 層。請大家注意它的兩個 native 函數:native_init() 和 native_setup(),以及兩個重要成員變量:一個是mClient成員,另一個是 mNativeContext,後面會詳細說明。
/frameworks/base/media/java/android/media/MediaScanner.java
MediaScanner的相關代碼截選如下:

public class MediaScanner implements AutoCloseable {
    static {
        System.loadLibrary("media_jni");
        native_init();    // 將java層和c++層聯繫起來
    }
    ...
    private long mNativeContext;
    ...
    public MediaScanner(Context c, String volumeName) {
        native_setup();
        ...
    }
    ...
    // 一開始就具有明確的mClient對象
    private final MyMediaScannerClient mClient = new MyMediaScannerClient();
    ...
}

MediaScanner 類加載之時,就會同時加載動態鏈接庫“media_jni”,並調用 native_init() 將 Java 層和 C++ 層聯繫起來。
/frameworks/base/media/jni/android_media_MediaScanner.cpp

// This function gets a field ID, which in turn causes class initialization.
// It is called from a static block in MediaScanner, which won't run until the
// first time an instance of this class is used.
static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    // Java 層 MediaScanner 類
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }
    // Java 層 mNativeContext 對象(long 類型)保存在 JNI 層 fields.context 對象中
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

經過分析代碼,我們發現在 C++ 層會有個與 MediaScanner 相對應的類,叫作 StagefrightMediaScanner。當 Java層創建 MediaScanner 對象時,MediaScanner 的構造函數就調用了 native_setup(),該函數對應到 C++ 層就是 android_media_MediaScanner_native_setup(),其代碼如下:
/frameworks/base/media/jni/android_media_MediaScanner.cpp

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    ALOGV("native_setup");
    // 創建 native 層的 MediaScanner 對象,StagefrightMediaScanner(frameworks/av/ 中定義)
    MediaScanner *mp = new StagefrightMediaScanner;

    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }
    // 將 mp 指針保存在 Java 層 MediaScanner 類 mNativeContext 對象中
    env->SetLongField(thiz, fields.context, (jlong)mp);
}

最後一句 env->SetLongField() 其實就是在爲 Java 層 MediaScanner 的 mNativeContext 域賦值。

後續我們會看到,每當 C++ 層執行掃描動作時,還會再創建一個 MyMediaScannerClient 對象,這個對象和 Java 層的同名類對應。我們畫一張圖來說明:

3.1 scanSingleFile() 動作

// this function is used to scan a single file
public Uri scanSingleFile(String path, String mimeType) {
    try {
        prescan(path, true); // ① 掃描前預準備

        File file = new File(path);
        if (!file.exists() || !file.canRead()) {
            return null;
        }

        // lastModified is in milliseconds on Files.
        long lastModifiedSeconds = file.lastModified() / 1000;

        // always scan the file, so we can return the content://media Uri for existing files
        // ② 掃描前預準備
        return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
                false, true, MediaScanner.isNoMediaPath(path));
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
        return null;
    } finally {
        releaseResources();
    }
}

先看①處代碼, prescan 函數比較關鍵,首先讓我們試想一個問題。

在媒體掃描過程中,有個令人頭疼的問題,來舉個例子:假設某次掃描前 SD 卡中有 100 個媒體文件,數據庫中會有 100 條關於這些文件的記錄。現刪除其中的 50 個文件,那麼媒體數據庫什麼時候會被更新呢?

MediaScanner 考慮到了這一點,prescan 函數的主要作用就是在掃描之前把上次掃描獲取的數據庫信息取出遍歷並檢測是否丟失,如果丟失,則從數據庫中刪除。

再看②處代碼,藉助了 mClient.doScanFile(),此處的 mClient 類型爲 MyMediaScannerClient。

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    try {
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
        ...

        // 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 {
                ...
                // we only extract metadata for audio and video files
                if (isaudio || isvideo) {
                    mScanSuccess = processFile(path, mimeType, this);
                }

                if (isimage) {
                    mScanSuccess = processImageFile(path);
                }
                ...
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    }
    ...
    return result;
}

因爲 MyMediaScannerClient 是 MediaScanner 的內部類,所以它可以直接調用 MediaScanner 的 processFile()。

現在我們畫一張 MediaScannerService.scanFile() 的調用關係圖:

3.2 scanDirectories() 動作

public void scanDirectories(String[] directories) {
    try {
        prescan(null, true);  // 掃描前預準備
        ...
        for (int i = 0; i < directories.length; i++) {
            // native 函數,調用它來對目標文件夾進行掃描
            processDirectory(directories[i], mClient); 
        }
        ...
        postscan(directories);  // 掃描後處理
    } catch (SQLException e) {
        ...
    } finally {
        ...
    }
}

我們畫一張 MediaScannerService .scan() 的調用關係圖:


4. 調用到 C++ 層

這裏就不深入展開,可以看下這篇文章《MediaScannerService研究》


5. MediaProvider 如何工作

/packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java

5.1 MediaProvider 何時創建數據庫

通常來說,數據庫的創建,應該在 ContentProvider 的 onCreate() 方法中完成 。

@Override
public boolean onCreate() {
    ...
    // DatabaseHelper緩存
    mDatabases = new HashMap<String, DatabaseHelper>();
    // 綁定內部存儲數據庫
    attachVolume(INTERNAL_VOLUME);
    ...
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
         // 如果已掛載外部存儲,綁定外部存儲數據庫
        attachVolume(EXTERNAL_VOLUME);
    }
    ...
    return true;
}

接下來分析attachVolume方法:

創建數據庫:

  • 如果此存儲卷已經鏈接上了,則不執行任何操作。
  • 否則,查詢存儲卷的 ID 並且建立對應的數據庫。
/**
 * Attach the database for a volume (internal or external).
 * Does nothing if the volume is already attached, otherwise
 * checks the volume ID and sets up the corresponding database.
 *
 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
 * @return the content URI of the attached volume.
 */
private Uri attachVolume(String volume) {
    ...
    // Update paths to reflect currently mounted volumes
    // 更新路徑以反映當前裝載的卷
    updateStoragePaths();

    DatabaseHelper helper = null;
    synchronized (mDatabases) {
        helper = mDatabases.get(volume);
        // 判斷是否已經attached過了
        if (helper != null) {
            if (EXTERNAL_VOLUME.equals(volume)) {
                // 確保默認的文件夾已經被創建在掛載的主要存儲設備上,
                // 對每個存儲卷只做一次這種操作,所以當用戶手動刪除時不會打擾
                ensureDefaultFolders(helper, helper.getWritableDatabase());
            }
            return Uri.parse("content://media/" + volume);
        }

        Context context = getContext();
        if (INTERNAL_VOLUME.equals(volume)) {
            // 如果是內部存儲則直接實例化DatabaseHelper
            helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
                    false, mObjectRemovedCallback);
        } else if (EXTERNAL_VOLUME.equals(volume)) {
            // 如果是外部存儲的操作,只獲取主要的外部卷 ID
            // Only extract FAT volume ID for primary public
            final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume();
            if (vol != null) {// 判斷是否存在主要的外部卷
                // 獲取主要的外部卷
                final StorageVolume actualVolume = mStorageManager.getPrimaryVolume();
                // 獲取主要的外部卷 ID
                final int volumeId = actualVolume.getFatVolumeId();

                // Must check for failure!
                // If the volume is not (yet) mounted, this will create a new
                // external-ffffffff.db database instead of the one we expect.  Then, if
                // android.process.media is later killed and respawned, the real external
                // database will be attached, containing stale records, or worse, be empty.
                // 數據庫都是以類似 external-ffffffff.db 的形式命名的,
                // 後面的 8 個 16 進制字符是該 SD 卡 FAT 分區的 Volume ID。
                // 該 ID 是分區時決定的,只有重新分區或者手動改變纔會更改,
                // 可以防止插入不同 SD 卡時數據庫衝突。
                if (volumeId == -1) {
                    String state = Environment.getExternalStorageState();
                    if (Environment.MEDIA_MOUNTED.equals(state) ||
                            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
                        // This may happen if external storage was _just_ mounted.  It may also
                        // happen if the volume ID is _actually_ 0xffffffff, in which case it
                        // must be changed since FileUtils::getFatVolumeId doesn't allow for
                        // that.  It may also indicate that FileUtils::getFatVolumeId is broken
                        // (missing ioctl), which is also impossible to disambiguate.
                        // 已經掛載但是sd卡是隻讀狀態
                        Log.e(TAG, "Can't obtain external volume ID even though it's mounted.");
                    } else {
                        // 還沒有掛載
                        Log.i(TAG, "External volume is not (yet) mounted, cannot attach.");
                    }

                    throw new IllegalArgumentException("Can't obtain external volume ID for " +
                            volume + " volume.");
                }

                // generate database name based on volume ID
                // 根據volume ID設置數據庫的名稱
                String dbName = "external-" + Integer.toHexString(volumeId) + ".db";
                // 創建外部存儲數據庫
                helper = new DatabaseHelper(context, dbName, false,
                        false, mObjectRemovedCallback);
                mVolumeId = volumeId;
            } else {
                // external database name should be EXTERNAL_DATABASE_NAME
                // however earlier releases used the external-XXXXXXXX.db naming
                // for devices without removable storage, and in that case we need to convert
                // to this new convention
                // 外部數據庫名稱應爲EXTERNAL_DATABASE_NAME
                // 但是較早的版本對沒有可移動存儲的設備使用external-XXXXXXXX.db命名
                // 在這種情況下,我們需要轉換爲新的約定
                ...
                // 根據之前轉換的數據庫名,創建數據庫
                helper = new DatabaseHelper(context, dbFile.getName(), false,
                        false, mObjectRemovedCallback);
            }
        } else {
            throw new IllegalArgumentException("There is no volume named " + volume);
        }
        // 緩存起來,標識已經創建過了數據庫
        mDatabases.put(volume, helper);
        ...
    }

    if (EXTERNAL_VOLUME.equals(volume)) {
        // 給外部存儲創建默認的文件夾
        ensureDefaultFolders(helper, helper.getWritableDatabase());
    }
    return Uri.parse("content://media/" + volume);
}

先來關注一下 getPrimaryPhysicalVolume() 這個相關的方法:
/frameworks/base/core/java/android/os/storage/StorageManager.java

// 獲取主要的外部的 VolumeInfo
public @Nullable VolumeInfo getPrimaryPhysicalVolume() {
    final List<VolumeInfo> vols = getVolumes();
    for (VolumeInfo vol : vols) {
        if (vol.isPrimaryPhysical()) {
            return vol;
        }
    }
    return null;
}

/frameworks/base/core/java/android/os/storage/VolumeInfo.java

// 判斷該 VolumeInfo 是否是主要的,並且是外部的
public boolean isPrimaryPhysical() {
    return isPrimary() && (getType() == TYPE_PUBLIC);
}

// 判斷該 VolumeInfo 是否是主要的
public boolean isPrimary() {
    return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
}

下面就是分析創建數據庫的源頭DatabaseHelper:

/**
 * Creates database the first time we try to open it.
 */
@Override
public void onCreate(final SQLiteDatabase db) {
    // 在此方法中對700版本以下的都會新建數據庫
    updateDatabase(mContext, db, mInternal, 0, getDatabaseVersion(mContext));
}

/**
 * Updates the database format when a new content provider is used
 * with an older database format.
 */
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
    // 對數據庫進行更新
    mUpgradeAttempted = true;
    updateDatabase(mContext, db, mInternal, oldV, newV);
}

這裏強調一句 getDatabaseVersion() 方法獲取的 fromVersion 不是數據庫版本,而是 /packages/providers/MediaProvider/AndroidManifest.xml 中的 versionCode。

現在已經找到創建數據庫的方法updateDatabase,現在大致分析一下此方法:

/**
 * This method takes care of updating all the tables in the database to the
 * current version, creating them if necessary.
 * This method can only update databases at schema 700 or higher, which was
 * used by the KitKat release. Older database will be cleared and recreated.
 * @param db Database
 * @param internal True if this is the internal media database
 */
private static void updateDatabase(Context context, SQLiteDatabase db, boolean internal,
        int fromVersion, int toVersion) {
    ...
    // 對不同版本的數據庫進行判斷
    if (fromVersion < 700) {
        // 小於700,重新創建數據庫
        createLatestSchema(db, internal);
    } else if (fromVersion < 800) {
        // 對700-800之間的數據庫處理
        updateFromKKSchema(db);
    } else if (fromVersion < 900) {
        // 對800-900之間的數據庫處理
        updateFromOCSchema(db);
    }
    // 檢查audio_meta的_data值是否是不同的,如果不同就刪除audio_meta,
    // 在掃描的時候重新創建
    sanityCheck(db, fromVersion);
}

那麼還有一個疑惑,我們知道 ContentProvider 的 onCreate() 執行時間,早於 Application onCreate(),那麼在 onCreate() 之後掛載外部存儲,是如何處理的呢?

搜索 attachVolume() 的調用位置,可以找到在 insertInternal() 中看到:

private Uri insertInternal(Uri uri, int match, ContentValues initialValues,
                           ArrayList<Long> notifyRowIds) {
    ...
    switch (match) {
        ...
        case VOLUMES:
        {
            String name = initialValues.getAsString("name");
            // 根據name綁定存儲數據庫
            Uri attachedVolume = attachVolume(name);
            ...
            return attachedVolume;
        }
    ...
}

根據 VOLUMES 找到對應的 URI:

URI_MATCHER.addURI("media", null, VOLUMES);

而調用 insertInternal() 方法的地方,是在 insert() 方法中。

那麼說明,必然存在一個調用 insert() 方法,並傳入了 "content://media/" 的URI,可以在 MediaScannerService 的 openDatabase() 方法中找到:
/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java

private void openDatabase(String volumeName) {
    try {
        ContentValues values = new ContentValues();
        values.put("name", volumeName);
        getContentResolver().insert(Uri.parse("content://media/"), values);
    } catch (IllegalArgumentException ex) {
        Log.w(TAG, "failed to open media database");
    }         
}

調用 openDatabase() 方法的地方就是在開始掃描外部存儲的時候,也就在這個時候,進行了 DatabaseHelper 的實例化,在前文已經分析了 scan() 的代碼,爲了方便查看,這裏再列該方法:

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

            try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                scanner.scanDirectories(directories);
            }
        } 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();
    }
}

至此,對於數據庫的創建已經分析完畢。

5.2 MediaProvider 更新

@Override
public int (Uri uri, ContentValues initialValues, String userWhere,
        String[] whereArgs) {
    // 將uri進行轉換成合適的格式,去除標準化
    uri = safeUncanonicalize(uri);
    int count;
    // 對uri進行匹配
    int match = URI_MATCHER.match(uri);
    // 返回查詢的對應uri的數據庫幫助類
    DatabaseHelper helper = getDatabaseForUri(uri);
    // 記錄更新的次數
    helper.mNumUpdates++;
    // 通過可寫的方式獲得數據庫實例
    SQLiteDatabase db = helper.getWritableDatabase();
    String genre = null;
    if (initialValues != null) {
        // 獲取流派的信息,然後刪除掉
        genre = initialValues.getAsString(Audio.AudioColumns.GENRE);
        initialValues.remove(Audio.AudioColumns.GENRE);
    }
    ...
    // 根據匹配的uri進行相應的操作
    switch (match) {
        case AUDIO_MEDIA:
        case AUDIO_MEDIA_ID:
        // 更新音樂人和專輯字段。首先從緩存中判斷是否有值,如果有直接用緩存中的
        // 數據,如果沒有再從數據庫中查詢是否有對應的信息,如果有則更新,
        // 如果沒有插入這條數據.接下來的操作是增加更新次數,並更新流派
        ...
        case IMAGES_MEDIA:
        case IMAGES_MEDIA_ID:
        case VIDEO_MEDIA:
        case VIDEO_MEDIA_ID:
        // 更新視頻,並且發出生成略縮圖請求
        ...
        case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
        // 更新播放列表數據
        ...
    }
    ...
}

至此,更新操作已完成。

5.3 MediaProvider 插入

關於插入,有兩個方法插入,一個是大量的插入 bulkInsert 方法傳入的是 ContentValues 數組;一個是 insert,傳入的是單一個 ContentValues。下面分別分析:

@Override
public int bulkInsert(Uri uri, ContentValues values[]) {
    // 首先對傳入的Uri進行匹配
    int match = URI_MATCHER.match(uri);
    if (match == VOLUMES) {
        // 如果是匹配的是存儲卷,則直接調用父類的方法,進行循環插入
        return super.bulkInsert(uri, values);
    }
    // 對DatabaseHelper和SQLiteDatabase的初始化
    DatabaseHelper helper = getDatabaseForUri(uri);
    if (helper == null) {
        throw new UnsupportedOperationException(
                "Unknown URI: " + uri);
    }
    SQLiteDatabase db = helper.getWritableDatabase();
    if (db == null) {
        throw new IllegalStateException("Couldn't open database for " + uri);
    }

    if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) {
        // 插入播放列表的數據,在playlistBulkInsert中是開啓的事務進行插入
        return playlistBulkInsert(db, uri, values);
    } else if (match == MTP_OBJECT_REFERENCES) {
        // 將MTP對象的ID轉換成音頻的ID,最終也是調用到playlistBulkInsert
        int handle = Integer.parseInt(uri.getPathSegments().get(2));
        return setObjectReferences(helper, db, handle, values);
    }

    ArrayList<Long> notifyRowIds = new ArrayList<Long>();
    int numInserted = 0;
    // insert may need to call getParent(), which in turn may need to update the database,
    // so synchronize on mDirectoryCache to avoid deadlocks
    synchronized (mDirectoryCache) {
         // 如果不滿足上述的條件,則開啓事務進行插入其他的數據
        db.beginTransaction();
        try {
            int len = values.length;
            for (int i = 0; i < len; i++) {
                if (values[i] != null) {
                    // 循環調用insertInternal去插入相關的數據
                    insertInternal(uri, match, values[i], notifyRowIds);
                }
            }
            numInserted = len;
            db.setTransactionSuccessful();
        } finally {
            // 結束事務
            db.endTransaction();
        }
    }

    // 通知更新
    getContext().getContentResolver().notifyChange(uri, null);
    return numInserted;
}

@Override
public Uri insert(Uri uri, ContentValues initialValues) {
    int match = URI_MATCHER.match(uri);

    ArrayList<Long> notifyRowIds = new ArrayList<Long>();
    // 只是調用insertInternal進行插入
    Uri newUri = insertInternal(uri, match, initialValues, notifyRowIds);

    // do not signal notification for MTP objects.
    // we will signal instead after file transfer is successful.
    if (newUri != null && match != MTP_OBJECTS) {
        // Report a general change to the media provider.
        // We only report this to observers that are not looking at
        // this specific URI and its descendants, because they will
        // still see the following more-specific URI and thus get
        // redundant info (and not be able to know if there was just
        // the specific URI change or also some general change in the
        // parent URI).
        getContext().getContentResolver().notifyChange(uri, null, match != MEDIA_SCANNER
                ? ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS : 0);
        // Also report the specific URIs that changed.
        if (match != MEDIA_SCANNER) {
            getContext().getContentResolver().notifyChange(newUri, null, 0);
        }
    }
    return newUri;
}

5.4 MediaProvider 刪除

@Override
public int delete(Uri uri, String userWhere, String[] whereArgs) {
    uri = safeUncanonicalize(uri);
    int count;
    int match = URI_MATCHER.match(uri);

    // handle MEDIA_SCANNER before calling getDatabaseForUri()
    if (match == MEDIA_SCANNER) {
        if (mMediaScannerVolume == null) {
            return 0;
        }
        DatabaseHelper database = getDatabaseForUri(
                Uri.parse("content://media/" + mMediaScannerVolume + "/audio"));
        if (database == null) {
            Log.w(TAG, "no database for scanned volume " + mMediaScannerVolume);
        } else {
            database.mScanStopTime = SystemClock.currentTimeMicro();
            String msg = dump(database, false);
            logToDb(database.getWritableDatabase(), msg);
        }
        if (INTERNAL_VOLUME.equals(mMediaScannerVolume)) {
            // persist current build fingerprint as fingerprint for system (internal) sound scan
            final SharedPreferences scanSettings =
                    getContext().getSharedPreferences(MediaScanner.SCANNED_BUILD_PREFS_NAME,
                            Context.MODE_PRIVATE);
            final SharedPreferences.Editor editor = scanSettings.edit();
            editor.putString(MediaScanner.LAST_INTERNAL_SCAN_FINGERPRINT, Build.FINGERPRINT);
            editor.apply();
        }
        mMediaScannerVolume = null;
        pruneThumbnails();
        return 1;
    }

    if (match == VOLUMES_ID) {
        detachVolume(uri);
        count = 1;
    } else if (match == MTP_CONNECTED) {
        synchronized (mMtpServiceConnection) {
            if (mMtpService != null) {
                // MTP has disconnected, so release our connection to MtpService
                getContext().unbindService(mMtpServiceConnection);
                count = 1;
                // mMtpServiceConnection.onServiceDisconnected might not get called,
                // so set mMtpService = null here
                mMtpService = null;
            } else {
                count = 0;
            }
        }
    } else {
        final String volumeName = getVolumeName(uri);
        final boolean isExternal = "external".equals(volumeName);

        DatabaseHelper database = getDatabaseForUri(uri);
        if (database == null) {
            throw new UnsupportedOperationException(
                    "Unknown URI: " + uri + " match: " + match);
        }
        database.mNumDeletes++;
        SQLiteDatabase db = database.getWritableDatabase();

        TableAndWhere tableAndWhere = getTableAndWhere(uri, match, userWhere);
        if (tableAndWhere.table.equals("files")) {
            String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
            if (deleteparam == null || ! deleteparam.equals("false")) {
                database.mNumQueries++;
                Cursor c = db.query(tableAndWhere.table,
                        sMediaTypeDataId,
                        tableAndWhere.where, whereArgs,
                        null /* groupBy */, null /* having */, null /* orderBy */);
                String [] idvalue = new String[] { "" };
                String [] playlistvalues = new String[] { "", "" };
                MiniThumbFile imageMicroThumbs = null;
                MiniThumbFile videoMicroThumbs = null;
                try {
                    while (c.moveToNext()) {
                        final int mediaType = c.getInt(0);
                        final String data = c.getString(1);
                        final long id = c.getLong(2);

                        if (mediaType == FileColumns.MEDIA_TYPE_IMAGE) {
                            deleteIfAllowed(uri, data);
                            MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                    volumeName, FileColumns.MEDIA_TYPE_IMAGE, id);

                            idvalue[0] = String.valueOf(id);
                            database.mNumQueries++;
                            Cursor cc = db.query("thumbnails", sDataOnlyColumn,
                                        "image_id=?", idvalue,
                                        null /* groupBy */, null /* having */,
                                        null /* orderBy */);
                            try {
                                while (cc.moveToNext()) {
                                    deleteIfAllowed(uri, cc.getString(0));
                                }
                                database.mNumDeletes++;
                                db.delete("thumbnails", "image_id=?", idvalue);
                            } finally {
                                IoUtils.closeQuietly(cc);
                            }
                            if (isExternal) {
                                if (imageMicroThumbs == null) {
                                    imageMicroThumbs = MiniThumbFile.instance(
                                            Images.Media.EXTERNAL_CONTENT_URI);
                                }
                                imageMicroThumbs.eraseMiniThumb(id);
                            }
                        } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) {
                            deleteIfAllowed(uri, data);
                            MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                    volumeName, FileColumns.MEDIA_TYPE_VIDEO, id);

                            idvalue[0] = String.valueOf(id);
                            database.mNumQueries++;
                            Cursor cc = db.query("videothumbnails", sDataOnlyColumn,
                                        "video_id=?", idvalue, null, null, null);
                            try {
                                while (cc.moveToNext()) {
                                    deleteIfAllowed(uri, cc.getString(0));
                                }
                                database.mNumDeletes++;
                                db.delete("videothumbnails", "video_id=?", idvalue);
                            } finally {
                                IoUtils.closeQuietly(cc);
                            }
                            if (isExternal) {
                                if (videoMicroThumbs == null) {
                                    videoMicroThumbs = MiniThumbFile.instance(
                                            Video.Media.EXTERNAL_CONTENT_URI);
                                }
                                videoMicroThumbs.eraseMiniThumb(id);
                            }
                        } else if (mediaType == FileColumns.MEDIA_TYPE_AUDIO) {
                            if (!database.mInternal) {
                                MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                        volumeName, FileColumns.MEDIA_TYPE_AUDIO, id);

                                idvalue[0] = String.valueOf(id);
                                database.mNumDeletes += 2; // also count the one below
                                db.delete("audio_genres_map", "audio_id=?", idvalue);
                                // for each playlist that the item appears in, move
                                // all the items behind it forward by one
                                Cursor cc = db.query("audio_playlists_map",
                                            sPlaylistIdPlayOrder,
                                            "audio_id=?", idvalue, null, null, null);
                                try {
                                    while (cc.moveToNext()) {
                                        playlistvalues[0] = "" + cc.getLong(0);
                                        playlistvalues[1] = "" + cc.getInt(1);
                                        database.mNumUpdates++;
                                        db.execSQL("UPDATE audio_playlists_map" +
                                                " SET play_order=play_order-1" +
                                                " WHERE playlist_id=? AND play_order>?",
                                                playlistvalues);
                                    }
                                    db.delete("audio_playlists_map", "audio_id=?", idvalue);
                                } finally {
                                    IoUtils.closeQuietly(cc);
                                }
                            }
                        } else if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
                            // TODO, maybe: remove the audio_playlists_cleanup trigger and
                            // implement functionality here (clean up the playlist map)
                        }
                    }
                } finally {
                    IoUtils.closeQuietly(c);
                    if (imageMicroThumbs != null) {
                        imageMicroThumbs.deactivate();
                    }
                    if (videoMicroThumbs != null) {
                        videoMicroThumbs.deactivate();
                    }
                }
                // Do not allow deletion if the file/object is referenced as parent
                // by some other entries. It could cause database corruption.
                if (!TextUtils.isEmpty(tableAndWhere.where)) {
                    tableAndWhere.where =
                            "(" + tableAndWhere.where + ")" +
                                    " AND (_id NOT IN (SELECT parent FROM files" +
                                    " WHERE NOT (" + tableAndWhere.where + ")))";
                } else {
                    tableAndWhere.where = ID_NOT_PARENT_CLAUSE;
                }
            }
        }

        switch (match) {
            case MTP_OBJECTS:
            case MTP_OBJECTS_ID:
                database.mNumDeletes++;
                count = db.delete("files", tableAndWhere.where, whereArgs);
                break;
            case AUDIO_GENRES_ID_MEMBERS:
                database.mNumDeletes++;
                count = db.delete("audio_genres_map",
                        tableAndWhere.where, whereArgs);
                break;

            case IMAGES_THUMBNAILS_ID:
            case IMAGES_THUMBNAILS:
            case VIDEO_THUMBNAILS_ID:
            case VIDEO_THUMBNAILS:
                // Delete the referenced files first.
                Cursor c = db.query(tableAndWhere.table,
                        sDataOnlyColumn,
                        tableAndWhere.where, whereArgs, null, null, null);
                if (c != null) {
                    try {
                        while (c.moveToNext()) {
                            deleteIfAllowed(uri, c.getString(0));
                        }
                    } finally {
                        IoUtils.closeQuietly(c);
                    }
                }
                database.mNumDeletes++;
                count = db.delete(tableAndWhere.table,
                        tableAndWhere.where, whereArgs);
                break;

            default:
                database.mNumDeletes++;
                count = db.delete(tableAndWhere.table,
                        tableAndWhere.where, whereArgs);
                break;
        }

        // Since there are multiple Uris that can refer to the same files
        // and deletes can affect other objects in storage (like subdirectories
        // or playlists) we will notify a change on the entire volume to make
        // sure no listeners miss the notification.
        Uri notifyUri = Uri.parse("content://" + MediaStore.AUTHORITY + "/" + volumeName);
        getContext().getContentResolver().notifyChange(notifyUri, null);
    }

    return count;
}

5.5 MediaProvider 查詢

public Cursor query(Uri uri, String[] projectionIn, String selection,
        String[] selectionArgs, String sort) {
    uri = safeUncanonicalize(uri);
    int table = URI_MATCHER.match(uri);
    List<String> prependArgs = new ArrayList<String>();
    // handle MEDIA_SCANNER before calling getDatabaseForUri()
    if (table == MEDIA_SCANNER) {
        if (mMediaScannerVolume == null) {
            return null;
        } else {
            // create a cursor to return volume currently being scanned by the media scanner
            MatrixCursor c = new MatrixCursor(
                new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
            c.addRow(new String[] {mMediaScannerVolume});
            //直接返回的是有關存儲卷的cursor
            return c;
        }
    }
    // Used temporarily (until we have unique media IDs) to get an identifier
    // for the current sd card, so that the music app doesn't have to use the
    // non-public getFatVolumeId method
    if (table == FS_ID) {
        MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
        c.addRow(new Integer[] {mVolumeId});
        return c;
    }
    if (table == VERSION) {
        MatrixCursor c = new MatrixCursor(new String[] {"version"});
        c.addRow(new Integer[] {getDatabaseVersion(getContext())});
        return c;
    }
    //初始化DatabaseHelper和SQLiteDatabase
    String groupBy = null;
    DatabaseHelper helper = getDatabaseForUri(uri);
    if (helper == null) {
        return null;
    }
    helper.mNumQueries++;
    SQLiteDatabase db = null;
    try {
        db = helper.getReadableDatabase();
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    if (db == null) return null;
    // SQLiteQueryBuilder類是組成查詢語句的幫助類
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    //獲取uri裏面的查詢字符
    String limit = uri.getQueryParameter("limit");
    String filter = uri.getQueryParameter("filter");
    String [] keywords = null;
    if (filter != null) {
        filter = Uri.decode(filter).trim();
        if (!TextUtils.isEmpty(filter)) {
            //對字符進行篩選
            String [] searchWords = filter.split(" ");
            keywords = new String[searchWords.length];
            for (int i = 0; i < searchWords.length; i++) {
                String key = MediaStore.Audio.keyFor(searchWords[i]);
                key = key.replace("\\", "\\\\");
                key = key.replace("%", "\\%");
                key = key.replace("_", "\\_");
                keywords[i] = key;
            }
        }
    }
    if (uri.getQueryParameter("distinct") != null) {
        qb.setDistinct(true);
    }
    boolean hasThumbnailId = false;
    //對匹配的其他類型進行設置查詢語句的操作
    switch (table) {
        case IMAGES_MEDIA:
                //設置查詢的表是images
                qb.setTables("images");
                if (uri.getQueryParameter("distinct") != null)
                    //設置爲唯一的
                    qb.setDistinct(true);
                break;
         //其他類型相類似
         ... ...
    }
    //根據拼裝的搜索條件,進行查詢
    Cursor c = qb.query(db, projectionIn, selection,
             combine(prependArgs, selectionArgs), groupBy, null, sort, limit);

    if (c != null) {
        String nonotify = uri.getQueryParameter("nonotify");
        if (nonotify == null || !nonotify.equals("1")) {
            //通知更新數據庫
            c.setNotificationUri(getContext().getContentResolver(), uri);
        }
    }
    return c;
}

6. MediaProvider 如何更新數據庫

站在 Java 層來看,不管是掃描具體的文件,還是掃描一個目錄,最終都會走到 Java 層 MyMediaScannerClient 的 doScanFile()。在前文我們已經列出過這個函數的代碼,爲了說明問題,這裏再列一下其中的重要句子:
/frameworks/base/media/java/android/media/MediaScanner.java

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    try {
        // ① beginFile
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
        ...

        // 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 {
                // 正常文件處理走到這裏
                ...
                // we only extract metadata for audio and video files
                if (isaudio || isvideo) {
                    // ② processFile 這邊主要是解析媒體文件的元數據,以便後續存入到數據庫中
                    mScanSuccess = processFile(path, mimeType, this);
                }

                if (isimage) {
                    mScanSuccess = processImageFile(path);
                }
                ...
                // ③ endFile
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    }
    ...
    return result;
}

重看一下其中和 MediaProvider 相關的 beginFile() 和 endFile()。

beginFile()是爲了後續和MediaProvider打交道,準備一個FileEntry。FileEntry的定義如下:

private static class FileEntry {
    long mRowId;
    String mPath;
    long mLastModified;
    int mFormat;
    boolean mLastModifiedChanged;

    FileEntry(long rowId, String path, long lastModified, int format) {
        mRowId = rowId;
        mPath = path;
        mLastModified = lastModified;
        mFormat = format;
        mLastModifiedChanged = false;
    }
    ...
}

FileEntry 的幾個成員變量,其實體現了查表時的若干列的值。
beginFile()的代碼截選如下:

public FileEntry beginFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean noMedia) {
    ...
    FileEntry entry = makeEntryFor(path); // 從MediaProvider中查出該文件或目錄對應的入口
    ...
    if (entry == null || wasModified) {
        // 不管原來表中是否存在這個路徑文件數據,這裏面都會執行到
        if (wasModified) {
            // 更新最後編輯時間
            entry.mLastModified = lastModified;
        } else {
            // 如果前面沒查到FileEntry,就在這裏new一個新的FileEntry
            entry = new FileEntry(0, path, lastModified,
                    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
        }
        entry.mLastModifiedChanged = true;
    }
    ...
    return entry;
}

其中調用的 makeEntryFor() 內部就會查詢 MediaProvider:

FileEntry makeEntryFor(String path) {
    String where;
    String[] selectionArgs;

    Cursor c = null;
    try {
        where = Files.FileColumns.DATA + "=?";
        selectionArgs = new String[] { path };
        c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
                where, selectionArgs, null, null);
        if (c.moveToFirst()) {
            long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
            int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
            long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
            return new FileEntry(rowId, path, lastModified, format);
        }
    } catch (RemoteException e) {
    } finally {
        if (c != null) {
            c.close();
        }
    }
    return null;
}

查詢語句中用的 FILES_PRESCAN_PROJECTION 的定義如下:

private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
        Files.FileColumns._ID, // 0
        Files.FileColumns.DATA, // 1
        Files.FileColumns.FORMAT, // 2
        Files.FileColumns.DATE_MODIFIED, // 3
};

看到了嗎,特意要去查一下 MediaProvider 中記錄的待查文件的最後修改日期。能查到就返回一個 FileEntry,如果查詢時出現異常就返回 null。beginFile() 的 lastModified 參數可以理解爲是從文件系統裏拿到的待查文件的最後修改日期,它應該是最準確的。而 MediaProvider 裏記錄的信息則有可能“較老”。beginFile() 內部通過比對這兩個“最後修改日期”,就可以知道該文件是不是真的改動了。如果的確改動了,就要把 FileEntry 裏的 mLastModified 調整成最新數據。

基本上而言,beginFile() 會返回一個 FileEntry。如果該階段沒能在MediaProvider裏找到文件對應的記錄,那麼 FileEntry 對象的mRowId會爲0,而如果找到了,則爲非0值。

與 beginFile() 相對的,就是 endFile() 了。endFile() 是真正向 MediaProvider 數據庫插入數據或更新數據的地方。當 FileEntry 的 mRowId 爲0時,會考慮調用:

result = mMediaProvider.insert(tableUri, values);

而當 mRowId 爲非0值時,則會考慮調用:

mMediaProvider.update(result, values, null, null);

這就是改變 MediaProvider 中相關信息的最核心代碼。

endFile() 的代碼截選如下:

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
        boolean alarms, boolean music, boolean podcasts)
        throws RemoteException {
    ...
    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);
    }
    ...
    long rowId = entry.mRowId;
    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);
    } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
            || mFileType == MediaFile.FILE_TYPE_HEIF
            || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
        ...
    }
    ...
    if (rowId == 0) {
        // 掃描的是新文件,insert記錄。如果是目錄的話,必須比它所含有的所有文件更早插入記錄,
        // 所以在批量插入時,就需要有更高的優先權。如果是文件的話,而且我們現在就需要其對應
        // 的rowId,那麼應該立即進行插入,此時不過多考慮批量插入。
        // 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 {
        ...
        mMediaProvider.update(result, values, null, null);
    }
    ...
    return result;
}

除了直接調用 mMediaProvider.insert() 向 MediaProvider 中寫入數據,函數中還有一種方式是經由 inserter 對象,其類型爲 MediaInserter。

MediaInserter 也是向 MediaProvider 中寫入數據,最終大體上會走到其 flush() 函數,該函數的代碼如下:

private void flush(Uri tableUri, List<ContentValues> list) throws RemoteException {
    if (!list.isEmpty()) {
        ContentValues[] valuesArray = new ContentValues[list.size()];
        valuesArray = list.toArray(valuesArray);
        mProvider.bulkInsert(tableUri, valuesArray);
        list.clear();
    }
}

參考

Android-MediaScanner&MediaProvider學習
Android MediaScanner
Android掃描多媒體文件剖析
MediaScannerService研究
android_9.0 MediaScanner 媒體掃描詳解
Android 多媒體掃描 MediaScannerConnection
Android多媒體總綱
MediaProvider流程分析
多媒體文件管理-數據庫external.db,internal.db (一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章