簡介:
本文介紹如何在 Android 中,開發人員的 APP 如何使用媒體儲存服務(包含MediaScanner、MediaProvider以及媒體資訊解析等部分),包括如何把 APP 新增或修改的檔案更新到媒體資料庫、如何在多媒體應用中隱藏 APP 產生的檔案、如何監聽媒體資料庫的變化等等。
Android 原生有一套媒體儲存服務,進程名是 android.process.media,主要負責把磁碟中的檔案資訊儲存到資料庫當中,供其他 APP 使用以及 MTP 模式使用。因此 APP 可以隨時快速查詢到機器上有多少音樂,音樂的時間長度、標題、藝術家、專輯封面都可以擷取到。下面就介紹我們開發的 APP 如何與這個媒體儲存服務打交道。
Note:MTP 模式是 Android 3.0 開始引入的,其資料來源於媒體儲存服務。
隱藏多媒體檔案
應用情境:APP 產生了圖片/音樂/視頻類檔案,不想讓它顯示在圖庫/音樂播放器。市面上有不少遊戲,它的圖片和音效檔案沒有做隱藏,出現在使用者的圖庫/音樂播放器當中,引起使用者反感。如果使用者把它刪除了,又可能會影響 APP 正常運行。
方法一:把檔案設為隱藏。Linux 裡檔案前加點就是隱藏,例如“檔案A”改成“.檔案A”。或者把副檔名去掉,這樣媒體儲存服務掃描時就不會將其當作多媒體檔案。
方法二:在檔案夾下產生一個名為“.nomedia”的空白檔案。這樣同一個檔案夾下的所有檔案都不會被當作多媒體檔案。
添加/修改多媒體檔案
應用情境:APP 建立了一個新的多媒體檔案,或者修改了一個已有多媒體檔案。例如 APP 下載了一個音樂檔案,需要通知媒體儲存服務,使用者就能在音樂播放器中看到這個檔案。否則,只有下次媒體儲存服務開始掃描整個磁碟,才會發現 APP 產生的新檔案。
方法一
如果只有一個檔案,並且不需要得到結果返回,直接發 Intent 通知媒體儲存服務即可。
複製代碼 代碼如下:
import java.io.File;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
private static void requestScanFile(Context context, File file) {
Intent i = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
i.setData(Uri.fromFile(file));
context.sendBroadcast(i);
}
private static void requestScanFile(Context context, String file) {
Intent i = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
i.setData(Uri.parse("file://" + Uri.encode(file)));
context.sendBroadcast(i);
}
Note:如果使用 Uri.parse() 從檔案名稱產生 Uri,檔案名稱必須要先 Uri.encode(),作用是把保留字元轉義。例如檔案名稱若包含“?”,不經過 Uri.encode 轉義的話會被當作是查詢參數,這樣 uri.getPath() 擷取的檔案路徑會丟失“?”之後的部分。
方法二
如果只有一個檔案,並且需要檔案 uri 結果返回,則使用回呼函數。
複製代碼 代碼如下:
import android.media.MediaScannerConnection;
import android.net.Uri;
private void requestScanFile(Context context, String file) {
MediaScannerConnection.scanFile(context, new String[] {file},
null, // mime types,可不指定
mListener);
}
MediaScannerConnection.OnScanCompletedListener mListener =
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
// TODO: 擷取到該檔案在多媒體資料庫中的 uri,進行下一步動作
}
};
Note:還有一種方法,可以先往多媒體資料庫插入一條包含檔案路徑的記錄,插入後可以得到其 uri;然後再使用方法一通知媒體儲存服務掃描此檔案,把檔案資訊(如專輯名)補充完整。但不推薦使用這種方法,因為獲得 uri 時檔案資訊並不完整。
方法三
如果檔案較多,則發 Intent 通知媒體儲存服務掃描整個磁碟。這種做法不是特別好,但又沒發現其他更好的介面。第三方檔案管理如“ES 檔案管理工具”就是使用這種方法的。
複製代碼 代碼如下:
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
private static void requestScanDisk(Context context) {
Intent i = new Intent(Intent.ACTION_MEDIA_MOUNTED);
String path = Environment.getExternalStorageDirectory().getPath();
i.setData(Uri.parse("file://" + Uri.encode(path)));
context.sendBroadcast(i);
}
監聽資料變化
應用情境:多媒體資料庫有變化,需要重新整理 APP 顯示介面。比較好理解,磁碟中的多媒體檔案有新增、刪除或者修改,APP 介面上要即時反應這些變化,重新整理顯示介面。
方法一
監聽媒體儲存相關 Intent。接受到 Intent 後,重新查詢一次資料庫。我們需要注意的 Intent 主要有以下幾個:
1、Intent.ACTION_MEDIA_SCANNER_FINISHED: 媒體儲存服務掃描整個磁碟完成後會發這個 Intent。可能有新增較多檔案或者被刪除。
2、Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: 媒體儲存服務掃描單個檔案。
複製代碼 代碼如下:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
private void registerReceiver(Context context) {
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
filter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
filter.addAction(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
filter.addDataScheme("file");
context.registerReceiver(mReceiver, filter);
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Uri uri = intent.getData();
if (uri != null && uri.getScheme().equals("file")) {
Log.v("Receiver", "BroadcastReceiver action = " + action + ", uri = " + uri);
if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)) {
String filePath = uri.getPath();
// TODO: filePath 檔案已改變,APP 重新整理介面
} else if (Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)) {
// TODO: 整個磁碟掃描完成,APP 重新整理介面
}
}
}
};
另外,在 Intent.MEDIA_SCANNER_STARTED 和 Intent.ACTION_MEDIA_SCANNER_FINISHED 之間的時間裡,媒體儲存服務正在掃描檔案,資料庫會有變化,所以只有在收到 Intent.ACTION_MEDIA_SCANNER_FINISHED 之後查詢的結果才是準確的。如果要檢測媒體儲存服務是否在掃描可以用以下方法:
複製代碼 代碼如下:
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Environment;
import android.provider.MediaStore;
private static boolean isMediaScannerScanning(ContentResolver cr) {
Cursor cursor = null;
try {
cursor = cr.query(MediaStore.getMediaScannerUri(), new String[] {
MediaStore.MEDIA_SCANNER_VOLUME}, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
return "external".equals(cursor.getString(0));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return false;
}
Note:APP 可能還需要監聽存放裝置的變化,例如 SD 記憶卡拔出、磁碟掛載出去(USB 大型存放區模式)等等,這些情況下可能要把檔案顯示介面清空,或者退出程式。各個手機對於每個 Intent 定義可能略有不同,但基本上是以下幾個:
1、Intent.ACTION_MEDIA_EJECT: 存放裝置正常移除,例如在設定裡卸載儲存空間。
2、Intent.ACTION_MEDIA_UNMOUNTED: 存放裝置正常卸載,通常與 EJECT 先後出現。
3、Intent.ACTION_MEDIA_BAD_REMOVAL: 非正常移除存放裝置,例如硬插拔 SD 記憶卡。
方法二
監聽資料庫變化。如果需要在資料庫發生變化時能即時接收到通知,可以使用 ContentObserver。
複製代碼 代碼如下:
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
private ContentObserver mContentObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) { // 往後相容
onChange(selfChange, null);
}
public void onChange(boolean selfChange, Uri uri) {
// TODO: 資料已改變,APP 重新查詢資料庫並重新整理介面
}
};
private void setupCursor(Context context, Cursor c) {
c.unregisterContentObserver(mContentObserver); // c 為需要顯示的資料
}
此外,使用 CursorAdapter 和顯示的 ListView 綁定,也可以達到同樣目的。當 Cursor 內容發現變化,ListView 也會相應自動重新整理。