在開(kāi)發(fā)的過(guò)程中楔壤,有時(shí)候會(huì)遇到需要讀取多媒體文件的需求鹤啡,面對(duì)這樣的需求,通常我們有兩種解決方案:自己掃描全盤文件蹲嚣,或者使用ContentResolver讀取系統(tǒng)記錄递瑰。
一般需求不是特別復(fù)雜的情況下,直接讀取系統(tǒng)數(shù)據(jù)就OK隙畜。以查看系統(tǒng)中文檔為例:
// 查詢的文件MIME類型
public static final String MIME_TYPE_DOC = "application/msword";
public static final String MIME_TYPE_TXT = "text/plain";
public static final String MIME_TYPE_PDF = "application/pdf";
private static final Uri DOC_URI = MediaStore.Files.getContentUri("external");
// 查詢字段
private static final String[] sColumns = {
MediaStore.Files.FileColumns.TITLE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.MIME_TYPE,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATE_MODIFIED
};
public List<DocItem> getDocByTypes(Context context, String... mimeType) {
Cursor cursor = null;
try {
// 按時(shí)間倒序查詢系統(tǒng)中文檔文件
cursor = context.getContentResolver().query(DOC_URI, sColumns, buildDocSelection(mimeType),
null, MediaStore.Files.FileColumns.DATE_MODIFIED + " DESC");
if (cursor != null) {
List<DocItem> docItems = new ArrayList<>();
int titleIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.TITLE);
int pathIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA);
int typeIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE);
int dateIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.DATE_MODIFIED);
int sizeIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.SIZE);
while (cursor.moveToNext()) {
DocItem docItem = new DocItem();
docItem.setTitle(cursor.getString(titleIndex));
docItem.setPath(cursor.getString(pathIndex));
docItem.setType(cursor.getString(typeIndex));
docItem.setModifyDate(cursor.getLong(dateIndex));
docItem.setSize(cursor.getLong(sizeIndex));
docItems.add(docItem);
}
return docItems;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return Collections.emptyList();
}
// 按文件類型構(gòu)建查詢條件
private String buildDocSelection(String... mimeType) {
StringBuilder selection = new StringBuilder();
for (String type : mimeType) {
selection.append("(" + MediaStore.Files.FileColumns.MIME_TYPE + "=='").append(type).append("') OR ");
}
return selection.substring(0, selection.lastIndexOf(")") + 1);
}
調(diào)用getDocByTypes即可查詢SDCard中存在的文檔文件抖部。
這部分文件信息其實(shí)是存儲(chǔ)在系統(tǒng)MediaStore的數(shù)據(jù)庫(kù)中,每次系統(tǒng)啟動(dòng)议惰、或者插入SDCard后慎颗,系統(tǒng)都會(huì)通知MediaScanner進(jìn)行全盤掃描,將掃描到的媒體文件信息全部存儲(chǔ)在MediaStore的數(shù)據(jù)庫(kù)中言询,并以ContentProvider的形式向外部提供查詢接口俯萎。
但是也會(huì)存在一些特殊情況。
有時(shí)我們會(huì)讀取不到某些文件信息运杭,比如剛剛從微信下載的文件夫啊,這是因?yàn)樵撐募螺d后并沒(méi)有將信息添加到MediaStore的數(shù)據(jù)庫(kù)中。
那么辆憔,為了讓系統(tǒng)能夠找到該文件撇眯,我們需要進(jìn)行一次全盤掃描谆趾。
在Android 4.4以下的系統(tǒng)中,我們可以通過(guò)模擬一個(gè)SDCard掛載的廣播來(lái)通知系統(tǒng)進(jìn)行全盤掃描叛本。
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
Uri.parse("file://" + Environment.getExternalStorageDirectory())));
考慮到系統(tǒng)耗電原因沪蓬,Android 4.4以上的版本不再支持應(yīng)用內(nèi)發(fā)送的ACTION_MEDIA_MOUNTED廣播,那么我們就只能尋找其他方案了来候。
我們先來(lái)分析一下ACTION_MEDIA_MOUNTED廣播發(fā)送之后跷叉,系統(tǒng)做了哪些工作。
MediaScannerReceiver
系統(tǒng)中用于接收ACTION_MEDIA_MOUNTED廣播的是MediaScannerReceiver营搅。
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)) {
// 系統(tǒng)啟動(dòng)的時(shí)候云挟,同時(shí)掃描內(nèi)部存儲(chǔ)和外部存儲(chǔ)
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)) {
// 掃描所有掛載的外部存儲(chǔ)
scan(context, MediaProvider.EXTERNAL_VOLUME);
} else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
path != null && path.startsWith(externalStoragePath + "/")) {
// 掃描單個(gè)文件路徑
scanFile(context, path);
}
}
}
}
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
// 啟動(dòng) MediaScannerService
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));
}
}
從以上代碼可以看到,MediaScannerReceiver接收到ACTION_MEDIA_MOUNTED廣播后转质,調(diào)用scan(context, MediaProvider.EXTERNAL_VOLUME)
啟動(dòng)了 MediaScannerService园欣,并將掃描任務(wù)交給其進(jìn)行處理。
MediaScannerService
下面我們對(duì) MediaScannerService 中部分代碼進(jìn)行分析休蟹。
public class MediaScannerService extends Service implements Runnable {
...
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
...
// 將掃描任務(wù)交給ServiceHandler處理
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;
}
...
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) {
// 單個(gè)路徑的掃描
IBinder binder = arguments.getIBinder("listener");
IMediaScannerListener listener =
(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
Uri uri = null;
try {
uri = scanFile(filePath, arguments.getString("mimetype"));
} catch (Exception e) {
Log.e(TAG, "Exception scanning file", e);
}
if (listener != null) {
listener.scanCompleted(filePath, uri);
}
} else {
// 全盤掃描
String volume = arguments.getString("volume");
String[] directories = null;
if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
// 掃描內(nèi)部存儲(chǔ)
directories = new String[] {
Environment.getRootDirectory() + "/media",
Environment.getOemDirectory() + "/media",
};
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
// 掃描外部存儲(chǔ)
directories = mExternalStoragePaths;
}
if (directories != null) {
if (false) Log.d(TAG, "start scanning volume " + volume + ": "
+ Arrays.toString(directories));
// 調(diào)起掃描
scan(directories, volume);
if (false) Log.d(TAG, "done scanning volume " + volume);
}
}
} catch (Exception e) {
Log.e(TAG, "Exception in handleMessage", e);
}
stopSelf(msg.arg1);
}
};
...
}
ServiceHandler 在一個(gè)獨(dú)立的線程中創(chuàng)建沸枯,并非主線程。其中分別對(duì)單個(gè)文件路徑掃描和全盤掃描進(jìn)行了處理赂弓,接下來(lái)看看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);
// 開(kāi)始掃描,發(fā)送廣播通知
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
try {
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
openDatabase(volumeName);
}
// 創(chuàng)建 MediaScanner盈魁,交由其完成實(shí)際的掃描工作
MediaScanner scanner = createMediaScanner();
scanner.scanDirectories(directories, volumeName);
} catch (Exception e) {
Log.e(TAG, "exception in MediaScanner.scan()", e);
}
getContentResolver().delete(scanUri, null, null);
} finally {
// 掃描結(jié)束翔怎,發(fā)送廣播通知
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
mWakeLock.release();
}
}
最終的掃描任務(wù)是由MediaScanner完成的,至此杨耙,我們差不多將 ACTION_MEDIA_MOUNTED 廣播實(shí)現(xiàn)通知系統(tǒng)全盤掃描的流程走完了赤套。
在整個(gè)過(guò)程中,我們看到 MediaScannerReceiver 接收到 ACTION_MEDIA_MOUNTED 廣播后直接啟動(dòng)了 MediaScannerService 服務(wù)珊膜,所以我們可以直接繞過(guò)系統(tǒng)的安全監(jiān)測(cè)容握,直接拉起 MediaScannerService。
public void startMediaScannerService(Context context) {
if (context != null && !scannerServiceStarted) {
// 構(gòu)建一個(gè)拉起 MediaScannerService 的intent
scannerIntent = genScannerServiceIntent();
// 判斷系統(tǒng)是否能夠處理我們的intent
if (context.getPackageManager().resolveService(scannerIntent, 0) != null) {
context.startService(scannerIntent);
// 注冊(cè)用于接收掃描開(kāi)始辅搬、結(jié)束廣播的接收器
scannerReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (scannerListener != null) {
String action = intent.getAction();
Uri uri = intent.getData();
if (Intent.ACTION_MEDIA_SCANNER_STARTED.equals(action)) {
scannerListener.onScannerStarted(uri);
} else if (Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)) {
scannerListener.onScannerFinished(uri);
}
}
}
};
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_STARTED);
filter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
filter.addDataScheme("file");
context.registerReceiver(scannerReceiver, filter);
scannerServiceStarted = true;
} else {
if (scannerListener != null) {
scannerListener.onError();
}
}
}
}
// 構(gòu)建調(diào)起ScannerService的Intent
private Intent genScannerServiceIntent() {
Intent intent = new Intent("android.media.IMediaScannerService");
intent.setComponent(new ComponentName("com.android.providers.media",
"com.android.providers.media.MediaScannerService"));
intent.putExtra("volume", "external");
return intent;
}
// 注銷Service
public void stopMediaScannerService(Context context) {
if (context != null && scannerServiceStarted) {
context.stopService(scannerIntent);
context.unregisterReceiver(scannerReceiver);
scannerIntent = null;
scannerReceiver = null;
scannerServiceStarted = false;
}
}
現(xiàn)在通過(guò)調(diào)用startMediaScannerService就可以通知系統(tǒng)進(jìn)行全盤文件掃描了唯沮。
這種方法雖然比較簡(jiǎn)單,但是弊端還是非常明顯的:
- 每次啟動(dòng)該服務(wù)都會(huì)進(jìn)行全盤掃描堪遂,不僅占用系統(tǒng)資源介蛉,還耗電;
- 只能監(jiān)控到開(kāi)始掃描溶褪、結(jié)束掃描兩個(gè)事件币旧,不能對(duì)掃描過(guò)程進(jìn)行監(jiān)控;
- 掃描的耗時(shí)比較長(zhǎng)猿妈,通常在一個(gè)經(jīng)常使用的手機(jī)掃描一次至少在一分鐘以上
酌情使用吹菱。