拖不得了,Android11真的要來(lái)了,最全適配實(shí)踐指南奉上

前言

最近看到一條新聞,Android 11(version 30,Andorid R)最終Beta版 如期發(fā)布,看到這個(gè)新聞我知道我不能再拖了,再不好好準(zhǔn)備好迎接Android11的到來(lái),到時(shí)候迎接我的就是客戶的指責(zé),甚至老板的一封休書(shū)了 ??。

今天就和大家一起看看Android11到底改了些什么,以及最重要的,我們需要怎么適配?targetversion不改到30,是不是就不用適配了呢?

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

  • Adnroid11 為目標(biāo)版本的應(yīng)用(targetSdkVersion>=30才有影響)?
  • 所有應(yīng)用在Android11設(shè)備上適配改動(dòng)(無(wú)論targetSdkVersion是多少,只要在Android11設(shè)備上運(yùn)行的應(yīng)用都有影響)

為什么先說(shuō)targetSdkVersion>=30的模塊呢?因?yàn)橐话銇?lái)說(shuō)為了Google為了讓我們更長(zhǎng)時(shí)間適應(yīng)新的內(nèi)容以及保障線上應(yīng)用的穩(wěn)定,都會(huì)把改動(dòng)大的,需要花時(shí)間適配的內(nèi)容放到新的targetSdkVersion對(duì)應(yīng)的應(yīng)用上,如果你暫時(shí)沒(méi)有適配targetSdkVersion30的需求,也可以看看第二模塊,看看是否有涉及你的應(yīng)用相關(guān)內(nèi)容。GOGOGO!

Tips:此適配文章會(huì)不間斷更新,根據(jù)Android11發(fā)布進(jìn)度調(diào)整,歡迎點(diǎn)贊關(guān)注。(打?的格外注意哦)

適配targetSdkVersion30

此模塊的修改內(nèi)容只針對(duì)targetSdkVersion 30或者以上才生效。

分區(qū)存儲(chǔ)強(qiáng)制執(zhí)行?

對(duì)外部存儲(chǔ)目錄的訪問(wèn)僅限于應(yīng)用專屬目錄,以及應(yīng)用已創(chuàng)建的特定類(lèi)型的媒體。

關(guān)于分區(qū)存儲(chǔ),在Android10就已經(jīng)推行了,簡(jiǎn)單的說(shuō),就是應(yīng)用對(duì)于文件的讀寫(xiě)只能在沙盒環(huán)境,也就是屬于自己應(yīng)用的目錄里面讀寫(xiě)。其他媒體文件可以通過(guò)MediaStore進(jìn)行訪問(wèn)。

但是在android10的時(shí)候,Google還是為開(kāi)發(fā)者考慮,留了一手。在targetSdkVersion = 29應(yīng)用中,設(shè)置android:requestLegacyExternalStorage="true",就可以不啟動(dòng)分區(qū)存儲(chǔ),讓以前的文件讀取正常使用。但是targetSdkVersion = 30中不行了,強(qiáng)制開(kāi)啟分區(qū)存儲(chǔ)。
當(dāng)然,作為人性化的android,還是為開(kāi)發(fā)者留了一小手,如果是覆蓋安裝呢,可以增加android:preserveLegacyExternalStorage="true",暫時(shí)關(guān)閉分區(qū)存儲(chǔ),好讓開(kāi)發(fā)者完成數(shù)據(jù)遷移的工作。為什么是暫時(shí)呢?因?yàn)橹灰?code>卸載重裝,就會(huì)失效了。以下是關(guān)于分區(qū)存儲(chǔ)會(huì)遇到的所有情況,給大家羅列出來(lái)了,先上代碼:

    fun saveFile() {
        if (checkPermission()) {
            //getExternalStoragePublicDirectory被棄用,分區(qū)存儲(chǔ)開(kāi)啟后就不允許訪問(wèn)了
            val filePath = Environment.getExternalStoragePublicDirectory("").toString() + "/test3.txt"
            val fw = FileWriter(filePath)
            fw.write("hello world")
            fw.close()
            showToast("文件寫(xiě)入成功")
        }
    }

分情況運(yùn)行:
1) targetSdkVersion = 28,運(yùn)行后正常讀寫(xiě)。
2) targetSdkVersion = 29,不刪除應(yīng)用,targetSdkVersion 由28修改到29,覆蓋安裝,運(yùn)行后正常讀寫(xiě)。
3) targetSdkVersion = 29,刪除應(yīng)用,重新運(yùn)行,讀寫(xiě)報(bào)錯(cuò),程序崩潰(open failed: EACCES (Permission denied))
4) targetSdkVersion = 29,添加android:requestLegacyExternalStorage="true"(不啟用分區(qū)存儲(chǔ)),讀寫(xiě)正常不報(bào)錯(cuò)
5) targetSdkVersion = 30,不刪除應(yīng)用,targetSdkVersion 由29修改到30,讀寫(xiě)報(bào)錯(cuò),程序崩潰(open failed: EACCES (Permission denied))
6) targetSdkVersion = 30,不刪除應(yīng)用,targetSdkVersion 由29修改到30,增加android:preserveLegacyExternalStorage="true",讀寫(xiě)正常不報(bào)錯(cuò)
7) targetSdkVersion = 30,刪除應(yīng)用,重新運(yùn)行,讀寫(xiě)報(bào)錯(cuò),程序崩潰(open failed: EACCES (Permission denied))

ok,那到底應(yīng)該怎么改呢?三種方法訪問(wèn)文件:

1)應(yīng)用專屬目錄

//分區(qū)存儲(chǔ)空間
val file = File(context.filesDir, filename)

//應(yīng)用專屬外部存儲(chǔ)空間
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)

2)訪問(wèn)公共媒體目錄文件

