Android 11 变动及适配攻略

Android R 终于开始了Android 11的适配工做。记录一下,供须要的人参考。java

1. 准备工做

老规矩,首先将咱们项目中的 targetSdkVersion 改成 30。或者使用兼容性调试工具,后面我会说到。android

2. 存储机制更新

Scoped Storage(分区存储)

具体适配方法和去年的Android 10 适配攻略中的没有太大区别。shell

不过须要注意的是,应用targetSdkVersion >= 30,强制执行分区存储机制。以前在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"的适配方式已不起做用。c#

还有一个变化:Android 11 容许使用除 MediaStore API 以外的 API 经过文件路径直接访问共享存储空间中的媒体文件。其中包括:api

  • File API。
  • 原生库,例如 fopen()

若是你以前没有适配Android 10,这一点对你来讲是个好消息。Android 10在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"来适配,Android 11上直接使用File API访问媒体文件。不得不说,等等党的胜利?安全

不过,使用原始文件路径直接访问共享存储空间中的媒体文件会重定向到 MediaStore API,此次重定向会形成性能影响(随机读写慢一倍左右)。并且直接使用原始文件路径,并不会比使用 MediaStore API 有更多优点,所以官方强烈建议直接使用 MediaStore API。微信

MANAGE_EXTERNAL_STORAGE

固然还有一种简单粗暴的适配方法,获取外部存储管理权限。若是你的应用是手机管家、文件管理器这类须要访问大量文件的app,能够申请MANAGE_EXTERNAL_STORAGE权限,将用户引导至系统设置页面开启。代码以下:markdown

<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上须要说明为何已有的SAFMediaStore不知足你的应用需求,审核经过才容许上架使用。因此通常状况下,我我的不推荐你为了适配简单,直接申请使用MANAGE_EXTERNAL_STORAGE权限。微信开发

其余细节变动见文档:Android 11 中的存储机制更新

相关api变动及使用推荐郭霖大神的这篇:Android 11新特性,Scoped Storage又有了新花样

存储访问框架 (SAF)变动

Android 11对SAF添加如下限制:

  • 使用 ACTION_OPEN_DOCUMENT_TREEACTION_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会被杀死,可是安装页面依然会弹出

目前对于这一变动我没有发现能够适配处理的方式,详细介绍见:Android 11特性调整:安装外部来源应用须要重启APP


这里补充一下,由于其余应用没法访问应用的Android/data/Android/obb/目录及其全部子目录。因此须要注意保存在这里面的文件是否会被其余程序访问。

好比我在用系统的裁切功能时,由于设置的MediaStore.EXTRA_OUTPUT文件是私有目录下的,致使裁剪后的图片没法正确生成。因此须要针对android 11进行适配:

String fileName = System.currentTimeMillis() + ".jpg";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    // 裁剪没法访问App的私有目录,因此能够保存至公有目录
    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Crop");
    Uri uri = this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
} else {
    ...
}
复制代码

或者保存至Android/media共享文件目录,这样不用适配版本。

String fileName = System.currentTimeMillis() + ".jpg";
File file = new File(this.getExternalMediaDirs()[0].getAbsolutePath() + File.separator + fileName);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
复制代码

固然若是你是本身实现的裁剪功能,那么不受影响。

3.权限变化

单次权限受权

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

单次权限受权

单次权限受权的应用能够在一段时间内访问相关数据,具体时间取决于应用的行为和用户的操做:

  • 当应用的 Activity 可见时,应用能够访问相关数据。
  • 若是用户将应用转为后台运行,应用能够在短期内继续访问相关数据。
  • 若是您在 Activity 可见时启动了一项前台服务,而且用户随后将您的应用转到后台,那么您的应用能够继续访问相关数据,直到该前台服务中止。
  • 若是用户撤消单次受权(例如在系统设置中撤消),不管您是否启动了前台服务,应用都没法访问相关数据。与任何权限同样,若是用户撤消了应用的单次受权,应用进程就会终止。

当用户下次打开应用而且应用中的某项功能请求访问位置信息、麦克风或摄像头时,系统会再次提示用户授予权限。

