最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

Android11最全适配实践指南–应用端

开发进阶 loading 262浏览 0评论

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

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

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

为什么先说targetSdkVersion>=30的模块呢?因为一般来说为了 Google 为了让我们更长时间适应新的内容以及保障线上应用的稳定,都会把改动大的,需要花时间适配的内容放到新的targetSdkVersion对应的应用上,如果你暂时没有适配targetSdkVersion30的需求,也可以看看第二模块,看看是否有涉及你的应用相关内容。

适配 targetSdkVersion30

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

分区存储强制执行

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

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

但是在 Android 10 的时候,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, 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()
} 

3)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: 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")        
    }    
} 

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

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

说来说去,反正应用数据私有化是大势所趋,还是早点适配分区存储。

所有文件访问权限

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

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

来张截图过过瘾:

ia_100000007

电话号码相关权限

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

自定义消息框视图被屏蔽

从 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

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

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

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

从 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

Android 11 添加了在您的应用中支持 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)    
}

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

后台位置信息访问权限

  • 从 Android 10 系统的设备开始,就需要请求后台位置权限(ACCESS_BACKGROUND_LOCATION),并选择Allow all the time (始终允许)才能获得后台位置权限。Android 11 设备上再次加强对后台权限的管理,主要表现在系统对话框上,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要在设置页面选择始终允许才能获得后台位置权限。
  • 在搭载Android 11 系统的设备上,targetVersion 小于 11 的时候,可以前台后台位置权限一起申请,并且对话框提供了文字说明,表示需要随时获取用户位置信息,进入设置选择始终允许即可。但是 targetVersion 为 30 的时候,你必须单独申请后台位置权限,而且要在获取前台权限之后,顺序不能乱。并且无任何提示,需要开发者自己设计提示样式。

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

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

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

执行效果:

ia_100000008

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

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

执行效果:

ia_100000009

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

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

执行无反应

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

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

执行效果:

ia_100000010

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

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

ia_100000011

所以,该怎么适配呢?

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

软件包可见性

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

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

1)元素中加入具体包名

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

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

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

可能还是有人会疑惑,那我的应用是浏览器或者设备管理器咋办呢?我就要获取所有包名啊?放心,Android 11 还引入了 QUERY_ALL_PACKAGES 权限,清单文件中加入即可。

文档访问限制

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

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

无法再使用 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 数据库,会生成安全异常。

自动重置权限

如果应用以 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>

适配 Android 11 手机

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

数据访问审核

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

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

对于工程庞大或者使用较多 SDK 的工程比较适合用上这个功能,让自己应用的私有数据管理更加透明规范,否则对于私有数据的使用和管理并不全面和方便。而且还可以对权限使用添加归因,也就是一个 tag,标志权限用到了什么地方。方便回调的时候知晓哪里使用了私有数据<font face="-apple-system, system-ui, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif" color="#333333"><span style="font-size: 16px; background-color: rgb(255, 255, 255);">。</span></font>

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

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

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

单次授权

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

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

ia_100000012

应用使用情况统计信息

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

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

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

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

  • 用户在系统启动后首次解锁其设备
  • 用户在设备上切换到自己的帐号

JobScheduler API 调用限制调试

JobScheduler任务调度器,可以在设备空闲时做一些任务处理。Android 11 中如果你设置为debug模式(debuggable 清单属性设置为 true),超出速率限制的JobScheduler API调用将返回 RESULT_FAILURE。这个有什么用呢?应该可以帮助我们发现一些性能问题,感兴趣的可以自己试试。

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

非SDK接口限制

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

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

转载请注明:Android开发中文站 » Android11最全适配实践指南–应用端

您必须 登录 才能发表评论!