val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {
    while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
        val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
        println("image uri is $uri")
    }
    cursor.close()
}

  1. SAF(存儲(chǔ)訪問(wèn)框架--Storage Access Framework)
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "image/*"
    startActivityForResult(intent, 100)

    @RequiresApi(Build.VERSION_CODES.KITKAT)
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (data == null || resultCode != Activity.RESULT_OK) return
        if (requestCode == 100) {
            val uri = data.data
            println("image uri is $uri")
        }
    }

具體還有很多操作可以看看網(wǎng)上關(guān)于分區(qū)存儲(chǔ)的資料,因?yàn)锳ndroid10已經(jīng)出來(lái)很久了,所以資料還是很多的,這里推薦幾篇
訪問(wèn)應(yīng)用專屬文件
Android 10適配要點(diǎn),作用域存儲(chǔ)
AndroidQ(10)分區(qū)存儲(chǔ)完美適配

說(shuō)到這里可能又有人問(wèn)了,那我的應(yīng)用就是個(gè)手機(jī)管理器,總不能不讓我清其他應(yīng)用的緩存了吧,有辦法!Android提供了兩個(gè)intent入口:

  • 調(diào)用ACTION_MANAGE_STORAGE intent 操作檢查可用空間。
  • 調(diào)用ACTION_CLEAR_APP_CACHE intent 操作清除所有緩存。

說(shuō)來(lái)說(shuō)去,反正應(yīng)用數(shù)據(jù)私有化是大勢(shì)所趨,還是早點(diǎn)適配分區(qū)存儲(chǔ),別等以后手機(jī)只有沙盒機(jī)制的時(shí)候,就來(lái)不及了

媒體文件訪問(wèn)權(quán)限 ?

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

1)執(zhí)行批量操作

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

  • createWriteRequest() 用戶向應(yīng)用授予對(duì)指定媒體文件組的寫(xiě)入訪問(wèn)權(quán)限的請(qǐng)求。
  • createFavoriteRequest()用戶將設(shè)備上指定的媒體文件標(biāo)記為“收藏”的請(qǐng)求。對(duì)該文件具有讀取訪問(wèn)權(quán)限的任何應(yīng)用都可以看到用戶已將該文件標(biāo)記為“收藏”。
  • createTrashRequest() 用戶將指定的媒體文件放入設(shè)備垃圾箱的請(qǐng)求。垃圾箱中的內(nèi)容會(huì)在系統(tǒng)定義的時(shí)間段后被永久刪除。
  • createDeleteRequest() 用戶立即永久刪除指定的媒體文件(而不是先將其放入垃圾箱)的請(qǐng)求。

直接看個(gè)例子:

val urisToModify = listOf(uri,uri,...)
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)


override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}    

傳入uri的集合,獲取用戶的同意后,就可以進(jìn)行操作了。

2)直接文件路徑和原生庫(kù)訪問(wèn)文件

沒(méi)錯(cuò)!Android11又恢復(fù)了使用直接文件路徑訪問(wèn)訪問(wèn)媒體文件!哈哈,這樣就方便多了。也就是除了 MediaStore API之外還有兩種方式可以訪問(wèn)媒體文件:

  • File API。
  • 原生庫(kù),例如 fopen()。

Android10咋辦呢??要不就用MediaStore,要不就直接把分區(qū)存儲(chǔ)關(guān)了吧(requestLegacyExternalStorage=true)

所有文件訪問(wèn)權(quán)限 ?

雖然說(shuō)了這么多,但是還有些應(yīng)用就要訪問(wèn)所有文件,比如殺毒軟件,文件管理器。放心,有辦法!MANAGE_EXTERNAL_STORAGE 這不來(lái)了嗎。
這個(gè)權(quán)限就是用來(lái)獲取所有文件的管理權(quán)限。??:

    <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權(quán)限:
    val isHasStoragePermission= Environment.isExternalStorageManager()

來(lái)張截圖過(guò)過(guò)癮:

申請(qǐng)所有文件訪問(wèn)權(quán)限

電話號(hào)碼相關(guān)權(quán)限 ?

Android 11 更改了您的應(yīng)用在讀取電話號(hào)碼時(shí)使用的與電話相關(guān)的權(quán)限。

具體改了什么呢?其實(shí)就是兩個(gè)API:

  • TelecomManager 類(lèi)中的 getLine1Number() 方法
  • TelecomManager 類(lèi)中的 getMsisdn() 方法

也就是當(dāng)用到這兩個(gè)API的時(shí)候,原來(lái)的READ_PHONE_STATE權(quán)限不管用了,需要READ_PHONE_NUMBERS權(quán)限才行。

下面具體說(shuō)說(shuō),targetSdkVersion修改到30,然后運(yùn)行一個(gè)獲取電話號(hào)碼的程序:


    ActivityCompat.requestPermissions(this,
        arrayOf(Manifest.permission.READ_PHONE_STATE), 100)

        btn2.setOnClickListener {
            val tm = this.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
            val phoneNumber = tm.line1Number
            showToast(phoneNumber)
        }

崩潰了:


java.lang.SecurityException: getLine1NumberForDisplay: Neither user 10151 nor current process has android.permission.READ_PHONE_STATE, android.permission.READ_SMS, or android.permission.READ_PHONE_NUMBERS

預(yù)想之中哈,Andmanifest.xml中注冊(cè)好權(quán)限,并且添加動(dòng)態(tài)權(quán)限申請(qǐng):


    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />


    ActivityCompat.requestPermissions(this,
        arrayOf(Manifest.permission.READ_PHONE_STATE,Manifest.permission.READ_PHONE_NUMBERS), 100)

搞定,如果你只需要獲取手機(jī)號(hào)碼這一個(gè)功能,也可以只申請(qǐng)READ_PHONE_NUMBERS這一個(gè)權(quán)限:

    <uses-permission android:name="android.permission.READ_PHONE_STATE"  android:maxSdkVersion="29" />
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />

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

從 Android 11 開(kāi)始,已棄用自定義消息框視圖。如果您的應(yīng)用以 Android 11 為目標(biāo)平臺(tái),包含自定義視圖的消息框在從后臺(tái)發(fā)布時(shí)會(huì)被屏蔽

