38

拖不得了,Android11 最全适配实践指南奉上

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzIxNzU1Nzk3OQ%3D%3D&%3Bmid=2247491303&%3Bidx=1&%3Bsn=85ebcfbd36e762e5fc17453231a95790
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

没错! Android 11(version 30,Andorid R) 正式发布了!看到这个新闻我知道我不能再拖了,再不好好准备好迎接 Android11 的到来,到时候迎接我的就是客户的指责,甚至老板的一封休书了 :joy:。

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

以下我分为两部分讲述,分别是

  • Android11 为目标版本的应用( targetSdkVersion>=30 才有影响):star:
  • 所有应用在 Android11 设备上适配改动(无论targetSdkVersion是多少,只要在 Android11设备 上运行的应用都有影响)
targetSdkVersion>=30
targetSdkVersion
targetSdkVersion30

Tips:此适配文章会不间断更新,根据 Android11 发布进度调整,欢迎点赞关注。(打:star:的格外注意哦)

适配targetSdkVersion30

此模块的修改内容只针对 targetSdkVersion 30 或者以上才生效。

分区存储强制执行:star:

对外部存储目录的访问仅限于应用专属目录,以及应用已创建的特定类型的媒体。

关于分区存储,在 Android10 就已经推行了,简单的说,就是应用对于文件的读写只能在沙盒环境,也就是属于自己应用的目录里面读写。其他媒体文件可以通过 MediaStore 进行访问。

但是在android10的时候,Google还是为开发者考虑,留了一手。在 targetSdkVersion = 29 应用中,设置 android:requestLegacyExternalStorage="true" ,就可以不启动分区存储,让以前的文件读取正常使用。但是 targetSdkVersion = 30 中不行了,强制开启分区存储。

当然,作为人性化的android,还是为开发者留了一小手,如果是覆盖安装呢,可以增加 android:preserveLegacyExternalStorage="true" ,暂时关闭分区存储,好让开发者完成数据迁移的工作。为什么是暂时呢?因为只要 卸载重装 ,就会失效了。以下是关于分区存储会遇到的 所有情况 ,给大家罗列出来了,先上代码:

fun saveFile () {

if (checkPermission()) {

//getExternalStoragePublicDirectory被弃用,分区存储开启后就不允许访问了

val filePath = Environment.getExternalStoragePublicDirectory( "" ).toString() +  "/test3.txt"

val fw = FileWriter(filePath)

fw.write( "hello world" )

fw.close()

showToast( "文件写入成功" )

}

}

分情况运行:

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))

ok,那到底应该怎么改呢?三种方法访问文件:

1)应用专属目录

//分区存储空间

val file = File(context.filesDir, filename)

//应用专属外部存储空间

val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)

2)访问公共媒体目录文件

val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,  nullnullnull " ${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(存储访问框架--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:  IntdataIntent ?) {

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 " )

}

}

具体还有很多操作可以看看网上关于分区存储的资料,因为Android10已经出来很久了,所以资料还是很多的,这里推荐几篇

访问应用专属文件

Android 10适配要点,作用域存储

AndroidQ(10)分区存储完美适配

说到这里可能又有人问了,那我的应用就是个手机管理器,总不能不让我清其他应用的缓存了吧,有办法!Android提供了两个intent入口:

  • 调用 ACTION_MANAGE_STORAGE intent 操作检查可用空间。
  • 调用 ACTION_CLEAR_APP_CACHE intent 操作清除所有缓存。

说来说去,反正应用数据私有化是大势所趋,还是早点适配分区存储,别等以后手机只有沙盒机制的时候, 就来不及了

媒体文件访问权限 :star:

为了在保证用户隐私的同时可以更轻松地访问媒体,Android 11 增加了以下功能。执行批量操作和使用直接文件路径和原生库访问文件。

1)执行批量操作

这里的批量操作指的是Android 11 向 MediaStore API 中添加了多种方法,用于简化特定媒体文件更改流程(例如在原位置编辑照片),分别是:

  • createWriteRequest() 用户向应用授予对指定媒体文件组的写入访问权限的请求。
  • createFavoriteRequest() 用户将设备上指定的媒体文件标记为“收藏”的请求。对该文件具有读取访问权限的任何应用都可以看到用户已将该文件标记为“收藏”。
  • createTrashRequest() 用户将指定的媒体文件放入设备垃圾箱的请求。垃圾箱中的内容会在系统定义的时间段后被永久删除。
  • createDeleteRequest() 用户立即永久删除指定的媒体文件(而不是先将其放入垃圾箱)的请求。

