本文以 Android 9.0 為準(zhǔn)
Android 系統(tǒng)提供了對多媒體的統(tǒng)一處理機(jī)制叁征,通過一套良好的框架實(shí)現(xiàn)了多媒體信息的掃描码泞、存儲谁榜、讀取幅聘。用戶可以基于這套框架非常方便的對多媒體信息進(jìn)行處理,這套框架主要包含了三部分:
- MediaScannerReceiver:多媒體掃描廣播接收者窃植,繼承 BroadcastReceiver帝蒿,主要響應(yīng)APP發(fā)送的廣播命令,并開啟 MediaScannerService 執(zhí)行掃描工作巷怜。
- MediaScannerService:多媒體掃描服務(wù)葛超,繼承 Service,主要是處理 APP 發(fā)送的請求延塑,要用到 Framework 中的 MediaScanner 來共同完成具體掃描工作绣张,并獲取媒體文件的 metadata,最后將數(shù)據(jù)寫入或刪除 MediaProvider 提供的數(shù)據(jù)庫中关带。
- MediaProvider:多媒體內(nèi)容提供者侥涵,繼承 ContentProvider,主要是負(fù)責(zé)操作數(shù)據(jù)庫宋雏,并提供給別的程序 insert芜飘、query、delete磨总、update 等操作嗦明。
本文就從上面三部分作為入口,分析它們是如何工作的蚪燕,如何對設(shè)備上的多媒體進(jìn)行掃描娶牌,如何將多媒體信息進(jìn)行存儲奔浅,用戶如何讀取、修改多媒體信息裙戏?
1. 如何調(diào)用 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.");
}
}
主要做了兩件事:
- 注冊掃描開始和結(jié)束的廣播,用來展示掃描狀態(tài)灵嫌;
- 在點(diǎn)擊事件中壹罚,發(fā)送了 ACTION_MEDIA_MOUNTED 廣播。
那么系統(tǒng)肯定存在一個的接收者寿羞,在收到 ACTION_MEDIA_MOUNTED 后進(jìn)行掃描猖凛,這就是 MediaScannerReceiver。
1.2 MediaScannerReceiver
首先關(guān)注 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)) {
// 開機(jī)廣播,只處理內(nèi)部存儲
scan(context, MediaProvider.INTERNAL_VOLUME);
} else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
// 處理系統(tǒng)語言變換
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)) {
// 每當(dāng)掛載外部存儲時
scan(context, MediaProvider.EXTERNAL_VOLUME);
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {
// 掃描單個文件玖院,并且路徑是在外部存儲路徑下
scanFile(context, path);
}
}
}
}
// 掃描內(nèi)部或者外部存儲,根據(jù)volume進(jìn)行區(qū)分
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));
}
// 掃描可轉(zhuǎn)換語言的多媒體
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));
}
}
掃描的時機(jī)為以下幾點(diǎn):
-
Intent.ACTION_BOOT_COMPLETED.equals(action)
6.0 中接到設(shè)備重啟的廣播毙死,對 Internal 和 External 掃描柜去,而 9.0 中只對 Internal 掃描。 -
Intent.ACTION_LOCALE_CHANGED.equals(action)
9.0 相比 6.0 增加了系統(tǒng)語言發(fā)生改變時的廣播郊酒,用于進(jìn)行掃描可以轉(zhuǎn)換語言的多媒體遇绞。 -
uri.getScheme().equals("file")
6.0 和 9.0 處理的一致,都是先過濾 scheme 為 "file" 的 Intent燎窘,再通過下面兩個 action 對 External 進(jìn)行掃描:-
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倘屹,并實(shí)現(xiàn) Runnable 工作線程。通過 ServiceHandler 這個 Handler 把主線程需要大量計(jì)算的工作放到工作線程中慢叨。
/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() 里會啟動最重要的工作線程烛缔,該線程也是個消息泵線程馏段。每當(dāng)用戶需要掃描媒體文件時,基本上都是在向這個消息泵里發(fā)送 Message践瓷,并在處理 Message 時完成真正的 scan 動作院喜。請注意,創(chuàng)建 Thread 時傳入的第二個參數(shù)就是 MediaScannerService 自身晕翠,也就是說線程的主要行為其實(shí)就是 MediaScannerService 的 run() 方法喷舀,該方法的代碼如下:
@Override
public void run() {
// reduce priority below other background threads to avoid interfering
// with other services at boot time.
// 設(shè)置進(jìn)程優(yōu)先級,媒體掃描比較費(fèi)時淋肾,防止 CPU 一直被 MediaScannerService 占用
// 這會導(dǎo)致用戶感覺系統(tǒng)變得很慢
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();
mServiceLooper = Looper.myLooper(); // 消息looper
mServiceHandler = new ServiceHandler(); // 發(fā)送消息的handler
Looper.loop();
}
后續(xù)就是通過上面那個 mServiceHandler 向消息隊(duì)列發(fā)送 Message 的硫麻。
2.2 向工作線程發(fā)送 Message
比較常見的向消息泵發(fā)送 Message 的做法是調(diào)用 startService(),并在 MediaScannerService 的 onStartCommand() 方法里 sendMessage()樊卓。比如拿愧,和 MediaScannerService 配套提供的 MediaScannerReceiver,當(dāng)它收到類似 ACTION_BOOT_COMPLETED 這樣的系統(tǒng)廣播時碌尔,就會調(diào)用自己的 scan() 或 scanFile() 方法浇辜,里面的 startService() 動作會導(dǎo)致走到 service 的 onStartCommand(),并進(jìn)一步發(fā)送消息唾戚,其方法截選如下:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
...
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent.getExtras();
mServiceHandler.sendMessage(msg); // 發(fā)送消息
// Try again later if we are killed before we can finish scanning.
return Service.START_REDELIVER_INTENT;
}
另外一種比較常見的發(fā)送 Message 的做法是先直接或間接 bindService()奢赂,綁定成功后會得到一個 IMediaScannerService 接口,而后外界再通過該接口向 MediaScannerService 發(fā)起命令颈走,請求其掃描特定文件或目錄膳灶。
IMediaScannerService 接口只提供了兩個接口方法:
- void requestScanFile(String path, String mimeType, in IMediaScannerListener listener);
- void scanFile(String path, String mimeType);
處理這兩種請求的實(shí)體是服務(wù)內(nèi)部的 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);
}
};
說到底還是在調(diào)用 startService()立由。
具體處理消息泵線程里的消息時轧钓,執(zhí)行的是 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 {
// 掃描內(nèi)部或外部
String volume = arguments.getString("volume");
String[] directories = null;
if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
// 如果是掃描內(nèi)部存儲,實(shí)際掃描目錄為
directories = new String[] {
Environment.getRootDirectory() + "/media",
Environment.getOemDirectory() + "/media",
Environment.getProductDirectory() + "/media",
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
// 如果是掃描外部存儲锐膜,實(shí)際掃描目錄為
if (getSystemService(UserManager.class).isDemoUser()) {
directories = ArrayUtils.appendElement(String.class,
mExternalStoragePaths,
Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
} else {
directories = mExternalStoragePaths;
}
}
// 調(diào)用 scan 函數(shù)開展文件夾掃描工作
if (directories != null) {
scan(directories, volume);
}
}
} catch (Exception e) {
Log.e(TAG, "Exception in handleMessage", e);
}
stopSelf(msg.arg1); // 掃描結(jié)束毕箍,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() 方法而柑,用來掃描內(nèi)部或外部存儲的路徑:
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 做一些準(zhǔn)備工作
Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
// 發(fā)送開始掃描的廣播
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
try {
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
// 打開數(shù)據(jù)庫文件
openDatabase(volumeName);
}
// 創(chuàng)建媒體掃描器荷逞,并調(diào)用 scanDirectories 掃描目標(biāo)文件夾
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 {
// 發(fā)送結(jié)束掃描的廣播
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
mWakeLock.release();
}
}
上面的代碼中,比較復(fù)雜的是 MediaScannerService 和 MediaProvider 的交互种远。MediaScannerService 經(jīng)常使用一些特殊 Uri 做數(shù)據(jù)庫操作涩澡,而 MediaProvider 針對這些 Uri 會走一些特殊的處理,例如打開數(shù)據(jù)庫文件等坠敷,后面在 MediaProvider 中會重點(diǎn)說到妙同,我們還先回到掃描的邏輯射富。
scanFile() 或 scan() 才是實(shí)際進(jìn)行掃描的地方,掃描動作中主要借助的是 MediaScanner粥帚,它是打通 Java 層和 C++ 層的關(guān)鍵胰耗,掃描動作最終會調(diào)用到 MediaScanner的某個 native 函數(shù),于是程序流程開始走到 C++ 層芒涡。
現(xiàn)在柴灯,我們可以畫一張示意圖:
3. MediaScanner 如何工作?
顧名思義拖陆,MediaScanner 就是個“媒體文件掃描器”弛槐。它必須打通 Java 層和 C++ 層懊亡。請大家注意它的兩個 native 函數(shù):native_init() 和 native_setup()依啰,以及兩個重要成員變量:一個是mClient成員,另一個是 mNativeContext店枣,后面會詳細(xì)說明速警。
/frameworks/base/media/java/android/media/MediaScanner.java
MediaScanner的相關(guān)代碼截選如下:
public class MediaScanner implements AutoCloseable {
static {
System.loadLibrary("media_jni");
native_init(); // 將java層和c++層聯(lián)系起來
}
...
private long mNativeContext;
...
public MediaScanner(Context c, String volumeName) {
native_setup();
...
}
...
// 一開始就具有明確的mClient對象
private final MyMediaScannerClient mClient = new MyMediaScannerClient();
...
}
MediaScanner 類加載之時,就會同時加載動態(tài)鏈接庫“media_jni”鸯两,并調(diào)用 native_init() 將 Java 層和 C++ 層聯(lián)系起來闷旧。
/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;
}
}
經(jīng)過分析代碼,我們發(fā)現(xiàn)在 C++ 層會有個與 MediaScanner 相對應(yīng)的類钧唐,叫作 StagefrightMediaScanner忙灼。當(dāng) Java層創(chuàng)建 MediaScanner 對象時,MediaScanner 的構(gòu)造函數(shù)就調(diào)用了 native_setup()钝侠,該函數(shù)對應(yīng)到 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");
// 創(chuàng)建 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() 其實(shí)就是在為 Java 層 MediaScanner 的 mNativeContext 域賦值帅韧。
后續(xù)我們會看到里初,每當(dāng) C++ 層執(zhí)行掃描動作時,還會再創(chuàng)建一個 MyMediaScannerClient 對象忽舟,這個對象和 Java 層的同名類對應(yīng)双妨。我們畫一張圖來說明:
3.1 scanSingleFile() 動作
// this function is used to scan a single file
public Uri scanSingleFile(String path, String mimeType) {
try {
prescan(path, true); // ① 掃描前預(yù)準(zhǔn)備
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
// ② 掃描前預(yù)準(zhǔn)備
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 函數(shù)比較關(guān)鍵叮阅,首先讓我們試想一個問題刁品。
在媒體掃描過程中,有個令人頭疼的問題浩姥,來舉個例子:假設(shè)某次掃描前 SD 卡中有 100 個媒體文件哑诊,數(shù)據(jù)庫中會有 100 條關(guān)于這些文件的記錄。現(xiàn)刪除其中的 50 個文件及刻,那么媒體數(shù)據(jù)庫什么時候會被更新呢镀裤?
MediaScanner 考慮到了這一點(diǎn)竞阐,prescan 函數(shù)的主要作用就是在掃描之前把上次掃描獲取的數(shù)據(jù)庫信息取出遍歷并檢測是否丟失,如果丟失暑劝,則從數(shù)據(jù)庫中刪除骆莹。
再看②處代碼,借助了 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;
}
因?yàn)?MyMediaScannerClient 是 MediaScanner 的內(nèi)部類,所以它可以直接調(diào)用 MediaScanner 的 processFile()傅联。
現(xiàn)在我們畫一張 MediaScannerService.scanFile() 的調(diào)用關(guān)系圖:
3.2 scanDirectories() 動作
public void scanDirectories(String[] directories) {
try {
prescan(null, true); // 掃描前預(yù)準(zhǔn)備
...
for (int i = 0; i < directories.length; i++) {
// native 函數(shù)先改,調(diào)用它來對目標(biāo)文件夾進(jìn)行掃描
processDirectory(directories[i], mClient);
}
...
postscan(directories); // 掃描后處理
} catch (SQLException e) {
...
} finally {
...
}
}
我們畫一張 MediaScannerService .scan() 的調(diào)用關(guān)系圖:
4. 調(diào)用到 C++ 層
這里就不深入展開,可以看下這篇文章《MediaScannerService研究》蒸走。
5. MediaProvider 如何工作
/packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
5.1 MediaProvider 何時創(chuàng)建數(shù)據(jù)庫
通常來說仇奶,數(shù)據(jù)庫的創(chuàng)建,應(yīng)該在 ContentProvider 的 onCreate() 方法中完成 比驻。
@Override
public boolean onCreate() {
...
// DatabaseHelper緩存
mDatabases = new HashMap<String, DatabaseHelper>();
// 綁定內(nèi)部存儲數(shù)據(jù)庫
attachVolume(INTERNAL_VOLUME);
...
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// 如果已掛載外部存儲该溯,綁定外部存儲數(shù)據(jù)庫
attachVolume(EXTERNAL_VOLUME);
}
...
return true;
}
接下來分析attachVolume方法:
創(chuàng)建數(shù)據(jù)庫:
- 如果此存儲卷已經(jīng)鏈接上了,則不執(zhí)行任何操作别惦。
- 否則狈茉,查詢存儲卷的 ID 并且建立對應(yīng)的數(shù)據(jù)庫。
/**
* 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
// 更新路徑以反映當(dāng)前裝載的卷
updateStoragePaths();
DatabaseHelper helper = null;
synchronized (mDatabases) {
helper = mDatabases.get(volume);
// 判斷是否已經(jīng)attached過了
if (helper != null) {
if (EXTERNAL_VOLUME.equals(volume)) {
// 確保默認(rèn)的文件夾已經(jīng)被創(chuàng)建在掛載的主要存儲設(shè)備上掸掸,
// 對每個存儲卷只做一次這種操作氯庆,所以當(dāng)用戶手動刪除時不會打擾
ensureDefaultFolders(helper, helper.getWritableDatabase());
}
return Uri.parse("content://media/" + volume);
}
Context context = getContext();
if (INTERNAL_VOLUME.equals(volume)) {
// 如果是內(nèi)部存儲則直接實(shí)例化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.
// 數(shù)據(jù)庫都是以類似 external-ffffffff.db 的形式命名的扰付,
// 后面的 8 個 16 進(jìn)制字符是該 SD 卡 FAT 分區(qū)的 Volume ID堤撵。
// 該 ID 是分區(qū)時決定的,只有重新分區(qū)或者手動改變才會更改悯周,
// 可以防止插入不同 SD 卡時數(shù)據(jù)庫沖突粒督。
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.
// 已經(jīng)掛載但是sd卡是只讀狀態(tài)
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
// 根據(jù)volume ID設(shè)置數(shù)據(jù)庫的名稱
String dbName = "external-" + Integer.toHexString(volumeId) + ".db";
// 創(chuàng)建外部存儲數(shù)據(jù)庫
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
// 外部數(shù)據(jù)庫名稱應(yīng)為EXTERNAL_DATABASE_NAME
// 但是較早的版本對沒有可移動存儲的設(shè)備使用external-XXXXXXXX.db命名
// 在這種情況下,我們需要轉(zhuǎn)換為新的約定
...
// 根據(jù)之前轉(zhuǎn)換的數(shù)據(jù)庫名禽翼,創(chuàng)建數(shù)據(jù)庫
helper = new DatabaseHelper(context, dbFile.getName(), false,
false, mObjectRemovedCallback);
}
} else {
throw new IllegalArgumentException("There is no volume named " + volume);
}
// 緩存起來屠橄,標(biāo)識已經(jīng)創(chuàng)建過了數(shù)據(jù)庫
mDatabases.put(volume, helper);
...
}
if (EXTERNAL_VOLUME.equals(volume)) {
// 給外部存儲創(chuàng)建默認(rèn)的文件夾
ensureDefaultFolders(helper, helper.getWritableDatabase());
}
return Uri.parse("content://media/" + volume);
}
先來關(guān)注一下 getPrimaryPhysicalVolume() 這個相關(guān)的方法:
/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;
}
下面就是分析創(chuàng)建數(shù)據(jù)庫的源頭DatabaseHelper:
/**
* Creates database the first time we try to open it.
*/
@Override
public void onCreate(final SQLiteDatabase db) {
// 在此方法中對700版本以下的都會新建數(shù)據(jù)庫
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) {
// 對數(shù)據(jù)庫進(jìn)行更新
mUpgradeAttempted = true;
updateDatabase(mContext, db, mInternal, oldV, newV);
}
這里強(qiáng)調(diào)一句 getDatabaseVersion() 方法獲取的 fromVersion 不是數(shù)據(jù)庫版本闰挡,而是 /packages/providers/MediaProvider/AndroidManifest.xml
中的 versionCode锐墙。
現(xiàn)在已經(jīng)找到創(chuàng)建數(shù)據(jù)庫的方法updateDatabase,現(xiàn)在大致分析一下此方法:
/**
* 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) {
...
// 對不同版本的數(shù)據(jù)庫進(jìn)行判斷
if (fromVersion < 700) {
// 小于700长酗,重新創(chuàng)建數(shù)據(jù)庫
createLatestSchema(db, internal);
} else if (fromVersion < 800) {
// 對700-800之間的數(shù)據(jù)庫處理
updateFromKKSchema(db);
} else if (fromVersion < 900) {
// 對800-900之間的數(shù)據(jù)庫處理
updateFromOCSchema(db);
}
// 檢查audio_meta的_data值是否是不同的溪北,如果不同就刪除audio_meta,
// 在掃描的時候重新創(chuàng)建
sanityCheck(db, fromVersion);
}
那么還有一個疑惑,我們知道 ContentProvider 的 onCreate() 執(zhí)行時間之拨,早于 Application onCreate()茉继,那么在 onCreate() 之后掛載外部存儲,是如何處理的呢蚀乔?
搜索 attachVolume() 的調(diào)用位置烁竭,可以找到在 insertInternal() 中看到:
private Uri insertInternal(Uri uri, int match, ContentValues initialValues,
ArrayList<Long> notifyRowIds) {
...
switch (match) {
...
case VOLUMES:
{
String name = initialValues.getAsString("name");
// 根據(jù)name綁定存儲數(shù)據(jù)庫
Uri attachedVolume = attachVolume(name);
...
return attachedVolume;
}
...
}
根據(jù) VOLUMES 找到對應(yīng)的 URI:
URI_MATCHER.addURI("media", null, VOLUMES);
而調(diào)用 insertInternal() 方法的地方,是在 insert() 方法中吉挣。
那么說明派撕,必然存在一個調(diào)用 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");
}
}
調(diào)用 openDatabase() 方法的地方就是在開始掃描外部存儲的時候终吼,也就在這個時候,進(jìn)行了 DatabaseHelper 的實(shí)例化氯哮,在前文已經(jīng)分析了 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();
}
}
至此蛙粘,對于數(shù)據(jù)庫的創(chuàng)建已經(jīng)分析完畢垫卤。
5.2 MediaProvider 更新
@Override
public int (Uri uri, ContentValues initialValues, String userWhere,
String[] whereArgs) {
// 將uri進(jìn)行轉(zhuǎn)換成合適的格式威彰,去除標(biāo)準(zhǔn)化
uri = safeUncanonicalize(uri);
int count;
// 對uri進(jìn)行匹配
int match = URI_MATCHER.match(uri);
// 返回查詢的對應(yīng)uri的數(shù)據(jù)庫幫助類
DatabaseHelper helper = getDatabaseForUri(uri);
// 記錄更新的次數(shù)
helper.mNumUpdates++;
// 通過可寫的方式獲得數(shù)據(jù)庫實(shí)例
SQLiteDatabase db = helper.getWritableDatabase();
String genre = null;
if (initialValues != null) {
// 獲取流派的信息出牧,然后刪除掉
genre = initialValues.getAsString(Audio.AudioColumns.GENRE);
initialValues.remove(Audio.AudioColumns.GENRE);
}
...
// 根據(jù)匹配的uri進(jìn)行相應(yīng)的操作
switch (match) {
case AUDIO_MEDIA:
case AUDIO_MEDIA_ID:
// 更新音樂人和專輯字段。首先從緩存中判斷是否有值歇盼,如果有直接用緩存中的
// 數(shù)據(jù)舔痕,如果沒有再從數(shù)據(jù)庫中查詢是否有對應(yīng)的信息,如果有則更新豹缀,
// 如果沒有插入這條數(shù)據(jù).接下來的操作是增加更新次數(shù)伯复,并更新流派
...
case IMAGES_MEDIA:
case IMAGES_MEDIA_ID:
case VIDEO_MEDIA:
case VIDEO_MEDIA_ID:
// 更新視頻,并且發(fā)出生成略縮圖請求
...
case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
// 更新播放列表數(shù)據(jù)
...
}
...
}
至此邢笙,更新操作已完成啸如。
5.3 MediaProvider 插入
關(guān)于插入,有兩個方法插入氮惯,一個是大量的插入 bulkInsert 方法傳入的是 ContentValues 數(shù)組叮雳;一個是 insert,傳入的是單一個 ContentValues妇汗。下面分別分析:
@Override
public int bulkInsert(Uri uri, ContentValues values[]) {
// 首先對傳入的Uri進(jìn)行匹配
int match = URI_MATCHER.match(uri);
if (match == VOLUMES) {
// 如果是匹配的是存儲卷帘不,則直接調(diào)用父類的方法,進(jìn)行循環(huán)插入
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) {
// 插入播放列表的數(shù)據(jù)杨箭,在playlistBulkInsert中是開啟的事務(wù)進(jìn)行插入
return playlistBulkInsert(db, uri, values);
} else if (match == MTP_OBJECT_REFERENCES) {
// 將MTP對象的ID轉(zhuǎn)換成音頻的ID寞焙,最終也是調(diào)用到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) {
// 如果不滿足上述的條件,則開啟事務(wù)進(jìn)行插入其他的數(shù)據(jù)
db.beginTransaction();
try {
int len = values.length;
for (int i = 0; i < len; i++) {
if (values[i] != null) {
// 循環(huán)調(diào)用insertInternal去插入相關(guān)的數(shù)據(jù)
insertInternal(uri, match, values[i], notifyRowIds);
}
}
numInserted = len;
db.setTransactionSuccessful();
} finally {
// 結(jié)束事務(wù)
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>();
// 只是調(diào)用insertInternal進(jìn)行插入
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});
//直接返回的是有關(guān)存儲卷的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)) {
//對字符進(jìn)行篩選
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;
//對匹配的其他類型進(jìn)行設(shè)置查詢語句的操作
switch (table) {
case IMAGES_MEDIA:
//設(shè)置查詢的表是images
qb.setTables("images");
if (uri.getQueryParameter("distinct") != null)
//設(shè)置為唯一的
qb.setDistinct(true);
break;
//其他類型相類似
... ...
}
//根據(jù)拼裝的搜索條件,進(jìn)行查詢
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")) {
//通知更新數(shù)據(jù)庫
c.setNotificationUri(getContext().getContentResolver(), uri);
}
}
return c;
}
6. MediaProvider 如何更新數(shù)據(jù)庫
站在 Java 層來看捣郊,不管是掃描具體的文件辽狈,還是掃描一個目錄,最終都會走到 Java 層 MyMediaScannerClient 的 doScanFile()呛牲。在前文我們已經(jīng)列出過這個函數(shù)的代碼稻艰,為了說明問題,這里再列一下其中的重要句子:
/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 這邊主要是解析媒體文件的元數(shù)據(jù)侈净,以便后續(xù)存入到數(shù)據(jù)庫中
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 相關(guān)的 beginFile() 和 endFile()尊勿。
beginFile()是為了后續(xù)和MediaProvider打交道,準(zhǔn)備一個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 的幾個成員變量元扔,其實(shí)體現(xiàn)了查表時的若干列的值。
beginFile()的代碼截選如下:
public FileEntry beginFile(String path, String mimeType, long lastModified,
long fileSize, boolean isDirectory, boolean noMedia) {
...
FileEntry entry = makeEntryFor(path); // 從MediaProvider中查出該文件或目錄對應(yīng)的入口
...
if (entry == null || wasModified) {
// 不管原來表中是否存在這個路徑文件數(shù)據(jù)旋膳,這里面都會執(zhí)行到
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;
}
其中調(diào)用的 makeEntryFor() 內(nèi)部就會查詢 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擅羞,如果查詢時出現(xiàn)異常就返回 null。beginFile() 的 lastModified 參數(shù)可以理解為是從文件系統(tǒng)里拿到的待查文件的最后修改日期义图,它應(yīng)該是最準(zhǔn)確的减俏。而 MediaProvider 里記錄的信息則有可能“較老”。beginFile() 內(nèi)部通過比對這兩個“最后修改日期”碱工,就可以知道該文件是不是真的改動了娃承。如果的確改動了,就要把 FileEntry 里的 mLastModified 調(diào)整成最新數(shù)據(jù)怕篷。
基本上而言历筝,beginFile() 會返回一個 FileEntry。如果該階段沒能在MediaProvider里找到文件對應(yīng)的記錄廊谓,那么 FileEntry 對象的mRowId會為0梳猪,而如果找到了,則為非0值蒸痹。
與 beginFile() 相對的春弥,就是 endFile() 了。endFile() 是真正向 MediaProvider 數(shù)據(jù)庫插入數(shù)據(jù)或更新數(shù)據(jù)的地方电抚。當(dāng) FileEntry 的 mRowId 為0時惕稻,會考慮調(diào)用:
result = mMediaProvider.insert(tableUri, values);
而當(dāng) mRowId 為非0值時,則會考慮調(diào)用:
mMediaProvider.update(result, values, null, null);
這就是改變 MediaProvider 中相關(guān)信息的最核心代碼蝙叛。
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記錄。如果是目錄的話,必須比它所含有的所有文件更早插入記錄蜘渣,
// 所以在批量插入時淌铐,就需要有更高的優(yōu)先權(quán)。如果是文件的話蔫缸,而且我們現(xiàn)在就需要其對應(yīng)
// 的rowId腿准,那么應(yīng)該立即進(jìn)行插入,此時不過多考慮批量插入拾碌。
// 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;
}
除了直接調(diào)用 mMediaProvider.insert() 向 MediaProvider 中寫入數(shù)據(jù)吐葱,函數(shù)中還有一種方式是經(jīng)由 inserter 對象,其類型為 MediaInserter校翔。
MediaInserter 也是向 MediaProvider 中寫入數(shù)據(jù)弟跑,最終大體上會走到其 flush() 函數(shù),該函數(shù)的代碼如下:
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學(xué)習(xí)
Android MediaScanner
Android掃描多媒體文件剖析
MediaScannerService研究
android_9.0 MediaScanner 媒體掃描詳解
Android 多媒體掃描 MediaScannerConnection
Android多媒體總綱
MediaProvider流程分析
多媒體文件管理-數(shù)據(jù)庫external.db防症,internal.db (一)