本文同步發表于我的微信公眾號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每個作業日都有文章更新,
距離Android 11正式發布已經半年有余,也該是時候寫寫Android 11新特性這方面的文章了,
當初我有大概了解過一些Android 11上的行為變更,總體變化雖然不少,但是要求我們必須去適配的地方并不算多,其中一個可能需要適配的地方是Android 11的權限變更,關于這部分內容我在 PermissionX現在支持Java了!還有Android 11權限變更講解 這篇文章中已經做了比較詳細的講解,
除此之外,在Scoped Storage這塊,Android 11上又有了一些新的變化,本篇文章我們就重點來討論一下這部分內容,
Scoped Storage
事實上,Scoped Storage并不是Android 11上推出的新功能,而是在Android 10中就已經有了,并且我當時還專門寫了一篇文章講解此功能,可以參考 Android 10適配要點,作用域存盤 ,
不用擔心,之前這篇文章中介紹的內容并沒有過時,當時在Android 10上可以使用的功能,現在在Android 11上依然可以使用,只不過Android 11對于Scoped Storage又做了一些豐富與擴展,那么毫無疑問,這就是我們本篇文章的重點,
強制啟用Scoped Storage
首先,在Android 11中,Scoped Storage被強制啟用了,
那么強制啟用是什么意思呢?
在Android 10中雖然也有Scoped Storage功能,但是Google考慮到廣大應用程式適配也是需要時間的,因此并沒有強制啟用這個功能,
只要應用程式指定的targetSdkVersion低于29,或targetSdkVersion等于29,但在AndroidManifest.xml中加入了如下配置:
<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:name="android.permission.MANAGE_EXTERNAL_STORAGE"
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中使用任何手段都無法訪問,因為很多應用程式的資料資訊都會存放在這個目錄下,做這個限制的目的主要還是考慮到用戶的資料安全吧,不然的話,允許微信去讀取淘寶中的資料,怎么想好像都是不合適的,
Batch operations
下面我們再來看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() 用于請求對多個檔案的寫入權限,
- createFavoriteRequest() 用于請求將多個檔案加入到Favorite(收藏)的權限,
- createTrashRequest() 用于請求將多個檔案移至回收站的權限,
- createDeleteRequest() 用于請求將多個檔案洗掉的權限,
其中最常用的主要是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上,有需要的朋友可以到以下網址查看:
https://github.com/guolindev/ScopedStorageDemo
另外,如果想要學習Kotlin和最新的Android知識,可以參考我的新書 《第一行代碼 第3版》,點擊此處查看詳情,
關注我的技術公眾號,每個作業日都有優質技術文章推送,
微信掃一掃下方二維碼即可關注:
![]()
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/262956.html
標籤:其他
