Android11新特性及部分適配

Android11新特性及部分適配

以下我分為兩部分講述,分別是

以Android11為目標版本的應用(targetSdkVersion>=30才有影響)

所有應用在Android11設備上適配改動(無論targetSdkVersion是多少,只要在Android11設備上運行的應用都有影響)

一般來說為了Google為了讓我們更長時間適應新的內容以及保障線上應用的穩定,都會把改動大的,需要花時間適配的內容放到新的targetSdkVersion對應的應用上。

一、適配targetSdkVersion=30

此模塊的修改內容只針對targetSdkVersion 30或者以上才生效。

1.1分區存儲強制執行

●? 公共目錄:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等

? ? ■ 公共目錄的文件在App卸載后,不會刪除

? ? ■ 可以通過SAF(Storage Access Framework)、MediaStore接口訪問

? ? ■ 擁有權限,也能通過路徑直接訪問

●??應用專屬目錄

? ? ■ 應用專屬目錄只能自己直接訪問

? ? ■ App卸載,數據會清除。

關于分區存儲,在Android10就已經推行了,簡單的說,就是應用對于文件的讀寫只能在沙盒環境,也就是屬于自己應用的目錄里面讀寫。其他媒體文件可以通過MediaStore進行訪問。

但是在android10的時候,Google還是為開發者考慮,留了一手。在targetSdkVersion = 29應用中,設置android:requestLegacyExternalStorage="true",就可以不啟動分區存儲,讓以前的文件讀取正常使用。但是targetSdkVersion = 30中不行了,強制開啟分區存儲。 當然,作為人性化的android,還是為開發者留了一小手,如果是覆蓋安裝呢,可以增加android:preserveLegacyExternalStorage="true",暫時關閉分區存儲,好讓開發者完成數據遷移的工作。為什么是暫時呢?因為只要卸載重裝,就會失效了。以下是關于分區存儲會遇到的所有情況,給大家羅列出來了。

private void saverFile() {

? ? try {

? ? ? ? //APP私有目錄:/storage/emulated/0/Android/data/包名/files

? ? ? ? //File externalFilesDir = getExternalFilesDir(null);

? ? ? ? //分區存儲開啟后就不允許訪問了,被棄用

? ? ? ? //公有目錄/storage/emulated/0/text1.txt

? ? ? ? String filePath = Environment.getExternalStoragePublicDirectory("").toString()+"/text1.txt";

? ? ? ? FileWriter fw = new FileWriter(filePath);

? ? ? ? fw.write("hello world");

? ? ? ? fw.close();

? ? ? ? Log.e(TAG, "saverFile: 文件寫入成功" );

? ? } catch (IOException e) {

? ? ? ? e.printStackTrace();

? ? }

}

分情況運行:

1) targetSdkVersion = 28,運行后正常讀寫。

2) targetSdkVersion = 29,不刪除應用,targetSdkVersion 由28修改到29,覆蓋安裝,運行后正常讀寫。

3) targetSdkVersion = 29,刪除應用,重新運行,讀寫報錯,程序崩潰(open failed: EACCES (Permission denied))

4) targetSdkVersion = 29,添加android:requestLegacyExternalStorage="true"(不啟用分區存儲),讀寫正常不報錯

5) targetSdkVersion = 30,不刪除應用,targetSdkVersion 由29修改到30,讀寫報錯,程序崩潰(open failed: EACCES (Permission denied))

6) targetSdkVersion = 30,不刪除應用,targetSdkVersion 由29修改到30,增加android:preserveLegacyExternalStorage="true",讀寫正常不報錯

7) targetSdkVersion = 30,刪除應用,重新運行,讀寫報錯,程序崩潰(open failed: EACCES (Permission denied))

1.2媒體文件訪問權限

為了在保證用戶隱私的同時可以更輕松地訪問媒體,Android 11 增加了以下功能。執行批量操作和使用直接文件路徑和原生庫訪問文件。

1)執行批量操作

這里的批量操作指的是Android 11 向 MediaStore API 中添加了多種方法,用于簡化特定媒體文件更改流程(例如在原位置編輯照片),分別是:

createWriteRequest() 用戶向應用授予對指定媒體文件組的寫入訪問權限的請求。

