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

// 按文件类型构建查询条件
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就可以通知系统进行全盘文件扫描了。


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

  1. 每次启动该服务都会进行全盘扫描,不仅占用系统资源,还耗电;
  2. 只能监控到开始扫描、结束扫描两个事件,不能对扫描过程进行监控;
  3. 扫描的耗时比较长,通常在一个经常使用的手机扫描一次至少在一分钟以上

酌情使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容