可能有人會(huì)奇怪了,什么是自定義消息框視圖啊?我說(shuō)英文你就知道了,英文是custom toast views,也就是自定義toast。簡(jiǎn)單寫(xiě)個(gè)代碼:

    Toast toast = new Toast(context);
    toast.setDuration(show_length);
    toast.setView(view);
    toast.show();

糟了糟了,自定義toast被棄用了?我們項(xiàng)目就是用的這個(gè)啊!不用擔(dān)心,只是不允許自定義toast從后臺(tái)顯示了。
比如我寫(xiě)一個(gè)3秒后再顯示toast,然后應(yīng)用一打開(kāi)就進(jìn)入后臺(tái),看看會(huì)發(fā)生什么:

    Handler().postDelayed({
          IToast.show("你好,我是自定義toast")
     }, 3000)


     W/NotificationService: Blocking custom toast from package com.example.studynote due to package not in the foreground

啥也沒(méi)顯示,只是發(fā)出來(lái)一個(gè)警告。
所以不用太過(guò)擔(dān)心,如果實(shí)在需要后臺(tái)顯示,就用普通的toast吧!

現(xiàn)在需要 APK 簽名方案 v2 ?

對(duì)于以 Android 11(API 級(jí)別 30)為目標(biāo)平臺(tái),且目前僅使用 APK 簽名方案 v1 簽名的應(yīng)用,現(xiàn)在還必須使用 APK 簽名方案 v2 或更高版本進(jìn)行簽名。用戶無(wú)法在搭載 Android 11 的設(shè)備上安裝或更新僅通過(guò) APK 簽名方案 v1 簽名的應(yīng)用。

這個(gè)介紹已經(jīng)很明顯了吧,如果你的targetSdkVersion修改到30,那么你就必須要加上v2簽名才行。否則無(wú)法安裝和更新。

媒體intent操作需要系統(tǒng)默認(rèn)相機(jī) ?

從 Android 11 開(kāi)始,只有預(yù)裝的系統(tǒng)相機(jī)應(yīng)用可以響應(yīng)以下 intent 操作:

android.media.action.VIDEO_CAPTURE
android.media.action.IMAGE_CAPTURE
android.media.action.IMAGE_CAPTURE_SECURE

也就是說(shuō),如果我調(diào)用intent喚起照相機(jī),使用VIDEO_CAPTURE的action,只有系統(tǒng)的相機(jī)能夠響應(yīng),而第三方的相機(jī)應(yīng)用不會(huì)響應(yīng)了。

    val intent=Intent()
    intent.action=android.provider.MediaStore.ACTION_IMAGE_CAPTURE
    startActivity(intent)

    //無(wú)法喚起第三方相機(jī)了,只能喚起系統(tǒng)相機(jī)

這點(diǎn)對(duì)普通的相機(jī)應(yīng)用還是有點(diǎn)打擊的,官方給的建議是如果要使用特定的第三方相機(jī)應(yīng)用來(lái)代表其捕獲圖片或視頻,可以通過(guò)為intent設(shè)置軟件包名稱或組件來(lái)使這些intent變得明確。

5G ?

Android 11 添加了在您的應(yīng)用中支持 5G 的功能

新的Android11也是支持了5G相關(guān)的一些功能,包括:

  • 檢測(cè)是否連接到了5G網(wǎng)絡(luò)
  • 檢查按流量計(jì)費(fèi)性

首先是檢測(cè)5G網(wǎng)絡(luò),通過(guò)TelephonyManager的監(jiān)聽(tīng)方法:

    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("高級(jí)專業(yè)版 LTE (5Ge)")
                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA -> showToast("NR (5G) - 5G Sub-6 網(wǎng)絡(luò)")
                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE -> showToast("5G+/5G UW - 5G mmWave 網(wǎng)絡(luò)")
                    else -> showToast("other")
                }
            }

        }, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)
    }

如果是5g網(wǎng)絡(luò),就免不了要去判斷是不是按流量計(jì)費(fèi)的,否則5G的流量可不是開(kāi)玩笑的。

檢測(cè)流量計(jì)費(fèi)方法也很簡(jiǎn)單,監(jiān)聽(tīng)網(wǎng)絡(luò),在回調(diào)中判斷:

    val manager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
     manager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
        override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
          super.onCapabilitiesChanged(network, networkCapabilities)

            //true 代表連接不按流量計(jì)費(fèi)
            val isNotFlowPay=networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ||
                            networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
          }
    })

判斷該值,如果為 true,則將連接視為不按流量計(jì)費(fèi)。

后臺(tái)位置信息訪問(wèn)權(quán)限 ?

在搭載 Android 11 的設(shè)備上,當(dāng)應(yīng)用中的某項(xiàng)功能請(qǐng)求在后臺(tái)訪問(wèn)位置信息時(shí),用戶看到的系統(tǒng)對(duì)話框不再包含用于啟用后臺(tái)位置信息訪問(wèn)權(quán)限的按鈕。如需啟用后臺(tái)位置信息訪問(wèn)權(quán)限,用戶必須在設(shè)置頁(yè)面上針對(duì)應(yīng)用的位置權(quán)限設(shè)置一律允許選項(xiàng)。

什么意思呢?主要涉及到兩點(diǎn):

  • 從Android10系統(tǒng)的設(shè)備開(kāi)始,就需要請(qǐng)求后臺(tái)位置權(quán)限(ACCESS_BACKGROUND_LOCATION),并選擇Allow all the time (始終允許)才能獲得后臺(tái)位置權(quán)限。Android11設(shè)備上再次加強(qiáng)對(duì)后臺(tái)權(quán)限的管理,主要表現(xiàn)在系統(tǒng)對(duì)話框上,對(duì)話框不再提示始終允許字樣,而是提供了位置權(quán)限的設(shè)置入口,需要在設(shè)置頁(yè)面選擇始終允許才能獲得后臺(tái)位置權(quán)限。
  • 在搭載Android11系統(tǒng)的設(shè)備上,targetVersion小于11的時(shí)候,可以前臺(tái)后臺(tái)位置權(quán)限一起申請(qǐng),并且對(duì)話框提供了文字說(shuō)明,表示需要隨時(shí)獲取用戶位置信息,進(jìn)入設(shè)置選擇始終允許即可。但是targetVersion為30的時(shí)候,你必須單獨(dú)申請(qǐng)后臺(tái)位置權(quán)限,而且要在獲取前臺(tái)權(quán)限之后,順序不能亂。并且無(wú)任何提示,需要開(kāi)發(fā)者自己設(shè)計(jì)提示樣式。

