网易新闻 Android12 适配之显式声明 android:exported - ravegenius/share GitHub Wiki
Android12 升级的官方申明:
Android四大组件 Activity,Service,Provider,Receiver 四大组件中都具有该属性:如果四大组件中有intent-filter节点,则需要指定android:exported为true或为false。
android:exported 这个字段出现在 Android APP Project 的 AndroidManifest.xml 中,作用是:是否支持其它应用调用当前组件。
我们自己代码中的 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 Plugin 包括:一个唯一的 id 标识、一个执行环境(包括 maven 仓库和依赖)、一个配置项、插件需要实现的功能。那么我们在自定义一个 Gradle Plugin 的时候也应该包括这四部分的内容。现在就从这四方面去说明下怎么创建一个简单的Gradle插件。
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
在 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')
// }
}
}
}
(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
经过上面三步之后,一个简单的自定义 Gradle Plugin 就完成了。那么还要经过打包发布出去。这就是我们在第二步中提到的配置 build.gradle 中的 uploadArchives{} 内容。如果是上传到远程服务器就是配置以下内容:
//提交到远程服务器
repository(url:"服务器地址"){
authentication(userName:'admin',password:'admin')
}
如果仅仅是放在本地的化,可以直接采用下面的方式
//本地maven地址
repository(url: uri('../plugins'))
上面的两个 url 就是我们在后面使用这个自定义插件的仓库
配置好之后,还是点击右侧的 gradle 工具的 Tasks 下面的 upload 的 uploadArchives,即可完成打包发布。
完成之后就会在项目的根目录下生成下面结构的文件
到目前为止,已经完成一个自定义 Gradle Plugin 的过程。
- 该 Gradle Plugin 的唯一 ID 为 com.netease.plugin 也就是在 resources/META-INF/gradle-plugins 下创建com.netease.plugin.properties 文件的文件名;
- 配置环境需要用到的 maven 仓库为 /plugins 和依赖的 classpath 为 com.netease.plugin:AppPlugin:1.0.0
(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'
}
上文中已经展示代码 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 队列,我们知道:
- processDebugMainManifest 合并所有的 Manifest 文件(包含各个依赖包的 AndroidManifest.xml)
- 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
}
}
代码解析:
- doTaskAction 作为 Task 执行入口。
- 执行之前调用 setManifestsFileCollection 和 setMainManifestFile 设置 ManifestFileCollection 和 MainManifest 文件。
- 通过 ManifestFileCollection 遍历除 MainManifest 之外的所有 Manifest 文件;
- readManifestFromPackageManifest 方法获取需要添加 android:exported 的组件,条件是:判断该组件是否满足“含有intent-filter && 没有添加android:exported”
- writeManifestForPackageManifest 方法为所有需要添加的组件添加 android:exported,重新写入AndroidManifest.xml文件
- MainManifest 打印提示。只打印不提示的原因:MainManifest 都是开发编写,如果修改格式会不统一并且会有 Git 等提交提示。
- 开发 Gradle Plugin 对 Gradle 版本依赖较轻,也能适配 Android 版本升级;
- Gradle Plugin 需要找到“合适”的锚点任务,设置“合适”的执行顺序;
- processDebugManifest 会为所有的变体生成最终的 AndroidManifest.xml 文件文件,可以通过 processDebugManifest 作为锚点来解决修改最终 AndroidManifest.xml文件的问题;