距離 Android 11 正式發佈已經半年有餘,也該是時候寫寫 Android 11 新特性這方面的文章了。android
當初我有大概瞭解過一些 Android 11 上的行爲變動,整體變化雖然很多,可是要求咱們必須去適配的地方並不算多。其中一個可能須要適配的地方是 Android 11 的權限變動,關於這部份內容我在 PermissionX 如今支持 Java 了!還有 Android 11 權限變動講解 這篇文章中已經作了比較詳細的講解。git
除此以外,在 Scoped Storage 這塊,Android 11 上又有了一些新的變化,本篇文章咱們就重點來討論一下這部份內容。github
事實上,Scoped Storage 並非 Android 11 上推出的新功能,而是在 Android 10 中就已經有了,而且我當時還專門寫了一篇文章講解此功能,能夠參考 Android 10 適配要點,做用域存儲 。瀏覽器
不用擔憂,以前這篇文章中介紹的內容並無過期。當時在 Android 10 上可使用的功能,如今在 Android 11 上依然可使用,只不過 Android 11 對於 Scoped Storage 又作了一些豐富與擴展。那麼毫無疑問,這就是咱們本篇文章的重點。安全
首先,在 Android 11 中,Scoped Storage 被強制啓用了。微信
那麼強制啓用是什麼意思呢?markdown
在 Android 10 中雖然也有 Scoped Storage 功能,可是 Google 考慮到廣大應用程序適配也是須要時間的,所以並無強制啓用這個功能。app
只要應用程序指定的 targetSdkVersion 低於 29,或 targetSdkVersion 等於 29,但在 AndroidManifest.xml 中加入了以下配置:ide
<manifest ... >
<application android:requestLegacyExternalStorage="true" ...>
...
</application>
</manifest>
複製代碼
那麼 Scoped Storage 功能就不會被啓用。函數
在 Android 11 中以上配置依然有效,但僅限於 targetSdkVersion 小於或等於 29 的狀況。若是你的 targetSdkVersion 等於 30,Scoped Storage 就會被強制啓用,requestLegacyExternalStorage 標記將會被忽略。
那麼強制啓用了 Scoped Storage 以後對開發者而言有什麼影響嗎?
其實若是你的應用程序已經按照 Android 10 適配要點,做用域存儲 這篇文章中講解的方式對 Scoped Storage 進行了適配,那麼恭喜你,如今你什麼都不須要作,就已經可以適配 Android 11 系統了。
也就是說,對於絕大部分開發者而言,強制啓用 Scoped Storage 其實並無什麼影響,只要你的應用程序在以前已經適配了 Android 10 的 Scoped Storage。
可是有一類應用程序很是特殊,就是文件瀏覽器,如 Root Explorer、ES Explorer 等。這類程序自己提供的功能就是對 SD 上的文件進行瀏覽與管理,而強制啓用了 Scoped Storage 以後,本質上就沒有文件瀏覽的概念了,咱們也沒法以文件的真實路徑來對文件進行管理。
從這個角度上看,Scoped Storage 對於文件瀏覽器類的程序形成了毀滅性的打擊。不過不用擔憂,Google 仍然仍是給這類程序提供了另一種解決方案,下面咱們就來學習一下。
首先明確一點,Android 11 中強制啓用 Scoped Storage 是爲了更好地保護用戶的隱私,以及提供更加安全的數據保護。對於絕大部分應用程序來講,使用 MediaStore 提供的 API 就已經能夠知足你們的開發需求了。若是你沒有相似於開發文件瀏覽器這種需求,請儘量不要使用接下來即將介紹的技術。
擁有對整個 SD 卡的讀寫權限,在 Android 11 上被認爲是一種很是危險的權限,同時也可能會對用戶的數據安全形成比較大的影響。
但文件瀏覽器就是要對設備的整個 SD 卡進行管理的,這怎麼辦呢?對於這類危險程度比較高的權限,Google 一般採用的作法是,使用 Intent 跳轉到一個專門的受權頁面,引導用戶手動受權,好比懸浮窗,無障礙服務等。
沒錯,在 Android 11 中,若是你想要管理整個設備上的文件,也須要使用相似的技術。
首先,你必須在 AndroidManifest.xml 中聲明 MANAGE_EXTERNAL_STORAGE 權限,以下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.scopedstoragedemo">
<uses-permission android:
tools:ignore="ScopedStorage" />
</manifest>
複製代碼
注意相比於傳統聲明一個權限,這裏增長了 tools:ignore="ScopedStorage" 這樣一個屬性。由於若是不加上這個屬性,Android Studio 會用一個警告提醒咱們,絕大部分的應用程序都不該該申請這個權限,正如我前面介紹的同樣。
接下來的工做也至關簡單,咱們可使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 這個 action 來跳轉到指定的受權頁面,能夠經過 Environment.isExternalStorageManager() 這個函數來判斷用戶是否已受權,下面我寫了一段比較簡單的代碼來演示這個功能:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R ||
Environment.isExternalStorageManager()) {
Toast.makeText(this, "已得到訪問全部文件權限", Toast.LENGTH_SHORT).show()
} else {
val builder = AlertDialog.Builder(this)
.setMessage("本程序須要您贊成容許訪問全部文件權限")
.setPositiveButton("肯定") { _, _ ->
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
builder.show()
}
複製代碼
能夠看到,這裏首先判斷若是系統版本低於 Android 11,或者 Environment.isExternalStorageManager() 返回 true,那麼就說明咱們已經擁有管理整個 SD 卡的權限了。如今你能夠直接使用傳統的寫法,以文件真實路徑的形式對文件進行操做。
而若是尚未管理 SD 卡的權限,則會彈出一個對話框,告知用戶申請權限的緣由,而後使用 Intent 跳轉到指定的受權頁面,讓用戶手動進行受權。
程序的運行效果以下圖所示:
有了這個權限以後,你就能夠用過去熟知的方式去開發文件瀏覽器了。
不過還有一點須要注意,即便咱們得到了管理 SD 卡的權限,對於 Android 這個目錄下的不少資源仍然是訪問受限的,好比說 Android/data 這個目錄在 Android 11 中使用任何手段都沒法訪問。由於不少應用程序的數據信息都會存放在這個目錄下,作這個限制的目的主要仍是考慮到用戶的數據安全吧。否則的話,容許微信去讀取淘寶中的數據,怎麼想好像都是不合適的。
下面咱們再來看 Android 11 中關於 Scoped Storage 的另一個新特性。
Scoped Storage 規定,每一個應用程序都有權限向 MediaStore 貢獻數據,好比說插入一張圖片到手機相冊當中。也有權限讀取其餘應用程序所貢獻的數據,好比說獲取手機相冊中的全部圖片。這些功能我在 Android 10 適配要點,做用域存儲 這篇文章中都進行了演示。
可是,假如你要修改其餘應用程序所貢獻的數據,那很差意思,Scoped Storage 是不容許你這樣作的。
緣由也很簡單,若是一張圖片是你插入到手機相冊的,你固然有權限對它進行任意修改。可是若是這張圖片是其餘應用程序插入到手機相冊的,你還能對它進行任意修改,這在 Google 看來就又是一個安全隱患,因此 Scoped Storage 限制了這個功能。
不過,若是有些應用程序就是須要修改別的應用所貢獻的數據呢?這種例子也不難找,好比 Photoshop、美圖秀秀等,它們的目的就是爲了修改手機相冊中的圖片,無論這個圖片是否是它們本身所建立的。
針對這個問題,Android 10 中提供了一種解決方案:
try {
contentResolver.openFileDescriptor(imageContentUri, "w")?.use {
Toast.makeText(this, "如今能夠修改圖片的灰度了", Toast.LENGTH_SHORT).show()
}
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException = securityException as?
RecoverableSecurityException ?:
throw RuntimeException(securityException.message, securityException)
val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
intentSender?.let {
startIntentSenderForResult(intentSender, IMAGE_REQUEST_CODE,
null, 0, 0, 0, null)
}
} else {
throw RuntimeException(securityException.message, securityException)
}
}
複製代碼
下面我來簡單解釋一下這段代碼。
首先這段代碼的目的是爲了修改一張圖片的灰度,但因爲這張圖片並非由當前應用程序所貢獻的,因此理論上當前應用程序並無權限去修改這張圖片的灰度。
那麼明明沒有權限去修改,可是咱們仍是執意去修改會發生什麼狀況呢?這個很好理解,固然是拋異常了。因而這裏用 try catch 的方式包裹了修改圖片灰度的操做,而後在 catch 的代碼塊中判斷,若是當前系統版本大於等於 Android 10,而且異常的類型是 RecoverableSecurityException,那麼就說明這是一個因爲 Scoped Storage 限制致使操做沒有權限的異常。
接下來會從 RecoverableSecurityException 對象中獲取一個 intentSender,再借助這個 intentSender 進行頁面跳轉,引導用戶手動授予咱們修改這張圖片的權限。運行效果以下:
這種方式雖然可行,但卻有一個很是明顯的缺點:每次咱們只能操做一張圖片。若是一個程序須要修改不少張圖片,沒有什麼好辦法,只能每張圖片都用上述方式去申請權限。
相信 Google 也是意識到了這個問題,因而在 Android 11 中引入了一個新的功能,叫做 Batch operations,從而容許咱們能夠一次性對多個文件的操做權限進行申請。
關於 Batch operations 的用法也很好理解,Google 一共提供了 4 種類型的權限申請 API,以下所示:
其中最經常使用的主要是 createWriteRequest() 和 createDeleteRequest() 這兩個接口,這裏咱們以 createWriteRequest() 舉例。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val urisToModify = listOf(uri1, uri2, uri3, uri4)
val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
null, 0, 0, 0)
}
複製代碼
代碼很是簡單,首先咱們建立了一個集合,用於存放全部要批量申請權限的文件 Uri,而後調用 createWriteRequest() 函數去建立一個 PendingIntent,接下來再調用 startIntentSenderForResult 進行權限申請便可。
關於權限申請的結果,咱們能夠在 onActivityResult() 中進行監聽:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EDIT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(this, "用戶已受權", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "用戶沒有受權", Toast.LENGTH_SHORT).show()
}
}
}
}
複製代碼
程序的運行結果以下圖所示:
其它幾個 API 的用法都是徹底相同的,這裏就再也不重複舉例了。
看到這裏,有的朋友可能會說,Android 10 和 Android 11 提供的 API 徹底不一樣,Android 10 是要依賴於異常捕獲機制,從 RecoverableSecurityException 中解析出 intentSender,而 Android 11 能夠藉助 Batch operations 提供的 API 直接建立 intentSender。我該不會須要在一個項目中針對 Android 10 和 Android 11 分別寫兩套代碼去進行適配吧?
這確實是個頭疼的問題,並且我以爲主要是因爲 Google 一開始在 Android 10 中 API 設計不合理所致使的。依賴於異常捕獲機制的方案,不管如何都不能說是一種出色的 API 設計。
不過隨着後來更多的思考,我發現這並非一個沒法解決的問題,而且解決方案還很是簡單。
爲何呢?別忘了,Android 10 中的 Scoped Storage 並非強制啓用的,咱們能夠在 AndroidManifest.xml 中配置 requestLegacyExternalStorage 標記來禁用 Scoped Storage。這樣的話,Android 10 就是不須要適配的,咱們只須要在 Android 11 中使用更加科學規範的 API 來進行 Scoped Storage 適配就能夠了。
好了,本篇文章就到這裏,文中全部的代碼示例我都寫成了一個 Demo,放到了 GitHub 上,有須要的朋友能夠到如下網址查看:
另外,若是想要學習 Kotlin 和最新的 Android 知識,能夠參考個人新書 《第一行代碼 第 3 版》
關注個人技術公衆號「郭霖」,每一個工做日都有優質技術文章推送。