应用 targetSdkVersion >= 30,强制执行分区存储机制。之前在AndroidManifest.xml中添加
android:requestLegacyExternalStorage="true"的适配方式已不起作用。
允许使用除MediaStore API之外的API通过文件路径直接访问共享存储空间中的媒体文件。其中包括:
File APIfopen()获取外部存储管理权限,如果你的应用是手机管家、文件管理器这类需要访问大量文件的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不满足你的应用需求,审核通过才允许上架使用。
Android11对SAF添加以下限制:
ACTION_OPEN_DOCUMENT_TREE 或 ACTION_OPEN_DOCUMENT,无法浏览到 Android/data/ 和 Android/obb/ 目录及其所有子目录。ACTION_OPEN_DOCUMENT_TREE无法授权访问存储根目录、Download文件夹。在8.0的适配中,安装apk包之前需要申请“安装未知来源应用”的权限。一般来说首次是跳转到授权页面让用户手动开启,然后返回app进行安装。
在Android 11中当用户开启“安装未知来源应用”的权限,app就会被杀死。该行为与强制分区存储有关,因为持有REQUEST_INSTALL_PACKAGES权限的应用可以访问其他应用的Android/obb目录。但用户授权权限之后,虽然app会被杀死,但是安装页面依然会弹出。
从Android 11开始,每当应用请求与位置信息、麦克风或摄像头相关的权限时,面向用户的权限对话框会包含仅限这一次选项。如果用户在对话框中选择此选项,系统会向应用授予临时的单次授权。
单次权限授权的应用可以再一段时间内访问相关数据,具体时间取决于应用的行为和用户的操作:
在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启动的,它是从另一个可见的应用程序发送过来的。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>
Android 11 为目标平台的应用,从后台发送自定义view的Toast消息系统会进行屏蔽。前台使用不受影响。Toast相应的setView和getView也已经废弃不建议使用。
如果要在后台使用,推荐使用默认的Toast或Snackbar替代。
Android 11为目标平台的应用,仅通过v1签名的应用无法在Android 11的设备上安装或更新。必须使用v2或更高版本进行签名。
同时Android 11添加了对APK签名方案v4的支持。
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新增的类,用于获取窗口边界,同样可以用来获取导航栏高度。