gradle android: post-process APK - android

I need to post-process my APK by adding a second signature, so I'm trying to understand how to hook into the build process at that point. my first attempt,
task doubleSign {
if (signing.androidBuildExists) {
android.buildTypes.all { type ->
tasks.("assemble${type.name.capitalize()}").finalizedBy doDoubleSign
}
}
}
okay fine. but if any of the install* tasks are executed, then much to my surprise, assemble* isn't in the dependency tree. so next i tried adding the line,
tasks.("install${type.name.capitalize()}").dependsOn doDoubleSign
however, gradle tells me that task doesn't exist!
> Could not find property 'installDebug' on task set.

installDebug won't be present when that code is executed because the android plugin creates its tasks in the afterEvaluate phase.
What you should do:
android {
applicationVariants.all { variant ->
if (!variant.isSigningReady()) {
// only add double signing on variants that are signed with
// default signing method.
return
}
// get output file
def outputFile = variant.outputFile
// add new signing task.
def signingTask = project.tasks.create("double${variant.name}Sign", MyCustomSigning)
// configure task
signingTask.inputFile = outputFile
// create the final apk name using baseName to guarantee it's unique.
signingTask.outputFile = new File(project.buildDir, "apks/${project.name}-${variant.baseName}-2sign.apk")
// configure the task to be run after the default signing task.
signingTask.dependsOn variant.packageApplication
// configure zip align task to use the output of the 2nd signing task,
// and also to run after 2nd signing task.
variant.zipAlign.inputFile = signingTask.outputFile
variant.zipAlign.dependsOn signingTask
}
}
Note that if you don't run zipalign (but really you should) you'll have to tweak the end to make variant.assemble depend on your signing task instead and to set the output of your signing task to variant.outputFile so that deployment from command line or IDE still works.
For the actual signing you would do a custom task with the annotations so that it only runs if the input file actually change.
class MyCustomSigning extends DefaultTask {
#InputFile
File inputFile
#OutputFile
File outputFile
#TaskAction
void sign() {
// call script to sign inputFile into outputFile
...
}
}

Even though this question has been answered, I want to share the solution that worked best for me.
All I needed was a suitable hook to post-process a completely finished apk. For this I used the hook "assemble.doLast":
android {
/* ... */
applicationVariants.all { variant ->
variant.assemble.doLast {
variant.outputs.each { output ->
File apk = output.outputFile
// Post process apk
}
}
}
}

Related

Firebase App Distribution with apk splits unable to find apk

I'm trying to bend firebase app distribution to work with apk splits.
I almost have it, however my issue is this
Could not find the APK. Make sure you build first by running ./gradlew assemble[Variant],
or set the apkPath parameter to point to your APK
My task
task firebaseAllEnvRelease() {
group = "publishing"
dependsOn ordered(
":printVersionCode",
":foo:app:assembleAllRelease"
":foo:app:firebasePublishAllEnvRelease")
}
For whatever reason, the firebase task runs the apk check (not upload) beforehand, before assemble, so obviously the apk is not there -- how can I force it to respect the order of tasks?
I know gradle creates the tasks graph hopwever it likes, but I do have a utility ordered for what, which chains them via mustRunAfter and it is for sure correct.
Plan b is to run the assemble ina separate gradlew command before that, that works but -- why :/
The problem is that the gradle plugin
doesn't declare dependency on assemble task (in general, regardless of apk splits, by gradle convention, you shouldn't just "expect" the apks to be there)
doesn't generate tasks per apk splits -- but you do for flavors
So here is the work around for it:
// Generate firebase app distribution task variants for all abis
applicationVariants.all { variant ->
variant.outputs.all { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi == null) return
def abiName = abi.replace("_", "").replace("-", "")
task("appDistributionUpload${abiName.capitalize()}${variant.name.capitalize()}", type: com.google.firebase.appdistribution.gradle.UploadDistributionTask_Decorated) {
appDistributionProperties = new com.google.firebase.appdistribution.gradle.AppDistributionProperties(
new com.google.firebase.appdistribution.gradle.AppDistributionExtension(),
project,
variant
)
appDistributionProperties.apkPath = output.outputFile.absolutePath
appDistributionProperties.serviceCredentialsFile = project.file("secrets/ci-firebase-account.json")
appDistributionProperties.releaseNotes = abi
appDistributionProperties.groups = "ra-testers"
// Add dependsOn respective assemble task, so it actually
// builds apk it wants to upload, not just expect it to be there
dependsOn "assemble${variant.name.capitalize()}"
}
}
}

