終於開始了Android 11的適配工做。記錄一下,供須要的人蔘考。java
老規矩,首先將咱們項目中的 targetSdkVersion
改成 30。或者使用兼容性調試工具,後面我會說到。android
具體適配方法和去年的Android 10 適配攻略中的沒有太大區別。shell
不過須要注意的是,應用targetSdkVersion >= 30
,強制執行分區存儲機制。以前在AndroidManifest.xml
中添加 android:requestLegacyExternalStorage="true"
的適配方式已不起做用。c#
還有一個變化:Android 11 容許使用除 MediaStore
API 以外的 API 經過文件路徑直接訪問共享存儲空間中的媒體文件。其中包括:api
File
API。fopen()
。若是你以前沒有適配Android 10,這一點對你來講是個好消息。Android 10在AndroidManifest.xml
中添加 android:requestLegacyExternalStorage="true"
來適配,Android 11上直接使用File
API訪問媒體文件。不得不說,等等黨的勝利?安全
不過,使用原始文件路徑直接訪問共享存儲空間中的媒體文件會重定向到 MediaStore
API,此次重定向會形成性能影響(隨機讀寫慢一倍左右)。並且直接使用原始文件路徑,並不會比使用 MediaStore
API 有更多優點,所以官方強烈建議直接使用 MediaStore
API。微信
固然還有一種簡單粗暴的適配方法,獲取外部存儲管理權限。若是你的應用是手機管家、文件管理器這類須要訪問大量文件的app,能夠申請MANAGE_EXTERNAL_STORAGE
權限,將用戶引導至系統設置頁面開啓。代碼以下:markdown
<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
不知足你的應用需求,審覈經過才容許上架使用。因此通常狀況下,我我的不推薦你爲了適配簡單,直接申請使用MANAGE_EXTERNAL_STORAGE
權限。微信開發
其餘細節變動見文檔:Android 11 中的存儲機制更新。
相關api變動及使用推薦郭霖大神的這篇:Android 11新特性,Scoped Storage又有了新花樣。
Android 11對SAF添加如下限制:
ACTION_OPEN_DOCUMENT_TREE
或 ACTION_OPEN_DOCUMENT
,沒法瀏覽到Android/data/
和 Android/obb/
目錄及其全部子目錄。ACTION_OPEN_DOCUMENT_TREE
沒法受權訪問存儲根目錄、Download文件夾。在8.0的適配中,咱們安裝apk包以前須要申請「安裝未知來源應用」的權限。通常來講首次是跳轉到受權頁面讓用戶手動開啓,而後返回app進行安裝。
在Android 11中當用戶開啓「安裝未知來源應用」的權限,app就會被殺死。該行爲與強制分區存儲有關,由於持有 REQUEST_INSTALL_PACKAGES
權限的應用能夠訪問其餘應用的Android/obb
目錄。
好在用戶授予權限以後,雖然app會被殺死,可是安裝頁面依然會彈出。
目前對於這一變動我沒有發現能夠適配處理的方式,詳細介紹見:Android 11特性調整:安裝外部來源應用須要重啓APP
這裏補充一下,由於其餘應用沒法訪問應用的Android/data/
和 Android/obb/
目錄及其全部子目錄。因此須要注意保存在這裏面的文件是否會被其餘程序訪問。
好比我在用系統的裁切功能時,由於設置的MediaStore.EXTRA_OUTPUT
文件是私有目錄下的,致使裁剪後的圖片沒法正確生成。因此須要針對android 11進行適配:
String fileName = System.currentTimeMillis() + ".jpg";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 裁剪沒法訪問App的私有目錄,因此能夠保存至公有目錄
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Crop");
Uri uri = this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
} else {
...
}
複製代碼
或者保存至Android/media
共享文件目錄,這樣不用適配版本。
String fileName = System.currentTimeMillis() + ".jpg";
File file = new File(this.getExternalMediaDirs()[0].getAbsolutePath() + File.separator + fileName);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
複製代碼
固然若是你是本身實現的裁剪功能,那麼不受影響。
從 Android 11 開始,每當應用請求與位置信息、麥克風或攝像頭相關的權限時,面向用戶的權限對話框會包含僅限這一次選項。若是用戶在對話框中選擇此選項,系統會嚮應用授予臨時的單次受權。
單次權限受權的應用能夠在一段時間內訪問相關數據,具體時間取決於應用的行爲和用戶的操做:
當用戶下次打開應用而且應用中的某項功能請求訪問位置信息、麥克風或攝像頭時,系統會再次提示用戶授予權限。
若是你以前就是使用權限時才請求相關權限,那麼這一變動對於你的應用沒有影響。
這部分在Android 10的適配有過調整,當時規則以下:
請求
ACCESS_FINE_LOCATION
或ACCESS_COARSE_LOCATION
權限表示在前臺時擁有訪問設備位置信息的權限。在請求彈框中,選擇「始終容許」表示先後臺均可以獲取位置信息,選擇「僅在應用使用過程當中容許」只表示擁有前臺的權限。
在Android 11中,請求彈框中取消了「始終容許」這一選項。也就是說默認不會授予你後臺訪問設備位置信息的權限。若是嘗試請求ACCESS_BACKGROUND_LOCATION
權限的同時請求任何其餘權限,系統會拋出異常,不會嚮應用授予其中的任一權限。
官方給出的適配建議及緣由以下:
建議應用對位置權限執行遞增請求,先請求前臺位置信息訪問權限,再請求後臺位置信息訪問權限。執行遞增請求能夠爲用戶提供更大的控制權和透明度,由於他們能夠更好地瞭解應用中的哪些功能須要後臺位置信息訪問權限。
總結一下得出兩點:
這裏還須要注意不一樣目標平臺應用在Android 11上的表現:
選擇「始終容許」表示具備先後臺位置信息訪問權限,若是用戶拒絕兩次應用定位訪問請求(直接返回等),後面請求相同權限都會被直接提示請求失敗。(這裏就須要咱們給用戶以引導了)
這裏解釋一下「拒絕兩次」,這是Android 11 上添加的權限對話框的可見性
,之前咱們點擊了「再也不詢問」表示拒絕受權。如今還包含相似上面這種轉到系統設置,而後點返回按鈕,也算是拒絕受權。固然,用戶按返回按鈕關閉權限對話框,此操做不算。
總結一下,與Android 10的區別就是將後臺權限的申請分離了出來,增長了用戶「拒絕」的條件,避免了應用重複請求用戶已拒絕的權限。
軟件包可見性是Android 11上提高系統隱私安全性的一個新特性。它的做用是限制app隨意獲取其餘app的信息和安裝狀態。避免病毒軟件、間諜軟件利用,引起網絡釣魚、用戶安裝信息泄露等安全事件。
獲取自動可見應用的列表,能夠執行命令adb shell dumpsys package queries
,找到 forceQueryable
部分。下面是在vivo iqoo手機的執行結果。
Queries:
system apps queryable: false
forceQueryable:
[com.android.BBKCrontab,com.vivo.fingerprint,com.vivo.epm,com.vivo.abe,com.vivo.fingerprintengineer,com.vivo.contentcatcher,com.vivo.floatingball,com.vivo.agent,com.vivo.nightpearl,android,com.wapi.wapicertmanage,com.vivo.vms,co
m.android.providers.settings,com.vivo.upslide,com.vivo.assistant,com.vivo.vivokaraoke,com.vivo.fingerprintui,com.android.wallpaperbackup,com.bbk.facewake,com.vivo.faceunlock,com.vivo.doubleinstance,com.vivo.audiofx,com.iqoo.powersav
ing,com.bbk.SuperPowerSave,com.vivo.vibrator4d,com.vivo.smartunlock,com.vivo.globalanimation,com.vivo.appfilter,com.vivo.voicewakeup,com.vivo.minscreen,com.android.bbklog,com.mobile.cos.iroaming,com.vivo.networkstate,com.vivo.daemon
Service,com.vivo.smartshot,com.vivo.vtouch,com.android.networkstack.tethering.inprocess,com.android.localtransport,com.vivo.pem,com.vivo.wifiengineermode,com.android.server.telecom,com.vivo.gamecube,com.vivo.aiengine,com.vivo.multin
lp,com.vivo.smartmultiwindow,com.vivo.permissionmanager,com.qti.diagservices,com.vivo.bsptest,com.qti.snapdragon.qdcm_ff,com.vivo.dr,com.vivo.sps,com.android.dynsystem,com.vivo.setupwizard,com.vivo.gamewatch,com.android.keychain,com
.vivo.faceui,com.android.networkstack.inprocess,com.android.location.fused,com.android.inputdevices,com.android.settings,com.iqoo.engineermode,com.vivo.fuelsummary]
[com.qualcomm.uimremoteserver,com.vivo.devicereg,com.qti.qualcomm.deviceinfo,com.volte.config,com.android.mms.service,com.android.ons,com.qualcomm.qcrilmsgtunnel,com.vivo.sim.contacts,com.qualcomm.qti.uimGbaApp,com.qualcomm.qti.
modemtestmode,com.android.stk,com.android.vendors.bridge.softsim,com.qualcomm.uimremoteclient,com.qti.qualcomm.datastatusnotification,com.qualcomm.qti.uim,com.android.phone,com.qualcomm.qti.dynamicddsservice,com.qualcomm.qti.telepho
nyservice,com.android.cellbroadcastservice,com.android.providers.telephony,com.qti.dpmserviceapp,com.android.incallui]
[com.android.vivo.tws.vivotws,com.android.bluetooth]
com.android.nfc
com.android.se
com.android.networkstack.permissionconfig
com.android.shell
com.android.providers.media.module
com.android.wifi.resources.overlay.common
com.android.theme.icon_pack.filled.themepicker
com.android.theme.icon_pack.circular.themepicker
com.android.server.telecom.overlay.common
......
複製代碼
能夠看到都是系統應用包名,因此咱們的三方應用默認是不可見的。此項變動影響比較多的是分享支付一類須要與其餘應用交互的功能。下面舉一個簡單的例子:
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" /> <- 指定微信包名
</queries>
...
</manifest>
複製代碼
我在適配中用到的還有下面的包名,咱們能夠按需添加:
<queries>
<!-- 微博 -->
<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>
複製代碼
除了直接添加包名的方式外,咱們能夠按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
啓動的,它是從另外一個可見的應用程序發送過來的。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);
}
}
複製代碼
這部分我在適配中沒有用到,直接照搬文檔:
在 Android 11 中,系統會根據請求自動向某些類型的應用授予 SYSTEM_ALERT_WINDOW
權限:
系統會自動向具備 ROLE_CALL_SCREENING
且請求 SYSTEM_ALERT_WINDOW
的全部應用授予該權限。若是應用失去 ROLE_CALL_SCREENING
,就會失去該權限。
系統會自動向經過 MediaProjection
截取屏幕且請求 SYSTEM_ALERT_WINDOW
的全部應用授予該權限,除非用戶已明確拒絕嚮應用授予該權限。當應用中止截取屏幕時,就會失去該權限。此用例主要用於遊戲直播應用。
這些應用無需發送 ACTION_MANAGE_OVERLAY_PERMISSION
以獲取 SYSTEM_ALERT_WINDOW
權限,它們只需直接請求 SYSTEM_ALERT_WINDOW
便可。
MANAGE_OVERLAY_PERMISSION
intent 始終會將用戶轉至系統權限屏幕
從 Android 11 開始,ACTION_MANAGE_OVERLAY_PERMISSION
intent 始終會將用戶轉至頂級設置屏幕,用戶可在其中授予或撤消應用的 SYSTEM_ALERT_WINDOW
權限。intent 中的任何 package:
數據都會被忽略。
在更低版本的 Android 中,ACTION_MANAGE_OVERLAY_PERMISSION
intent 能夠指定一個軟件包,它會將用戶轉至應用專用屏幕以管理權限。從 Android 11 開始將再也不支持此功能,而是必須由用戶先選擇要授予或撤消哪些應用的權限。此變動可讓權限的授予更有目的性,從而達到保護用戶的目的。
若是你是經過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>
複製代碼
Android 11
爲目標平臺的應用,從後臺發送自定義view的Toast消息系統會進行屏蔽。前臺使用不受影響。Toast
相應的setView
和 getView
也已經廢棄不建議使用。
若是要在後臺使用,推薦使用默認的toast或Snackbar
替代。
Android 11
爲目標平臺的應用,僅經過v1 簽名的應用沒法在Android 11
的設備上安裝或更新。必須使用v2或更高版本進行簽名。
同時Android 11
添加了對 APK 簽名方案 v4 的支持。
AsyncTask
在Android 11已經不建議使用,建議遷移至kotlin的協程。
此外Handler
未指定Looper
的構造方法也已不建議使用。
建議明確指定Looper
:
private Handler handler = new Handler(Looper.myLooper());
// 或
private Handler handler = new Handler(Looper.getMainLooper());
複製代碼
發現系統爲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新增的類,用於獲取窗口邊界,一樣能夠用來獲取導航欄高度。
以往咱們作適配的時候,須要先將咱們項目中的 targetSdkVersion
修改成對應版本。這就致使你適配過程當中有可能受到其餘變動的影響,而這個新增的兼容性調試工具可讓你在不升級targetSdkVersion
的狀況下,針對每項變動逐個開啓適配。
使用方法:
上面第一行
DEFAULT_SCOPED_STORAGE
就是啓用分區儲存,這些常量詳細的含義見:Android 11 變動列表。
對於兼容性調試工具詳細的使用方法見:兼容性框架工具,這裏限於篇幅就不展開說了。
Android 11的開發者選項中添加了一個無線調試的功能。相似於鏈接藍牙耳機功能,能夠無需USB鏈接線進行平常開發調試工做。(區別於之前的Android WIFI ADB,這個是真無線,哈哈)
使用方法:
adb pair ipaddr:port
後輸入配對碼進行鏈接。注意事項:
adb --version
查看。不過我本身體驗下來,感受鏈接不是很穩定,不知是AS的問題仍是手機問題。同時鎖屏後也會斷開鏈接,體驗不是很好。。。期待後續的優化吧。
本篇內容有點多。總結一下,Android 11在權限上的變動比較多,但若是你一直遵照申請權限相關的最佳作法,那麼基本上不須要額外的適配工做。
最後強調一下,對於單次受權,權限對話框的可見性,SYSTEM_ALERT_WINDOW 權限,安裝apk這些變動只要在Android 11上就會生效,不論你是否適配Android 11。對於其餘變動和API(相機、5G、瀑布屏、鍵盤等),由於我暫時沒有遇到,也就沒有列出,有須要的能夠點擊文末的官方文檔連接查看。
截止發這篇博客時,我手機上只發現嗶哩嗶哩已經適配了Android 11。大多數停留在2八、29,更有甚者還在26(Android 8.0 國內上架的最低適配標準)。
因此我順便附上以前寫的Android 九、10的適配攻略:
可能本篇你暫時也用不上,你能夠不用,可是不能沒有。點贊收藏一波不過度吧~~