Android 13 媒體許可權適配指南

語言: CN / TW / HK

theme: hydrogen highlight: androidstudio


我正在參加「掘金·啟航計劃」

在 Android 系統最近的幾個大版本里,更新方向有很大一部分都集中在了隱私安全這一方面,每個版本都會新增隱私安全限制,或者是對之前的隱私項進行進一步的升級

  • Android 10。分割槽儲存、限制訪問不可重置的硬體識別符號、限制對剪貼簿資料的訪問許可權
  • Android 11。強制執行分割槽儲存、單次授權、自動重置許可權、軟體包可見性
  • Android 12。授予大致位置資訊許可權、剪貼簿訪問通知、更安全的元件匯出
  • Android 13。細化的媒體許可權、內建圖片選擇器、隱藏剪貼簿中的敏感內容、遮蔽不匹配的 Intent、針對 Wifi 裝置的新執行時許可權、廣告 ID 許可權

Android 13 在最近也釋出了正式版,此次版本中新增的隱私安全限制也終於能夠解決眾多應用長久以來的兩個問題了

在很多 Android 應用中,都會通過內建一個圖片選擇器來向用戶展示系統相簿內的所有圖片,常見於“上傳使用者頭像、傳送圖片”等業務場景,這就需要通過獲得 READ_EXTERNAL_STORAGE 許可權來實現了。而這個許可權也存在極大的隱私風險,應用也許會向用戶說明該許可權僅僅只會在選擇圖片時使用,但除了應用開發者外,誰又能確保應用不會依靠該許可權在後臺偷偷做些什麼呢?而對於開發者來說也屬於無奈之舉,應用也許僅僅只是想拿到系統相簿內的圖片而已,卻由於 Android 系統的機制被迫要去申請一個應用範圍更高的許可權

平心而論,我覺得在應用中內建一個圖片選擇器的確算作是一個比較能提升使用者體驗的點。因為 Android 系統內建的圖片選擇器的功能長久以來一直很弱,應用的業務場景往往都需要限制圖片的型別、允許選擇多張圖片並限制最大選擇數,但系統卻無法滿足這些需求,如果我們等到使用者選擇圖片返回後再提示使用者不支援該圖片格式,或者是讓使用者多次往返選擇圖片的話,那的確是挺讓人反感的

以上兩個問題,依靠在 Android 13 中新增的兩個隱私安全項:細化的媒體許可權內建圖片選擇器,也終於能夠得到解決了,後面來一一進行講解

細化的媒體許可權

在 Android 13 之前,應用如果想要訪問裝置中的媒體資源的話,都必須通過 READ_EXTERNAL_STORAGE 許可權才能實現。從 Android 13 開始,系統將 READ_EXTERNAL_STORAGE 細分為了三個更加明確的許可權,分別用於訪問使用者的三類媒體資源:Image、Video、Audio,從而讓使用者能夠按需授權,避免隱私風險無序擴大

| 媒體型別 | 請求許可權 | | ---- | ----------------- | | 圖片 | READ_MEDIA_IMAGES | | 影片 | READ_MEDIA_VIDEO | | 音訊 | READ_MEDIA_AUDIO |

我們需要同時通過 應用的 targetSdkVersion裝置的系統版本 來適配這三個許可權

  • 如果應用還未適配 Android 13,也即 targetSdkVersion 小於 33

    • 此時不管系統版本是多少,依然還是通過 READ_EXTERNAL_STORAGE 許可權來訪問媒體資源
  • 如果應用已適配 Android 13,也即 targetSdkVersion 大於等於 33

    • 如果系統版本小於 33,此時依然要通過 READ_EXTERNAL_STORAGE 才能訪問媒體資源
    • 如果系統版本大於等於 33,此時必須通過這三個細分許可權才能訪問媒體資源,READ_EXTERNAL_STORAGE 許可權已失效

簡單來說,如果系統版本和 targetSdkVersion 都大於等於 33 的話,此時就必須通過這三個細分許可權才能訪問媒體資源,其它情況還是需要依賴於 READ_EXTERNAL_STORAGE 許可權

看個實際的例子

幾個月前我釋出了一個開源庫, 一個用 Jetpack Compose 實現的 Android 圖片選擇框架:Matisse

也發表了一篇文章進行介紹:Jetpack Compose 實現一個圖片選擇框架

Matisse 的特點和優勢有:

  • 完全用 Kotlin 實現,拒絕 Java
  • UI 層完全用 Jetpack Compose 實現,拒絕原生 View 體系
  • 支援精細自定義主題,預設提供了 日間 和 夜間 兩種主題
  • 支援精準篩選圖片型別,只顯示想要的圖片型別
  • 支援在圖片列表頁開啟拍照入口,同時支援 FileProvider 和 MediaStore 兩種拍照策略
  • 支援詳細獲取圖片資訊,一共包含 uri、displayName、mimeType、width、height、orientation、size、path、bucketId、bucketDisplayName 等十個屬性值
  • 適配到 Android 12

當然,目前 Android 13 已釋出正式版,這陣子我也將 Matisse 適配到了 Android 13

Matisse 作為一個開源的圖片選擇框架,自然需要同時顧及 引用方的 targetSdkVersion裝置的系統版本 這兩個變數,針對 Android 13 的 READ_MEDIA_IMAGES 許可權,其實也僅需要將以前固定申請 READ_EXTERNAL_STORAGE 許可權的方式,改為選擇性申請即可