直接看个例子:

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,

null000 )

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的集合,获取用户的同意后,就可以进行操作了。

2)直接文件路径和原生库访问文件

没错!Android11又恢复了使用 直接文件路径 访问访问媒体文件!哈哈,这样就方便多了。也就是除了 MediaStore API 之外还有两种方式可以访问媒体文件:

  • File API。

  • 原生库,例如 fopen()。

Android10 咋办呢??要不就用 MediaStore ,要不就直接把分区存储关了吧(requestLegacyExternalStorage=true)

所有文件访问权限 :star:

虽然说了这么多,但是还有些应用就要访问所有文件,比如杀毒软件,文件管理器。放心,有办法! MANAGE_EXTERNAL_STORAGE 这不来了吗。这个权限就是用来获取 所有文件 的管理权限。:chestnut::

<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()

来张截图过过瘾:

nMNbQzI.jpg!mobile 申请所有文件访问权限

电话号码相关权限 :star:

Android 11 更改了您的应用在读取电话号码时使用的与电话相关的权限。

具体改了什么呢?其实就是两个API:

  • TelecomManager 类中的 getLine1Number() 方法
  • TelecomManager 类中的 getMsisdn() 方法

也就是当用到这两个API的时候,原来的 READ_PHONE_STATE 权限不管用了,需要 READ_PHONE_NUMBERS 权限才行。

下面具体说说, targetSdkVersion 修改到30,然后运行一个获取电话号码的程序:

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

预想之中哈, Andmanifest.xml 中注册好权限,并且添加动态权限申请:

<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 )

搞定,如果你只需要获取手机号码这一个功能,也可以只申请 READ_PHONE_NUMBERS 这一个权限:

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

<uses-permission android:name=

"android.permission.READ_PHONE_NUMBERS"

/>

自定义消息框视图被屏蔽 :star:

从 Android 11 开始,已弃用自定义消息框视图。如果您的应用以 Android 11 为目标平台,包含自定义视图的消息框在从后台发布时会被屏蔽

可能有人会奇怪了,什么是 自定义消息框视图 啊?我说英文你就知道了,英文是 custom toast views ,也就是自定义toast。简单写个代码:

Toast toast = new Toast(context);

toast.setDuration(show_length);

toast.setView(view);

toast.show();

糟了糟了,自定义toast被弃用了?我们项目就是用的这个啊!不用担心,只是不允许自定义toast从后台显示了。比如我写一个3秒后再显示toast,然后应用一打开就进入后台,看看会发生什么:

Handler().postDelayed({

IToast.show( "你好,我是自定义toast" )

},  3000 )

W/NotificationService: Blocking custom toast from  package com.example.studynote due to  package

not 

in

the foreground

啥也没显示,只是发出来一个警告。所以不用太过担心,如果实在需要后台显示,就用普通的toast吧!

现在需要 APK 签名方案 v2 :star:

对于以 Android 11(API 级别 30)为目标平台,且目前仅使用 APK 签名方案 v1 签名的应用,现在还必须使用 APK 签名方案 v2 或更高版本进行签名。用户无法在搭载 Android 11 的设备上安装或更新仅通过 APK 签名方案 v1 签名的应用。

这个介绍已经很明显了吧,如果你的 targetSdkVersion 修改到30,那么你就必须要加上v2签名才行。否则无法安装和更新。

媒体intent操作需要系统默认相机 :star:

从 Android 11 开始,只有预装的系统相机应用可以响应以下 intent 操作:

android.media.action.VIDEO_CAPTURE

android.media.action.IMAGE_CAPTURE

android.media.action.IMAGE_CAPTURE_SECURE

也就是说,如果我调用 intent 唤起照相机,使用 VIDEO_CAPTURE 的action,只有系统的相机能够响应,而第三方的相机应用不会响应了。

val intent=Intent()

intent.action=android.provider.MediaStore.ACTION_IMAGE_CAPTURE

startActivity(intent)

//无法唤起第三方相机了,只能唤起系统相机

这点对普通的相机应用还是有点打击的,官方给的建议是如果要使用特定的第三方相机应用来代表其捕获图片或视频,可以通过为 intent 设置软件包名称或组件来使这些intent变得明确。

5G :star:

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

新的Android11也是支持了 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)

}

