Android 11 ( R )适配
1.存储机制更新
Scoped Storage(分区存储)
应用 targetSdkVersion >= 30
,强制执行分区存储机制。之前在AndroidManifest.xml
中添加
android:requestLegacyExternalStorage="true"
的适配方式已不起作用。
允许使用除MediaStore
API之外的API通过文件路径直接访问共享存储空间中的媒体文件。其中包括:
File
API- 原生库,例如
fopen()
使用原始文件路径直接访问共享存储空间中的媒体文件会重定向到MediaStore API,这次重定向会造成性能影响(随机读写慢一倍左右)。
MANAGE_EXTERNAL_STORAGE
获取外部存储管理权限,如果你的应用是手机管家、文件管理器这类需要访问大量文件的app,可以申请MANAGE_EXTERNAL_STORAGE
权限,将用户引导值系统设置页面开启。代码如下
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
public static void checkStorageManagerPermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
需要注意的是即使你有了MANAGE_EXTERNAL_STORAGE
权限,也无法访问 Android/data/
目录下的文件。
对于MANAGE_EXTERNAL_STORAGE
权限,国内使用没有什么影响,但是在Google Play上需要说明为什么已有的SAF或MediaStore不满足你的应用需求,审核通过才允许上架使用。
存储访问框架 (SAF)变更
Android11对SAF添加以下限制:
- 使用
ACTION_OPEN_DOCUMENT_TREE
或ACTION_OPEN_DOCUMENT
,无法浏览到Android/data/
和Android/obb/
目录及其所有子目录。 - 使用
ACTION_OPEN_DOCUMENT_TREE
无法授权访问存储根目录、Download文件夹。
REQUEST_INSTALL_PACKAGES
在8.0的适配中,安装apk包之前需要申请“安装未知来源应用”的权限。一般来说首次是跳转到授权页面让用户手动开启,然后返回app进行安装。
在Android 11中当用户开启“安装未知来源应用”的权限,app就会被杀死。该行为与强制分区存储有关,因为持有REQUEST_INSTALL_PACKAGES
权限的应用可以访问其他应用的Android/obb
目录。但用户授权权限之后,虽然app会被杀死,但是安装页面依然会弹出。
2.权限变化
单次权限授权
从Android 11开始,每当应用请求与位置信息、麦克风或摄像头相关的权限时,面向用户的权限对话框会包含仅限这一次选项。如果用户在对话框中选择此选项,系统会向应用授予临时的单次授权。
单次权限授权的应用可以再一段时间内访问相关数据,具体时间取决于应用的行为和用户的操作:
- 当应用的 Activity 可见时,应用可以访问相关数据。
- 如果用户将应用转为后台运行,应用可以再短时间内继续访问相关数据。
- 如果在Activity 可见时启动了一项前台服务,并且用户随后将应用转到后台,那么应用可以继续访问相关数据,直到该前台服务停止。
- 如果用户侧小单次授权(例如在系统设置中撤销),无论是否启动了前台服务,应用都无法访问相关数据。与任何权限一样,如果用户撤销了应用的单次授权,应用进程就会终止。
请求位置权限
在Android 10中请求位置权限规则如下
请求
ACCESS_FINE_LOCATION
或ACCESS_COARSE_LOCATION
权限表示在前台时拥有访问设备位置信息的权限。在请求弹框中,选择“始终允许”表示前后台都可以获取位置信息,选择“仅在应用使用过程中允许”只表示拥有前台的权限。
在Android 11中,请求弹框中取消了“始终允许”这一选项,也就是说默认不会授予你后台访问设备位置信息的权限。如果尝试请求ACCESS_BACKGROUND_LOCATION
权限的同时请求任何其他权限,系统会抛出异常,不会向应用授予其中的任一权限。
官方给出的适配建议及原因如下:
建议应用对位置权限执行递增请求,先请求前台位置信息访问权限,再请求后台位置信息访问权限。执行递增请求可以为用户提供更大的控制权和透明度,因为他们可以更好的了解应用中的哪些功能需要后台位置信息访问权限。
总结一下就是两点:
- 先请求前台位置信息访问权限,再请求后台位置信息访问权限。
- 单独请求后台位置信息访问权限,不要与其他权限一同请求。
软件包可见性
软件包可见性是Android 11上提升系统隐形安全性的一个新特性。它的作用是限制app随意获取其他app的信息和安装状态。避免病毒软件、间谍软件利用,引发网络钓鱼、用户安装信息泄露等安全事件。
举一个例子:
private static boolean hasActivity(Context context, Intent intent) {
PackageManager packageManager = context.getPackageManager();
return packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
}
public void test() {
Intent intent = new Intent();
intent.setClassName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareImgUI");
Log.d("hasActivity:", hasActivity(this, intent) + "");
}
hasActivity
方法中通过queryIntentActivities
来判断此页面是否存在。但是在targetSdkVersion >= 30
中,这些三方默认都是不可见的。所以都会返回false。类似方法getInstalledPackages
、getPackageInfo
也受到相应的限制。
解决方法很简单,在AndroidManifest.xml
中添加queries
元素,里面添加需要可见的应用包名。
<manifest package="com.example.app">
<queries>
<!-- 微信 -->
<package android:name="com.tencent.mm" />
<!-- 微博 -->
<package android:name="com.sina.weibo" />
<!-- QQ -->
<package android:name="com.tencent.mobileqq" />
<!-- 支付宝 -->
<package android:name="com.eg.android.AlipayGphone" />
<!-- AlipayHK -->
<package android:name="hk.alipay.wallet" />
</queries>
...
</manifest>
除了直接添加包名的方式外,我们可以按intent和provider来添加:
<manifest package="com.example.app">
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
<provider android:authorities="com.example.settings.files" />
</queries>
...
</manifest>
当然,还有一种简单粗暴的方式,可以直接申请权限QUERY_ALL_PACKAGES
。如果你的应用需要上架Google Play
,那么可能要注意相关政策。为了尊重用户隐私,建议我们的应用按正常工作所需的最小软件包可见性来适配。
有一点需要说明,我们日常使用的startActivity
方法不受系统软件包可见性行为的影响,即使hasActivity
为false,一样可以跳转。如果我们在做跳转前,进行类似hasActivity
的判断,那么会受影响。
最后需要注意的是,使用queries
元素需要Android Gradle
插件版本是4.1及以上,因为旧版本的插件并不兼容此元素,出现合并manifest
的错误。
前台服务类型
Android 10中,在前台服务访问位置信息,需要在对应的service
中添加location
服务类型。
同样的,在Android 11中,在前台服务访问摄像头或麦克风,需要在对应的service
中添加camera
或microphone
服务类型。
<manifest>
...
<service
android:name="MyService"
android:foregroundServiceType="microphone|camera" />
</manifest>
这一限制的变更,使得程序无法在后台启动服务访问摄像头和麦克风。如需使用,只能是前台开启前台服务。除非有如下情况:
- 服务由系统组件启动
- 服务是通过应用小部件启动
- 服务是通过与通知交互启动的
- 服务是
PendingIntent
启动的,它是从另一个可见的应用程序发送过来的。 - 服务由一个应用程序启动,该应用是一个DPC,且在设备所有者模式下运行。
- 服务由一个提供
VoiceInteractionService
的应用启动。 - 服务由一个具有
START_ACTIVITIES_FROM_BACKGROUND
权限的应用启动。
权限自动重置
如果应用以Android 11或更高版本为目标平台并且数月未使用,系统会通过自动重置用户已授予应用的运行时敏感权限来保护用户数据
注意上图中有一个自动重置权限的开关。如果我们的应用有特殊需要,可以引导用户关闭它。示例代码如下:
public void checkAutoRevokePermission(Context context) {
// 判断是否开启
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!context.getPackageManager().isAutoRevokeWhitelisted()) {
// 跳转设置页
Intent intent = new Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.fromParts("package", context.getPackageName(), null));
context.startActivity(intent);
}
}
读取手机号
如果你是通过TelecomManager的getLine1Number方法,或TelephonyManager的getMsisdn方法获取电话号码。那么在Android 11中需要增加READ_PHONE_NUMBERS权限,使用其他方法不受限。
<manifest>
<!-- 如果应用仅在 Android 10及更低版本中使用该权限,可以添加 maxSdkVersion="29" -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
</manifest>
3.其他行为变更
自定义view的Toast
Android 11
为目标平台的应用,从后台发送自定义view的Toast消息系统会进行屏蔽。前台使用不受影响。Toast
相应的setView
和getView
也已经废弃不建议使用。
如果要在后台使用,推荐使用默认的Toast
或Snackbar
替代。
APK签名
Android 11
为目标平台的应用,仅通过v1签名的应用无法在Android 11
的设备上安装或更新。必须使用v2或更高版本进行签名。
同时Android 11
添加了对APK签名方案v4的支持。
AsyncTask
AsyncTask
在Android 11已经不建议使用,建议迁移至kotlin的协程。
此外Handler
未指定Looper
的构造方法也已不建议使用。
状态栏高度
系统为Android 11的手机上targetSdkVersion是30时获取状态栏高度为0,低于30获取值正常。因此需要使用WindowMetrics
适配一下:
public static int getStatusBarHeight(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
WindowInsets windowInsets = windowMetrics.getWindowInsets();
Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout());
return insets.top;
}
....
}
WindowMetrics
是Android 11
新增的类,用于获取窗口边界,同样可以用来获取导航栏高度。