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