Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Editing AndroidManifest.xml in Gradle task processManifest.doLast has no effect when running app from Android Studio

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.

like image 841
Floern Avatar asked Apr 26 '16 19:04

Floern


3 Answers

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...

like image 108
Floern Avatar answered Nov 03 '22 20:11

Floern


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. 

            }

        }
    }
}

UPDATE:

For Gradle 7.0.2 and Gradle Plugin 7.0.2

instead of task.getManifestOutputDirectory() use task.getMultiApkManifestOutputDirectory()

Hope this will help someone.

like image 6
Dmitriy Miyai Avatar answered Nov 03 '22 21:11

Dmitriy Miyai


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.

like image 2
VinceStyling Avatar answered Nov 03 '22 22:11

VinceStyling