Android SD卡 全盤文件掃描

在開(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)單,但是弊端還是非常明顯的:

  1. 每次啟動(dòng)該服務(wù)都會(huì)進(jìn)行全盤掃描堪遂,不僅占用系統(tǒng)資源介蛉,還耗電;
  2. 只能監(jiān)控到開(kāi)始掃描溶褪、結(jié)束掃描兩個(gè)事件币旧,不能對(duì)掃描過(guò)程進(jìn)行監(jiān)控;
  3. 掃描的耗時(shí)比較長(zhǎng)猿妈,通常在一個(gè)經(jīng)常使用的手機(jī)掃描一次至少在一分鐘以上

酌情使用吹菱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末巍虫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鳍刷,更是在濱河造成了極大的恐慌占遥,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件输瓜,死亡現(xiàn)場(chǎng)離奇詭異瓦胎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)尤揣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門搔啊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人北戏,你說(shuō)我怎么就攤上這事负芋。” “怎么了嗜愈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵旧蛾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我芝硬,道長(zhǎng)蚜点,這世上最難降的妖魔是什么罩阵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任而账,我火速辦了婚禮兽泣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘迟赃。我一直安慰自己,他們只是感情好厂镇,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布纤壁。 她就那樣靜靜地躺著,像睡著了一般捺信。 火紅的嫁衣襯著肌膚如雪酌媒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,821評(píng)論 1 290
  • 那天迄靠,我揣著相機(jī)與錄音秒咨,去河邊找鬼。 笑死掌挚,一個(gè)胖子當(dāng)著我的面吹牛雨席,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吠式,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼陡厘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼抽米!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起糙置,我...
    開(kāi)封第一講書(shū)人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤云茸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后谤饭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體标捺,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年网持,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宜岛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡功舀,死狀恐怖萍倡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辟汰,我是刑警寧澤列敲,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站帖汞,受9級(jí)特大地震影響戴而,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翩蘸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一所意、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧催首,春花似錦扶踊、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至舶治,卻和暖如春分井,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霉猛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工尺锚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人韩脏。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓缩麸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杭朱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容