网易新闻 Android12 适配之显式声明 android:exported - ravegenius/share GitHub Wiki

网易新闻 Android12 适配之显式声明 android:exported

引言

Android12 升级的官方申明:

Android四大组件 Activity,Service,Provider,Receiver 四大组件中都具有该属性:如果四大组件中有intent-filter节点,则需要指定android:exported为true或为false。

android:exported 这个字段出现在 Android APP Project 的 AndroidManifest.xml 中,作用是:是否支持其它应用调用当前组件。

Gradle:4.1.3 适配 - Gradle 脚本

我们自己代码中的 AndroidManifest.xml 可以自行调整,但是第三方库就存在没有显示声明的问题,需要通过在编译时统一修改。

网易新闻使用的 Gradle Tools Version 如下:

dependencies {
    classpath "com.android.tools.build:gradle:4.1.3"
    ······
}

在 App-Moudle 中的 build.gradle 文件中新增如下代码:

/**
 * 显示声明 android:exported
 */
android.applicationVariants.all { variant ->
    variant.outputs.all { output ->
        output.processResources.doFirst { pm ->
            String manifestPath = output.processResources.manifestFile
            // Manifest 文件
            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')
            //挑选要修改的节点,没有指定的 exported 的才需要增加
            def nodes = xml.application[0].'*'.findAll {
                (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") {
                        isMain = true
                    }
                }
                it.attributes().put(exportedTag, "${isMain}")
            }
            // 重新 Manifest 文件
            PrintWriter pw = new PrintWriter(manifestFile)
            pw.write(groovy.xml.XmlUtil.serialize(xml))
            pw.close()
        }
    }
}

后来因为项目依赖需要 Gradle Tools 升级,新版本如下:

dependencies {
    classpath "com.android.tools.build:gradle:4.2.0"
    ······
}

使用原来的 build.gradle 中的脚本已无效,编译时失败,报错:

Error: android:exported needs to be explicitly specified for . 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_manifest 下 AndroidManifest.xml 也还没创建成功,没办法进入 task ,也就是该脚本对于 Gradle:4.2.0 无效,于是决定使用编写 Gradle Plugin 来解决问题。

Gradle:4.2.0 适配 - Android Gradle Plugin

一个 Gradle Plugin 包括:一个唯一的 id 标识、一个执行环境(包括 maven 仓库和依赖)、一个配置项、插件需要实现的功能。那么我们在自定义一个 Gradle Plugin 的时候也应该包括这四部分的内容。现在就从这四方面去说明下怎么创建一个简单的Gradle插件。

1.构建 Gradle Plugin 及 唯一标识

Android Studio 暂时还没有提供类似的 Gradle Plugin 快捷方式,那么我们就需要自己来创建这个 Gradle Plugin。

  • (1)新建一个 Android Project,如果在已有工程中创建插件就不需要执行此步骤。
  • (2)新建一个 Module。这里可以的 Module 的类型任意,因为创建该 Module 只是用来放插件的容器,里面的大部分内容都是要删除的。我们命名该 Module 为 AppPlugin,然后将里面的文件除去 build.gradle 和 src/main 目录,其余的全部删除,src/main 目录仅留目录,里面的文件全部删除。
  • (3)添加相关的文件目录
    • 1)由于 Gradle 基于 groovy,在 src/main 下创建一个 groovy 的目录
    • 2)在 groovy 的目录创建一个包名为 com.netease.plugin 的目录
    • 3)在 src/main 下创建存放库和名字的配置文件,即 resources/META-INF/gradle-plugins 下创建 com.netease.plugin.properties 文件,注意这个com.netease.plugin 就为这个自定义插件的名字,也就是唯一id

2.配置Gradle开发环境

在 AppPlugin 的根目录下的 build.gradle 文件添加以下内容:

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.2.0'
}

repositories {
    google()
    mavenCentral()
}

uploadArchives {
    repositories {
        mavenDeployer {
            pom.groupId = "com.netease.plugin" //定义项目属于哪个组
            pom.artifactId = "AppPlugin"       //定义当前maven项目在组的唯一Id
            pom.version = "1.0.0"              //当前版本号
            //最终通过这三个值来指定该 Gradle 的 classpath 为 'groupId:artifaceId:version' 
            //也就是我们在使用该插件时的依赖的 classpath:com.netease.plugin:AppPlugin:1.0.0
            
            //本地maven地址
            repository(url: uri('../plugins'))
            //提交到远程服务器
            // repository(url:"服务器地址"){
            //    authentication(userName:'admin',password:'admin')
            // }
        }
    }
}

