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