External Storage 中的檔案能與其他 APP 共享資源,也可以使用手機內建的檔案管理 APP 來瀏覽這些檔案。 傳統的 External Storage 通常代表 SD 卡,但實際上 External Storage 區分 Primary External Storage 及 Secondary External Storage 兩種類型,每個類型還區分 APP 專屬目錄及共享目錄。 這幾種類型主要差異只有目錄取得的方式,後續的各項操作都大同小異。 由於 External Storage 儲存設備可能被使用者移除,因此比較適合用來儲存 APP 執行時非必要的檔案,就是被刪除也不影響 APP 執行的檔案。 在使用檔案前,也建議應該先檢查設備是否可讀可寫。

External Storage 類型介紹

下圖是 External Storage 不同類型的目錄位置,位於 APP 專屬目錄 (app-specific) 中的檔案,會在 APP 解除安裝時一併被移除,共享目錄則不會移除

Primary External Storage

Primary External Storage 直接翻譯就是主要外部儲存空間,這個空間是位於手機內部的儲存空間,因此無論有沒有 SD 卡,APP 都一定有 Primary External Storage 可以使用。

APP 專屬目錄 (app-specific)

                
                    // 取得 app-specific 目錄路徑
                    // 路徑:/storage/emulated/0/Android/data/[Package Name]/files
                    ContextCompat.getExternalFilesDirs(getApplicationContext(), null)[0];
                    getExternalFilesDir(null);

                    // 取得 app-specific 中特定類型目錄路徑
                    // 路徑:/storage/emulated/0/Android/data/[Package Name]/files/Download
                    ContextCompat.getExternalFilesDirs(getApplicationContext(), Environment.DIRECTORY_DOWNLOADS)[0];
                    getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);

                    // 取得 app-specific 快取目錄路徑
                    // 路徑:/storage/emulated/0/Android/data/[Package Name]/cache
                    ContextCompat.getExternalCacheDirs(getApplicationContext())[0];
                    getExternalCacheDir();
                
            

共享目錄 (專屬目錄以外)
這類型操作需要額外的權限,詳細說明請參考第 3 章

                
                    // 取得共享目錄路徑
                    // 路徑:/storage/emulated/0
                    Environment.getExternalStorageDirectory();

                    // 取得特定類型共享目錄
                    // 路徑:/storage/emulated/0/Download
                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                
            

Secondary External Storage

Secondary External Storage 就是指 Primary External Storag 以外的儲存空間,一般常見的就是 SD 卡或是 USB 外接隨身碟。

APP 專屬目錄 (app-specific)

                
                    // 取得 app-specific 目錄路徑
                    // 路徑:/storage/0000-0000/Android/data/[Package Name]/files
                    ContextCompat.getExternalFilesDirs(getApplicationContext(), null)[1];

                    // 取得 app-specific 快取目錄路徑
                    // 路徑:/storage/0000-0000/Android/data/[Package Name]/cache
                    ContextCompat.getExternalCacheDirs(getApplicationContext())[1];
                
            

共享目錄 (專屬目錄以外)
目前尚無有效方法可操作此類型目錄。

確認儲存空間是否可用

由於 External Storage 使用的儲存設備可能會被使用者移除,因此在使用前需要先檢查儲存空間是否可以使用。

            
                // 確認 External Storage 是否可讀取可寫入
                private boolean isExternalStorageWritable() {
                    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
                }

                // 確認 External Storage 是否可讀取
                private boolean isExternalStorageReadable() {
                    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ||
                            Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
                }
            
        

存取共享目錄所需權限

存取共享目錄需要在 AndroidManifest.xml 檔案中,設定 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 權限,並將 requestLegacyExternalStorage 設定為 true

            
                <manifest>
                    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
                    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

                    <application android:requestLegacyExternalStorage="true">
                        ...
                    </application>
                </manifest>
            
        

之後在程式碼中,檢查是否具備權限。

            
                public int REQUEST_CODE_PERMISSION_STORAGE = 100;

                protected void onCreate(Bundle savedInstanceState) {
                    String[] permissions = {
                        Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                    };

                    for (String string : permissions) {
                        if (this.checkSelfPermission(string) != PackageManager.PERMISSION_GRANTED) {
                            this.requestPermissions(permissions, REQUEST_CODE_PERMISSION_STORAGE);
                            return;
                        }
                    }
                }

                @Override
                public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
                    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

                    if (requestCode == REQUEST_CODE_PERMISSION_STORAGE) {
                        if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                            // 使用者拒絕權限
                        } else {
                            // 使用者允許權限
                        }
                    }
                }
            
        

檔案操作

以下是 External Storage 常用操作程式碼,提供大家參考。

取得目錄

                
                    // 取得 app-specific 目錄 (Primary External Storage)
                    ContextCompat.getExternalFilesDirs(getApplicationContext(), null)[0];
                    getExternalFilesDir(null);

                    // 取得 app-specific 快取目錄 (Primary External Storage)
                    ContextCompat.getExternalCacheDirs(getApplicationContext())[0];
                    getExternalCacheDir()

                    // 取得 app-specific 目錄 (Secondary External Storage)
                    ContextCompat.getExternalFilesDirs(getApplicationContext(), null)[1];

                    // 取得 app-specific 快取目錄 (Secondary External Storage)
                    ContextCompat.getExternalCacheDirs(getApplicationContext())[1];

                    // 取得共享目錄 (需要權限)
                    Environment.getExternalStorageDirectory();
                
            

寫入

                
                    public void writeFileToExternal(String fileName, String fileContents) throws IOException {
                        File file = new File(getExternalFilesDir(null), fileName);
                        FileOutputStream outputStream = new FileOutputStream(file);
                        outputStream.write(fileContents.getBytes());
                        outputStream.close();
                    }

                    writeFileToExternal("test", "測試資料");
                
            

讀取

                
                    public void readFileFromExternal(String fileName) throws IOException {
                        File file = new File(getExternalFilesDir(null), fileName);
                        FileInputStream inputStream = new FileInputStream(file);
                        BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
                        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                        for (int result = bufferedStream.read(); result != -1; result = bufferedStream.read()) {
                            outputStream.write((byte) result);
                        }

                        // 關閉串流
                        inputStream.close();

                        // 回傳檔案字串
                        return outputStream.toString("UTF-8");
                    }

                    readFileFromExternal("test", "測試資料");
                
            

刪除

                
                    public void removeFileFromExternal(String fileName) {
                        File file = new File(getExternalFilesDir(null), fileName);
                        File file = new File(parentFile, fileName);
                        file.delete();
                    }

                    removeFileFromExternal("test");
                
            

從網址取得檔案儲存至 External Storage

上述範例是將純文字寫入至本機檔案儲存,但在實際案例中,很少單純將文字寫入到檔案而已。 通常都是透過網址將遠端的檔案下載至本機儲存,請參考以下範例。

            
                new Thread(() -> {
                    try {
                        URL url = new URL ("檔案網址");
                        InputStream inputStream = url.openStream();
                        OutputStream outputStream = new FileOutputStream(getExternalFilesDir(null).getPath() + "/檔名");
                        byte [] buffer = new byte[1024];
                        int length;
                        while ((length = inputStream.read(buffer)) > 0) {
                            outputStream.write(buffer, 0, length);
                        }
                        outputStream.flush();
                        outputStream.close();
                        inputStream.close();
                    } catch (IOException ignored) {
                    }
                }).start();