How can I reduce the size of xml assets when building my apk

In my app I use the assets directory to store some XML files (some are large).
In my design time I want the files to use indentation and also put some comments in it.
This is enlarging my xml files and can add up to a large size.
Is it possible to add a task to the gradle build to remove all indentation and comments for the xml files before packaging it in the apk? If so how?
This will not only shrink my apk, but will also assist at run time with the xml processing.
EDIT
The answer by fhomovc was correct, but was missing some part.
I will mark it as correct but if anyone else will need it, here are the details:
In general I need a task that will run the minify utility and it should look like:
task minifyAssets(type:Exec) {
workingDir dirName // the directory of the merged assets under the build directory
commandLine 'minify', '-r', '-o', '.', '.'
doFirst {
println 'minifyAssets...'
}
}
This task should only be executed after the merged assets task is executed and before the package task is executed.
The main problem is that there should be a dedicated task for each variant, so I needed to do it dynamically:
First create the exec task and make it dependent on the merge task
applicationVariants.all { variant ->
// dynamically add minify task for specific variant
def dirName = new File("app\\build\\intermediates\\merged_assets\\" + variant.name + "\\out\\levels").getAbsolutePath()
def minifyTaskName = "minifyAssets" + variant.name
def mergeAssetsTaskName = "merge" + variant.name + "Assets"
def myTask = tasks.register(minifyTaskName, Exec) {
workingDir dirName
// require that minify utility will be in the path. Download from https://github.com/tdewolff/minify/tree/master/cmd/minify
commandLine 'minify', '-r', '-o', '.', '.'
doFirst {
println 'minifyAssets...' + workingDir
}
}
// set the minify task dependant on the merge assets task
myTask.get().dependsOn mergeAssetsTaskName
}
Now we need to make the specific package task depend on the minify task:
// when the package task is added make it dependant on the minify task
tasks.whenTaskAdded { theTask ->
if (theTask.name.startsWith("package") && (theTask.name.endsWith("Debug") || theTask.name.endsWith("Release"))) {
def minifyTaskName = theTask.name.replace("package", "minifyAssets")
theTask.dependsOn minifyTaskName
}
}
You can run a custom script with an xml minifier.
For the minifier: you can install minify following the installation steps provided.
For the script: you can refer to this answer. Essentially your task would look something like this
task executeScript(type:Exec) {
println 'Minifying xmls...'
//on linux
commandLine 'minify -r -o ./ --match=\.xml ./values' // ./values should be the path to your resources directory
}
Check the documentation to understand better how minify works. I haven't tested this solution myself, so it may need a few adjustments but you get the general idea. If you are using a Windows machine then the Script (commandLine) should be different, if I can find any examples online I'll add them.

Getting flavor in gradle task

I want to process generated .apk after gradle build. However, the place where the generated file resides depends on product flavor and build type. So how can I get those in the task?
Here's the relevant section of build.gradle:
productFlavors {
normal { }
tinybuild {}
}
// The following two blocks copy the generated .apk to server so that tablet you're working on can download it fast
task copyDebugAPK(type: Exec) {
workingDir "$projectDir/.."
commandLine 'python', 'myscript.py', 'script parameter'
}
afterEvaluate {
packageNormalDebug.finalizedBy(copyDebugAPK)
packageTinybuildDebug.finalizedBy(copyDebugAPK)
}
I could simply declare multiple tasks, one for each flavor / build, but I'm pretty sure that even using the script in flavor / build-type-dependent context should provide me with the relevant fields right within the task. Am I wrong?
Edit: after Robert's suggestion, I modified my task like so:
task copyDebugAPK(type: Exec) {
android.applicationVariants.all { variant ->
print("${com.android.build.OutputFile.ABI}")
print("${variant.name}")
print("${variant.flavorName}")
print("${variant.versionName}")
workingDir "$projectDir/.."
commandLine 'python', 'myscript.py', '${variant.name}'
}
}
However, no prints are being printed. I also added the variables to script input, but they are not resolved.