```

private fun requestReadImagesPermission() {    val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&        applicationInfo.targetSdkVersion >= Build.VERSION_CODES.TIRAMISU   ) {        Manifest.permission.READ_MEDIA_IMAGES   } else {        Manifest.permission.READ_EXTERNAL_STORAGE   }    if (PermissionUtils.checkSelfPermission(context = this, permission = permission)) {        //已獲得必要許可權,可以去載入系統相簿圖片了   } else {        //去申請必要許可權   } } ```

此外,我們也要根據實際情況來宣告最合適且最少的許可權

如果應用的 targetSdkVersion 小於 33,則還是繼續宣告 READ_EXTERNAL_STORAGE 許可權即可

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

如果應用的 targetSdkVersion 大於等於 33,則需要同時宣告 READ_EXTERNAL_STORAGE 和 READ_MEDIA_IMAGES 兩個許可權。此外,在這種情況下,READ_EXTERNAL_STORAGE 許可權已無法用於 Android 13 開始之後的系統版本了,所以可以將此許可權的 maxSdkVersion 設為 32,從而不會出現在 Android 13 開始之後的系統版本中

<uses-permission    android:name="android.permission.READ_EXTERNAL_STORAGE"    android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

內建圖片選擇器

從 Android 13 開始,Android 系統內建了一個功能更為強大的圖片選擇器,可以讓應用更加靈活地訪問裝置中的媒體資源,且無需擁有檢視裝置上所有媒體檔案的許可權。此外,雖然官方將其命名為圖片選擇器,但實際上也支援選擇裝置中的影片檔案

引用 Google 官方的描述:

照片選擇器提供了一個可瀏覽、可搜尋的介面,其中按日期從最近到最早的順序向用戶呈現其媒體庫中的檔案。此工具為使用者提供了一種安全的內建圖片和影片選擇方式,讓其無需嚮應用授予對整個媒體庫的訪問許可權

如果您允許系統配置照片選擇器,則該工具適用於滿足以下條件的裝置(Android Go 裝置除外):

  • 搭載 Android 11(API 級別 30)或更高版本
  • 通過 Google 系統更新接收對模組化系統元件的更改

此外,如果您允許系統配置照片選擇器,該工具會自動更新,並隨著時間推移為應用的使用者提供擴充套件的功能,而無需對程式碼進行任何更改

再來看下應用如何來使用該圖片選擇器

首先,由於 Google 會通過 Google Play 將該新型的圖片選擇器推送給 Android 11 及以上系統的裝置 (不包括 Go 裝置),所以該功能不僅僅只能在 Android 13 開始後的系統可以使用,我們可以通過如下方法來檢查當前裝置是否支援該功能

//compileSdkVersion 需要至少為 33 才可以呼叫此方法 fun isPhotoPickerAvailable(): Boolean {    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {        true   } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {        getExtensionVersion(Build.VERSION_CODES.R) >= 2   } else {        false   } }

我們可以通過以下兩種 ActivityResultContract 來啟動圖片選擇器:

  • PickVisualMedia。用於選擇單張圖片或單個影片
  • PickMultipleVisualMedia。用於選擇多張圖片或多個影片

例如,在只需要選擇單張圖片或者單個影片的情況下,可以這麼使用:

private val pickMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->    if (uri != null) {        //TODO   } } ​ //選擇圖片或影片 pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) ​ //僅選擇圖片 pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) ​ //僅選擇影片 pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) ​ //僅選擇 Gif 圖片 val mimeType = "image/gif" pickMedia.launch(    PickVisualMediaRequest(        ActivityResultContracts.PickVisualMedia.SingleMimeType(            mimeType       )   ) )

在這種情況下,圖片選擇器會以半屏模式開啟

類似的,如果想要選擇多張圖片或者是多個影片,可以通過 PickMultipleVisualMedia 來限定最大的選取數量

private val pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(5)) { uris ->    if (uris.isNotEmpty()) {        //TODO   } }

此時,圖片選擇器會以全屏展開的形式進行展示,當資源數量超限時也會對使用者進行提示

需要注意,以上程式碼需要新增 1.6.0 或更高版本的 androidx.activity 庫後才可以使用。此外,從 PickVisualMedia 和 PickMultipleVisualMedia 的原始碼可以看到,Android 13 內建的圖片選擇器對應的是 MediaStore.ACTION_PICK_IMAGES 這個新增的 Intent,而如果當前裝置不支援媒體選擇器功能的話,就會改為通過呼叫 Intent.ACTION_OPEN_DOCUMENT 來選擇媒體資源,這種情況下 PickMultipleVisualMedia 設定的數量上限自然也就失效了

結尾

Android 13 細化的媒體許可權解決了 READ_EXTERNAL_STORAGE 帶來的隱私風險無序擴大的問題,讓使用者可以按需授權。而內建的圖片選擇器又能夠讓應用在無需使用者授權的情況下就可以靈活地選取媒體資源,應用也就完全沒有必要自己內建一個圖片選擇器了

所以說,平衡使用者體驗和使用者隱私安全最好的做法應該是:

  • 在 Android 13 之前依然還是通過應用內建的圖片選擇器來實現業務功能
  • 在 Android 13 開始之後的版本僅使用系統內建的圖片選擇器,完全棄用 READ_EXTERNAL_STORAGE 許可權

此次 Android 13 版本還是有點東西的 ~