若是你以前就是使用权限时才请求相关权限,那么这一变动对于你的应用没有影响。

请求位置权限

这部分在Android 10的适配有过调整,当时规则以下:

请求ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION权限表示在前台时拥有访问设备位置信息的权限。在请求弹框中,选择“始终容许”表示先后台均可以获取位置信息,选择“仅在应用使用过程当中容许”只表示拥有前台的权限。

在Android 11中,请求弹框中取消了“始终容许”这一选项。也就是说默认不会授予你后台访问设备位置信息的权限。若是尝试请求ACCESS_BACKGROUND_LOCATION权限的同时请求任何其余权限,系统会抛出异常,不会向应用授予其中的任一权限。

官方给出的适配建议及缘由以下:

建议应用对位置权限执行递增请求,先请求前台位置信息访问权限,再请求后台位置信息访问权限。执行递增请求能够为用户提供更大的控制权和透明度,由于他们能够更好地了解应用中的哪些功能须要后台位置信息访问权限。

总结一下得出两点:

  • 先请求前台位置信息访问权限,再请求后台位置信息访问权限。
  • 单独请求后台位置信息访问权限,不要与其余权限一同请求。

这里还须要注意不一样目标平台应用在Android 11上的表现:

  • Android 10 为目标平台的应用 容许同时访问先后台的位置信息权限,但一样不会有“始终容许”这一选项。
  1. 没有先后台的位置信息权限时:

没有先后台的位置信息权限

  1. 有前台的位置信息权限时:

有前台的位置信息权限

  • Android 11 为目标平台的应用
  1. 没有先后台的位置信息权限时,只能先请求前台的位置信息权限:

在这里插入图片描述

  1. 有前台的位置信息权限,请求后台的位置信息时系统会跳转到下面的设置页面。

定位权限设置页 选择“始终容许”表示具备先后台位置信息访问权限,若是用户拒绝两次应用定位访问请求(直接返回等),后面请求相同权限都会被直接提示请求失败。(这里就须要咱们给用户以引导了)

这里解释一下“拒绝两次”,这是Android 11 上添加的权限对话框的可见性,之前咱们点击了“再也不询问”表示拒绝受权。如今还包含相似上面这种转到系统设置,而后点返回按钮,也算是拒绝受权。固然,用户按返回按钮关闭权限对话框,此操做不算。

总结一下,与Android 10的区别就是将后台权限的申请分离了出来,增长了用户“拒绝”的条件,避免了应用重复请求用户已拒绝的权限。

软件包可见性

软件包可见性是Android 11上提高系统隐私安全性的一个新特性。它的做用是限制app随意获取其余app的信息和安装状态。避免病毒软件、间谍软件利用,引起网络钓鱼、用户安装信息泄露等安全事件。

获取自动可见应用的列表,能够执行命令adb shell dumpsys package queries,找到 forceQueryable 部分。下面是在vivo iqoo手机的执行结果。