可能有點(diǎn)繞,操作幾個(gè)例子說(shuō)明:

1)Android10設(shè)備,申請(qǐng)前臺(tái)和后臺(tái)位置權(quán)限(任意targetSdkVersion):

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

執(zhí)行效果:


Android10設(shè)備申請(qǐng)位置權(quán)限

2)Android11設(shè)備,targetSdkVersion<=29(Android 10),申請(qǐng)前臺(tái)和后臺(tái)位置權(quán)限:

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

執(zhí)行效果:


Android11設(shè)備targetSdkVersion29申請(qǐng)位置權(quán)限

3)Android11設(shè)備,targetSdkVersion=30(Android 11),申請(qǐng)前臺(tái)和后臺(tái)位置權(quán)限:

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

執(zhí)行無(wú)反應(yīng)

4)Android11設(shè)備,targetSdkVersion=30(Android 11),先申請(qǐng)前臺(tái)位置權(quán)限,后申請(qǐng)后臺(tái)位置權(quán)限:

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

執(zhí)行效果:


Android11設(shè)備targetSdkVersion30申請(qǐng)位置權(quán)限
requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

執(zhí)行效果(直接跳轉(zhuǎn)到設(shè)置頁(yè)面,無(wú)任何說(shuō)明):


權(quán)限圖

所以,該怎么適配呢?

  • targetSdkVersion<30情況下,如果你之前就有判斷過(guò)前臺(tái)和后臺(tái)位置權(quán)限,那就無(wú)需擔(dān)心,沒(méi)有什么需要適配。
  • targetSdkVersion>30情況下,需要分開(kāi)申請(qǐng)前后臺(tái)位置權(quán)限,并且對(duì)后臺(tái)位置權(quán)限申請(qǐng)做好說(shuō)明和引導(dǎo),當(dāng)然也是為了更好的服務(wù)用戶。

權(quán)限申請(qǐng)的demo代碼:

    val permissionAccessCoarseLocationApproved = ActivityCompat
        .checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION) ==
        PackageManager.PERMISSION_GRANTED

    if (permissionAccessCoarseLocationApproved) {
       val backgroundLocationPermissionApproved = ActivityCompat
           .checkSelfPermission(this, permission.ACCESS_BACKGROUND_LOCATION) ==
           PackageManager.PERMISSION_GRANTED

       if (backgroundLocationPermissionApproved) {
            //前后臺(tái)位置權(quán)限都有
       } else {
            //申請(qǐng)后臺(tái)權(quán)限
            if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){
                ActivityCompat.requestPermissions(this,
                        arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                        200)
            }else{
                AlertDialog.Builder(this).setMessage("需要提供后臺(tái)位置權(quán)限,請(qǐng)?jiān)谠O(shè)置頁(yè)面選擇始終允許")
                        .setPositiveButton("確定", DialogInterface.OnClickListener { dialog, which ->
                            ActivityCompat.requestPermissions(this,
                                    arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                                    200)
                        }).create().show()
            }
       }
    } else {
        if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){
            //申請(qǐng)前臺(tái)和后臺(tái)位置權(quán)限
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                    100)
        }else{
            //申請(qǐng)前臺(tái)位置權(quán)限
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
                    100)
        }
    }
    

軟件包可見(jiàn)性 ?

Android 11 更改了應(yīng)用查詢用戶已在設(shè)備上安裝的其他應(yīng)用以及與之交互的方式。使用新的 <queries> 元素,應(yīng)用可以定義一組自身可訪問(wèn)的其他應(yīng)用。通過(guò)告知系統(tǒng)應(yīng)向您的應(yīng)用顯示哪些其他應(yīng)用,此元素有助于鼓勵(lì)最小權(quán)限原則。此外,此元素還可幫助 Google Play 等應(yīng)用商店評(píng)估應(yīng)用為用戶提供的隱私權(quán)和安全性。

也就是說(shuō),Android11中,如果你想去獲取其他應(yīng)用的信息,比如包名,名稱等等,不能直接獲取了,必須在清單文件中添加<queries>元素,告知系統(tǒng)你要獲取哪些應(yīng)用信息或者哪一類(lèi)應(yīng)用。

比如我這段查詢應(yīng)用信息的代碼:

    val pm = this.packageManager
    val listAppcations: List<ApplicationInfo> = pm
            .getInstalledApplications(PackageManager.GET_META_DATA)
    for (app in listAppcations) {
        Log.e("lz",app.packageName)
    }

Android11版本,只能查詢到自己應(yīng)用和系統(tǒng)應(yīng)用的信息,查不到其他應(yīng)用的信息了。怎么呢?添加<queries>元素,兩種方式:

1)<queries>元素中加入具體包名

<manifest package="com.example.game">
    <queries>
        <package android:name="com.example.store" />
        <package android:name="com.example.services" />
    </queries>
    ...
</manifest>

1)<queries>元素中加入固定過(guò)濾的intent

<manifest package="com.example.game">
    <queries>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="image/jpeg" />
        </intent>
    </queries>
</manifest>

可能還是有人會(huì)疑惑,那我的應(yīng)用是瀏覽器或者設(shè)備管理器咋辦呢?我就要獲取所有包名啊?
放心,Android11還引入了 QUERY_ALL_PACKAGES 權(quán)限,清單文件中加入即可。但是Google Play可不一定能濫用哦,它為需要QUERY_ALL_PACKAGES 權(quán)限的應(yīng)用會(huì)提供相關(guān)指南,但是還沒(méi)出來(lái),具體要看后面的消息了。

