Android 12 自动适配 exported 深入解析避坑

众所周知,从 Android 12 开始,使用了 TargetSDK 31 之后,四大组件如果使用了 intent-filter, 但是没显性质配置 exported App 将会无法安装,甚至编译不通过。

比如启动的 Activity 就需要设置 exportedtrue ,至于其他组件是否设置为 true 则看它是否需要被其它应用调用。

然而这个事情的状态是这样的:

  • 如果出现问题的 AndroidManifest 文件是你本地的,那手动修改即可;
  • 但如果出现问题的是第三方远程依赖,并且对方并没有提供源码和更新,你就无法直接修改;
  • 如果第三方依赖太多,查找哪些出了问题十分费时费力。

脚本

所以在之前的 《Android 12 快速适配要点》 一文中提供了一套脚本,专门用于适配 Android 12 下缺少 android:exported 无法编译或者安装的问题,但是在这期间收到了不少问题反馈:

com.android.tools.build:gradle:4.0.0 以及其下版本

一下脚本经过测试最高可到支持的版本: gradle:4.0.0 & gradle-6.1.1-all.zip

/**
 * 修改 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.0.0 以上版本

以下脚本经过测试支持的版本: gradle:4.1.0 & gradle-6.5.1-all.zip

/**
 * 修改 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 下执行,也可以单独放到一个 gradle 文件之后 apply 引入,它的作用就是:

在打包过程中检索所有没有设置 exported 的组件,给他们动态配置上 exported,这里有个特殊需要注意的是,因为启动 Activity 默认就是需要被 Launcher 打开的,所以 "android.intent.action.MAIN" 需要 exported 设置为 true 。(PS:更正规应该是用 LAUNCHER 类别,这里故意用 MAIN

而后综合问题,具体反馈的问题有 :

  • label直接写死中文,不是引用 @string 导致的在 3.x 的版本可以正常运行,但不能打包 ;

  • XmlParser 类找不到,这个首先确定 AGP 版本和 Gradle 版本是否匹配,具体可见 gradle-plugin,另外可以通过 groovy.util.XmlParser 或者 groovy.xml.XmlParser 全路径指定使用 ,如果是 gradle 文件里显示红色并不会影响运行;

  • 运行报错提示 android:exported needs,这个就是今天需要输入聊的

Error: android:exported needs to be explicitly specified for <xxxx>. Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined.

基于上述脚本测试和反馈,目前的结论是:

gradle:4.2.0 & gradle-6.7.1-all.zip 开始,TargetSDK 31 下脚本会有异常,因为在 processDebugMainManifest (带有Main) 的阶段,会直接扫描依赖库的 AndroidManifest.xml 然后抛出直接报错,从而进不去 processDebugManifest 任务阶段就编译停止,所以实际上脚本并没有成功运行

所以此时拿不到 mergerd_manifest 下的文件,因为 mergerd_manifestAndroidManifest.xml 也还没创建成功,没办法进入 task ,也就是该脚本目前只能针对 gradle:4.1.0 以及其下版本安装 apk 到 Android12 的机器上, 有 intent-filter 但没有 exoprted 的适配问题,基于这个问题,不知道各位是否有什么好的建议?

新脚本

而目前基于这个问题,这里提供了如下脚本,在 gradle:4.2.0 & gradle-6.7.1-all.zip 以及 7.0 的版本上,该脚本的作用是在运行时自动帮你打印出现问题的 aar 包依赖路径和组建名称

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        //println("=============== ${variant.getBuildType().name.toUpperCase()} ===============")
        //println("=============== ${variant.getFlavorName()} ===============")
        def vn
        if (variant.getFlavorName() != null && variant.getFlavorName() != "") {
            vn = variant.name;
        } else {
            if (variant.getBuildType().name == "release") {
                vn = "Release"
            } else {
                vn = "Debug"
            }
        }
        def taskName = "process${vn}MainManifest";
        try {
            println("=============== taskName ${taskName} ===============")
            project.getTasks().getByName(taskName)
        } catch (Exception e) {
            return
        }
        ///你的自定义名字
        project.getTasks().getByName(taskName).doFirst {
            //def method = it.getClass().getMethods()
            it.getManifests().getFiles().each {
                if (it.exists() && it.canRead()) {
                    def manifestFile = it
                    def exportedTag = "android:exported"
                    def nameTag = "android:name"
                    ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
                    def xml = new XmlParser(false, false).parse(manifestFile)
                    if (xml.application != null && xml.application.size() > 0) {
                        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

                        }
                        if (nodes.application != null && nodes.application.size() > 0) {
                            nodes.each {
                                def t = it
                                it.each {
                                    if (it.name() == "intent-filter") {
                                        println("$manifestFile \n .....................${t.attributes().get(nameTag)}......................")
                                    }
                                }
                            }
                        }
                    }


                }
            }
        }
    }
}

如下图所示,因为目前官方如红色信息内容其实指向并不正确,容易误导问题方向,所以通过上述脚本打印,可以快速查找到问题所在的点,然后通过 tool:replace 临时解决

image

具体为什么之前的脚本在高版本 AGP 下无法使用,原因在于新版本在 processDebugMainManifest ,或者说 processXXXXXXMainManifest 的处理逻辑发生了变化,通过找到 processDebugMainManifest 的实现类,可以看到问题出现就是在于 Merging library manifest

processDebugMainManifest 的实现在 ProcessApplicationManifest 里,对应路径是 ProcessApplicationManifest -> MainfestHelper mergeManifestsForApplication -> MainfestMerger2

错误是在 Merging library manifest 的阶段出现异常,但是这个阶段的 task 里对于第三方依赖路径的输入,主要是从 private fun computeFullProviderList 方法开始,所以输入到 mergeManifestsForApplication 里的第三方路径是通过这个私有方法生成。

image

感觉唯一可以考虑操作的就是内部的 manifests 对象去变换路径,但是它是 private ,并且内部并不能很好复写其内容。

image

另外因为 aar 文件里的 AndroidManifset 是 readOnly ,所以如果真的要修改,感觉只能在输入之前读取到对应 AndroidManifset, 并生成临时文件,在 manifests 对象中更改其路径来完成,不知道大家有没有什么比较好的思路 。

如果有好的解决办法,后续再更新。

最后

最后再说一个坑 ,如果你是低版本 Gradle 可以打包成功,但是运行到 Android12 机器的时候,可能会因为没有 exported 遇到安装失败的问题:

1、如果是模拟器 12,你可能会看到如下所示的错误提示 ,提示上显示还是很直观的, 直接告诉你是 android:exported 的问题:

* What went wrong:
Execution failed for task ':app:installDebug'.
> java.util.concurrent.ExecutionException: com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: Failed parse during installPackageLI: /data/app/vmdl487461761.tmp/base.apk (at Binary XML file line #358): xxxxx.Activity: Targeting S+ (version 31 and above) requires that an explicit value for android:exported be defined when intent filters are present

2、如果你是真机 12,那可能就是这样的提示,提示然是 INSTALL_FAILED_USER_RESTRICTED不得不说小米系统这个安装失败很具误导性,比如 minSDK 太高导致无法安装,在小米上也会是 INSTALL_FAILED_USER_RESTRICTED

image

基本上内容就这些,具体如何进一步优化还待后续测试, 所以针对脚本实现,你还有什么问题或者想法,欢迎评论交流 ~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容