createFavoriteRequest()用戶將設備上指定的媒體文件標記為“收藏”的請求。對該文件具有讀取訪問權限的任何應用都可以看到用戶已將該文件標記為“收藏”。

createTrashRequest() 用戶將指定的媒體文件放入設備垃圾箱的請求。垃圾箱中的內容會在系統定義的時間段后被永久刪除。

createDeleteRequest() 用戶立即永久刪除指定的媒體文件(而不是先將其放入垃圾箱)的請求。

例如,以下是構建?createWriteRequest()?調用的方法,傳入uri的集合,獲取用戶的同意后,就可以進行操作了。

List<Uri> urisToModify = /* A collection of content URIs to modify. */

PendingIntent editPendingIntent = MediaStore.createWriteRequest(contentResolver,

? ? ? ? ? ? ? ? ? urisToModify);

// Launch a system prompt requesting user permission for the operation.

startIntentSenderForResult(editPendingIntent.getIntentSender(),

? ? EDIT_REQUEST_CODE, null, 0, 0, 0);

@Override

protected void onActivityResult(int requestCode, int resultCode,

? ? ? ? ? ? ? ? ? ?@Nullable Intent data) {

? ? ...

? ? if (requestCode == EDIT_REQUEST_CODE) {

? ? ? ? if (resultCode == Activity.RESULT_OK) {

? ? ? ? ? ? /* Edit request granted; proceed. */

? ? ? ? } else {

? ? ? ? ? ? /* Edit request not granted; explain to the user. */

? ? ? ? }

? ? }

}

2)直接文件路徑和原生庫訪問文件

Android11又恢復了使用直接文件路徑訪問訪問媒體文件!也就是除了 MediaStore API之外還有兩種方式可以訪問媒體文件:

File API。

原生庫,例如 fopen()。

性能:

當您使用直接文件路徑依序讀取媒體文件時,其性能與?MediaStore?API 相當。

但是,當您使用直接文件路徑隨機讀取和寫入媒體文件時,進程的速度可能最多會慢一倍。在此類情況下,我們建議您改為使用?MediaStore?API。

1.3 軟件包可見性?

Android11中,如果你想去獲取其他應用的信息,比如包名,名稱等等,不能直接獲取了,必須在清單文件中添加<queries>元素,告知系統你要獲取哪些應用信息或者哪一類應用。

PackageManager packageManager = this.getPackageManager();

List<ApplicationInfo> listAppcations = packageManager

? ? ? ? .getInstalledApplications(PackageManager.GET_META_DATA);

for (ApplicationInfo ap:listAppcations){

? ? Log.d(TAG, "包名:? "+ap.packageName);

}

在Android11版本,只能查詢到自己應用和系統應用的信息,查不到其他應用的信息。

1.3.1查詢特定軟件包及與之交互

如果您知道要查詢或與之交互的一組特定應用(例如,與您的應用集成的應用或您使用其服務的應用),請將其軟件包名稱添加到?<queries>?元素內的一組?<package>?元素中:

<manifest package="com.example.game">

? ? <queries>

? ? ? ? <package android:name="com.example.store" />

? ? ? ? <package android:name="com.example.services" />

? ? </queries>

? ? ...

</manifest>

1.3.2 在給定 intent 過濾器的情況下查詢應用及與之交互

您的應用可能需要查詢一組具有特定用途的應用或與之交互,但您可能不知道要添加的具體軟件包名稱。在這種情況下,您可以在?<queries>?元素中列出 intent 過濾器簽名。然后,您的應用就可以發現具有匹配的?<intent-filter>?元素的應用。

<manifest package="com.example.game">

? ? <queries>

? ? ? ? <intent>

? ? ? ? ? ? <action android:name="android.intent.action.SEND" />

? ? ? ? ? ? <data android:mimeType="image/jpeg" />

? ? ? ? </intent>

? ? </queries>

? ? ...

</manifest>

1.3.3 查詢所有應用及與之交互

在極少數情況下,您的應用可能需要查詢設備上的所有已安裝應用或與之交互,不管這些應用包含哪些組件。為了允許您的應用看到其他所有已安裝應用,Android?11 引入了QUERY_ALL_PACKAGES權限。

下面列出了適合添加?QUERY_ALL_PACKAGES?權限的用例的一些示例:

啟動器應用

無障礙應用

瀏覽器

點對點 (P2P) 共享應用

設備管理應用

安全應用

不過,在絕大多數情況下,可以通過聲明?<queries>?元素實現應用的用例。為了尊重用戶隱私,您的應用應請求應用正常工作所需的最小軟件包可見性。

1.4 所有文件訪問權限

絕大多數需要共享存儲空間訪問權限的應用都可以遵循分區存儲最佳做法,例如存儲訪問框架或 MediaStore API。但是,某些應用的核心用例需要廣泛訪問設備上的文件,但無法采用注重隱私保護的存儲最佳做法高效地完成這些操作。

例如,防病毒應用的主要用例可能需要定期掃描不同目錄中的許多文件。如果此掃描需要反復的用戶交互,讓其使用系統文件選擇器選擇目錄,可能就會帶來糟糕的用戶體驗。其他用例(如文件管理器應用、備份和恢復應用以及文檔管理應用)可能也需要考慮類似情況。

應用可通過執行以下操作,向用戶請求名為“所有文件訪問權限”的特殊應用訪問權限:

在清單中聲明MANAGE_EXTERNAL_STORAGE權限。

使用ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSIONintent 操作將用戶引導至一個系統設置頁面,在該頁面上,用戶可以為您的應用啟用以下選項:授予所有文件的管理權限。

如需確定您的應用是否已獲得?MANAGE_EXTERNAL_STORAGE?權限,請調用Environment.isExternalStorageManager()

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

? ? val intent = Intent()

? ? intent.action= Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION

? ? startActivity(intent)

? ? //判斷是否獲取MANAGE_EXTERNAL_STORAGE權限:

? ? val isHasStoragePermission= Environment.isExternalStorageManager()

1.5 電話號碼相關權限

Android 11 更改了您的應用在讀取電話號碼時使用的與電話相關的權限。

其實就是兩個API:

TelecomManager 類中的 getLine1Number() 方法

TelecomManager 類中的 getMsisdn() 方法

也就是當用到這兩個API的時候,原來的READ_PHONE_STATE權限不管用了,需要READ_PHONE_NUMBERS權限才行。

1.6 自定義消息框視圖被屏蔽

從 Android 11 開始,已棄用自定義消息框視圖。如果您的應用以 Android 11 為目標平臺,包含自定義視圖的消息框在從后臺發布時會被屏蔽。

? ? Toast toast = new Toast(context);

? ? toast.setDuration(show_length);

? ? toast.setView(view);//棄用

? ? /**

? ? *如果您希望在消息框(文本消息框或自定義消息框)出現或消失時收到通知,請使用新的?addCallback()?方法

? ? */

? ? toast.addCallback(new Toast.Callback() {

? ? @Override

? ? public void onToastShown() {

? ? ? ? super.onToastShown();

? ? }

? ? @Override

? ? public void onToastHidden() {

? ? ? ? super.onToastHidden();

? ? }

});

? ? toast.show();

如果您的應用仍嘗試從后臺發布包含自定義視圖的消息框,系統不會向用戶顯示相應的消息,而是會在 logcat 中記錄以下消息:

W/NotificationService: Blocking custom toast from package \

? <package> due to package not in the foreground

1.7 媒體intent操作需要系統默認相機

從 Android?11 開始,只有預裝的系統相機應用可以響應以下 intent 操作:

android.media.action.VIDEO_CAPTURE

android.media.action.IMAGE_CAPTURE

android.media.action.IMAGE_CAPTURE_SECURE

如果有多個預裝的系統相機應用可用,系統會顯示一個對話框,供用戶選擇應用。如果您希望自己的應用使用特定的第三方相機應用來代表其捕獲圖片或視頻,可以通過為 intent 設置軟件包名稱或組件來使這些 intent 變得明確。

1.8 5G

Android 11 添加了在您的應用中支持 5G 的功能

檢測是否連接到了5G網絡

檢查按流量計費性

首先是檢測5G網絡,通過TelephonyManager的監聽方法:

