Android 12(S)适配指北

惯例先贴上官方文档:
developer.android.com/about/versi…

设置 Android 12 SDK

如需使用 Android 12 API 进行开发并在您的应用中测试 Android 12 行为变更,您需要设置 Android 12 SDK。请按照此页面中的说明在 Android Studio 中设置 Android 12 SDK,并在 Android 12 上构建和运行您的应用。

获取 Android Studio

Android 12 SDK 包含与某些旧版 Android Studio 不兼容的变更。为了获得 Android 12 SDK 的最佳开发体验,请使用 Android Studio Arctic Fox | 2020.3.1 或更高版本。

获取 Android Studio

安装 SDK

在 Android Studio 中,您可以按如下方式安装 Android 12 SDK:

  1. 依次点击 Tools > SDK Manager
  2. 在 SDK Platforms 标签页中,选择 Android 12
  3. 在 SDK Tools 标签页中,选择 Android SDK Build-Tools 31
  4. 点击 OK 安装 SDK。

更新应用的构建配置

如需访问 Android 12 API 并测试应用与 Android 12 的兼容性,请打开模块级 build.gradle 或 build.gradle.kts 文件,并使用 Android 12 的值更新 compileSdkVersion 和 targetSdkVersion

android {
    compileSdkVersion 31

    defaultConfig {
        targetSdkVersion 31
    }
}
复制代码

注意:如果您尚未准备好完全支持 Android 12,您仍可使用可调试的应用、Android 12 设备和兼容性框架来执行应用兼容性测试,而无需更改应用的 compileSdkVersion 或 targetSdkVersion

如需了解哪些变更可能会影响您,以及如何在应用中测试这些变更,请参阅以下主题:

如需详细了解 Android 12 中提供的新 API 和功能,请参阅 Android 12 功能

安全和隐私设置

一、更安全的组件导出 android:exported

<activity
    android:name=".TestActivity"
    android:exported="true">
    <intent-filter>
        ......
    </intent-filter>
</activity>
复制代码

主要是设置activityservicereceiver 组件是否可由其他应用的组件启动。 true表示可以,false表示不可以。

一般情况下,如果使用了intent-filter,则exported默认为true,不能将exported设置成false,这样在activity被调用时,系统会抛出ActivityNotFoundException异常;

相反,如果没有intent-fileter,则exported默认为false,就不应该将exported设置成true,这样可能会在安全扫描时被定义为安全漏洞。

如果应用组件包含 LAUNCHER 类别,请将 android:exported 设置为 true。在大多数其他情况下,请将 android:exported 设置为 false

Android 12 中如果未显示声明,App将无法安装,错误日志如下:

Installation did not succeed.
The application could not be installed: INSTALL_FAILED_VERIFICATION_FAILURE
List of apks:
[0] '.../build/outputs/apk/debug/app-debug.apk'
Installation failed due to: 'null'
复制代码

这时候你可能会选择去 AndroidManifest 一个一个手动修改,但是如果你使用的 SDK 或者第三方库没有支持怎么办?或者你想要打出不同 target 平台的包?这时候下面这段 gradle 脚本可以给你省心:

com.android.tools.build:gradle:3.4.3 以下版本

/**
 * 修改 Android 12 因为 exported 的构建问题
 */
android.applicationVariants.all { variant ->
    variant.outputs.all { output ->
        output.processResources.doFirst { pm ->
            String manifestPath = output.processResources.manifestFile
            def manifestFile = new File(manifestPath)
            def xml = new XmlParser(false, true).parse(manifestFile)
            def exportedTag = "android:exported"
            ///指定 space
            def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')

            def nodes = xml.application[0].'*'.findAll {
                //挑选要修改的节点,没有指定的 exported 的才需要增加
                (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(androidSpace.exported) == null

            }
            ///添加 exported,默认 false
            nodes.each {
                def isMain = false
                it.each {
                    if (it.name() == "intent-filter") {
                        it.each {
                            if (it.name() == "action") {
                                if (it.attributes().get(androidSpace.name) == "android.intent.action.MAIN") {
                                    isMain = true
                                    println("......................MAIN FOUND......................")
                                }
                            }
                        }
                    }
                }
                it.attributes().put(exportedTag, "${isMain}")
            }

            PrintWriter pw = new PrintWriter(manifestFile)
            pw.write(groovy.xml.XmlUtil.serialize(xml))
            pw.close()
        }
    }

}
复制代码

com.android.tools.build:gradle:4.1.0 以上版本

/**
 * 修改 Android 12 因为 exported 的构建问题
 */

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        def processManifest = output.getProcessManifestProvider().get()
        processManifest.doLast { task ->
            def outputDir = task.multiApkManifestOutputDirectory
            File outputDirectory
            if (outputDir instanceof File) {
                outputDirectory = outputDir
            } else {
                outputDirectory = outputDir.get().asFile
            }
            File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
            println("----------- ${manifestOutFile} ----------- ")

            if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
                def manifestFile = manifestOutFile
                ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
                def xml = new XmlParser(false, false).parse(manifestFile)
                def exportedTag = "android:exported"
                def nameTag = "android:name"
                ///指定 space
                //def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')

                def nodes = xml.application[0].'*'.findAll {
                    //挑选要修改的节点,没有指定的 exported 的才需要增加
                    //如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
                    (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null

                }
                ///添加 exported,默认 false
                nodes.each {
                    def isMain = false
                    it.each {
                        if (it.name() == "intent-filter") {
                            it.each {
                                if (it.name() == "action") {
                                    //如果 nameTag 拿不到可以尝试 it.attribute(androidSpace.name)
                                    if (it.attributes().get(nameTag) == "android.intent.action.MAIN") {
                                        isMain = true
                                        println("......................MAIN FOUND......................")
                                    }
                                }
                            }
                        }
                    }
                    it.attributes().put(exportedTag, "${isMain}")
                }

                PrintWriter pw = new PrintWriter(manifestFile)
                pw.write(groovy.xml.XmlUtil.serialize(xml))
                pw.close()

            }

        }
    }
}
复制代码

这段脚本你可以直接放到 app/build.gradle/android{} 下执行,也可以单独放到一个 gradle 文件之后 apply 引入,
它的作用就是: 在打包过程中检索所有没有设置 exported 的组件,给他们动态配置上 exported
这里有个特殊需要注意的是,因为启动 Activity 默认就是需要被 Launcher 打开的,所以 "android.intent.action.MAIN" 需要 exported 设置为 true 。(PS:应该是用 LAUNCHER 类别,这里故意用 MAIN

如果有需要,还可以自己增加判断设置了 "intent-filter" 的才配置 exported
如果4.1.0以上版本的这段脚本抛异常,可尝试当前App的Manifest下做手动配置,脚本主要是针对第三方aar依赖编译时动态修改

  • 为什么在 Android 12 上需要显示声明 android:exported 属性?

正因为 android:exported 的属性的默认值的问题,Twicca App 发生过一次安全性问题,因为另一个没有访问 SD 卡或网络权限的 App,可以通过 Twicca App 将存储在 SD 卡上的图片或电影上传到 Twicca 用户的 Twitter 账户上的社交网络上。

产生问题的代码如下所示:

<activity android:configChanges="keyboard|keyboardHidden|orientation" android:name=".media.yfrog.YfrogUploadDialog" android:theme="@style/Vulnerable.Dialog" android:windowSoftInputMode="stateAlwaysHidden">           
    <intent-filter android:icon="@drawable/yfrog_icon" android:label="@string/YFROG">
        <action android:name="jp.co.vulnerable.ACTION_UPLOAD" />                
        <category android:name="android.intent.category.DEFAULT" />                
        <data android:mimeType="image/*" />                
        <data android:mimeType="video/*" />            
    </intent-filter>        
</activity>
复制代码

因为添加了 intent-filter 所以 android:exported 的属性的默认值为 true,因此可以接受来自其他 App 的访问,进而造成了上述问题(通过 Twicca App 将存储在 SD 卡上的图片或电影上传到 Twicca 用户的 Twitter 账户上的社交网络上),而解决方案有两个:

  • 方案一:添加 android:exported="false" 属性
<activity 
    android:exported="false"
    android:configChanges="keyboard|keyboardHidden|orientation"
    android:name=".media.yfrog.YfrogUploadDialog"
    android:theme="@style/VulnerableTheme.Dialog"
    android:windowSoftInputMode="stateAlwaysHidden" >    
</activity>
复制代码
  • 方案二: Twicca App 没有使用方式一,而是检查调用者的包名是否与自身的包名相同
public void onCreate(Bundle arg5) {
    super.onCreate(arg5);
    ...
    ComponentName v0 = this.getCallingActivity();
    if(v0 == null) {
        this.finish();
    } else if(!jp.r246.twicca.equals(v0.getPackageName())) {
        this.finish();
        } else {
            this.a = this.getIntent().getData();
            if(this.a == null) {
                this.finish();
            }
            ...
        }
    }
}
复制代码

这种方案也是可行的,因为在一台设备上,不可能会出现两个包名相同的应用,更多详细的信息可以前往查看 Restrict access to sensitive activities

这仅仅是关于 activity 的安全漏洞的其中一个,在不同的场景下利用这些漏洞做的事情也可能不一样。当然还有 servicereceiver 组件也都是一样,存在安全性问题。

二、PendingIntent

在Android 12 中创建 PendingIntent 需要显示的声明是否可变。

  • 可变PendingIntent.FLAG_IMMUTABLE 
  • 不可变PendingIntent.FLAG_MUTABLE 

如果应用试图在不设置任何可变标志的情况下创建 PendingIntent 对象,系统会抛出 IllegalArgumentException 异常。错误日志如下:

PACKAGE_NAME: Targeting S+ (version 10000 and above) requires that one of \
FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.

Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if \
some functionality depends on the PendingIntent being mutable, e.g. if \
it needs to be used with inline replies or bubbles.
复制代码
  • 为什么在 Android 12 上需要显示指定 PendingIntent 的可变性?

在 Adnroid 12 之前,默认创建一个 PendingIntent 它是可变的,因此其他恶意应用程序可能会拦截,重定向或修改此 Intent。(但是是有条件限制的)

一个 PendingIntent 是一个可以给另一个应用程序使用的 Intent,PendingIntent 接收待处理意图的应用程序可以使用与产生待处理意图的应用程序相同的权限和身份执行待处理意图中指定的操作。

因此,创建待处理意图时必须小心,为了安全性 Google 在 Android 12 中需要开发者自己来指定 PendingIntent 的可变性。

更多关于 PendingIntent 安全性介绍,可以前往查看 Always pass explicit intents to a PendingIntent

三、adb 备份限制

Android 开发者都应该知道这个命令 adb backup , 它可以备份应用的数据,在 Android 12 中,为了保护私有应用数据,用户运行 adb backup 命令时,从设备导出的任何其他系统数据都不包含应用数据。

如果你在测试和开发过程中需要使用 adb backup 来备份应用数据,你可以在 AndroidManifest 中将 android:debuggable 设置为 true 来导出应用数据。

<application
    android:name=".App"
    android:debuggable="true"
    ....../>
复制代码

注意:在发布应用前务必将 android:debuggable 设置为 false。

为什么在 Android 12 上限制了 adb backup 命令的默认行为

因为这个存在严重的安全问题,当初 Google 为了提供 App 数据备份和恢复功能,可以在 AndroidManifest 中添加 android:allowBackup 属性,默认值为 true, 当你创建一个应用的时候,会默认添加这个属性,如下所示。

<application
    android:name=".App"
    android:allowBackup="true"
    ....../>
复制代码

android:allowBackup="true" 时,用户可以通过 adb backupadb restore 命令对应用数据进行备份和恢复,也就是说可以在其他的 Android 手机上安装同一个应用,通过如上命令恢复用户的数据。

为了安全起见,我们在发布出去的 Apk 中一定要将 android:allowBackup 属性设置为 false 来关闭应用程序的备份和恢复功能,以免造成信息泄露。

国民级应用 XX 信, 在曾今发出的版本中 allowBackup 的属性值是 true,被其他逆向开发者利用之后,现在的版本中这个值已经修改为 false了,有兴趣的小伙们可以反编译看看。

Tips:
如果在清单文件设置了 android:allowBackup = "false"后发现编译报错

Attribute application@allowBackup value=(false) from AndroidManifest.xml:22:9-36
    is also present at [*****] AndroidManifest.xml:12:9-35 value=(true).
    Suggestion: add 'tools:replace="android:allowBackup"' to <application> element at AndroidManifest.xml:20:5-105:19 to override.
复制代码

可增加以下代码 tools:replace = "android:allowBackup",作用是把当前项目所有依赖的三方SDK 不管他的allowBackup 是什么,全部替换成 android:allowBackup = "false",保持跟当前项目的配置一致。

有想具体了解 Backup 功能的同学,可以移步这里了解 juejin.cn/post/695533…

四、安全和隐私设置

4.1、大致位置

使用 TargetSDK 为 31 的 App,用户可以请求应用只能访问大致位置信息

如果 App 请求 ACCESS_COARSE_LOCATION 但未请求 ACCESS_FINE_LOCATION 那么不会有任何影响。

TargetSDK 为 31 的 App 请求 ACCESS_FINE_LOCATION 运行时权限,还必须请求 ACCESS_COARSE_LOCATION 权限。当 App 同时请求这两个权限时,系统权限对话框将为用户提供以下新选项:

4.2、SameSite Cookie

Cookie 的 SameSite 属性决定了它是可以与任何请求一起发送,还是只能与同站点请求一起发送。

  • 没有 SameSite 属性的 Cookie 被视为 SameSite=Lax
  • 带有 SameSite=None 的 Cookie 还必须指定 Secure 属性,这意味着它们需要安全的上下文,需要通过 HTTPS 发送。
  • 站点的 HTTP 版本和 HTTPS 版本之间的链接现在被视为跨站点请求,因此除非将 Cookie 正确标记为 SameSite=None; Secure,否则 Cookie 不会被发送。

在 WebView devtools 中 切换界面标志 webview-enable-modern-cookie-same-site,可以在测试设备上手动启用 SameSite 行为。

五、应用休眠

Android 12 在 Android 11 中引入的自动重置权限行为 的基础上进行了扩展。

如果 TargetSDK 为 31 的 App 用户几个月不打开,则系统会自动重置授予的所有权限并将App 置于休眠状态。

更多可以查阅:developer.android.com/topic/perfo…

六、SplashScreen

Android 12 新增加了 SplashScreen 的 API,它包括启动时的进入应用的动作、显示应用的图标画面,以及展示应用本身的过渡效果。

1ebe850346cf4f31a250cc6fe6601500_tplv-k3u1fbpfcp-watermark.webp 它大概由如下 4 个部分组成,这里需要注意:

  • 1 最好是矢量的可绘制对象,当然它可以是静态或动画形式。
  • 2 是可选的,也就是图标的背景。
  • 与自适应图标一样,前景的三分之一被遮盖 (3)。
  • 4 就是窗口背景。

启动画面动画机制由进入动画和退出动画组成。

  • 进入动画由系统视图到启动画面组成,这由系统控制且不可自定义。
  • 退出动画由隐藏启动画面的动画运行组成。如果要对其进行自定义,可以通过 SplashScreenView 自定义。

官方资料: developer.android.com/guide/topic… ,这里主要介绍下如何适配和使用的问题。

首先不管你的 TargetSDK 什么版本,当你运行到 Android 12 的手机上时,所有的 App 都会增加 SplashScreen 的功能

如果你什么都不做,那 App 的 Launcher 图标会变成 SplashScreen 界面的那个图标,而对应的原主题下 windowBackground 属性指定的颜色,就会成为 SplashScreen 界面的背景颜色。这个启动效果在所有应用的冷启动和热启动期间会出现。

其实不适配好像也没啥问题。

关于如何迁移和使用 SplashScreen 可以查阅官方详细文档: developer.android.com/guide/topic…

另外还可以参考 《Jetpack新成员SplashScreen:打造全新的App启动画面》 这篇文章,文章详细介绍了如果使用官方的 Jetpack 库来让这个效果适配到更低的 Target 平台。

而正常情况下我们可以做的就是:

  • 1、升级 compileSdkVersion 31targetSdkVersion 31 & buildToolsVersion '31.0.0'
  • 2、 添加依赖 implementation "androidx.core:core-splashscreen:1.0.0-alpha02"
  • 3、增加 values-v31 的目录
  • 4、添加 styles.xml 对应的主题,例如:
<resources>
    <style name="LaunchTheme" parent="Theme.SplashScreen">
        <item name="windowSplashScreenBackground">@color/splashScreenBackground</item>
        <!--<item name="windowSplashScreenAnimatedIcon">@drawable/splash</item>-->
        <item name="windowSplashScreenAnimationDuration">500</item>
        <item name="postSplashScreenTheme">@style/AppTheme</item>
    </style>
</resources>
复制代码
复制代码
  • 5、给你的启动 Activity 添加这个主题,不同目录下使用不同主题来达到适配效果。

七、通知中心

Android 12 更改了可以完全自定义通知外观和行为,以前自定义通知能够使用整个通知区域并提供自己的布局和样式,现在它行为变了

使用 TargetSDK 为 31 的 App,包含自定义内容视图的通知将不再使用完整通知区域;而是使用系统标准模板。

此模板可确保自定义通知在所有状态下都与其他通知长得一模一样,例如在收起状态下的通知图标和展开功能,以及在展开状态下的通知图标、应用名称和收起功能,与 Notification.DecoratedCustomViewStyle 的行为几乎完全相同。

八、Android App Links验证

Android App Links 是一种特殊类型的 DeepLink ,用于让 Web 直接在 Android 应用中打开相应对应 App 内容而无需用户选择应用。使用它需要执行以下步骤:

如何使用可查阅:developer.android.com/training/ap…

使用 TargetSDK 为 31 的 App,系统对 Android App Links 的验证方式进行了一些调整,这些调整会提升应用链接的可靠性。

如果你的 App 是依靠 Android App Links 验证在应用中打开网页链接,那么在为 Android App Links 验证添加 intent 过滤器时,请确保使用正确的格式,尤其需要注意的是确保这些 intent-filter 包含 BROWSABLE 类别并支持 https 方案

九、Activity 生命周期

按下“返回”按钮时,不再完成根启动器 activity

Android 12 更改了在按下“返回”按钮时系统对为其任务根的启动器 activity 的默认处理方式。在以前的版本中,系统会在按下“返回”按钮时完成这些 activity。在 Android 12 中,现在系统会将 activity 及其任务移到后台,而不是完成 activity。当使用主屏幕按钮或手势从应用中导航出应用时,新行为与当前行为一致。

注意:系统仅会将新行为应用于为其任务根的启动器 activity,即使用 ACTION_MAIN 和 CATEGORY_LAUNCHER 声明 intent 过滤器的 activity。对于其他 activity,在按下“返回”按钮时,系统会像以前一样完成 activity。

对于大多数应用而言,此变更意味着使用“返回”按钮退出应用的用户可以更快地从温状态恢复应用,而不必从冷状态完全重启应用。

建议您针对此变更测试您的应用。如果您的应用目前替换 onBackPressed() 来处理返回导航并完成 Activity,请更新您的实现来调用 super.onBackPressed() 而不是完成 Activity。调用 super.onBackPressed() 可在适当时将 activity 及其任务移至后台,并可为不同应用中的用户提供更一致的导航体验。

另请注意,通常,我们建议您使用 AndroidX Activity API 提供自定义返回导航,而不是替换 onBackPressed()。如果没有组件拦截系统按下“返回”按钮,AndroidX Activity API 会自动遵循适当的系统行为。


文中部分内容转载自以下作者

作者:恋猫de小郭
链接:juejin.cn/post/703710…
作者:DHL
链接:juejin.cn/post/704358… 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自juejin.im/post/7067748263113261087