至于國(guó)內(nèi)市場(chǎng)。。。(希望能有個(gè)應(yīng)用市場(chǎng)一統(tǒng)天下好好管理這混亂的市場(chǎng)吧!)

文檔訪問(wèn)限制

為讓開(kāi)發(fā)者有時(shí)間進(jìn)行測(cè)試,以下與存儲(chǔ)訪問(wèn)框架 (SAF) 相關(guān)的變更只有在應(yīng)用以 Android 11 為目標(biāo)平臺(tái)時(shí)才會(huì)生效。

上文存儲(chǔ)的時(shí)候說(shuō)過(guò)可以通過(guò)SAF(存儲(chǔ)訪問(wèn)框架--Storage Access Framework)來(lái)訪問(wèn)公共目錄,但是Android11再次升級(jí),部分目錄和文件不能訪問(wèn)了,具體如下:

無(wú)法再使用 ACTION_OPEN_DOCUMENT_TREE intent 操作請(qǐng)求訪問(wèn)以下目錄:

  • 內(nèi)部存儲(chǔ)卷的根目錄。
  • 設(shè)備制造商認(rèn)為可靠的各個(gè) SD 卡卷的根目錄,無(wú)論該卡是模擬卡還是可移除的卡。可靠的卷是指應(yīng)用在大多數(shù)情況下可以成功訪問(wèn)的卷。
  • Download 目錄。

無(wú)法再使用 ACTION_OPEN_DOCUMENT_TREEACTION_OPEN_DOCUMENT intent 操作請(qǐng)求用戶從以下目錄中選擇單獨(dú)的文件:

  • Android/data/ 目錄及其所有子目錄。
  • Android/obb/ 目錄及其所有子目錄。

限制對(duì) APN 數(shù)據(jù)庫(kù)的讀取訪問(wèn)

以 Android 11 為目標(biāo)平臺(tái)的應(yīng)用現(xiàn)在必須具備 Manifest.permission.WRITE_APN_SETTINGS 特權(quán),才能讀取或訪問(wèn)電話提供程序 APN 數(shù)據(jù)庫(kù)。如果在不具備此權(quán)限的情況下嘗試訪問(wèn) APN 數(shù)據(jù)庫(kù),會(huì)生成安全異常。

問(wèn)題來(lái)了,APN是啥?

  • 指一種網(wǎng)絡(luò)接入技術(shù),是通過(guò)手機(jī)上網(wǎng)時(shí)必須配置的一個(gè)參數(shù),APN配置參數(shù)包括名字,運(yùn)營(yíng)商編號(hào),APN接入點(diǎn)等等。

就是說(shuō)如果沒(méi)有Manifest.permission.WRITE_APN_SETTINGS權(quán)限就不能讀取APN數(shù)據(jù)庫(kù)了,但是!這個(gè)權(quán)限很早之前就被限定只有系統(tǒng)程序才能申請(qǐng)這個(gè)權(quán)限了,現(xiàn)在這個(gè)特權(quán)沒(méi)理解到是什么意思,難道系統(tǒng)程序都不能隨便申請(qǐng)了?有大神可以評(píng)論區(qū)留言告知。

在元數(shù)據(jù)文件中聲明“無(wú)障礙”按鈕使用情況

從 Android 11 開(kāi)始,您的無(wú)障礙服務(wù)無(wú)法在運(yùn)行時(shí)聲明與系統(tǒng)的“無(wú)障礙”按鈕的關(guān)聯(lián)。如果您將 AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON 附加到 AccessibilityServiceInfo 對(duì)象的 flags 屬性,框架就不會(huì)將“無(wú)障礙”按鈕回調(diào)事件傳遞給您的服務(wù)。

做過(guò)無(wú)障礙輔助功能的應(yīng)該都知道AccessibilityServiceInfo要設(shè)置flag為FLAG_REQUEST_ACCESSIBILITY_BUTTON,getAccessibilityButtonController方法獲取輔助功能按鈕控制器,并且可用于查詢輔助功能按鈕的狀態(tài)并注冊(cè)監(jiān)聽(tīng)器以進(jìn)行交互和輔助功能按鈕的狀態(tài)更改。

但是,Android 11開(kāi)始,這樣寫(xiě)不能獲取輔助按鈕回調(diào)事件了,得換成另外一種寫(xiě)法。在元數(shù)據(jù)文件(通常為 res/raw/accessibilityservice.xml)中使用 flagRequestAccessibilityButton 標(biāo)記聲明您的無(wú)障礙服務(wù)與“無(wú)障礙”按鈕的關(guān)聯(lián)。

Firebase JobDispatcher 和 GCMNetworkManager

如果您的應(yīng)用以 API 級(jí)別 30 或更高級(jí)別為目標(biāo)平臺(tái),在搭載 Android 6.0(API 級(jí)別 23)或更高版本的設(shè)備上會(huì)停用 Firebase JobDispatcher 和 GcmNetworkManager API 調(diào)用。

這兩個(gè)api國(guó)內(nèi)都用不了,主要用于后臺(tái)任務(wù)。官方給出的替代意見(jiàn)是WorkManager,這個(gè)國(guó)內(nèi)是可以用的,屬于jetpack組件,主要用于調(diào)度和執(zhí)行可延期的后臺(tái)工作。

設(shè)備到設(shè)備文件傳輸

如果您的應(yīng)用以 Android 11 為目標(biāo)平臺(tái),您將無(wú)法再使用 allowBackup 屬性停用應(yīng)用文件的設(shè)備到設(shè)備遷移。系統(tǒng)會(huì)自動(dòng)啟用此功能。不過(guò),即使您的應(yīng)用以 Android 11 為目標(biāo)平臺(tái),您也可以通過(guò)將 allowBackup 屬性設(shè)置為 false 來(lái)停用應(yīng)用文件的云端備份和恢復(fù)。