? ? private fun getNetworkType(){

? ? ? ? val tManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager

? ? ? ? tManager.listen(object : PhoneStateListener() {

? ? ? ? ? ? @RequiresApi(Build.VERSION_CODES.R)

? ? ? ? ? ? override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {

? ? ? ? ? ? ? ? if (ActivityCompat.checkSelfPermission(this@Android11Test2Activity, android.Manifest.permission.READ_PHONE_STATE) != android.content.pm.PackageManager.PERMISSION_GRANTED) {

? ? ? ? ? ? ? ? ? ? return

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? super.onDisplayInfoChanged(telephonyDisplayInfo)

? ? ? ? ? ? ? ? when(telephonyDisplayInfo.networkType) {

? ? ? ? ? ? ? ? ? ? TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO -> showToast("高級專業版 LTE (5Ge)")

? ? ? ? ? ? ? ? ? ? TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA -> showToast("NR (5G) - 5G Sub-6 網絡")

? ? ? ? ? ? ? ? ? ? TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE -> showToast("5G+/5G UW - 5G mmWave 網絡")

? ? ? ? ? ? ? ? ? ? else -> showToast("other")

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)

? ? }

檢測流量計費方法也很簡單,監聽網絡,在回調中判斷:

? ? val manager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager

? ? manager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {

? ? ? ? override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {

? ? ? ? ? super.onCapabilitiesChanged(network, networkCapabilities)

? ? ? ? ? ? //true 代表連接不按流量計費

? ? ? ? ? ? val isNotFlowPay=networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ||

? ? ? ? ? ? ? ? ? ? ? ? ? ? networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)

? ? ? ? ? }

? ? })

判斷該值,如果為 true,則將連接視為不按流量計費。

1.9 現在需要 APK 簽名方案 v2

對于以 Android 11(API 級別 30)為目標平臺,且目前僅使用 APK 簽名方案 v1 簽名的應用,現在還必須使用 APK 簽名方案 v2 或更高版本進行簽名。用戶無法在搭載 Android 11 的設備上安裝或更新僅通過 APK 簽名方案 v1 簽名的應用。

如果你的targetSdkVersion修改到30,那么你就必須要加上v2簽名才行。否則無法安裝和更新。

2.0 后臺位置信息訪問權限

在搭載 Android 11 的設備上,當應用中的某項功能請求在后臺訪問位置信息時,用戶看到的系統對話框不再包含用于啟用后臺位置信息訪問權限的按鈕。如需啟用后臺位置信息訪問權限,用戶必須在設置頁面上針對應用的位置權限設置一律允許選項。

在較低版本的Android系統中,當應用獲得前臺位置信息訪問權限時,也會自動獲得后臺位置信息訪問權限。比如我請求一個前臺位置訪問權限:

requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 100)

授權后,就能同時獲取前臺位置權限和后臺位置權限(ACCESS_BACKGROUND_LOCATION)。

但是現在不行了,你必須單獨申請后臺位置權限,而且,要在獲取前臺權限之后,順序還不能亂。

requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

如果在沒獲取前臺權限的時候執行這個獲取后臺權限的代碼會沒反應,等獲取前臺權限(ACCESS_COARSE_LOCATION)之后,申請后臺權限就會跳轉到一個新的權限頁面了,而且必須選擇Allow all the time (始終允許)才能獲得后臺位置權限,看圖:

二、適配Android11手機

此模塊的修改內容針對所有項目在Android11手機上存在的改動,與targetSdkVersion無關。

2.1數據訪問審核

為了讓應用及其依賴項訪問用戶私密數據的過程更加透明,Android 11 引入了數據訪問審核功能。

其實就是危險權限的調用,所以這個功能就是提供了可以監聽危險權限調用的監聽。主要涉及到的方法是AppOpsManager.OnOpNotedCallback。無論是應用本身,還是依賴庫或者SDK中的代碼,只要訪問到私密數據(危險權限),都會回調給我們。

該例子主要展示了一個獲取位置信息的功能,如果調用到getLocation方法,就會觸發onNoted回調,回調信息包括危險權限code以及歸因。

其中OnOpNotedCallback 一共三個回調方法:

1、onNoted 正常情況下都會回調到該方法

2、onAsyncNoted 如果數據訪問并非發生在應用調用API期間,就會調用onAsyncNoted(),比如一些監聽器的回調。

3、onSelfNoted 在極少數情況下,如果應用將自身的UID傳遞到 noteOp(),需要調用 onSelfNoted()。

//創建歸因(attribute)?

shareLocation = createAttributionContext("shareLocation");

//監聽事件

AppOpsManager.OnOpNotedCallback myAppTag = new AppOpsManager.OnOpNotedCallback() {

? ? private void logPrivateDataAccess(String opCode,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? String attributionTag, String trace) {

? ? ? ? Log.i("MY_APP_TAG", "Private data accessed. \n" +

? ? ? ? ? ? ? ? "Operation: " + opCode + "\n " +

? ? ? ? ? ? ? ? "Attribution Tag:" + attributionTag + "\nStack Trace:\n" + trace);

? ? }

? ? @Override

? ? public void onNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {

? ? ? ? logPrivateDataAccess(syncNotedAppOp.getOp(),

? ? ? ? ? ? ? ? syncNotedAppOp.getAttributionTag(),

? ? ? ? ? ? ? ? Arrays.toString(new Throwable().getStackTrace()));

? ? }

? ? @Override

? ? public void onSelfNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {

? ? ? ? logPrivateDataAccess(syncNotedAppOp.getOp(),

? ? ? ? ? ? ? ? syncNotedAppOp.getAttributionTag(),

? ? ? ? ? ? ? ? Arrays.toString(new Throwable().getStackTrace()));

? ? }

? ? @Override

? ? public void onAsyncNoted(@NonNull AsyncNotedAppOp asyncNotedAppOp) {

? ? ? ? logPrivateDataAccess(asyncNotedAppOp.getOp(),

? ? ? ? ? ? ? ? asyncNotedAppOp.getAttributionTag(),

? ? ? ? ? ? ? ? asyncNotedAppOp.getMessage());

? ? }

};

//打開私密數據監聽

AppOpsManager appOpsManager = getSystemService(AppOpsManager.class);

if (appOpsManager != null) {

? ? appOpsManager.setOnOpNotedCallback(getMainExecutor(), myAppTag);

}

private void getLocation() {

? ? LocationManager locationManager = shareLocation.getSystemService(LocationManager.class);

? ? if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

? ? ? ? return;

? ? }

? ? Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);

}

