Android 11 ( R )适配
1.存儲機制更新
Scoped Storage(分區存儲)
應用
targetSdkVersion >= 30
,強制執行分區存儲機制。之前在
AndroidManifest.xml
中添加
android:requestLegacyExternalStorage="true"
的适配方式已不起作用。
允許使用除
MediaStore
API之外的API通過檔案路徑直接通路共享存儲空間中的媒體檔案。其中包括:
-
APIFile
- 原生庫,例如
使用原始檔案路徑直接通路共享存儲空間中的媒體檔案會重定向到MediaStore API,這次重定向會造成性能影響(随機讀寫慢一倍左右)。fopen()
MANAGE_EXTERNAL_STORAGE
擷取外部存儲管理權限,如果你的應用是手機管家、檔案管理器這類需要通路大量檔案的app,可以申請
MANAGE_EXTERNAL_STORAGE
權限,将使用者引導值系統設定頁面開啟。代碼如下
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
public static void checkStorageManagerPermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
需要注意的是即使你有了
MANAGE_EXTERNAL_STORAGE
權限,也無法通路
Android/data/
目錄下的檔案。
對于
MANAGE_EXTERNAL_STORAGE
權限,國内使用沒有什麼影響,但是在Google Play上需要說明為什麼已有的SAF或MediaStore不滿足你的應用需求,稽核通過才允許上架使用。
存儲通路架構 (SAF)變更
Android11對SAF添加以下限制:
- 使用
或ACTION_OPEN_DOCUMENT_TREE
,無法浏覽到ACTION_OPEN_DOCUMENT
和Android/data/
目錄及其所有子目錄。Android/obb/
- 使用
無法授權通路存儲根目錄、Download檔案夾。ACTION_OPEN_DOCUMENT_TREE
REQUEST_INSTALL_PACKAGES
在8.0的适配中,安裝apk包之前需要申請“安裝未知來源應用”的權限。一般來說首次是跳轉到授權頁面讓使用者手動開啟,然後傳回app進行安裝。
在Android 11中當使用者開啟“安裝未知來源應用”的權限,app就會被殺死。該行為與強制分區存儲有關,因為持有
REQUEST_INSTALL_PACKAGES
權限的應用可以通路其他應用的
Android/obb
目錄。但使用者授權權限之後,雖然app會被殺死,但是安裝頁面依然會彈出。
2.權限變化
單次權限授權
從Android 11開始,每當應用請求與位置資訊、麥克風或攝像頭相關的權限時,面向使用者的權限對話框會包含僅限這一次選項。如果使用者在對話框中選擇此選項,系統會向應用授予臨時的單次授權。
單次權限授權的應用可以再一段時間内通路相關資料,具體時間取決于應用的行為和使用者的操作:
- 當應用的 Activity 可見時,應用可以通路相關資料。
- 如果使用者将應用轉為背景運作,應用可以再短時間内繼續通路相關資料。
- 如果在Activity 可見時啟動了一項前台服務,并且使用者随後将應用轉到背景,那麼應用可以繼續通路相關資料,直到該前台服務停止。
- 如果使用者側小單次授權(例如在系統設定中撤銷),無論是否啟動了前台服務,應用都無法通路相關資料。與任何權限一樣,如果使用者撤銷了應用的單次授權,應用程序就會終止。
請求位置權限
在Android 10中請求位置權限規則如下
請求或
ACCESS_FINE_LOCATION
權限表示在前台時擁有通路裝置位置資訊的權限。在請求彈框中,選擇“始終允許”表示前背景都可以擷取位置資訊,選擇“僅在應用使用過程中允許”隻表示擁有前台的權限。
ACCESS_COARSE_LOCATION
在Android 11中,請求彈框中取消了“始終允許”這一選項,也就是說預設不會授予你背景通路裝置位置資訊的權限。如果嘗試請求
ACCESS_BACKGROUND_LOCATION
權限的同時請求任何其他權限,系統會抛出異常,不會向應用授予其中的任一權限。
官方給出的适配建議及原因如下:
建議應用對位置權限執行遞增請求,先請求前台位置資訊通路權限,再請求背景位置資訊通路權限。執行遞增請求可以為使用者提供更大的控制權和透明度,因為他們可以更好的了解應用中的哪些功能需要背景位置資訊通路權限。
總結一下就是兩點:
- 先請求前台位置資訊通路權限,再請求背景位置資訊通路權限。
- 單獨請求背景位置資訊通路權限,不要與其他權限一同請求。
軟體包可見性
軟體包可見性是Android 11上提升系統隐形安全性的一個新特性。它的作用是限制app随意擷取其他app的資訊和安裝狀态。避免病毒軟體、間諜軟體利用,引發網絡釣魚、使用者安裝資訊洩露等安全事件。
舉一個例子:
private static boolean hasActivity(Context context, Intent intent) {
PackageManager packageManager = context.getPackageManager();
return packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
}
public void test() {
Intent intent = new Intent();
intent.setClassName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareImgUI");
Log.d("hasActivity:", hasActivity(this, intent) + "");
}
hasActivity
方法中通過
queryIntentActivities
來判斷此頁面是否存在。但是在
targetSdkVersion >= 30
中,這些三方預設都是不可見的。是以都會傳回false。類似方法
getInstalledPackages
、
getPackageInfo
也受到相應的限制。
解決方法很簡單,在
AndroidManifest.xml
中添加
queries
元素,裡面添加需要可見的應用包名。
<manifest package="com.example.app">
<queries>
<!-- 微信 -->
<package android:name="com.tencent.mm" />
<!-- 微網誌 -->
<package android:name="com.sina.weibo" />
<!-- QQ -->
<package android:name="com.tencent.mobileqq" />
<!-- 支付寶 -->
<package android:name="com.eg.android.AlipayGphone" />
<!-- AlipayHK -->
<package android:name="hk.alipay.wallet" />
</queries>
...
</manifest>
除了直接添加包名的方式外,我們可以按intent和provider來添加:
<manifest package="com.example.app">
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
<provider android:authorities="com.example.settings.files" />
</queries>
...
</manifest>
當然,還有一種簡單粗暴的方式,可以直接申請權限
QUERY_ALL_PACKAGES
。如果你的應用需要上架
Google Play
,那麼可能要注意相關政策。為了尊重使用者隐私,建議我們的應用按正常工作所需的最小軟體包可見性來适配。
有一點需要說明,我們日常使用的
startActivity
方法不受系統軟體包可見性行為的影響,即使
hasActivity
為false,一樣可以跳轉。如果我們在做跳轉前,進行類似
hasActivity
的判斷,那麼會受影響。
最後需要注意的是,使用
queries
元素需要
Android Gradle
插件版本是4.1及以上,因為舊版本的插件并不相容此元素,出現合并
manifest
的錯誤。
前台服務類型
Android 10中,在前台服務通路位置資訊,需要在對應的
service
中添加
location
服務類型。
同樣的,在Android 11中,在前台服務通路攝像頭或麥克風,需要在對應的
service
中添加
camera
或
microphone
服務類型。
<manifest>
...
<service
android:name="MyService"
android:foregroundServiceType="microphone|camera" />
</manifest>
這一限制的變更,使得程式無法在背景啟動服務通路攝像頭和麥克風。如需使用,隻能是前台開啟前台服務。除非有如下情況:
- 服務由系統元件啟動
- 服務是通過應用小部件啟動
- 服務是通過與通知互動啟動的
- 服務是
啟動的,它是從另一個可見的應用程式發送過來的。PendingIntent
- 服務由一個應用程式啟動,該應用是一個DPC,且在裝置所有者模式下運作。
- 服務由一個提供
的應用啟動。VoiceInteractionService
- 服務由一個具有
權限的應用啟動。START_ACTIVITIES_FROM_BACKGROUND
權限自動重置
如果應用以Android 11或更高版本為目标平台并且數月未使用,系統會通過自動重置使用者已授予應用的運作時敏感權限來保護使用者資料
注意上圖中有一個自動重置權限的開關。如果我們的應用有特殊需要,可以引導使用者關閉它。示例代碼如下:
public void checkAutoRevokePermission(Context context) {
// 判斷是否開啟
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!context.getPackageManager().isAutoRevokeWhitelisted()) {
// 跳轉設定頁
Intent intent = new Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.fromParts("package", context.getPackageName(), null));
context.startActivity(intent);
}
}
讀取手機号
如果你是通過TelecomManager的getLine1Number方法,或TelephonyManager的getMsisdn方法擷取電話号碼。那麼在Android 11中需要增加READ_PHONE_NUMBERS權限,使用其他方法不受限。
<manifest>
<!-- 如果應用僅在 Android 10及更低版本中使用該權限,可以添加 maxSdkVersion="29" -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
</manifest>
3.其他行為變更
自定義view的Toast
Android 11
為目标平台的應用,從背景發送自定義view的Toast消息系統會進行屏蔽。前台使用不受影響。
Toast
相應的
setView
和
getView
也已經廢棄不建議使用。
如果要在背景使用,推薦使用預設的
Toast
或
Snackbar
替代。
APK簽名
Android 11
為目标平台的應用,僅通過v1簽名的應用無法在
Android 11
的裝置上安裝或更新。必須使用v2或更高版本進行簽名。
同時
Android 11
添加了對APK簽名方案v4的支援。
AsyncTask
AsyncTask
在Android 11已經不建議使用,建議遷移至kotlin的協程。
此外
Handler
未指定
Looper
的構造方法也已不建議使用。
狀态欄高度
系統為Android 11的手機上targetSdkVersion是30時擷取狀态欄高度為0,低于30擷取值正常。是以需要使用
WindowMetrics
适配一下:
public static int getStatusBarHeight(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
WindowInsets windowInsets = windowMetrics.getWindowInsets();
Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout());
return insets.top;
}
....
}
WindowMetrics
是
Android 11
新增的類,用于擷取視窗邊界,同樣可以用來擷取導航欄高度。