Android Q之——分區存儲
爲了讓用戶更好地管理自己的文件並減少混亂,並且增強文件的安全性,以Android 10(API 級別 29)及更高版本爲目標平臺的應用在默認情況下被賦予了對外部存儲設備的分區訪問權限(即分區存儲)。此類應用只能看到本應用專有的目錄(通過Context.getExternalFilesDir()訪問)以及特定類型的媒體。除非您的應用需要訪問存放在應用的專有目錄以及 MediaStore之外的文件,否則最好使用分區存儲。
分區存儲對文件訪問的影響
文件位置 | 所需權限 | 訪問方法 (*) | 卸載應用時是否移除文件? |
---|---|---|---|
特定於應用的目錄 | 無 | getExternalFilesDir() | 是 |
媒體集合(照片、視頻、音頻) | READ_EXTERNAL_STORAGE(僅當訪問其他應用的文件時) | MediaStore | 否 |
下載內容(文檔和電子書籍) | 無 | 存儲訪問框架 | 否 |
注:我們可以使用存儲訪問框架訪問上表中顯示的每一個位置,而無需請求任何權限。
文件訪問權限
Android Q之前版本
Android Q之前,訪問外部存儲時,需要申請讀或寫的權限:READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE。應用獲取到讀或寫權限之後,就可以自由訪問相應的外部存儲目錄了。
注意,在Android 4.4(API 級別 19)之前,訪問特定於應用的目錄(應用的外部私有目錄,位於:/Android/data/<app包名>/),是需要讀或寫權限的,但4.4之後的版本就不需要該權限了。
如果我們的應用支持4.4之前的版本,可以在AndroidManifest中聲明:
<manifest ...>
<!-- If you need to modify files in external storage, request
WRITE_EXTERNAL_STORAGE instead. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
</manifest>
Android Q及之後版本
訪問應用自己創建的文件
在Android Q中,應用對自己創建的文件始終擁有讀/寫權限,無論文件是否位於應用的專有目錄內。
例如,應用可以查看外部存儲設備內以下類型的文件,無需請求任何與存儲相關的用戶權限:
- 特定於應用的目錄中的文件(使用 getExternalFilesDir() 訪問)。
- 應用創建的照片、視頻和音頻片段(通過媒體庫訪問)。
訪問其他應用創建的文件
Android Q中,若要訪問其他應用創建的文件,則必須滿足以下兩個條件:
-
應用已獲得 READ_EXTERNAL_STORAGE 權限。
-
這些文件位於以下其中一個明確定義的媒體集合中:
- 照片:存儲在 MediaStore.Images 中。
- 視頻:存儲在 MediaStore.Video 中。
- 音頻文件:存儲在 MediaStore.Audio 中。
如果要訪問其他文件及目錄,包括“downloads”目錄下的文件,應用必須使用存儲訪問框架,該框架允許用戶選擇特定文件。
如果應用嘗試通過原始文件系統視圖打開此目錄之外的文件,則會發生錯誤:
- 在託管代碼中,會發生 FileNotFoundException 錯誤。
- 在原生代碼中,會發生 EPERM 內核錯誤。
訪問媒體數據文件
分區存儲會對訪問媒體數據文件增加一些限制:
- 若應用未獲得ACCESS_MEDIA_LOCATION權限,照片文件中的Exif元數據會被修改。一些照片在其Exif元數據中包含位置信息,以便用戶查看照片的拍攝地點。但是,由於此位置信息屬於敏感信息,如果應用使用了分區存儲,默認情況下Android 10會對應用隱藏此信息。
- MediaStore.Files表格本身會經過過濾,僅顯示照片、視頻和音頻文件。例如,表格中不顯示PDF文件。
- 必須使用MediaStore在Java或Kotlin代碼中訪問媒體文件。
訪問照片的位置信息示例
- 在應用的清單中請求ACCESS_MEDIA_LOCATION權限。
- 從MediaStore對象調用setRequireOriginal(),並傳入照片的URI,如以下代碼段中所示:
Uri photoUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
final double[] latLong;
// Get location data from the ExifInterface class.
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
double[] returnedLatLong = exifInterface.getLatLong();
// If lat/long is null, fall back to the coordinates (0, 0).
latLong = returnedLatLong != null ? returnedLatLong : new double[2];
// Don't reuse the stream associated with the instance of "ExifInterface".
stream.close();
} else {
// Failed to load the stream, so return the coordinates (0, 0).
latLong = new double[2];
}
唯一卷名稱
面向Android Q(API 級別 29)或更高版本的應用可以訪問系統分配給每個外部存儲設備的唯一名稱。此命名系統可幫助您高效地整理內容並將內容編入索引,還可讓您控制新內容的存儲位置。
- 主要共享存儲設備始終名爲:VOLUME_EXTERNAL_PRIMARY。
- 可以通過調用MediaStore.getExternalVolumeNames()來發現其他卷。
要查詢、插入、更新或刪除特定卷,請將卷名稱傳入 MediaStore API 中提供的任何 getContentUri() 方法,如以下代碼段中所示:
// Assumes that the storage device of interest is the 2nd one
// that your app recognizes.
val volumeNames = MediaStore.getExternalVolumeNames(context)
val selectedVolumeName = volumeNames[1]
val collection = MediaStore.Audio.Media.getContentUri(selectedVolumeName)
// ... Use a ContentResolver to add items to the returned media collection.
另外,在舊版本的Android系統中,當存在多個不同的外部存儲目錄時(例如2個):
- Android4.4(API 級別 19)及之後版本,可以通過調用 getExternalFilesDirs() 來訪問這兩個位置,這會返回一個 File 數組,其中包含了每個存儲位置的條目。數組中的第一個條目被視爲主要外部存儲,除非該位置已滿或不可用,否則應該一律使用該位置。
- 如果您的應用支持 Android 4.3 及更低版本,則應使用支持庫的靜態方法ContextCompat.getExternalFilesDirs()。這始終會返回一個 File 數組,但如果設備搭載的是 Android 4.3及更低版本,數組中將僅包含主要外部存儲的條目。(如果有第二個存儲位置,您將無法在 Android 4.3 及更低版本上訪問它。)
停用分區存儲
如果我們的應用還行使用Android Q之前的存儲策略,可以選擇暫時停用“分區存儲”。
我們可以使用2種方式來暫停分區存儲:
- 以Android 9(API 級別 28)或更低版本爲目標平臺。
- 如果以Android Q或更高版本爲目標平臺,請在應用的清單文件中將requestLegacyExternalStorage的值設爲true:
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
同樣,要測試以Android 9 或更低版本爲目標平臺的應用在使用分區存儲時的行爲,您可以通過將requestLegacyExternalStorage的值設爲false來選擇啓用該行爲。
警告:未來,主要平臺版本將要求所有應用都使用分區存儲,無論應用的目標SDK級別是多少。因此,我們應該提前確保我們的應用能夠使用分區存儲。爲此,請確保針對搭載Android 10(API 級別 29)及更高版本的設備啓用了該行爲。
我們在處理文件存儲時,一定要考慮Android版本之間的差異,然後合理安排App的文件存儲位置,避免使用不當導致的各種問題。