看下回調的結果日志:

com.example.myandroid11 I/MY_APP_TAG: Private data accessed.

? ? ? ? Operation: android:fine_location

? ? Attribution Tag:shareLocation

? ? [com.example.myandroid11.MainActivity$1.onNoted(MainActivity.java:62), android.app.AppOpsManager.readAndLogNotedAppops(AppOpsManager.java:8204), android.os.Parcel.readExceptionCode(Parcel.java:2304), android.os.Parcel.readException(Parcel.java:2279), android.location.ILocationManager$Stub$Proxy.getLastLocation(ILocationManager.java:1225), android.location.LocationManager.getLastKnownLocation(LocationManager.java:648), com.example.myandroid11.MainActivity.getLocation(MainActivity.java:149), com.example.myandroid11.MainActivity.access$100(MainActivity.java:38), com.example.myandroid11.MainActivity$4.onClick(MainActivity.java:109), android.view.View.performClick(View.java:7448), android.view.View.performClickInternal(View.java:7425), android.view.View.access$3600(View.java:810), android.view.View$PerformClick.run(View.java:28305), android.os.Handler.handleCallback(Handler.java:938), android.os.Handler.dispatchMessage(Handler.java:99), android.os.Looper.loop(Looper.java:223), android.app.ActivityThread.main(ActivityThread.java:7656), java.lang.reflect.Method.invoke(Native Method), com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592), com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)]

2.2 單次授權

就是在申請與位置信息、麥克風或攝像頭相關的權限時,系統會自動提供一個單次授權的選項,只供這一次權限獲取。然后用戶下次打開app的時候,系統會再次提示用戶授予權限。這個影響應該不大,只要我們每次使用的時候都去判斷權限,沒有就去申請即可。放一張新版本權限獲取樣式:

2.3 權限對話框的可見性

Android 11 建議不要請求用戶已選擇拒絕的權限。在應用安裝到設備上后,如果用戶在使用過程中屢次針對某項特定的權限點按拒絕,此操作表示其希望“不再詢問”。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。