Queries:
  system apps queryable: false
  forceQueryable:
    [com.android.BBKCrontab,com.vivo.fingerprint,com.vivo.epm,com.vivo.abe,com.vivo.fingerprintengineer,com.vivo.contentcatcher,com.vivo.floatingball,com.vivo.agent,com.vivo.nightpearl,android,com.wapi.wapicertmanage,com.vivo.vms,co
m.android.providers.settings,com.vivo.upslide,com.vivo.assistant,com.vivo.vivokaraoke,com.vivo.fingerprintui,com.android.wallpaperbackup,com.bbk.facewake,com.vivo.faceunlock,com.vivo.doubleinstance,com.vivo.audiofx,com.iqoo.powersav
ing,com.bbk.SuperPowerSave,com.vivo.vibrator4d,com.vivo.smartunlock,com.vivo.globalanimation,com.vivo.appfilter,com.vivo.voicewakeup,com.vivo.minscreen,com.android.bbklog,com.mobile.cos.iroaming,com.vivo.networkstate,com.vivo.daemon
Service,com.vivo.smartshot,com.vivo.vtouch,com.android.networkstack.tethering.inprocess,com.android.localtransport,com.vivo.pem,com.vivo.wifiengineermode,com.android.server.telecom,com.vivo.gamecube,com.vivo.aiengine,com.vivo.multin
lp,com.vivo.smartmultiwindow,com.vivo.permissionmanager,com.qti.diagservices,com.vivo.bsptest,com.qti.snapdragon.qdcm_ff,com.vivo.dr,com.vivo.sps,com.android.dynsystem,com.vivo.setupwizard,com.vivo.gamewatch,com.android.keychain,com
.vivo.faceui,com.android.networkstack.inprocess,com.android.location.fused,com.android.inputdevices,com.android.settings,com.iqoo.engineermode,com.vivo.fuelsummary]
    [com.qualcomm.uimremoteserver,com.vivo.devicereg,com.qti.qualcomm.deviceinfo,com.volte.config,com.android.mms.service,com.android.ons,com.qualcomm.qcrilmsgtunnel,com.vivo.sim.contacts,com.qualcomm.qti.uimGbaApp,com.qualcomm.qti.
modemtestmode,com.android.stk,com.android.vendors.bridge.softsim,com.qualcomm.uimremoteclient,com.qti.qualcomm.datastatusnotification,com.qualcomm.qti.uim,com.android.phone,com.qualcomm.qti.dynamicddsservice,com.qualcomm.qti.telepho
nyservice,com.android.cellbroadcastservice,com.android.providers.telephony,com.qti.dpmserviceapp,com.android.incallui]
    [com.android.vivo.tws.vivotws,com.android.bluetooth]
    com.android.nfc
    com.android.se
    com.android.networkstack.permissionconfig
    com.android.shell
    com.android.providers.media.module
    com.android.wifi.resources.overlay.common
    com.android.theme.icon_pack.filled.themepicker
    com.android.theme.icon_pack.circular.themepicker
    com.android.server.telecom.overlay.common
......
复制代码

能够看到都是系统应用包名,因此咱们的三方应用默认是不可见的。此项变动影响比较多的是分享支付一类须要与其余应用交互的功能。下面举一个简单的例子:

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。相似方法getInstalledPackagesgetPackageInfo也受到相应的限制。

解决方法很简单,在AndroidManifest.xml 中添加queries元素,里面添加须要可见的应用包名。

<manifest package="com.example.app">
    <queries>
        <package android:name="com.tencent.mm" /> <- 指定微信包名
    </queries>
    ...
</manifest>
复制代码

我在适配中用到的还有下面的包名,咱们能够按需添加:

<queries>
    <!-- 微博 -->
    <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>
复制代码

除了直接添加包名的方式外,咱们能够按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中添加cameramicrophone 服务类型。

<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);
    }
}
复制代码

SYSTEM_ALERT_WINDOW权限

这部分我在适配中没有用到,直接照搬文档:

在 Android 11 中,系统会根据请求自动向某些类型的应用授予 SYSTEM_ALERT_WINDOW 权限:

  • 系统会自动向具备 ROLE_CALL_SCREENING 且请求 SYSTEM_ALERT_WINDOW 的全部应用授予该权限。若是应用失去 ROLE_CALL_SCREENING,就会失去该权限。

  • 系统会自动向经过 MediaProjection 截取屏幕且请求 SYSTEM_ALERT_WINDOW 的全部应用授予该权限,除非用户已明确拒绝向应用授予该权限。当应用中止截取屏幕时,就会失去该权限。此用例主要用于游戏直播应用。

这些应用无需发送 ACTION_MANAGE_OVERLAY_PERMISSION 以获取 SYSTEM_ALERT_WINDOW 权限,它们只需直接请求 SYSTEM_ALERT_WINDOW 便可。

MANAGE_OVERLAY_PERMISSION intent 始终会将用户转至系统权限屏幕

从 Android 11 开始,ACTION_MANAGE_OVERLAY_PERMISSION intent 始终会将用户转至顶级设置屏幕,用户可在其中授予或撤消应用的 SYSTEM_ALERT_WINDOW 权限。intent 中的任何 package: 数据都会被忽略。

