拖不得了,Android11 真的要来了,最全适配实践指南奉上
为了在保证用户隐私的同时可以更轻松地访问媒体,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,????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 的集合,获取用户的同意后,就可以进行操作了。
2)直接文件路径和原生库访问文件
没错!Android11 又恢复了使用直接文件路径访问访问媒体文件!哈哈,这样就方便多了。也就是除了 MediaStore API 之外还有两种方式可以访问媒体文件:
File API。
原生库,例如 fopen()。
那 Android10 咋办呢??要不就用 MediaStore,要不就直接把分区存储关了吧(requestLegacyExternalStorage=true)
所有文件访问权限 ?
虽然说了这么多,但是还有些应用就要访问所有文件,比如杀毒软件,文件管理器。
放心,有办法!MANAGE_EXTERNAL_STORAGE 这不来了吗。这个权限就是用来获取所有文件的管理权限。??:
<uses-permission?android:name="android.permission.MANAGE_EXTERNAL_STORAGE"?/>val?intent?=?Intent()intent.action=?Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSIONstartActivity(intent)//判断是否获取 MANAGE_EXTERNAL_STORAGE 权限:val?isHasStoragePermission=?Environment.isExternalStorageManager()
来张截图过过瘾:
![](https://static001.geekbang.org/infoq/69/69c9f3493050686ea5e5edfcfef8684a.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
电话号码相关权限 ?
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_CAPTUREandroid.media.action.IMAGE_CAPTUREandroid.media.action.IMAGE_CAPTURE_SECURE
也就是说,如果我调用 intent 唤起照相机,使用 VIDEO_CAPTURE 的 action,只有系统的相机能够响应,而第三方的相机应用不会响应了。
val?intent=Intent()intent.action=android.provider.MediaStore.ACTION_IMAGE_CAPTUREstartActivity(intent)//无法唤起第三方相机了,只能唤起系统相机
这点对普通的相机应用还是有点打击的,官方给的建议是如果要使用特定的第三方相机应用来代表其捕获图片或视频,可以通过为 intent 设置软件包名称或组件来使这些 intent 变得明确。
5G ?
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,则将连接视为不按流量计费。
后台位置信息访问权限 ?
在搭载 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 (始终允许)才能获得后台位置权限,看图:
![](https://static001.geekbang.org/infoq/49/4983c38717368e3f8fe61578190fa14b.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
软件包可见性 ?
Android 11 更改了应用查询用户已在设备上安装的其他应用以及与之交互的方式。使用新的 ?元素,应用可以定义一组自身可访问的其他应用。通过告知系统应向您的应用显示哪些其他应用,此元素有助于鼓励最小权限原则。此外,此元素还可帮助 Google Play 等应用商店评估应用为用户提供的隐私权和安全性。
就是说,Android11 中,如果你想去获取其他应用的信息,比如包名,名称等等,不能直接获取了,必须在清单文件中添加<queries>元素,告知系统你要获取哪些应用信息或者哪一类应用。
比如我这段查询应用信息的代码:
val?pm?=?this.packageManagerval?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>
2)元素中加入固定过滤的 intent
<manifest?package="com.example.game">????<queries>????????<intent>????????????<action?android:name="android.intent.action.SEND"?/>????????????<data?android:mimeType="image/jpeg"?/>????????</intent>????</queries></manifest>
可能还是有人会疑惑,那我的应用是浏览器或者设备管理器咋办呢?我就要获取所有包名啊?放心,Android11 还引入了 QUERY_ALL_PACKAGES 权限,清单文件中加入即可。
但是 Google Play 可不一定能滥用哦,它为需要 QUERY_ALL_PACKAGES 权限的应用会提供相关指南,但是还没出来,具体要看后面的消息了。
至于国内应用市场。。。(希望能有个应用市场一统天下好好管理这混乱的市场吧!)
文档访问限制
为让开发者有时间进行测试,以下与存储访问框架 (SAF) 相关的变更只有在应用以 Android 11 为目标平台时才会生效。
上文存储的时候说过可以通过 SAF(存储访问框架--Storage Access Framework)来访问公共目录,但是 Android11 再次升级,部分目录和文件不能访问了,具体如下:
无法再使用 ACTION_OPEN_DOCUMENT_TREE intent 操作请求访问以下目录:
内部存储卷的根目录。
设备制造商认为可靠的各个 SD 卡卷的根目录,无论该卡是模拟卡还是可移除的卡。可靠的卷是指应用在大多数情况下可以成功访问的卷。
Download 目录。
无法再使用 ACTION_OPEN_DOCUMENT_TREE 或 ACTION_OPEN_DOCUMENT intent 操作请求用户从以下目录中选择单独的文件:
Android/data/ 目录及其所有子目录。
Android/obb/ 目录及其所有子目录。
在元数据文件中声明“无障碍”按钮使用情况
从 Android 11 开始,您的无障碍服务无法在运行时声明与系统的“无障碍”按钮的关联。如果您将 AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON 附加到 AccessibilityServiceInfo 对象的 flags 属性,框架就不会将“无障碍”按钮回调事件传递给您的服务。
做过无障碍辅助功能的应该都知道 AccessibilityServiceInfo 要设置 flag 为 FLAG_REQUEST_ACCESSIBILITY_BUTTON,getAccessibilityButtonController 方法获取辅助功能按钮控制器,并且可用于查询辅助功能按钮的状态并注册监听器以进行交互和辅助功能按钮的状态更改。
但是,Android 11 开始,这样写不能获取辅助按钮回调事件了,得换成另外一种写法。在元数据文件(通常为 res/raw/accessibilityservice.xml)中使用 flagRequestAccessibilityButton 标记声明您的无障碍服务与“无障碍”按钮的关联。
设备到设备文件传输
如果您的应用以 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>
3 适配 Android11 手机
此模块的修改内容针对所有项目在 Android11 手机上存在的改动,与 targetSdkVersion 无关。
数据访问审核 ?
为了让应用及其依赖项访问用户私密数据的过程更加透明,Android 11 引入了数据访问审核功能。借助此流程得出的见解,您可以更好地识别和纠正可能出现的意外数据访问。
哪些范畴属于用户私密数据呢?
其实就是危险权限的调用,所以这个功能就是提供了可以监听危险权限调用的监听。
主要涉及到的方法是 AppOpsManager.OnOpNotedCallback。无论是应用本身,还是依赖库或者 SDK 中的代码,只要访问到私密数据(危险权限),都会回调给我们。
对于工程庞大或者使用较多 SDK 的工程比较适合用上这个功能,让自己应用的私有数据管理更加透明规范,否则对于私有数据的使用和管理并不全面和方便。而且还可以对权限使用添加归因,也就是一个 tag,标志权限用到了什么地方。方便回调的时候知晓哪里使用了私有数据。
??来:
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:?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.java)?as?AppOpsManager????appOpsManager.setOnOpNotedCallback(mainExecutor,?appOpsCallback)????btn1.setOnClickListener?{????????getLocation()????}}fun?getLocation()?{????val?locationManager?=?attributionContext.getSystemService(????????????LocationManager::class.java)?as?LocationManager????if?(!checkPermission())?{????????retu
rn????}????val?location:?Location??=?locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)????if?(location?!=?null)?{????????showToast("${location.latitude}")????}}
该例子主要展示了一个获取位置信息的功能,如果调用到 getLocation 方法,就会触发 onNoted 回调,回调信息包括危险权限 code 以及归因。
其中 OnOpNotedCallback 一共三个回调方法:
onNoted 正常情况下都会回调到该方法
onAsyncNoted 如果数据访问并非发生在应用调用 API 期间,就会调用 onAsyncNoted(),比如一些监听器的回调。
onSelfNoted 在极少数情况下,如果应用将自身的 UID 传递到 noteOp(),需要调用 onSelfNoted()。
最后点击按钮,看下回调的结果日志:
Private?data?accessed.?Operation:?android:coarse_location?????Attribution?Tag:shareLocation????Stack?Trace:????[Ljava.lang.StackTraceElement;@14f5a16
可以看到权限代码:android:coarse_location 以及归因 shareLocation
评论