3.添加自定义Gradle的相关代码

(1)在 src/main/groovy/com/netease/plugin 下创建插件的入口文件 ManifestPluginProject.groovy,并且该类要实现 Plugin,代码如下:

/**
 * Created by Jason on 2022/7/25.
 * <p>
 * 插件入口
 *
 * @author Jason
 */
class ManifestPluginProject implements Plugin<Project> {

    String variantName

    @Override
    void apply(Project project) {
        //在sync中无法获取到variantName
        getVariantNameInBuild(project)
        SystemPrint.outPrintln(String.format("Welcome %s ManifestProject", variantName))
        if (isValidVariantName()) {
            addTaskForVariantAfterEvaluate(project)
        }
    }

    /**
     * 获取当前变体名
     * (1)在执行build任务的时候,
     * project.gradle.getStartParameter().getTaskRequests()返回的内容:
     * [DefaultTaskExecutionRequest{args=[:wjplugin:assemble, :wjplugin:testClasses,
     *  :manifestplugin:assemble, :manifestplugin:testClasses, :firstplugin:assemble,
     *  :firstplugin:testClasses, :app:assembleHuaweiDebug],projectPath='null'}]
     * 可从该字符串中截取当前的variant,然后在该变体基础上创建各个task.
     * (2)在执行sync任务的时候,
     * project.gradle.getStartParameter().getTaskRequests()返回的内容:[DefaultTaskExecutionRequest{args=[],projectPath='null'}]
     * 解决方案:通过project.extensions.findByType(AppExtension.class)找到一个可用的变体(因为会将所有的变体task都加入到任务队列中),
     * 将该变体作为变体名来执行完sync任务(仅仅为了完成sync任务,没有任何意义,在执行build任务的时候还会通过{@link #getVariantNameInBuild}替换掉逻辑)
     * 但是最理想的解决方案是该在sync的时候,可以不执行该插件(判断逻辑就是获取的variantName为null的时候,{@link #apply()}直接返回即可)
     *
     * TODO 需要验证在debug release多个变体打包过程
     * @param project
     * @return "HuaweiDebug"\"Debug"...
     */
    void getVariantNameInBuild(Project project) {
        String parameter = project.gradle.getStartParameter().getTaskRequests().toString()
        //assemble(\w+)(Release|Debug)仅提取Huawei
        String regex = parameter.contains("assemble") ? "assemble(\\w+)" : "generate(\\w+)"
        Pattern pattern = Pattern.compile(regex)
        Matcher matcher = pattern.matcher(parameter)
        if (matcher.find()) {
            //group(0)就是指的整个串,group(1) 指的是第一个括号里的东西,group(2)指的第二个括号里的东西
            variantName = matcher.group(1)
            SystemPrint.outPrintln(String.format("Real variant name from all variant is \" %s \"", variantName))
        }
        //但是sync时返回的内容:[DefaultTaskExecutionRequest{args=[],projectPath='null'}].
        //所以此时走注释中的(2),实现"则直接但是最理想的解决方案是该在sync的时候,可以不执行该插件"这种方案,则直接隐藏下面的代码
        if (!isValidVariantName()) {
            //从AppExtension中获取所有变体,作为获取当前变体的备用方案
            getValidVariantNameFromAllVariant(project)
        }
    }

    /**
     * 获取所有的变体中的一个可用的变体名,仅仅用来保证sync任务可执行而已
     * project.extensions.findByType()有执行时机,所以会出现在getVariantNameInBuild()
     * 中直接调用getVariantNameFromAllVariant()将无法更新variantName
     *
     * @param project
     */
    void getValidVariantNameFromAllVariant(Project project) {
        if (isValidVariantName()) {
            return
        }
        //但是sync时返回的内容:[DefaultTaskExecutionRequest{args=[],projectPath='null'}],其实该过程可以不执行该插件也可以
        //直接从所有的变体中取一个可用的变体名,返回
        project.extensions.findByType(AppExtension.class).variantFilter {
            variantName = it.name.capitalize()
            SystemPrint.outPrintln(String.format("Fake variant name from all variant is \" %s \"", variantName))
            if (isValidVariantName()) {
                return true
            }
        }
    }