Android Gradle custom task to run multiple assembles? Tasks are not dsistinct

I am trying to create a custom Gradle task for an Android project that will build both my flavours at the same time, rename the APK's and then copy them to a different folder.
The issue is I have another version of this task that is almost identical to the first one and when I run one task a part of the other task gets run too.
Here is my first task:
task buildWithVersion(type: GradleBuild) {
delete "$buildDir/outputs/apk"
delete "$buildDir/outputs/verionapks"
tasks = ['assembleMyAppDebug', 'assembleMyAppRelease']
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "${variant.name}_${variant.versionName}.apk"
}
}
doLast {
copy {
from "$buildDir/outputs/apk"
into "$buildDir/outputs/verionapks"
include '**/release/*.apk'
}
}
}
And then the second one:
task buildWithoutVersion(type: GradleBuild) {
delete "$buildDir/outputs/apk"
delete "$buildDir/outputs/noneverionapks"
tasks = ['assembleMyAppDebug', 'assembleMyAppRelease']
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "${variant.name}.apk"
}
}
doLast {
copy {
from "$buildDir/outputs/apk"
into "$buildDir/outputs/noneverionapks"
include '**/release/*.apk'
}
}
}
I need two seperate tasks for this but the problem is when I run the first task then the apks get renamed as if the second task has run, so the apk's are renamed in this format "${variant.name}.apk", instead of the format I wanted in the first task. (The second task is after the first in my apps build.gradle file)
It looks like the android.applicationVariants.all loop gets run every time even though I only want it to run inside the task when I call it. Why is this? And is it possible to only have this run specifically when a specific task is run?

Gradle Copy APK file using publish task in Android Studio 3.0

Prior to Android plugin version 3.0.0-alpha4, I have been using the following for publishing different variants of my APKs to a specific file path:
def publish = project.tasks.create("publishAll")
android.applicationVariants.all { variant ->
def task = project.tasks.create("publish${variant.name}Apk", Copy)
task.from(variant.outputs[0].outputFile)
task.into(buildDir)
task.dependsOn variant.assemble
publish.dependsOn task
}
I originally got it from this answer from Xavier Ducrohet: Copying APK file in Android Gradle project
As of the new updates to Android Studio Preview which uses version 3.0.0-alpha4, variant.outputFile is deprecated. What is the new suggested way to achieve something like this?
EDIT:
Looks like there is no way to currently access the variant output file as pointed out here: https://developer.android.com/studio/preview/features/new-android-plugin-migration.html#variant_api
Looks like we'll have to wait until they introduce those apis
If you don't use abi splits next snippet works
project.afterEvaluate {
android.applicationVariants.all { variant ->
// create file where to copy
def backupFolder = rootProject.file("backup")
def backupFile = new File(backupFolder, String.format("%s_v%s.%d.apk", variant.flavorName, variant.versionName, variant.versionCode))
variant.outputs.all { output ->
Task copyAndRenameAPKTask = project.task("copyAndRename${variant.name.capitalize()}APK", type: Copy) {
from output.outputFile.getParent()
into backupFolder
include output.outputFileName
rename(output.outputFileName, backupFile.getName())
}
// if copyAndRenameAPKTask needs to automatically execute assemble before
copyAndRenameAPKTask.dependsOn(variant.assemble)
copyAndRenameAPKTask.mustRunAfter(variant.assemble)
// if assemble needs to automatically execute copyAndRenameAPKTask after
variant.assemble.finalizedBy(copyAndRenameAPKTask)
}
}
}

Categories

Resources