Android Q & Android 11存儲適配(一) 基礎知識點梳理

下一篇 👉 Android Q & Android 11存儲適配(二) FileOperator文件管理框架java

分區存儲(Scoped Storage)

  • 沙盒存儲(App-specific directory) 本應用專有的目錄(經過 Context.getExternalFilesDir() 訪問)
  • 公共目錄(Public Directory) MediaStore/SAF(Storage Access Framework) with ContentResolver

分區存儲如何影響文件訪問:android

文件位置 所需權限 訪問方法 (*) 卸載應用時是否移除文件?
特定於應用的目錄 getExternalFilesDir()
媒體集合
(照片、視頻、音頻)
READ_EXTERNAL_STORAGE
(僅當訪問其餘應用的文件時)
MediaStore
下載內容
(文檔和
電子書籍)
SAF存儲訪問框架
(加載系統的文件選擇器)

對應於MediaStore 類中僅包含五種文件類型 Image/Video/Audio以及FilesDownload , 其中 Image/Video/Audio 直接使用MediaStore+ContentResolver API便可訪問 , 而FilesDownload則是使用 SAF存儲訪問框架訪問。緩存

⭐ 注意:使用分區存儲的應用對於 /sdcard/DCIM/IMG1024.JPG 這類路徑不具備直接內核訪問權限。要訪問此類文件,應用必須使用 MediaStore,並調用 ContentResolver.openFile() 等方法。bash

App Specific 沙盒目錄

  • 若是配置了 FileProvider 而且配置了external-files-pathexternal-cache-path,應用會在啓動時自動建立 cachefiles目錄:
<!--context.getExternalFilesDirs()-->
 <external-files-path
     name="ando_file_external_files"
     path="." />
 <!-- getExternalCacheDirs() 此標籤須要 support 25.0.0以上纔可使用-->
 <external-cache-path
     name="ando_file_external_cache"
     path="." />
複製代碼

FileProvider :app

<provider
    android:name=".common.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
複製代碼

MediaStore

  • 媒體數據限制

分區存儲會施加如下媒體數據限制:框架

若您的應用未得到 ACCESS_MEDIA_LOCATION 權限,照片文件中的 Exif 元數據會被修改。要了解詳情,請參閱介紹如何訪問照片中的位置信息的部分。ide

MediaStore.Files 表格自己會通過過濾,僅顯示照片、視頻和音頻文件。例如,表格中不顯示 PDF 文件。 必須使用 MediaStore 在 Java 或 Kotlin 代碼中訪問媒體文件。請參閱有關如何從原生代碼訪問媒體文件的指南。 該指南介紹瞭如何處理媒體文件,並提供了有關訪問 MediaStore 內的單個文檔和文檔樹的最佳作法。若是您的應用使用分區存儲,則須要使用這些方法來訪問媒體。post

  • 如何從原生代碼訪問媒體文件

系統會自動掃描外部存儲,並將媒體文件添加到如下定義好的集合中:ui

  • Images, including photographs and screenshots, which are stored in the DCIM/ and Pictures/ directories. The system adds these files to the MediaStore.Images table.
  • Videos, which are stored in the DCIM/, Movies/, and Pictures/ directories. The system adds these files to the MediaStore.Video table.
  • Audio files, which are stored in the Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/, and Ringtones/ directories, as well as audio playlists that are in the Music/ or Movies/ directories. The system adds these files to the MediaStore.Audio table.
  • Downloaded files, which are stored in the Download/ directory. On devices that run Android 10 (API level 29) and higher, these files are stored in the MediaStore.Downloads table. This table isn't available on Android 9 (API level 28) and lower.
  • 權限管理

若是您的應用使用範圍存儲,則它僅應針對運行Android 9(API級別28)或更低版本的設備請求與存儲相關的權限。 您能夠經過在應用清單文件中的權限聲明中添加android:maxSdkVersion屬性來應用此條件:this

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="28" />
複製代碼

