Editing AndroidManifest.xml in Gradle task process

2020-07-13 08:27发布

问题:

I use the following Gradle script to make some modifications to the AndroidManifest.xml at compile time. In this example I want to inject a <meta-data> element. The code is based on 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')
            }
        }
    }
}

This works as expected when I do a Gradle sync in Android Studio or make a clean build from command line: The meta-data is accessible from within the app.

But when I run ▶ the application from Android Studio the modified manifest seems to be ignored, since the inserted meta-data is not part of the compiled manifest in the APK, and the app itself cannot find it at runtime either, the meta-data is simply not there.

In all cases the merged intermediate AndroidManifest.xml (in /build/intermediates/manifests/) does contain the changes, but for some reason it looks like it gets ignored if I run the app.

To make it even more obvious, I tried to insert some invalid XML: In this case, the Gradle sync and the clean build failed as expected because of a syntax error in the manifest. But I was still able to run the app from Android Studio, thus the modification effectively gets ignored..

The easiest way to reproduce this is to clean the project first (in Android Studio), which causes the manifest to be reprocessed (in case of the syntax error I get a failure as expected), and then run the app, which works even with an invalid manifest.

Note that the task in doLast gets executed everytime: A println() in the task is printed and the intermediate manifest contains the changes.

It's as if the manifest gets compiled into the APK before my task is executed.

Where is the issue here?

I'm using Android Studio 2.0 with the Android Gradle Plugin 2.0.0.

回答1:

I figured out that it's related to the Instant Run feature introduced in Android Studio 2.0. If I turn it off, everything works as expected. But since I want to use Instant Run, I digged a little further.

The thing is, with Instant Run enabled the intermediate AndroidManifest.xml file will be at another location, namely /build/intermediates/bundles/myflavor/instant-run/. That means I was effectively editing the wrong file. That other manifest file is accessible with the property instantRunManifestOutputFile, which can be used instead of manifestOutputFile.

To make it work in all use-cases I check both temporary manifest files whether they exist and modify them if so:

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')
                }
            })
        }
    }
}

There is literally no documentation of instantRunManifestOutputFile. The only Google search result I got was the Android Gradle Plugin source code. But then I also found a third potential manifest file property aaptFriendlyManifestOutputFile, which I don't know what it's about either...



回答2:

I want to add some additional information to this question. The answer from @Floern is a bit outdated. The code is working on old Gradle versions. The new version of Gradle says that manifestOutputFile is deprecated and will be removed soon. instantRunManifestOutputFile doesn't exists at all. So, here is the example for the new Gradle version:

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. 

            }
        }
    }
}

EDIT: Here is the newer variant for Gradle 5.4.1 and Grudle plugin 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. 

            }

        }
    }
}

Hope this will help someone.



回答3:

there are difference with various gradle version, for me, I Used gradle-5.5-rc-3 and com.android.tools.build:gradle:3.4.1 so this would work :

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 ""
    }
}

the location of intermediates/merged_manifests was found from module's build directory, it could be others depends on android-plugin version, just look into the build directory and find yours.