Android SD卡 全盘文件扫描

Android SD卡 全盘文件扫描

在开发的过程中,有时候会遇到需要读取多媒体文件的需求,面对这样的需求,通常我们有两种解决方案:自己扫描全盘文件,或者使用ContentResolver读取系统记录。

一般需求不是特别复杂的情况下,直接读取系统数据就OK。以查看系统中文档为例:

// 查询的文件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 getDocByTypes(Context context, String... mimeType) {

Cursor cursor = null;

try {

// 按时间倒序查询系统中文档文件

cursor = context.getContentResolver().query(DOC_URI, sColumns, buildDocSelection(mimeType),

null, MediaStore.Files.FileColumns.DATE_MODIFIED + " DESC");

if (cursor != null) {

List 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();

}

// 按文件类型构建查询条件

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

}

调用getDocByTypes即可查询SDCard中存在的文档文件。

这部分文件信息其实是存储在系统MediaStore的数据库中,每次系统启动、或者插入SDCard后,系统都会通知MediaScanner进行全盘扫描,将扫描到的媒体文件信息全部存储在MediaStore的数据库中,并以ContentProvider的形式向外部提供查询接口。

但是也会存在一些特殊情况。

有时我们会读取不到某些文件信息,比如刚刚从微信下载的文件,这是因为该文件下载后并没有将信息添加到MediaStore的数据库中。

那么,为了让系统能够找到该文件,我们需要进行一次全盘扫描。

在Android 4.4以下的系统中,我们可以通过模拟一个SDCard挂载的广播来通知系统进行全盘扫描。

context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,

Uri.parse("file://" + Environment.getExternalStorageDirectory())));

考虑到系统耗电原因,Android 4.4以上的版本不再支持应用内发送的ACTION_MEDIA_MOUNTED广播,那么我们就只能寻找其他方案了。

我们先来分析一下ACTION_MEDIA_MOUNTED广播发送之后,系统做了哪些工作。

MediaScannerReceiver

系统中用于接收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)) {

// 系统启动的时候,同时扫描内部存储和外部存储

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

// 启动 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广播后,调用scan(context, MediaProvider.EXTERNAL_VOLUME)启动了 MediaScannerService,并将扫描任务交给其进行处理。

MediaScannerService

下面我们对 MediaScannerService 中部分代码进行分析。

public class MediaScannerService extends Service implements Runnable {

...

@Override

public int onStartCommand(Intent intent, int flags, int startId) {

...

// 将扫描任务交给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) {

// 单个路径的扫描

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)) {

// 扫描内部存储

directories = new String[] {

Environment.getRootDirectory() + "/media",

Environment.getOemDirectory() + "/media",

};

}

else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {

// 扫描外部存储

directories = mExternalStoragePaths;

}

if (directories != null) {

if (false) Log.d(TAG, "start scanning volume " + volume + ": "

+ Arrays.toString(directories));

// 调起扫描

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 在一个独立的线程中创建,并非主线程。其中分别对单个文件路径扫描和全盘扫描进行了处理,接下来看看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);

}

// 创建 MediaScanner,交由其完成实际的扫描工作

MediaScanner scanner = createMediaScanner();

scanner.scanDirectories(directories, volumeName);

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

}

}

最终的扫描任务是由MediaScanner完成的,至此,我们差不多将 ACTION_MEDIA_MOUNTED 广播实现通知系统全盘扫描的流程走完了。

在整个过程中,我们看到 MediaScannerReceiver 接收到 ACTION_MEDIA_MOUNTED 广播后直接启动了 MediaScannerService 服务,所以我们可以直接绕过系统的安全监测,直接拉起 MediaScannerService。

public void startMediaScannerService(Context context) {

if (context != null && !scannerServiceStarted) {

// 构建一个拉起 MediaScannerService 的intent

scannerIntent = genScannerServiceIntent();

// 判断系统是否能够处理我们的intent

if (context.getPackageManager().resolveService(scannerIntent, 0) != null) {

context.startService(scannerIntent);

// 注册用于接收扫描开始、结束广播的接收器

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

}

}

}

}

// 构建调起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;

}

}

现在通过调用startMediaScannerService就可以通知系统进行全盘文件扫描了。

这种方法虽然比较简单,但是弊端还是非常明显的:

每次启动该服务都会进行全盘扫描,不仅占用系统资源,还耗电;

只能监控到开始扫描、结束扫描两个事件,不能对扫描过程进行监控;

扫描的耗时比较长,通常在一个经常使用的手机扫描一次至少在一分钟以上

酌情使用。

相关推荐

48365 手机闪存是什么意思?手机闪存的作用是什么?

手机闪存是什么意思?手机闪存的作用是什么?

48365 髂胫束在什么位置

髂胫束在什么位置

bt365备用网站 如何系长围巾(79张):美丽的方法来系针织,温暖的羊毛和冬天很长的围巾

如何系长围巾(79张):美丽的方法来系针织,温暖的羊毛和冬天很长的围巾