不要爲運行Android 10或更高版本的設備沒必要要地請求與存儲相關的權限。 您的應用程序能夠參與定義明確的媒體集合,包括MediaStore.Downloads集合,而無需請求任何與存儲相關的權限。 例如,若是您正在開發相機應用程序,則無需請求與存儲相關的權限,由於您的應用程序擁有您要寫入媒體存儲區的圖像。

  • MediaStore API 提供訪問如下類型的媒體文件的接口:
照片:存儲在 MediaStore.Images 中。
視頻:存儲在 MediaStore.Video 中。
音頻文件:存儲在 MediaStore.Audio 中。
MediaStore 還包含一個名爲 MediaStore.Files 的集合,該集合提供訪問全部類型的媒體文件的接口。其餘文件,例如 PDF 文件,沒法訪問到。
複製代碼

注意:若是您的應用使用分區存儲,MediaStore.Files 集合將僅顯示照片、視頻和音頻文件。

  • 若要加載媒體文件,請從 ContentResolver 調用如下方法之一:

    • 對於單個媒體文件,請使用 openFileDescriptor()
    • 對於單個媒體文件的縮略圖,請使用 loadThumbnail(),並傳入要加載的縮略圖的大小。
    • 對於媒體文件的集合,請使用 ContentResolver.query()
  • 🌰查詢一個媒體文件集合

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didnt create.

// Container for information about each video.
data class Video(val uri: Uri,
    val name: String,
    val duration: Int,
    val size: Int
)
val videoList = mutableListOf<Video>()

val projection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolver.query(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    // Cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
    val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val duration = cursor.getInt(durationColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList += Video(contentUri, name, duration, size)
    }
}
複製代碼
  • 媒體文件的掛起狀態

若是您的應用程序執行一些可能很是耗時的操做,好比寫入媒體文件,那麼在文件被處理時對其進行獨佔訪問是很是有用的。在運行Android 10或更高版本的設備上,您的應用程序能夠經過將IS_PENDING標誌的值設置爲1來得到這種獨佔訪問。只有您的應用程序能夠查看該文件,直到您的應用程序將IS_PENDING的值更改回0。

爲正在存儲的媒體文件提供待處理狀態
在搭載 Android 10(API 級別 29)及更高版本的設備上,您的應用能夠經過使用 IS_PENDING 標記在媒體文件寫入磁盤時得到對文件的獨佔訪問權限。

如下代碼段展現了在將圖片存儲到 MediaStore.Images 集合所對應的目錄時如何使用 IS_PENDING 標記:

    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG")
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val resolver = context.getContentResolver()
    val collection = MediaStore.Images.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    val item = resolver.insert(collection, values)

    resolver.openFileDescriptor(item, "w", null).use { pfd ->
        // Write data into the pending image.
    }

    // Now that we're finished, release the "pending" status, and allow other apps // to view the image. values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(item, values, null, null) 複製代碼

Android Q 上,MediaStore 中添加了一個 IS_PENDING Flag,用於標記當前文件是 Pending 狀態。

其餘 APP 經過 MediaStore 查詢文件,若是沒有設置 setIncludePending 接口,就查詢不到設置爲 Pending 狀態的文件,這就能使 APP 專享此文件。

這個 flag 在一些應用場景下可使用,例如在下載文件的時候:下載中,文件設置爲 Pending 狀態;下載完成,把文件 Pending 狀態置爲 0。

ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "myImage.PNG");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.IS_PENDING, 1);

ContentResolver resolver = context.getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri item = resolver.insert(uri, values);

try {
    ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null);
    // write data into the pending image.
} catch (IOException e) {
    LogUtil.log("write image fail");
}

// clear IS_PENDING flag after writing finished.
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(item, values, null, null);
複製代碼
  • 文件移動
Note: You can move files on disk during a call to update() by changing MediaColumns.RELATIVE_PATH or MediaColumns.DISPLAY_NAME.