如果是5g网络,就免不了要去判断是不是按 流量计费 的,否则5G的流量可不是开玩笑的。

检测流量计费方法也很简单,监听网络,在回调中判断:

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 ,则将连接视为不按流量计费。

后台位置信息访问权限 :star:

在搭载 Android 11 的设备上,当应用中的某项功能请求在后台访问位置信息时,用户看到的系统对话框不再包含用于启用后台位置信息访问权限的按钮。如需启用后台位置信息访问权限,用户必须在设置页面上针对应用的位置权限设置一律允许选项。

什么意思呢?主要涉及到两点:

  • (ACCESS_BACKGROUND_LOCATION)
    Allow all the time (始终允许)
    加强
    系统对话框
    始终允许
    
  • Android11系统
    始终允许
    单独申请
    顺序不能乱
    

可能有点绕,操作几个例子说明:

1) Android10设备 ,申请前台和后台位置权限(任意targetSdkVersion):

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

执行效果:

32m67nF.jpg!mobile

2) Android11设备 ,targetSdkVersion<=29(Android 10),申请前台和后台位置权限:

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

执行效果:

IZnQFfe.jpg!mobile

3) Android11设备 ,targetSdkVersion=30(Android 11),申请前台和后台位置权限:

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

执行无反应

4) Android11设备 ,targetSdkVersion=30(Android 11),先申请前台位置权限,后申请后台位置权限:

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

执行效果:

rmuuiuU.jpg!mobile

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

执行效果(直接跳转到设置页面,无任何说明):

vaI7Z3m.jpg!mobile

所以,该怎么适配呢?

  • targetSdkVersion<30 情况下,如果你之前就有判断过前台和后台位置权限,那就无需担心,没有什么需要适配。
  • targetSdkVersion>30 情况下,需要分开申请前后台位置权限,并且对后台位置权限申请做好说明和引导,当然也是为了更好的服务用户。

权限申请的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) {

//前后台位置权限都有

else {

//申请后台权限

if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.R){

ActivityCompat.requestPermissions( this ,

arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),

200 )

} else {

AlertDialog.Builder( this ).setMessage( "需要提供后台位置权限,请在设置页面选择始终允许" )

.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){

//申请前台和后台位置权限

ActivityCompat.requestPermissions( this ,

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

100 )

} else {

//申请前台位置权限

ActivityCompat.requestPermissions( this ,

arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),

100 )

}

}

软件包可见性 :star:

Android 11 更改了应用查询用户已在设备上安装的其他应用以及与之交互的方式。使用新的 元素,应用可以定义一组自身可访问的其他应用。通过告知系统应向您的应用显示哪些其他应用,此元素有助于鼓励最小权限原则。此外,此元素还可帮助 Google Play 等应用商店评估应用为用户提供的隐私权和安全性。

也就是说, Android11中 ,如果你想去获取其他应用的信息,比如包名,名称等等,不能直接获取了,必须在清单文件中添加 <queries> 元素,告知系统你要获取哪些应用信息或者哪一类应用。

比如我这段查询应用信息的代码:

val pm =  this .packageManager

val listAppcations: List<ApplicationInfo> = pm

.getInstalledApplications(PackageManager.GET_META_DATA)

for (app  in listAppcations) {

Log.e( "lz" ,app.packageName)

}

Android11 版本,只能查询到自己应用和系统应用的信息,查不到其他应用的信息了。怎么呢?添加 <queries> 元素,两种方式:

1) 元素中加入具体包名

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

<queries>

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

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

</queries>

...

</manifest>

1) 元素中加入固定过滤的 intent

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

<queries>

<intent>

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

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

</intent>

</queries>

</manifest>

浏览器或者设备管理器
QUERY_ALL_PACKAGES
Google Play

至于国内市场。。。(希望能有个应用市场一统天下好好管理这混乱的市场吧!)

文档访问限制

为让开发者有时间进行测试,以下与存储访问框架 (SAF) 相关的变更只有在应用以 Android 11 为目标平台时才会生效。

上文存储的时候说过可以通过 SAF(存储访问框架--Storage Access Framework) 来访问公共目录,但是Android11再次升级,部分目录和文件不能访问了,具体如下:

无法再使用 ACTION_OPEN_DOCUMENT_TREE intent 操作请求访问以下目录:

  • 内部存储卷的根目录。

  • 设备制造商认为可靠的各个 SD 卡卷的根目录,无论该卡是模拟卡还是可移除的卡。可靠的卷是指应用在大多数情况下可以成功访问的卷。

  • Download 目录。