在更低版本的 Android 中,ACTION_MANAGE_OVERLAY_PERMISSION intent 能够指定一个软件包,它会将用户转至应用专用屏幕以管理权限。从 Android 11 开始将再也不支持此功能,而是必须由用户先选择要授予或撤消哪些应用的权限。此变动可让权限的授予更有目的性,从而达到保护用户的目的。

读取手机号

若是你是经过TelecomManagergetLine1Number方法,或TelephonyManagergetMsisdn方法获取电话号码。那么在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>
复制代码

4.其余行为变动

自定义view的Toast

Android 11 为目标平台的应用,从后台发送自定义view的Toast消息系统会进行屏蔽。前台使用不受影响Toast相应的setViewgetView也已经废弃不建议使用。

若是要在后台使用,推荐使用默认的toast或Snackbar替代。

APK签名

Android 11 为目标平台的应用,仅经过v1 签名的应用没法在Android 11的设备上安装或更新。必须使用v2或更高版本进行签名。

同时Android 11 添加了对 APK 签名方案 v4 的支持。

AsyncTask

AsyncTask在Android 11已经不建议使用,建议迁移至kotlin的协程。

此外Handler未指定Looper的构造方法也已不建议使用。 Handler源码

建议明确指定Looper

private Handler handler = new Handler(Looper.myLooper());
// 或
private Handler handler = new Handler(Looper.getMainLooper());
复制代码

状态栏高度

发现系统为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新增的类,用于获取窗口边界,一样能够用来获取导航栏高度。

5.新增工具

兼容性调试工具

以往咱们作适配的时候,须要先将咱们项目中的 targetSdkVersion 修改成对应版本。这就致使你适配过程当中有可能受到其余变动的影响,而这个新增的兼容性调试工具可让你在不升级targetSdkVersion的状况下,针对每项变动逐个开启适配。

使用方法:

  • 开发者选项中找到应用兼容性变动选项。
  • 点击进入找到你须要调试的应用
  • 在变动列表中,找到想要开启或关闭的变动,而后点击相应的开关。

在这里插入图片描述 上面第一行DEFAULT_SCOPED_STORAGE就是启用分区储存,这些常量详细的含义见:Android 11 变动列表

对于兼容性调试工具详细的使用方法见:兼容性框架工具,这里限于篇幅就不展开说了。

无线调试

Android 11的开发者选项中添加了一个无线调试的功能。相似于链接蓝牙耳机功能,能够无需USB链接线进行平常开发调试工做。(区别于之前的Android WIFI ADB,这个是真无线,哈哈)

无线调试

使用方法:

  • 开发者选项中找到无线调试并打开。
  • 首次配对需点击“使用配对码配对设备”
  • 运行 adb pair ipaddr:port后输入配对码进行链接。

注意事项:

  • 保持电脑和手机在一个网络。
  • Platform Tools 版本需大于30.0。可以使用adb --version查看。

在这里插入图片描述

不过我本身体验下来,感受链接不是很稳定,不知是AS的问题仍是手机问题。同时锁屏后也会断开链接,体验不是很好。。。期待后续的优化吧。


本篇内容有点多。总结一下,Android 11在权限上的变动比较多,但若是你一直遵照申请权限相关的最佳作法,那么基本上不须要额外的适配工做。

最后强调一下,对于单次受权,权限对话框的可见性,SYSTEM_ALERT_WINDOW 权限,安装apk这些变动只要在Android 11上就会生效,不论你是否适配Android 11。对于其余变动和API(相机、5G、瀑布屏、键盘等),由于我暂时没有遇到,也就没有列出,有须要的能够点击文末的官方文档连接查看。

截止发这篇博客时,我手机上只发现哔哩哔哩已经适配了Android 11。大多数停留在2八、29,更有甚者还在26(Android 8.0 国内上架的最低适配标准)。

因此我顺便附上以前写的Android 九、10的适配攻略:

在这里插入图片描述

可能本篇你暂时也用不上,你能够不用,可是不能没有。点赞收藏一波不过度吧~~

参考