android:allowBackup屬性

  • 代表是否允許應(yīng)用參與備份和恢復(fù)基礎(chǔ)架構(gòu)。如果將此屬性設(shè)為 false,則永遠(yuǎn)不會(huì)為該應(yīng)用執(zhí)行備份或恢復(fù),即使是采用全系統(tǒng)備份方法也不例外(這種備份方法通常會(huì)通過(guò) adb 保存所有應(yīng)用數(shù)據(jù))。此屬性的默認(rèn)值為 true。

所以這里是不能停用文件的設(shè)備到設(shè)備遷移,但是可以停用云端備份和恢復(fù)

自動(dòng)重置權(quán)限

如果應(yīng)用以 Android 11 為目標(biāo)平臺(tái)并且數(shù)月未使用,系統(tǒng)會(huì)通過(guò)自動(dòng)重置用戶已授予應(yīng)用的運(yùn)行時(shí)敏感權(quán)限來(lái)保護(hù)用戶數(shù)據(jù)。此操作與用戶在系統(tǒng)設(shè)置中查看權(quán)限并將應(yīng)用的訪問(wèn)權(quán)限級(jí)別更改為拒絕的做法效果一樣。如果應(yīng)用已遵循有關(guān)在運(yùn)行時(shí)請(qǐng)求權(quán)限的最佳做法,那么您不必對(duì)應(yīng)用進(jìn)行任何更改。這是因?yàn)椋?dāng)用戶與應(yīng)用中的功能互動(dòng)時(shí),您應(yīng)該會(huì)驗(yàn)證相關(guān)功能是否具有所需權(quán)限。

官方說(shuō)明說(shuō)的很清楚了,而且只要應(yīng)用遵循有關(guān)在運(yùn)行時(shí)請(qǐng)求權(quán)限的最佳做法,也就是每次需要調(diào)用權(quán)限的時(shí)候都會(huì)去判斷,那么就不會(huì)有什么問(wèn)題。

如果需要關(guān)閉這個(gè)功能怎么辦呢?只有引導(dǎo)用戶去設(shè)置頁(yè)面關(guān)閉了,可以調(diào)用包含Settings.ACTION_APPLICATION_DETAILS_SETTINGS action的 Intent將用戶定向到系統(tǒng)設(shè)置中應(yīng)用的頁(yè)面。

怎么檢查應(yīng)用是否停用自動(dòng)重置功能呢?調(diào)用 PackageManager的isAutoRevokeWhitelisted()方法。如果此方法返回 true,代表系統(tǒng)不會(huì)自動(dòng)重置應(yīng)用的權(quán)限。

前臺(tái)服務(wù)類(lèi)型

從 Android 9 開(kāi)始,應(yīng)用僅限于在前臺(tái)訪問(wèn)攝像頭和麥克風(fēng)。為了進(jìn)一步保護(hù)用戶,Android 11 更改了前臺(tái)服務(wù)訪問(wèn)攝像頭和麥克風(fēng)相關(guān)數(shù)據(jù)的方式。如果您的應(yīng)用以 Android 11 為目標(biāo)平臺(tái)并且在某項(xiàng)前臺(tái)服務(wù)中訪問(wèn)這些類(lèi)型的數(shù)據(jù),您需要在該前臺(tái)服務(wù)的聲明的 foregroundServiceType 屬性中添加新的 camera 和 microphone 類(lèi)型。

舉例,如果應(yīng)用某項(xiàng)前臺(tái)服務(wù)需要訪問(wèn)位置信息、攝像頭和麥克風(fēng),那么就這樣添加:

<manifest>
    <service ...
        android:foregroundServiceType="location|camera|microphone" />
</manifest>

適配Android11手機(jī)

此模塊的修改內(nèi)容針對(duì)所有項(xiàng)目在Android11手機(jī)上存在的改動(dòng),與targetSdkVersion無(wú)關(guān)。

數(shù)據(jù)訪問(wèn)審核 ?

為了讓?xiě)?yīng)用及其依賴項(xiàng)訪問(wèn)用戶私密數(shù)據(jù)的過(guò)程更加透明,Android 11 引入了數(shù)據(jù)訪問(wèn)審核功能。借助此流程得出的見(jiàn)解,您可以更好地識(shí)別和糾正可能出現(xiàn)的意外數(shù)據(jù)訪問(wèn)。

哪些范疇屬于用戶私密數(shù)據(jù)呢?其實(shí)就是危險(xiǎn)權(quán)限的調(diào)用,所以這個(gè)功能就是提供了可以監(jiān)聽(tīng)危險(xiǎn)權(quán)限調(diào)用的監(jiān)聽(tīng)。主要涉及到的方法是AppOpsManager.OnOpNotedCallback。無(wú)論是應(yīng)用本身,還是依賴庫(kù)或者SDK中的代碼,只要訪問(wèn)到私密數(shù)據(jù)(危險(xiǎn)權(quán)限),都會(huì)回調(diào)給我們。

對(duì)于工程龐大或者使用較多SDK的工程比較適合用上這個(gè)功能,讓自己應(yīng)用的私有數(shù)據(jù)管理更加透明規(guī)范,否則對(duì)于不熟悉的地方私有數(shù)據(jù)的使用和管理并不方便。而且還可以對(duì)權(quán)限使用添加歸因,也就是一個(gè)tag,標(biāo)志權(quán)限用到了什么地方。方便回調(diào)的時(shí)候知曉哪里使用了私有數(shù)據(jù)