无法再使用 ACTION_OPEN_DOCUMENT_TREEACTION_OPEN_DOCUMENT intent 操作请求用户从以下目录中选择单独的文件:

  • Android/data/ 目录及其所有子目录。

  • Android/obb/ 目录及其所有子目录。

限制对 APN 数据库的读取访问

以 Android 11 为目标平台的应用现在必须具备 Manifest.permission.WRITE_APN_SETTINGS 特权,才能读取或访问电话提供程序 APN 数据库。如果在不具备此权限的情况下尝试访问 APN 数据库,会生成安全异常。

问题来了,APN是啥?

  • 指一种网络接入技术,是通过手机上网时必须配置的一个参数,APN配置参数包括名字,运营商编号,APN接入点等等。

就是说如果没有 Manifest.permission.WRITE_APN_SETTINGS 权限就不能读取APN数据库了,但是!这个权限很早之前就被限定只有系统程序才能申请这个权限了,现在这个特权没理解到是什么意思,难道系统程序都不能随便申请了?有 大神 可以评论区留言告知。

在元数据文件中声明“无障碍”按钮使用情况

从 Android 11 开始,您的无障碍服务无法在运行时声明与系统的“无障碍”按钮的关联。如果您将 AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON 附加到 AccessibilityServiceInfo 对象的 flags 属性,框架就不会将“无障碍”按钮回调事件传递给您的服务。

做过无障碍辅助功能的应该都知道 AccessibilityServiceInfo 要设置flag为 FLAG_REQUEST_ACCESSIBILITY_BUTTON,getAccessibilityButtonController 方法获取辅助功能按钮控制器,并且可用于查询辅助功能按钮的状态并注册监听器以进行交互和辅助功能按钮的状态更改。

但是,Android 11开始,这样写不能获取辅助按钮回调事件了,得换成另外一种写法。在元数据文件(通常为 res/raw/accessibilityservice.xml)中使用 flagRequestAccessibilityButton 标记声明您的无障碍服务与“无障碍”按钮的关联。

Firebase JobDispatcher 和 GCMNetworkManager

如果您的应用以 API 级别 30 或更高级别为目标平台,在搭载 Android 6.0(API 级别 23)或更高版本的设备上会停用 Firebase JobDispatcher 和 GcmNetworkManager API 调用。

这两个api国内都用不了,主要用于后台任务。官方给出的替代意见是 WorkManager ,这个国内是可以用的,属于jetpack组件,主要用于调度和执行可延期的后台工作。

设备到设备文件传输

如果您的应用以 Android 11 为目标平台,您将无法再使用 allowBackup 属性停用应用文件的设备到设备迁移。系统会自动启用此功能。不过,即使您的应用以 Android 11 为目标平台,您也可以通过将 allowBackup 属性设置为 false 来停用应用文件的云端备份和恢复。

android:allowBackup属性

  • 代表是否允许应用参与备份和恢复基础架构。如果将此属性设为 false,则永远不会为该应用执行 备份或恢复 ,即使是采用全系统备份方法也不例外(这种备份方法通常会通过 adb 保存所有应用数据)。此属性的默认值为 true。

所以这里是不能停用文件的 设备到设备 迁移,但是可以停用 云端备份和恢复

自动重置权限

如果应用以 Android 11 为目标平台并且数月未使用,系统会通过自动重置用户已授予应用的运行时敏感权限来保护用户数据。此操作与用户在系统设置中查看权限并将应用的访问权限级别更改为拒绝的做法效果一样。如果应用已遵循有关在运行时请求权限的最佳做法,那么您不必对应用进行任何更改。这是因为,当用户与应用中的功能互动时,您应该会验证相关功能是否具有所需权限。

官方说明说的很清楚了,而且只要应用遵循有关在运行时请求权限的最佳做法,也就是 每次需要调用权限 的时候都会去判断,那么就不会有什么问题。

如果需要关闭这个功能怎么办呢?只有引导用户去设置页面关闭了,可以调用包含 Settings.ACTION_APPLICATION_DETAILS_SETTINGS action 的 Intent将用户定向到系统设置中应用的页面。

怎么检查应用是否停用自动重置功能呢?调用 PackageManager的 isAutoRevokeWhitelisted() 方法。如果此方法返回 true,代表系统不会自动重置应用的权限。