    /**
     * 在项目配置完成之后添加task
     * @param project
     */
    void addTaskForVariantAfterEvaluate(Project project) {
        SystemPrint.outPrintln("初始化 AddExportForPackageManifestTask")
        AddExportForPackageManifestTask addExportTask = project.getTasks()
                .create(AddExportForPackageManifestTask.TAG, AddExportForPackageManifestTask)
        SystemPrint.outPrintln("在项目配置完成后,添加自定义Task......")
        //在项目配置完成后,添加自定义Task
        project.afterEvaluate {
            //为当前变体的task都加入到这个任务队列中。
            //所以通过project.getTasks().each {}去匹配每个task的startsWith&&endsWith的逻辑是一致的
            //并且这种性能会更高
            //直接通过task的名字找到ProcessApplicationManifest这个task
            addExportTaskForPackageManifest(project, addExportTask)
        }
    }

    /**
     * 为所有依赖的包的AndroidManifest添加android:exported
     * processHuaweiDebugMainManifest:合并所有依赖包以及主module中的AndroidManifest文件
     * processDebugManifest:为所有变体生成最终AndroidManifest文件
     * 不能使用ProcessDebugManifest.因为processHuaweiDebugMainManifest执行的时候就报错,还未执行到ProcessDebugManifest
     *
     * 1)BTask.dependsOn ATask:先执行完ATask,在执行BTask;
     * 2)BTask.mushRunAfter ATask:先执行完ATask,在执行BTask
     * 3)BTask.mushRunAfter ATask CTask.mushRunAfter ATask:按照ATask、BTask、CTask顺序执行
     * 4)BTask.shouldRunAfter ATask:先执行完ATask,在执行BTask
     *
     * 在整个Gradle构建过程中,通过下面的代码方式对该Task进行禁用:
     * taskActionTask.enabled false
     * @param project
     */
    void addExportTaskForPackageManifest(Project project, AddExportForPackageManifestTask beforeAddTask) {
        //找到processHuaweiDebugMainManifest,在这个之前添加export
        String manifestStr = String.format("process%sMainManifest", variantName)
        SystemPrint.outPrintln(manifestStr + " task dependsOn " + AddExportForPackageManifestTask.TAG)
        ProcessApplicationManifest processManifestTask = project.getTasks().getByName(manifestStr)
        beforeAddTask.setManifestsFileCollection(processManifestTask.getManifests())
        beforeAddTask.setMainManifestFile(processManifestTask.getMainManifest().get())
        processManifestTask.dependsOn(beforeAddTask)
    }

    boolean isValidVariantName() {
        variantName != null && variantName.length() > 0
    }
}

(2)在 resources/META-INF/gradle-plugins/com.netease.plugin.properties 文件下,配置插件的入口类,代码如下:

#配置插件的入口类
implementation-class=com.netease.plugin.ManifestPluginProject

4.插件发布

经过上面三步之后,一个简单的自定义 Gradle Plugin 就完成了。那么还要经过打包发布出去。这就是我们在第二步中提到的配置 build.gradle 中的 uploadArchives{} 内容。如果是上传到远程服务器就是配置以下内容:

//提交到远程服务器
repository(url:"服务器地址"){
    authentication(userName:'admin',password:'admin')
}

如果仅仅是放在本地的化,可以直接采用下面的方式

//本地maven地址
repository(url: uri('../plugins'))

上面的两个 url 就是我们在后面使用这个自定义插件的仓库

配置好之后,还是点击右侧的 gradle 工具的 Tasks 下面的 upload 的 uploadArchives,即可完成打包发布。

image

完成之后就会在项目的根目录下生成下面结构的文件

image

到目前为止,已经完成一个自定义 Gradle Plugin 的过程。

  1. 该 Gradle Plugin 的唯一 ID 为 com.netease.plugin 也就是在 resources/META-INF/gradle-plugins 下创建com.netease.plugin.properties 文件的文件名;
  2. 配置环境需要用到的 maven 仓库为 /plugins 和依赖的 classpath 为 com.netease.plugin:AppPlugin:1.0.0

5.使用插件

(1)在项目的根目录的 build.gradle 配置 maven 仓库和依赖的 classpath

buildscript {
    repositories {
        ......
        // AppPlugin 的 maven 仓库
        maven {
            // maven 仓库的地址
            url uri('plugins')
        }
    }
    dependencies {
        ......
        // AppPlugin 的依赖
        classpath "com.netease.plugin:AppPlugin:1.0.0"
    }
}

(2)在 App-Main-Module 下的的 build.gradle 使用该插件

plugins {
    //该插件的id
    id 'com.wj.firstplugin'
}

Gradle Plugin 动态修改 AndroidManifest 文件

上文中已经展示代码 ManifestPluginProject.groovy,其中:

    void addExportTaskForPackageManifest(Project project, AddExportForPackageManifestTask beforeAddTask) {
        //找到processHuaweiDebugMainManifest,在这个之前添加export
        String manifestStr = String.format("process%sMainManifest", variantName)
        SystemPrint.outPrintln(manifestStr + " task dependsOn " + AddExportForPackageManifestTask.TAG)
        ProcessApplicationManifest processManifestTask = project.getTasks()
                .getByName(manifestStr)
        beforeAddTask.setManifestsFileCollection(processManifestTask.getManifests())
        beforeAddTask.setMainManifestFile(processManifestTask.getMainManifest().get())
        processManifestTask.dependsOn(beforeAddTask)
    }

关于 process%sMainManifest,如果有华为渠道的话任务为 processHuaweiDebugMainManifest;没有渠道的话任务为 processDebugMainManifest。

因为篇幅有限,至于如何获取该Task中变量,组成获取全任务名的方法,后续再做介绍

关于 Gradle Task 队列,我们知道:

  1. processDebugMainManifest 合并所有的 Manifest 文件(包含各个依赖包的 AndroidManifest.xml)
  2. processDebugManifest 使用合并的AndroidManifest.xml文件,为所有的变体创建AndroidManifest.xml文件

不能使用 ProcessDebugManifest。因为 processHuaweiDebugMainManifest 或 processDebugMainManifest 执行的时候就报错,还未执行到 ProcessDebugManifest。

所以在 ManifestPluginProject.groovy 中书写成先执行 AddExportForPackageManifestTask 再去执行 processHuaweiDebugMainManifest 或 processDebugMainManifest。

而 AddExportForPackageManifestTask 代码如下:

/**
 * Created by Jason on 2022/7/25.
 * <p>
 * 适配Android12,为每个带有<intent-filter>添加android:exported="true"属性
 * 在合并所有的Manifest之前为所有的AndroidManifest文件添加
 * @author Jason
 */
class AddExportForPackageManifestTask extends DefaultTask {
    protected static String TAG = "AddExportForPackageManifestFromManifestProject"
    String ATTRIBUTE_EXPORT = "{http://schemas.android.com/apk/res/android}exported"
    private FileCollection manifestCollection
    private File mainManifestFile
    private boolean isMainManifestFile

    /**
     * 设置所有的 需要合并的Manifest文件
     * @param collection
     */
    void setManifestsFileCollection(FileCollection collection) {
        manifestCollection = collection
    }

    /**
     *
     * @param file
     */
    void setMainManifestFile(File file) {
        mainManifestFile = file
    }

    @TaskAction
    void doTaskAction() {
        //处理所有包下的AndroidManifest文件添加android:exported
        SystemPrint.outPrintln(TAG, "Running .....")
        isMainManifestFile = false
        manifestCollection.each {
            handlerVariantManifestFile(it)
        }
        //自己APP中的manifest文件只提示增加,不主动添加
        isMainManifestFile = true
        handlerVariantManifestFile(mainManifestFile)
    }

    /**
     * 处理单个变体的Manifest文件
     */
    void handlerVariantManifestFile(File manifestFile) {
        if (!manifestFile.exists()) {
            return
        }
        def node = readManifestFromPackageManifest(manifestFile)
        writeManifestForPackageManifest(manifestFile, node)
    }

    /**
     * 读manifest内容
     * @param manifestFile
     * @return
     */
    Node readManifestFromPackageManifest(File manifestFile) {
        try {
            XmlParser xmlParser = new XmlParser()
            def node = xmlParser.parse(manifestFile)

            //node.attributes();获取的一级内容<?xml> <manifest>里设置的内容如:key为package、encoding,value为对应的值
            //node.children();获取的二级内容 <application> <uses-sdk>
            //node.application直接可获取到<application>这级标签
            //第一步:处理<activity>
            node.application.activity.each {
                //如果已经有android:exported,则直接循环下一个:return true 相当于continue
                if (handlerEveryNodeWithoutExported(it)) {
                    return true
                }
            }
            //第二步:处理<service>
            node.application.service.each {
                //如果已经有android:exported,则直接循环下一个:return true 相当于continue
                if (handlerEveryNodeWithoutExported(it)) {
                    return true
                }
            }
            //第三步:处理<receiver>
            node.application.receiver.each {
                //如果已经有android:exported,则直接循环下一个:return true 相当于continue
                if (handlerEveryNodeWithoutExported(it)) {
                    return true
                }
            }
            return node

        } catch (ParserConfigurationException e) {
            e.printStackTrace()
        } catch (SAXException e) {
            e.printStackTrace()
        } catch (IOException e) {
            e.printStackTrace()
        }
    }

    /**
     * 第四步:保存到原AndroidManifest文件中
     * @param manifestFile
     * @param node
     */
    void writeManifestForPackageManifest(File manifestFile, Node node) {
        if (isMainManifestFile) {
            //如果是主module的manifest,自行添加
            return
        }
        String result = XmlUtil.serialize(node)
        manifestFile.write(result, "utf-8")
    }

    /**
     * 为每个需要添加android:exported的node
     * 在each{}中该方法不能定义为private,否则会提示找到该方法
     * @param it
     * @return
     */
    boolean handlerEveryNodeWithoutExported(Node it) {
        //attributes()取得是在<activity >里面配置的属性值,而里面嵌套的<></>可直接通过.xxx的形式取得
        def attrs = it.attributes()
        //如果含有了android:exported,则直接处理下一个.
        if (hasAttributeExported(attrs)) {
            //SystemPrint.errorPrintln(TAG, String.format("The \" %s \" already has \" android:exported \" , to next one .", it.name()))
            //结束本次循环,相当于continue find return true相当于break
            return true
        }
        //得到配置的<activity>里面的如<intent-filter>
        def children = it.children()
        if (hasIntentFilter(children)) {
            handlerAddExportForNode(it)
        }
        return false
    }

    /**
     * 添加android:export
     */
    private void handlerAddExportForNode(Node node) {
        if (isMainManifestFile) {
            //仅做提示
            String errorFormat = "To solve the build error \n \"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\"\n" +
                    "you must set \"android:exported\" based on actual demand for manifest in main module of \n \" %s \""
            SystemPrint.errorPrintln(TAG, String.format(errorFormat, node.attributes().toString()))
            return
        }
        SystemPrint.outPrintln(TAG, String.format("Handler third sdk of \"%s\" , so add \"android:exported=true\" .", node.name()))
        SystemPrint.outPrintln(TAG, String.format("In Handler:  \n %s", node.attributes()))
        //注意这里使用的是"android:exported"而不是ATTRIBUTE_EXPORT!!!!!!
        node.attributes().put("android:exported", true)
        //node.attributes().put(ATTRIBUTE_EXPORT,"true")
        //TODO 该种方式就可以替换,但是之前已有的不管采用{http://schemas.android.com/apk/res/android}name还是android:name都无法赋值成功
        /**这个原因跟在hasAttributeExported()使用attrs.containsKey(ATTRIBUTE_EXPORT)是一个原因,
         * 只能在attrs.each中取出里面key在进行判断才可以返回true,然后在调用下面的方法才可以替换成功
         * node.attributes().replace(new String("android:name"), "add")
         * node.attributes().replace(new String("{http://schemas.android.com/apk/res/android}name"), "ddd")
         * */
    }

    /**
     * 是否含有android:exported属性
     * TODO attrs.containsKey(ATTRIBUTE_EXPORT) 不起作用
     * @return
     */
    private boolean hasAttributeExported(Map attrs) {
        boolean isExported = false
        attrs.find {
            if (ATTRIBUTE_EXPORT.equals(it.key.toString())) {
                isExported = true
                //find return true相当于break
                return true
            }
        }
        return isExported
    }

    /**
     * 是否含有<intent-filter>
     * @param children
     * @return
     */
    private boolean hasIntentFilter(List children) {
        boolean isIntent = false
        children.find {
            if ("intent-filter".equals(it.name())) {
                isIntent = true
                //find return true相当于break
                return true
            }
        }
        return isIntent
    }

}

代码解析:

  1. doTaskAction 作为 Task 执行入口。
  2. 执行之前调用 setManifestsFileCollection 和 setMainManifestFile 设置 ManifestFileCollection 和 MainManifest 文件。
  3. 通过 ManifestFileCollection 遍历除 MainManifest 之外的所有 Manifest 文件;
  4. readManifestFromPackageManifest 方法获取需要添加 android:exported 的组件,条件是:判断该组件是否满足“含有intent-filter && 没有添加android:exported”
  5. writeManifestForPackageManifest 方法为所有需要添加的组件添加 android:exported,重新写入AndroidManifest.xml文件
  6. MainManifest 打印提示。只打印不提示的原因:MainManifest 都是开发编写,如果修改格式会不统一并且会有 Git 等提交提示。

总结

  1. 开发 Gradle Plugin 对 Gradle 版本依赖较轻,也能适配 Android 版本升级;
  2. Gradle Plugin 需要找到“合适”的锚点任务,设置“合适”的执行顺序;
  3. processDebugManifest 会为所有的变体生成最终的 AndroidManifest.xml 文件文件,可以通过 processDebugManifest 作为锚点来解决修改最终 AndroidManifest.xml文件的问题;
⚠️ **GitHub.com Fallback** ⚠️