注意:您能夠在調用update 的過程當中經過更改 MediaColumns.RELATIVE_PATH  或MediaColumns.DISPLAY_NAME 在磁盤上移動文件。
複製代碼

Storage Access Framework

相關視頻 (Youtube):

Android 4.4(API 級別 19)引入了存儲訪問框架 (SAF)。藉助 SAF,用戶可輕鬆在其全部首選文檔存儲提供程序中瀏覽並打開文檔、圖像及其餘文件。用戶可經過易用的標準界面,以統一方式在全部應用和提供程序中瀏覽文件,以及訪問最近使用的文件。

編輯文檔

⭐ Note: The DocumentFile class's canWrite() method doesn't necessarily indicate that your app can edit a document. That's because this method returns true if Document.COLUMN_FLAGS contains either FLAG_SUPPORTS_DELETE or FLAG_SUPPORTS_WRITE. To determine whether your app can edit a given document, query the value of FLAG_SUPPORTS_WRITE directly.

虛擬文件 👉 視頻

Android 7.0 在存儲訪問框架中加入了虛擬文件的概念。即便虛擬文件沒有二進制表示形式,客戶端應用也可將其強制轉換爲其餘文件類型,或使用 ACTION_VIEW Intent 查看這些文件,從而打開文件中的內容。

如要打開虛擬文件,您的客戶端應用需包含可處理此類文件的特殊邏輯。若想獲取文件的字節表示形式(例如爲了預覽文件),則需從文檔提供程序請求另外一種 MIME 類型。

爲得到應用中虛擬文件的 URI,您首先需建立 Intent 來打開文件選擇器界面(如先前搜索文檔中的代碼所示)。

⭐ 重要說明:因爲應用不能使用 openInputStream() 方法直接打開虛擬文件,所以若是您在 ACTION_OPEN_DOCUMENT Intent 中加入 CATEGORY_OPENABLE 類別,則您的應用不會收到任何虛擬文件。

SAF 使用情形 👉 官方文檔

經過上面的分析能夠看出, MediaStore 僅能夠處理公共目錄中的 圖片/視頻/音頻 文件, 當涉及到分組文件和其它類型文件的時候顯得捉襟見肘。

- [操做一組文件](https://developer.android.google.cn/training/data-storage/shared/media#manage-groups-files)
- [操做文檔和其餘文件](https://developer.android.google.cn/training/data-storage/shared/media#other-file-types)
- [把數據分享給其它應用](https://developer.android.google.cn/training/data-storage/shared/media#companion-apps)
複製代碼
  • 查看剩餘空間

若是您提早知道要存儲多少數據,則能夠經過調用getAllocatableBytes()找出設備能夠爲應用程序提供多少空間。 getAllocatableBytes()的返回值可能大於設備上當前的可用空間量。 這是由於系統已識別出能夠從其餘應用程序的緩存目錄中刪除的文件。

// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
        appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
    val storageIntent = Intent().apply {
        action = ACTION_MANAGE_STORAGE
    }
    // Display prompt to user, requesting that they choose files to remove.
}
複製代碼

⭐ 保存文件以前,不須要檢查可用空間量。 相反,您能夠嘗試當即寫入文件,而後在發生異常時捕獲IOException。

卸載應用

AndroidManifest.xml中聲明:android:hasFragileUserData="true",卸載應用會有提示是否保留 APP數據。默認應用卸載時App-specific目錄下的數據被刪除,但用戶能夠選擇保留。

分享 App-specific 目錄下文件👉 FileProvider

參考資料

文檔

ContentProvider官方文檔

DocumentsProvider官方文檔

惟一標識符最佳作法

視頻

Youtube 👉 www.youtube.com/watch?v=UnJ…

Bilibili 👉 www.bilibili.com/video/BV1NE…

相關文章
相關標籤/搜索