前台服务类型

从 Android 9 开始,应用仅限于在前台访问摄像头和麦克风。为了进一步保护用户,Android 11 更改了前台服务访问摄像头和麦克风相关数据的方式。如果您的应用以 Android 11 为目标平台并且在某项前台服务中访问这些类型的数据,您需要在该前台服务的声明的 foregroundServiceType 属性中添加新的 camera 和 microphone 类型。

举例,如果应用某项前台服务需要访问 位置信息、摄像头和麦克风 ,那么就这样添加:

<manifest>

<service ...

android:foregroundServiceType= "location|camera|microphone" />

</manifest>

适配Android11手机

此模块的修改内容针对所有项目在 Android11 手机上存在的改动,与 targetSdkVersion 无关。

数据访问审核 :star:

为了让应用及其依赖项访问用户私密数据的过程更加透明,Android 11 引入了数据访问审核功能。借助此流程得出的见解,您可以更好地识别和纠正可能出现的意外数据访问。

哪些范畴属于用户私密数据呢?其实就是危险权限的调用,所以这个功能就是提供了可以监听危险权限调用的监听。主要涉及到的方法是 AppOpsManager.OnOpNotedCallback 。无论是应用本身,还是依赖库或者SDK中的代码,只要访问到私密数据(危险权限),都会回调给我们。

对于 工程庞大或者使用较多SDK 的工程比较适合用上这个功能,让自己应用的私有数据管理更加 透明规范 ,否则对于私有数据的使用和管理并不全面和方便。而且还可以对权限使用 添加归因 ,也就是一个tag,标志权限用到了什么地方。方便回调的时候知晓哪里使用了 私有数据

:chestnut:来:

override fun   onCreate (savedInstanceState:  Bundle ?) {

super .onCreate(savedInstanceState)

setContentView(R.layout.activity_test1)

//创建归因(attribute)  

attributionContext = createAttributionContext( "shareLocation" )

//监听事件

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)

}

}

}

//开启私密数据监听

val appOpsManager =

getSystemService(AppOpsManager:: class . javaas AppOpsManager

appOpsManager.setOnOpNotedCallback(mainExecutor, appOpsCallback)

btn1.setOnClickListener {

getLocation()

}

}

fun getLocation () {

val locationManager = attributionContext.getSystemService(

LocationManager:: class . javaas LocationManager

if (!checkPermission()) {

return

}

val location: Location? = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)

if (location !=  null ) {

showToast( " ${location.latitude} " )

}

}

获取位置信息
getLocation
onNoted

其中 OnOpNotedCallback 一共三个回调方法:

  • onNoted 正常情况下都会回调到该方法
  • onAsyncNoted 如果数据访问并非发生在应用调用API期间,就会调用onAsyncNoted(),比如一些监听器的回调。
  • onSelfNoted 在极少数情况下,如果应用将自身的UID传递到 noteOp(),需要调用 onSelfNoted()。

最后点击按钮,看下回调的结果日志:

Private  data accessed. Operation: android:coarse_location

Attribution Tag:shareLocation

Stack Trace:

[Ljava.lang.StackTraceElement;@

14f

5a16

可以看到权限代码: android:coarse_location 以及归因 shareLocation

单次授权

在 Android 11 中,每当应用请求与位置信息、麦克风或摄像头相关的权限时,面向用户的权限对话框会包含仅限这一次选项。如果用户在对话框中选择此选项,系统会向应用授予临时的单次授权。

简单的说,就是在申请与 位置信息、麦克风或摄像头 相关的权限时,系统会自动提供一个 单次授权 的选项,只供这一次权限获取。然后用户下次打开app的时候,系统会再次提示用户授予权限。这个影响应该不大,只要我们每次使用的时候都去判断权限,没有就去申请即可。放一张新版本权限获取样式:

aI3qii.jpg!mobile 新权限弹窗

权限对话框的可见性

Android 11 建议不要请求用户已选择拒绝的权限。在应用安装到设备上后,如果用户在使用过程中屡次针对某项特定的权限点按拒绝,此操作表示其希望“不再询问”。

这个都算不上改动,只是官方的一个 良好建议 。建议在用户多次拒绝之后,不要再展示权限申请。

Scudo Hardened Allocator

Android 11 在内部使用 Scudo Hardened Allocator 为堆分配提供服务。Scudo 能够检测并减轻某些类型的内存安全违规行为。如果您在原生代码崩溃报告中发现与 Scudo 相关的崩溃(例如 Scudo ERROR:),请参阅 Scudo 问题排查文档。

Scudo 是一种动态的用户模式内存分配器,旨在抵御与堆相关的漏洞,同时保持良好的性能。它是一个开源的项目。Android 11中,将采用这个新的 heap分配器 ,性能更好,更安全。

文件描述符排错程序

Android 10 引入了 fdsan(文件描述符排错程序)。fdsan 检测错误处理文件描述符所有权的错误,例如 use-after-close 和 double-close。在 Android 11 中,fdsan 的默认模式发生了变化。现在,fdsan 会在检测到错误时中止,而以前的行为则是记录警告并继续。

问题来了, fdsan 是啥?先要了解fd是啥

文件描述符(FileDescriptor) 是 Unix/Linux 系统文件操作的相关概念,它在形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。系统的进程也就是使用了 这个fd 来标示打开的文件,有了它就能对文件做各种操作,获得文件的各种相关信息了。

所以 fdsan 也就是检测文件处理中发生的一些错误。

应用使用情况统计信息

为了更好地保护用户,Android 11 将每个用户的应用使用情况统计信息存储在凭据加密存储空间中。

这就涉及到了 UsageStatsManagerUsageStatsManager 是Android提供统计应用使用情况的服务。通过这个服务可以获取指定时间区间内应用使用统计数据、组件状态变化事件统计数据以及硬件配置信息统计数据。

比如 queryAndAggregateUsageStats 方法,可以获取指定时间区间内使用统计数据,以应用包名为键值进行数据合并。

但是在Android 11 设备中,不好意思,不能随意使用这些信息了。只有当 isUserUnlocked() 方法返回true的时候,才能正常访问这些数据。也就是以下两种情况:

  • 用户在系统启动后首次解锁其设备

  • 用户在设备上切换到自己的帐号

JobScheduler API 调用限制调试

JobScheduler
debug模式
JobScheduler API
RESULT_FAILURE

顺便提下,Jetpack组件 WorkManager 也是用到了JobScheduler,不熟悉的同学可以去了解下, JobScheduler 是由SystemServer进程启动的一个系统服务,所以才可以有这么大的权限。

无障碍操作

在以前的 Android 版本中,框架会向未正确处理基于点击的无障碍操作的微件分派触摸事件。通常,这些视图会直接处理触摸事件,而不是注册点击监听器。为了在正确定义无障碍操作的应用中创建更一致的行为,Android 11 绝不会分派触摸事件。相反,系统会完全依赖于基于点击的无障碍操作:ACTION_CLICK 和 ACTION_LONG_CLICK。此更改会影响屏幕阅读器的行为。

Android
TalkBack
点击,长按
OnTouchListener
Android11

class TriSwitch (context: Context) : Switch(context) {

// 0, 1, or 2.

var currentState:  Int0

private set

init {

updateAccessibilityActions()

}

private fun   updateAccessibilityActions () {

ViewCompat.replaceAccessibilityAction( this , ACTION_CLICK,

action-label) {

view, args -> moveToNextState()

})

}

private fun   moveToNextState () {

currentState = (currentState +  1 ) %  3

}

}

TriSwitch
Switch
ViewCompat.replaceAccessibilityAction()

非SDK接口限制

Android 11 包含更新后的受限制非 SDK 接口列表(基于与 Android 开发者之间的协作以及最新的内部测试)。在限制使用非 SDK 接口之前,我们会尽可能确保提供公开替代方案。

老样子,Android11也会限制一些接口,包括灰名单和白名单,具体看非SDK接口列表

总结

一路分析下来也可以看到,如果是重要的改动,特别是涉及到崩溃的改动还是放到了 targetSdkVersion=30 的内容中,这也是每次Android发版的一个潜规则吧,为了最大程度不影响已上线的app所作出的举动。

但是,这并不意味我们就可以不改。因为应用可拖不起,用户可拖不起,毕竟升级才能给到用户 最好的体验 。而且各大应用市场也都会建议或者强制应用升级 targetSdkVersion ,以便适配最新的手机。

所以,行动吧。

附件

官网改动介绍

适配相关

Android 升级适配爬坑历程

Android Q 适配

安卓 9.0 适配方案和踩坑

B3Q7faa.jpg!mobile

如果你有写博客的好习惯

欢迎投稿

赞+在看,小生感恩 :heart:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK