在 Gradle 任务 processManifest.doLast 中编辑 AndroidManifest.xml 在 Android Studio 中的 运行 应用时无效
Editing AndroidManifest.xml in Gradle task processManifest.doLast has no effect when running app from Android Studio
我使用下面的 Gradle 脚本在编译时对 AndroidManifest.xml 进行了一些修改。在这个例子中,我想注入一个 <meta-data>
元素。代码基于this answer.
android {
// ...
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def manifestOutFile = output.processManifest.manifestOutputFile
def newFileContents = manifestOutFile.getText('UTF-8').replace("</application>", "<meta-data ... /></application>")
manifestOutFile.write(newFileContents, 'UTF-8')
}
}
}
}
当我在 Android Studio 中执行 Gradle 同步或从命令行进行干净构建时,这按预期工作:可以从应用程序内部访问元数据。
但是当我 运行 ▶ 来自 Android Studio 的应用程序时,修改后的清单似乎被忽略了,因为插入的元数据不是 APK 中编译清单的一部分,并且应用程序本身也无法在 运行 时间找到它,元数据根本不存在。
在所有情况下,合并的中间体 AndroidManifest.xml(在 /build/intermediates/manifests/ 中)确实包含更改,但由于某种原因它看起来被忽略了如果我 运行 应用程序
为了使其更加明显,我尝试插入一些无效的 XML:在这种情况下,Gradle 同步和干净构建按预期失败,因为清单中存在语法错误.但我仍然能够从 Android Studio 运行 应用程序,因此修改实际上被忽略了..
重现此问题的最简单方法是首先清理项目(在 Android Studio 中),这会导致重新处理清单(如果出现语法错误,我会按预期失败),并且然后 运行 应用程序,即使清单无效也能正常工作。
请注意,doLast
中的任务每次都会执行:打印任务中的 println()
并且中间清单包含更改。
好像清单在我的任务执行之前被编译到 APK 中。
这里的问题在哪里?
我正在使用 Android Studio 2.0 和 Android Gradle 插件 2.0.0.
我发现它与 Android Studio 2.0 中引入的 Instant 运行 功能有关。如果我将其关闭,一切都会按预期进行。但是因为我想使用 Instant 运行,所以我进一步挖掘了一点。
事实是,启用 Instant 运行 后,中间 AndroidManifest.xml 文件将位于另一个位置,即 /build/intermediates/bundles/myflavor/instant-run/。这意味着我实际上是在编辑错误的文件。可以使用 属性 instantRunManifestOutputFile
访问其他清单文件,可以使用它代替 manifestOutputFile
.
为了使其适用于所有用例,我检查两个临时清单文件是否存在,如果存在则修改它们:
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
[output.processManifest.manifestOutputFile,
output.processManifest.instantRunManifestOutputFile
].forEach({ File manifestOutFile ->
if (manifestOutFile.exists()) {
def newFileContents = manifestOutFile.getText('UTF-8').replace("</application>", "<meta-data ... /></application>")
manifestOutFile.write(newFileContents, 'UTF-8')
}
})
}
}
}
几乎没有 instantRunManifestOutputFile
的文档。我得到的唯一 Google 搜索结果是 Android Gradle 插件源代码。但后来我还发现了第三个潜在的清单文件 属性 aaptFriendlyManifestOutputFile
,我也不知道它是关于什么的...
我想为这个问题添加一些额外的信息。 @Floern 的回答有点过时了。该代码适用于旧 Gradle 版本。 Gradle 的新版本表示 manifestOutputFile
已弃用,将很快被删除。 instantRunManifestOutputFile
根本不存在。因此,这里是新 Gradle 版本的示例:
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def outputDirectory = output.processManifest.manifestOutputDirectory
File manifestOutFile = file(new File(outputDirectory, 'AndroidManifest.xml'))
if(manifestOutFile.exists()){
// DO WHATEVER YOU WANT WITH MANIFEST FILE.
}
}
}
}
编辑:
这是 Gradle 5.4.1 和 Grudle 插件 3.5.1 的较新变体:
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getManifestOutputDirectory()
File outputDirectory
if (outputDir instanceof File) {
outputDirectory = outputDir
} else {
outputDirectory = outputDir.get().asFile
}
File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
// DO WHATEVER YOU WANT WITH MANIFEST FILE.
}
}
}
}
更新:
对于 Gradle 7.0.2 和 Gradle 插件 7.0.2
而不是 task.getManifestOutputDirectory()
使用 task.getMultiApkManifestOutputDirectory()
希望这会对某人有所帮助。
不同的 gradle 版本有所不同,对我来说,我使用了 gradle-5.5-rc-3
和 com.android.tools.build:gradle:3.4.1
所以这会起作用:
def static setVersions(android, project, channelId) {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processorTask = output.processManifestProvider.getOrNull()
processorTask.doLast { task ->
def directory = task.getBundleManifestOutputDirectory()
def srcManifestFile = "$directory/AndroidManifest.xml"
def manifestContent = new File(srcManifestFile).getText()
def xml = new XmlParser(false, false).parseText(manifestContent)
xml.application[0].appendNode("meta-data", ['android:name': 'channelId', 'android:value': '\' + channelId])
def serializeContent = groovy.xml.XmlUtil.serialize(xml)
def buildType = getPluginBuildType(project)
new File("${project.buildDir}/intermediates/merged_manifests/$buildType/AndroidManifest.xml").write(serializeContent)
}
}
}
}
def static getPluginBuildType(project) {
def runTasks = project.getGradle().startParameter.taskNames
if (runTasks.toString().contains("Release")) {
return "release"
} else if (runTasks.toString().contains("Debug")) {
return "debug"
} else {
return ""
}
}
intermediates/merged_manifests 的位置是从模块的构建目录中找到的,它可能是其他的取决于 android-plugin 版本,只需查看构建目录并找到你的。
我需要编辑合并的清单文件,这对我有用
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getManifestOutputDirectory()
File manifestOutFile = file("$outputDir/AndroidManifest.xml")
def newManifest = manifestOutFile.getText().replace("@string/app_name", "Broccoli")
manifestOutFile.write(newManifest, 'UTF-8')
}
}
}
在这个简单的例子中,我将应用程序名称替换为 Broccoli。
如果您需要更多信息,我写了一篇关于它的博客post
https://androidexplained.github.io/android/gradle/2020/09/28/editing-manifest.html
替换清单文件中内容的正确方法是使用 manifest placeholders.
属性 获取 key-value 对的映射。
android {
defaultConfig {
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
...
}
如果您使用多种口味,则可以按口味定义 属性。
flavorDimensions "environment"
productFlavors {
staging {
dimension "environment"
applicationIdSuffix ".test"
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
prod {
dimension "environment"
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
}
然后您可以像这样将占位符之一作为属性值插入到清单文件中
<intent-filter ... >
<data android:scheme="http" android:host="${hostName}" ... />
<data android:scheme="http" android:host="${hostName2}" ... />
...
</intent-filter>
我使用下面的 Gradle 脚本在编译时对 AndroidManifest.xml 进行了一些修改。在这个例子中,我想注入一个 <meta-data>
元素。代码基于this answer.
android {
// ...
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def manifestOutFile = output.processManifest.manifestOutputFile
def newFileContents = manifestOutFile.getText('UTF-8').replace("</application>", "<meta-data ... /></application>")
manifestOutFile.write(newFileContents, 'UTF-8')
}
}
}
}
当我在 Android Studio 中执行 Gradle 同步或从命令行进行干净构建时,这按预期工作:可以从应用程序内部访问元数据。
但是当我 运行 ▶ 来自 Android Studio 的应用程序时,修改后的清单似乎被忽略了,因为插入的元数据不是 APK 中编译清单的一部分,并且应用程序本身也无法在 运行 时间找到它,元数据根本不存在。
在所有情况下,合并的中间体 AndroidManifest.xml(在 /build/intermediates/manifests/ 中)确实包含更改,但由于某种原因它看起来被忽略了如果我 运行 应用程序
为了使其更加明显,我尝试插入一些无效的 XML:在这种情况下,Gradle 同步和干净构建按预期失败,因为清单中存在语法错误.但我仍然能够从 Android Studio 运行 应用程序,因此修改实际上被忽略了..
重现此问题的最简单方法是首先清理项目(在 Android Studio 中),这会导致重新处理清单(如果出现语法错误,我会按预期失败),并且然后 运行 应用程序,即使清单无效也能正常工作。
请注意,doLast
中的任务每次都会执行:打印任务中的 println()
并且中间清单包含更改。
好像清单在我的任务执行之前被编译到 APK 中。
这里的问题在哪里?
我正在使用 Android Studio 2.0 和 Android Gradle 插件 2.0.0.
我发现它与 Android Studio 2.0 中引入的 Instant 运行 功能有关。如果我将其关闭,一切都会按预期进行。但是因为我想使用 Instant 运行,所以我进一步挖掘了一点。
事实是,启用 Instant 运行 后,中间 AndroidManifest.xml 文件将位于另一个位置,即 /build/intermediates/bundles/myflavor/instant-run/。这意味着我实际上是在编辑错误的文件。可以使用 属性 instantRunManifestOutputFile
访问其他清单文件,可以使用它代替 manifestOutputFile
.
为了使其适用于所有用例,我检查两个临时清单文件是否存在,如果存在则修改它们:
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
[output.processManifest.manifestOutputFile,
output.processManifest.instantRunManifestOutputFile
].forEach({ File manifestOutFile ->
if (manifestOutFile.exists()) {
def newFileContents = manifestOutFile.getText('UTF-8').replace("</application>", "<meta-data ... /></application>")
manifestOutFile.write(newFileContents, 'UTF-8')
}
})
}
}
}
几乎没有 instantRunManifestOutputFile
的文档。我得到的唯一 Google 搜索结果是 Android Gradle 插件源代码。但后来我还发现了第三个潜在的清单文件 属性 aaptFriendlyManifestOutputFile
,我也不知道它是关于什么的...
我想为这个问题添加一些额外的信息。 @Floern 的回答有点过时了。该代码适用于旧 Gradle 版本。 Gradle 的新版本表示 manifestOutputFile
已弃用,将很快被删除。 instantRunManifestOutputFile
根本不存在。因此,这里是新 Gradle 版本的示例:
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def outputDirectory = output.processManifest.manifestOutputDirectory
File manifestOutFile = file(new File(outputDirectory, 'AndroidManifest.xml'))
if(manifestOutFile.exists()){
// DO WHATEVER YOU WANT WITH MANIFEST FILE.
}
}
}
}
编辑: 这是 Gradle 5.4.1 和 Grudle 插件 3.5.1 的较新变体:
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getManifestOutputDirectory()
File outputDirectory
if (outputDir instanceof File) {
outputDirectory = outputDir
} else {
outputDirectory = outputDir.get().asFile
}
File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
// DO WHATEVER YOU WANT WITH MANIFEST FILE.
}
}
}
}
更新:
对于 Gradle 7.0.2 和 Gradle 插件 7.0.2
而不是 task.getManifestOutputDirectory()
使用 task.getMultiApkManifestOutputDirectory()
希望这会对某人有所帮助。
不同的 gradle 版本有所不同,对我来说,我使用了 gradle-5.5-rc-3
和 com.android.tools.build:gradle:3.4.1
所以这会起作用:
def static setVersions(android, project, channelId) {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processorTask = output.processManifestProvider.getOrNull()
processorTask.doLast { task ->
def directory = task.getBundleManifestOutputDirectory()
def srcManifestFile = "$directory/AndroidManifest.xml"
def manifestContent = new File(srcManifestFile).getText()
def xml = new XmlParser(false, false).parseText(manifestContent)
xml.application[0].appendNode("meta-data", ['android:name': 'channelId', 'android:value': '\' + channelId])
def serializeContent = groovy.xml.XmlUtil.serialize(xml)
def buildType = getPluginBuildType(project)
new File("${project.buildDir}/intermediates/merged_manifests/$buildType/AndroidManifest.xml").write(serializeContent)
}
}
}
}
def static getPluginBuildType(project) {
def runTasks = project.getGradle().startParameter.taskNames
if (runTasks.toString().contains("Release")) {
return "release"
} else if (runTasks.toString().contains("Debug")) {
return "debug"
} else {
return ""
}
}
intermediates/merged_manifests 的位置是从模块的构建目录中找到的,它可能是其他的取决于 android-plugin 版本,只需查看构建目录并找到你的。
我需要编辑合并的清单文件,这对我有用
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
def outputDir = task.getManifestOutputDirectory()
File manifestOutFile = file("$outputDir/AndroidManifest.xml")
def newManifest = manifestOutFile.getText().replace("@string/app_name", "Broccoli")
manifestOutFile.write(newManifest, 'UTF-8')
}
}
}
在这个简单的例子中,我将应用程序名称替换为 Broccoli。
如果您需要更多信息,我写了一篇关于它的博客post
https://androidexplained.github.io/android/gradle/2020/09/28/editing-manifest.html
替换清单文件中内容的正确方法是使用 manifest placeholders.
属性 获取 key-value 对的映射。
android {
defaultConfig {
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
...
}
如果您使用多种口味,则可以按口味定义 属性。
flavorDimensions "environment"
productFlavors {
staging {
dimension "environment"
applicationIdSuffix ".test"
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
prod {
dimension "environment"
manifestPlaceholders = [
hostName:"www.example.com",
hostName2:"www.example2.com"
]
}
}
然后您可以像这样将占位符之一作为属性值插入到清单文件中
<intent-filter ... >
<data android:scheme="http" android:host="${hostName}" ... />
<data android:scheme="http" android:host="${hostName2}" ... />
...
</intent-filter>