??來(lái):


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test1)

        //創(chuàng)建歸因(attribute)  
        attributionContext = createAttributionContext("shareLocation")

        //監(jiān)聽(tīng)事件
        val appOpsCallback = object : AppOpsManager.OnOpNotedCallback() {
            private fun logPrivateDataAccess(
                    opCode: String, attributionTag: String, trace: String) {
                Log.i(TAG, "Private data accessed. " +
                        "Operation: $opCode\n " +
                        "Attribution Tag:$attributionTag\nStack Trace:\n$trace")
            }

            override fun onNoted(syncNotedAppOp: SyncNotedAppOp) {
                syncNotedAppOp.attributionTag?.let {
                    logPrivateDataAccess(syncNotedAppOp.op,
                            it,
                            Throwable().stackTrace.toString())
                }
            }

            override fun onSelfNoted(syncNotedAppOp: SyncNotedAppOp) {
                syncNotedAppOp.attributionTag?.let {
                    logPrivateDataAccess(syncNotedAppOp.op,
                            it,
                            Throwable().stackTrace.toString())
                }
            }

            override fun onAsyncNoted(asyncNotedAppOp: AsyncNotedAppOp) {
                asyncNotedAppOp.attributionTag?.let {
                    logPrivateDataAccess(asyncNotedAppOp.op,
                            it,
                            asyncNotedAppOp.message)
                }
            }
        }

        //開(kāi)啟私密數(shù)據(jù)監(jiān)聽(tīng)
        val appOpsManager =
                getSystemService(AppOpsManager::class.java) as AppOpsManager
        appOpsManager.setOnOpNotedCallback(mainExecutor, appOpsCallback)

        btn1.setOnClickListener {
            getLocation()
        }
    }

    fun getLocation() {
        val locationManager = attributionContext.getSystemService(
                LocationManager::class.java) as LocationManager
        if (!checkPermission()) {
            return
        }
        val location: Location? = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
        if (location != null) {
            showToast("${location.latitude}")
        }
    }

該例子主要展示了一個(gè)獲取位置信息的功能,如果調(diào)用到getLocation方法,就會(huì)觸發(fā)onNoted回調(diào),回調(diào)信息包括危險(xiǎn)權(quán)限code以及歸因。

其中OnOpNotedCallback 一共三個(gè)回調(diào)方法:

  • onNoted 正常情況下都會(huì)回調(diào)到該方法
  • onAsyncNoted 如果數(shù)據(jù)訪問(wèn)并非發(fā)生在應(yīng)用調(diào)用API期間,就會(huì)調(diào)用onAsyncNoted(),比如一些監(jiān)聽(tīng)器的回調(diào)。
  • onSelfNoted 在極少數(shù)情況下,如果應(yīng)用將自身的UID傳遞到 noteOp(),需要調(diào)用 onSelfNoted()。

最后點(diǎn)擊按鈕,看下回調(diào)的結(jié)果日志:

Private data accessed. Operation: android:coarse_location
     Attribution Tag:shareLocation
    Stack Trace:
    [Ljava.lang.StackTraceElement;@14f5a16

可以看到權(quán)限代碼:android:coarse_location 以及歸因 shareLocation

單次授權(quán)

在 Android 11 中,每當(dāng)應(yīng)用請(qǐng)求與位置信息、麥克風(fēng)或攝像頭相關(guān)的權(quán)限時(shí),面向用戶的權(quán)限對(duì)話框會(huì)包含僅限這一次選項(xiàng)。如果用戶在對(duì)話框中選擇此選項(xiàng),系統(tǒng)會(huì)向應(yīng)用授予臨時(shí)的單次授權(quán)。

簡(jiǎn)單的說(shuō),就是在申請(qǐng)與位置信息、麥克風(fēng)或攝像頭相關(guān)的權(quán)限時(shí),系統(tǒng)會(huì)自動(dòng)提供一個(gè)單次授權(quán)的選項(xiàng),只供這一次權(quán)限獲取。然后用戶下次打開(kāi)app的時(shí)候,系統(tǒng)會(huì)再次提示用戶授予權(quán)限。這個(gè)影響應(yīng)該不大,只要我們每次使用的時(shí)候都去判斷權(quán)限,沒(méi)有就去申請(qǐng)即可。放一張新版本權(quán)限獲取樣式:

新權(quán)限彈窗

權(quán)限對(duì)話框的可見(jiàn)性

Android 11 建議不要請(qǐng)求用戶已選擇拒絕的權(quán)限。在應(yīng)用安裝到設(shè)備上后,如果用戶在使用過(guò)程中屢次針對(duì)某項(xiàng)特定的權(quán)限點(diǎn)按拒絕,此操作表示其希望“不再詢問(wèn)”。

這個(gè)都算不上改動(dòng),只是官方的一個(gè)良好建議。建議在用戶多次拒絕之后,不要再展示權(quán)限申請(qǐng)。

Scudo Hardened Allocator

Android 11 在內(nèi)部使用 Scudo Hardened Allocator 為堆分配提供服務(wù)。Scudo 能夠檢測(cè)并減輕某些類(lèi)型的內(nèi)存安全違規(guī)行為。如果您在原生代碼崩潰報(bào)告中發(fā)現(xiàn)與 Scudo 相關(guān)的崩潰(例如 Scudo ERROR:),請(qǐng)參閱 Scudo 問(wèn)題排查文檔。

Scudo是一種用戶模式分配器,旨在提供額外的mitigation來(lái)防止堆的漏洞的方法,同時(shí)保持良好的性能。它是開(kāi)源的,是LLVM的editor-rt項(xiàng)目的一部分。
Android 11中,將采用這個(gè)新的heap分配器,性能更好,更安全。

文件描述符排錯(cuò)程序

Android 10 引入了 fdsan(文件描述符排錯(cuò)程序)。fdsan 檢測(cè)錯(cuò)誤處理文件描述符所有權(quán)的錯(cuò)誤,例如 use-after-close 和 double-close。在 Android 11 中,fdsan 的默認(rèn)模式發(fā)生了變化。現(xiàn)在,fdsan 會(huì)在檢測(cè)到錯(cuò)誤時(shí)中止,而以前的行為則是記錄警告并繼續(xù)。

問(wèn)題來(lái)了,fdsan是啥?先要了解fd是啥

文件描述符(FileDescriptor) 是Unix/Linux系統(tǒng)文件操作的相關(guān)概念,它在形式上是一個(gè)非負(fù)整數(shù)。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。
系統(tǒng)的進(jìn)程也就是使用了這個(gè)fd來(lái)標(biāo)示打開(kāi)的文件,有了它就能對(duì)文件做各種操作,獲得文件的各種相關(guān)信息了。

所以fdsan也就是檢測(cè)文件處理中發(fā)生的一些錯(cuò)誤。

應(yīng)用使用情況統(tǒng)計(jì)信息

為了更好地保護(hù)用戶,Android 11 將每個(gè)用戶的應(yīng)用使用情況統(tǒng)計(jì)信息存儲(chǔ)在憑據(jù)加密存儲(chǔ)空間中。

這就涉及到了UsageStatsManagerUsageStatsManager是Android提供統(tǒng)計(jì)應(yīng)用使用情況的服務(wù)。通過(guò)這個(gè)服務(wù)可以獲取指定時(shí)間區(qū)間內(nèi)應(yīng)用使用統(tǒng)計(jì)數(shù)據(jù)、組件狀態(tài)變化事件統(tǒng)計(jì)數(shù)據(jù)以及硬件配置信息統(tǒng)計(jì)數(shù)據(jù)。

比如queryAndAggregateUsageStats方法,可以獲取指定時(shí)間區(qū)間內(nèi)使用統(tǒng)計(jì)數(shù)據(jù),以應(yīng)用包名為鍵值進(jìn)行數(shù)據(jù)合并。

但是在Android 11 設(shè)備中,不好意思,不能隨意使用這些信息了。只有當(dāng)isUserUnlocked()方法返回true的時(shí)候,才能正常訪問(wèn)這些數(shù)據(jù)。也就是以下兩種情況:

  • 用戶在系統(tǒng)啟動(dòng)后首次解鎖其設(shè)備
  • 用戶在設(shè)備上切換到自己的帳號(hào)

JobScheduler API 調(diào)用限制調(diào)試

JobScheduler任務(wù)調(diào)度器,可以在設(shè)備空閑時(shí)做一些任務(wù)處理。Android11中如果你設(shè)置為debug模式(debuggable 清單屬性設(shè)置為 true),超出速率限制的JobScheduler API調(diào)用將返回 RESULT_FAILURE。這個(gè)有什么用呢?應(yīng)該可以幫助我們發(fā)現(xiàn)一些性能問(wèn)題,感興趣的可以自己試試。

順便提下,Jetpack組件WorkManager也是用到了JobScheduler,不熟悉的同學(xué)可以去了解下,JobScheduler是由SystemServer進(jìn)程啟動(dòng)的一個(gè)系統(tǒng)服務(wù),所以才可以有這么大的權(quán)限。

無(wú)障礙操作

在以前的 Android 版本中,框架會(huì)向未正確處理基于點(diǎn)擊的無(wú)障礙操作的微件分派觸摸事件。通常,這些視圖會(huì)直接處理觸摸事件,而不是注冊(cè)點(diǎn)擊監(jiān)聽(tīng)器。
為了在正確定義無(wú)障礙操作的應(yīng)用中創(chuàng)建更一致的行為,Android 11 絕不會(huì)分派觸摸事件。相反,系統(tǒng)會(huì)完全依賴于基于點(diǎn)擊的無(wú)障礙操作:ACTION_CLICK 和 ACTION_LONG_CLICK。此更改會(huì)影響屏幕閱讀器的行為。

Android手機(jī)上有個(gè)預(yù)安裝的屏幕閱讀服務(wù),叫做TalkBack,為視力障礙人士或者視力狀態(tài)不佳的老年人提供。那我們應(yīng)用為了讓這個(gè)閱讀器能夠讀懂你的自定義view操作,必須給與自定義控件定義處理程序,包括點(diǎn)擊,長(zhǎng)按等操作。原來(lái)版本可能對(duì)于OnTouchListener也支持無(wú)障礙觸摸事件,而在Android11中,必須專門(mén)制定點(diǎn)擊或者長(zhǎng)按事件才行了。給個(gè)??:

class TriSwitch(context: Context) : Switch(context) {
    // 0, 1, or 2.
    var currentState: Int = 0
        private set

    init {
        updateAccessibilityActions()
    }

    private fun updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label) {
            view, args -> moveToNextState()
        })
    }

    private fun moveToNextState() {
        currentState = (currentState + 1) % 3
    }
}

一個(gè)自定義控件TriSwitch,繼承自Switch,由于和Switch的點(diǎn)擊效果不一樣,所以必須通過(guò)替換 ViewCompat.replaceAccessibilityAction() 來(lái)重新定義相應(yīng)的無(wú)障礙操作。

非SDK接口限制

Android 11 包含更新后的受限制非 SDK 接口列表(基于與 Android 開(kāi)發(fā)者之間的協(xié)作以及最新的內(nèi)部測(cè)試)。在限制使用非 SDK 接口之前,我們會(huì)盡可能確保提供公開(kāi)替代方案。

老樣子,Android11也會(huì)限制一些接口,包括灰名單和白名單,具體看非SDK接口列表

總結(jié)

一路分析下來(lái)也可以看到,如果是重要的改動(dòng),特別是涉及到崩潰的改動(dòng)還是放到了targetSdkVersion=30的內(nèi)容中,這也是每次Android發(fā)版的一個(gè)潛規(guī)則吧,為了最大程度不影響已上線的app所作出的舉動(dòng)。
但是,這并不意味我們就可以不改。因?yàn)閼?yīng)用可拖不起,用戶可拖不起,畢竟升級(jí)才能給到用戶最好的體驗(yàn)。而且各大應(yīng)用市場(chǎng)也都會(huì)建議或者強(qiáng)制應(yīng)用升級(jí)targetSdkVersion,以便適配最新的手機(jī)。

所以,行動(dòng)吧。

附件

官網(wǎng)改動(dòng)介紹


謝謝你的閱讀,如果你覺(jué)得寫(xiě)的還行,就點(diǎn)個(gè)贊支持下吧!感謝!
你的一個(gè)??,就是我分享的動(dòng)力??。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。