2015年1月3日 星期六

玩一玩Android外部記憶體

2015/1/2 14:41-15:15, 22:32-24:04

年假在家裡休息無所事事,昨天花一個下午的時間稍微試一下Android的外部記憶體,也就是在裝置中的/sdcard/目錄的使用方式。

在Android 4.4之前,只要在AndroidManifest.xml中加入android.permission.WRITE_EXTERNAL_STORAGE,就可以自由地使用外部路徑,使用API Environment.getExternalStorageDirectory()可以取得外部記憶體的路徑。約在一年前,Android作業系統大部分都使用在手機上,那時的HTC最高階的手機是HTC Sensation,同一時期的各家廠牌手機,外部記憶體通常指的就是Micro SD卡,開發App時,將檔案寫在外部記憶體很方便,能透過ADB或USB隨身碟的方式將檔案拉到PC上。

一直到了HTC機皇是HTC One X的時期,原本是外部記憶體路徑的/sdcard/,變成虛擬外部記憶體,也就是說,這個/sdcard/目錄,並不是實體Micro SD卡的路徑;而是手機內部空間虛擬成的外部記憶體,這個虛擬而成的外部記憶體一樣須加上permission後才能正常讀寫檔案,在開發階段很方便,不需要真的取準備一張實體的Micro SD卡,就可以自由地將檔案從手機拉到電腦上。

但也衍生一個問題,如果真的想把檔案存到實體的Micro SD卡中,該怎麼辦呢?目前Android也沒有提供相關的API取得實體外部路徑,但在網路上有一些非官方的方法可以取得真正SD卡路徑的,但不是每隻手機都適用。

在Android 4.4剛出來的時候,很興奮的把我的Asus Nexus 7升級,升級之後原本的程式一直當機,追查後發現是Android 4.4以後的版本,sdcard有做一層保護,App不能直接使用外部記憶體(不管是虛擬或實體外部記憶體),需透過手機的設定,才能把整隻App移到sdcard上,但還是有一個缺點,把App移動sdcard上後,只要移除該App,所有和該App關聯的檔案(包含使用該App所產生出來的所有檔案)全部都會被刪除。天殺的Google這樣改,超級麻煩,在開發階段的App,動不動就須移除程式重新安裝,產生好的假資料每安裝一次就要再建立一次,怕麻煩的我決定重回Android 4.2的懷抱,把Nexus 7先丟一旁,拿實驗室的其他手機來開發。

最近使用ES File Browser (最初它也不能再Android 4.4上正常運作),突然發現它在我的Nexus 7上竟然可以正常存取外部記憶體中的檔案,於是我好奇地重回Android 4.4的懷抱,參考這裡寫了一隻在外部記憶體上寫檔案和讀檔案的程式,檔案竟然可以操作了!操作方法跟以前一模一樣。

在Android 4.4多了一個permission:android.permission.READ_EXTERNAL_STORAGE,顧名思義就是讀檔案的權限。
寫檔案的權限已經包含讀檔案的權限,也就是說如果加入android.permission.WRITE_EXTERNAL_STORAGE,就可以不用再加入android.permission.READ_EXTERNAL_STORAGE,Google也很用心地提供了isExternalStorageWritable()isExternalStorageReadable()兩個方法,用來判斷外部記憶體是否能讀寫。

於是我做了小實驗,觀察isExternalStorageWritable()isExternalStorageReadable()的回傳值:

在沒有加入任何權限的情況下,
isWritable()回傳true
isReadable()回傳true
但當我執行檔案讀取或檔案寫入都會失敗,
蝦咪!?明明不能讀寫檔案還給我回傳true。

追了一下程式發現這兩個方法都是藉由Environment.getExternalStorageState()的回傳字串判斷外部記憶體的狀態是否可讀可寫,於是我又在HTC Butterfly測試,HTC Butterfly可以插實體外部記憶體(Micro SD卡),觀察實體外部記憶體在插上或拔除的時候Environment.getExternalStorageState()會不會取得其他不同的狀態,結果當然沒有這麼順利,它竟然永遠都是回傳Environment.MEDIA_MOUNTED
PS: HTC Butterfly的"/sdcard/"目錄是屬於虛擬外部記憶體而非實體外部記憶體。

接著我決定實作看看『正確地判斷/sdcard/可讀或可寫』的方法,花了約六個小時的時間(因為嚐鮮透過Android Studio加上最近剛學的TDD實作),這邊直接給大家我完成的程式碼。

先定義一下我所個人認為的isExternalStorageReadable()isExternalStorageWritable(),兩個方法的理想行為

isExternalStorageReadable():

在沒有加入android.permission.READ_EXTERNAL_STORAGEandroid.permission.WRITE_EXTERNAL_STORAGE時,回傳false
反之則回傳true,代表可以讀取在外部記憶體的檔案。

isExternalStorageWritable():

在沒有加入android.permission.WRITE_EXTERNAL_STORAGE時,回傳false
反之則回傳true,代表可以寫資料在外部記憶體內。




實作內容如下(黃色部分):

----------------------------------------------------------------------------------------------------
boolean isExternalStorageReadable() {
        return Environment.getExternalStorageDirectory().canRead();
}

boolean isExternalStorageWritable() {
        boolean writable;
        final String filePath = Environment.getExternalStorageDirectory() + 
                                       "/" + "testExternalStorageWritableFile";
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(filePath);
            writable = true;
            // delete test file
            File file = new File(filePath);
            file.delete();
        } catch (FileNotFoundException e) {
            writable = false;
        } finally {
            if(fos != null)
                try {
                    fos.close();
                } catch(IOException e) {
                    e.printStackTrace();
                }
        }
        return writable;
}
----------------------------------------------------------------------------------------------------


最初的isExternalStorageWritable()使用Environment.getExternalStorageDirectory().canWrite();
但我加入android.permission.READ_EXTERNAL_STORAGE後它就回傳true了,在這個情境下要回傳false才對。

所以最後用了比較麻煩的方法:

如果沒有加入android.permission.WRITE_EXTERNAL_STORAGE,
執行new FileOutputStream(...)就會捕捉到FileNotFoundException,此時代表外部記憶體不可寫。

加入android.permission.WRITE_EXTERNAL_STORAGEnew FileOutputStream(...)不會發生例外且產生檔案,代表外部記憶體可寫,但最後必須將產生的檔案刪除。


以上是我小小的分享,祝各位新年快樂!



有疑問就去挖(一直"那些事"好煩喔,暫時讓它消失,有需要再讓它出現)

沒有留言:

張貼留言