Multiple google-services.json Per Product Flavour - android

I think this is a variation on other questions that have been posed on this subject.
I have two product flavors. I deploy my app in different environments, each of which talk to different Firebase projects. As such, for each flavor I need to be able to target a specific environment (dev, test, production, etc.)
Is there a way, I can make build variants of a flavor that select the appropriate google-services.json file without introducing new product flavors? Maybe I am approaching this problem in the wrong way...

The only way I was able to do this was to bypass use of google-services.json and create FirebaseApp instance dynamically e.g.
if (<is dev>) {
apiKey = <dev api key>;
databaseUrl = <dev database url>;
} else if (<is test> {
apiKey = <>;
databaseUrl = <>;
} else // production {
apiKey = <>;
databaseUrl = <>;
}
FirebaseOptions firebaseOptions = new FirebaseOptions.Builder()
.setApiKey(apiKey)
.setApplicationId(context.getString(R.string.google_app_id))
.setDatabaseUrl(databaseUrl)
.build();
return FirebaseApp.initializeApp(context, firebaseOptions, "MyApp");

First, place the respective google_services.json for each buildType in the following locations:
app/src/debug/google_services.json
app/src/main/google_services.json
Now, let’s whip up some gradle tasks in your :app’s build.gradle to automate moving the appropriate google_services.json to app/google_services.json
task switchToDebug(type: Copy) {
description = 'Switches to DEBUG google-services.json'
from "src/debug"
include "google-services.json"
into "."
}
task switchToRelease(type: Copy) {
description = 'Switches to RELEASE google-services.json'
from "src/release"
include "google-services.json"
into "."
}
Great — but having to manually run these tasks before you build your app is cumbersome. We would want the appropriate copy task above run sometime before :assembleDebug or :assembleRelease is run. Let’s see what happens when :assembleRelease is run:
Zaks-MBP:my_awesome_application zak$ ./gradlew assembleRelease
Parallel execution is an incubating feature.
.... (other tasks)
:app:processReleaseGoogleServices
....
:app:assembleRelease
Notice the :app:processReleaseGoogleServices task. This task is responsible for processing the root google_services.json file. We want the correct google_services.json to be processed, so we must run our copy task immediately beforehand.
Add this to your build.gradle. Note the afterEvaluate enclosing.
afterEvaluate {
processDebugGoogleServices.dependsOn switchToDebug
processReleaseGoogleServices.dependsOn switchToRelease
}
Now, anytime :app:processReleaseGoogleServices is called, our newly defined :app:switchToRelease will be called beforehand. Same logic for the debug buildType. You can run :app:assembleRelease and the release version google_services.json will be automatically copied to your app module’s root folder.
https://medium.com/google-cloud/automatic-per-variant-google-services-json-configurations-with-gradle-d3d3e40abc0e

Related

Gradle task to take a build of specific build flavors (more than one)

In my app, I have more than 30 build variants. Every time when I release the app, I need to publish it to different platforms, therefore I build 5 different build variants.
Currently, I am doing this:
switch to build variant A
wait for the Gradle build
build APK/Bundle of build variant A
the same steps for B, C, E, and D.
What I am looking for is a Gradle task that just builds me these specific build variants when I run it. I know there is a task to build all build variants but it is too much for me.
I searched SO but couldn't find anything related to a point that I started to think it is impossible.
Could someone point me in the right direction? Thanks.
Calling a gradle task from another gradle task is not the best idea. You should rather describe their relationship as it usually works with all other gradle parts - by using mustRunAfter, dependsOn and so on. For your purposes you can use GradleBuild. I think you're searching for this - if I got your point right :D
I would assume that your task will look something like that (add your flavours instead of mine mocked)
task assembleFlavourBuilds(type: GradleBuild) {
description = 'creating flavour builds for the provided config'
tasks = ['assembleFree', 'assemblePro']
}
If you can use CommandLine then Gradle has good support for that.
For example :
task makeDir(type: Exec) {
workingDir "."
commandLine("cmd", "/c", "mkdir", "example")
}
Am just trying to show an example of how this work, by creating a folder here named example.
You can even add this to be automated with an already defined task, Like a build task. This can be done by using finalizeBy
tasks.named("build") { finalizedBy("makeDir") }
This will only call the task after a successful build.
And my suggestion is to make a .bat file with all the needed commands and call it in the same way as the following code :
task BuildAll(type: Exec) {
workingDir "."
commandLine("cmd", "/c", "mybat.bat")
}
And mybat.bat will contain all the needed commands to
switch build variant
build
bundle
repeat
Switch Build variant
productFlavors {
variantA {
dimension "version"
versionNameSuffix ".a"
}
variantB {
dimension "version"
versionNameSuffix ".b"
}
variantC {
dimension "version"
versionNameSuffix ".c"
}
variantD {
dimension "version"
versionNameSuffix ".d"
}
variantE {
dimension "version"
versionNameSuffix ".e"
}
}
you can use gradle task. in your root build.gradle define task like bellow:
task assembleFlavorBuilds(type: GradleBuild) {
description = 'Description of task.'
tasks = ['assembleFlavorName1Debug', 'assembleFlavorName1Release']
}
each of item in tasks are like this: "assemble"+FlavorName(first of name must be capital)+build type(Debug or Release).
for instance if you have flavors with names "free" and "paid", your task must be like this:
task assembleFlavorBuilds(type: GradleBuild) {
description = 'Description of task.'
tasks = ['assembleFreeRelease', 'assemblePaidRelease']
}
after sync project you can see this task in gradle area in idea or just click on run icon next of this task.

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

Gradle task for Sentry not compiling

I have to add the Analytics tool Sentry to our Android project. In order to make it work, one needs to create mappings for the obfuscated code (from Proguard/R8) and upload it later to Sentry.
On the website https://docs.sentry.io/platforms/android/ it is even described how to do that.
There it is written that one needs to create a gradle task looking like this:
gradle.projectsEvaluated {
android.applicationVariants.each { variant ->
def variantName = variant.name.capitalize();
def proguardTask = project.tasks.findByName(
"transformClassesAndResourcesWithProguardFor${variantName}")
def dexTask = project.tasks.findByName(
"transformClassesWithDexFor${variantName}")
def task = project.tasks.create(
name: "processSentryProguardFor${variantName}",
type: Exec) {
workingDir project.rootDir
commandLine *[
"sentry-cli",
"upload-proguard",
"--write-properties",
"${project.rootDir.toPath()}/app/build/intermediates/assets" +
"/${variant.dirName}/sentry-debug-meta.properties",
variant.getMappingFile(),
"--no-upload"
]
}
dexTask.dependsOn task
task.dependsOn proguardTask
}
}
This shall wait until Proguard is finished, than copy this properties file to the assets. However, when I add this to my Android gradle script I get the error:
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
No signature of method: java.util.ArrayList.multiply() is applicable for argument types: (ArrayList) values: [[sentry-cli, upload-proguard,
--write-properties, {Application-Path}/app/build/intermediates/assets/playStoreStaging/debug/sentry-debug-meta.properties,
...]] Possible solutions: multiply(java.lang.Number),
multiply(java.lang.Number)
I assume there is something wrong with the multiplication symbol * before the commandLine array. But when I remove it I get the error
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
Cannot cast object 'sentry-cli' with class 'java.lang.String' to class 'int'
So I tried to test this with only that line
commandLine "sentry-cli", ...
Which gave me another error
What went wrong: Cannot invoke method dependsOn() on null object
Thus I assume something went really wrong with that gradle script since it seems the dependend task can't be found. Does anyone have any idea how to fix this (or optionally have any other idea how to copy that sentry-debug-meta.properties file to my assets in another way, once Proguard/R8 is finished)?
Thanks!
-------- EDIT --------
I noticed something important.
The gradle tasks are defined in a different name than what was defined in the manual. Looking at my tasks I have them named
transformClassesAndResourcesWithR8For...
and
transformClassesWithDexBuilderFor...
However, I print the variantName then for checking but it seems my tasks are incomplete.
In my tasks list there exist
transformClassesAndResourcesWithR8ForPlayStoreStagingDebug
but not
transformClassesAndResourcesWithR8ForPlayStoreStagingRelease
and thus the task can't be found. I think that is the real problem here. So where are these gradle tasks defined?
------- EDIT 2 --------
Okay I noticed something strange here. Some variants don't have tasks. It makes sense that DEBUG tasks don't have R8 tasks but I found this here:
Variant: PlayStoreStagingRelease DexTask is null
Variant: PlayStorePreviewRelease DexTask is null
Variant: HockeyAppRelease DexTask is null
Variant: LocalServerRelease DexTask is null
Variant: PlayStoreProductionRelease DexTask is null
So how can this be?
I'd recommend using the Sentry Gradle integration (Gradle plugin) which is described here https://docs.sentry.io/platforms/android/#gradle-integration
The official Android Gradle plugin changed its task names over versions, Gradle version also affects those code snippets.
Google also replaced Proguard with R8 and it also affected those code snippets.
Is there a reason why not using the Sentry Gradle integration? if so, We'll be looking into updating them.
Thanks.
java.util.ArrayList.multiply() hints for that * in front of the [ ] list, which looks strange to me. Try removing the *[ ], only keeping List<String> (there's no ArrayList expected, to begin with):
commandLine "sentry-cli", "upload-proguard", "--write-properties", "${project.rootDir.toPath()}/app/build/intermediates/assets/${variant.dirName}/sentry-debug-meta.properties", variant.getMappingFile(), "--no-upload"
You'd have to look up how your tasks are actually being called, but it should be something alike:
def r8Task = project.tasks.findByName("transformClassesAndResourcesWithR8For${variantName}")
def d8Task = project.tasks.findByName("transformClassesWithDexBuilderFor${variantName}")
With a null check, because not every variant might have minifyEnabled true set:
if(r8Task != null) {
d8Task.dependsOn task
task.dependsOn r8Task
}
Maybe even a previous null check is required, because variant.getMappingFile() needs R8.
And that some flavors have no D8 task might be based upon the absence of code (nothing to do).
Here's a summary of the steps that I followed for integrating Sentry with my Android app. These steps are to ensure the sentry gradle plugin works as expected and automatically uploads the proguard mapping files, without you having to worry about uploading using cli. I assume you would have setup the Sentry SDK as described here:
https://docs.sentry.io/platforms/android/#integrating-the-sdk
Ensure you have Android Studio gradle plugin 3.5.0 (Not 3.6.x, that seems to break the sentry plugin. I observed that the sentry proguard or native symbol upload tasks are not configured or executed at all). This value should be in your root project's build.gradle in dependencies block
Provide a sentry.properties file the root folder of your project. The sentry.properties file should have the following values at minimum:
defaults.project=your_sentry_project_name
defaults.org=your_sentry_org_name
auth.token=sentry_project_auth_token
You can get info about generating auth tokens here: https://sentry.io/settings/account/api/auth-tokens/
(Optional: If you have build flavors) In my case, I have different flavors for my app. So, I had to put the sentry.properties inside my flavor specific folder in /app/src/ folder. Then, I wrote a gradle task to copy the flavor specific sentry.properties file into the project's root folder during gradle's configuration phase. Example:
task copySentryPropertiesTask {
if (getBuildFlavor() != null && !getBuildFlavor().isEmpty()) {
println("Copying Sentry properties file: ${getBuildFlavor()}")
copy {
from "src/${getBuildFlavor()}/"
include "sentry.properties"
into "../"
}
}
}
def getBuildFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1)
else {
println "NO MATCH FOUND"
return ""
}
}
Note 1: You can place this task in your app/build.gradle anywhere (I had placed it at the end).
Note 2: If you followed step 3 for build flavors, you can also add the root folder's sentry.properties in .gitignore. Since, it will be copied everytime you create a build.
Sentry should now be able to upload the proguard files for any release builds (or if you set minifyEnabled=true for any buildType).

Copy only certain assets to APK based on gradle flavors

I have an Android Studio project which uses NDK and CMake and externalNativeBuild. To reduce packet size I have several flavors for different texture compression formats. There are no code changes, i.e. all the resulting APKs are using exactly the same code.
productFlavors {
ETC2 {
manifestPlaceholders = [supportedTexture: "GL_OES_compressed_ETC2_RGB8_texture"]
}
DXT {
manifestPlaceholders = [supportedTexture: "GL_EXT_texture_compression_dxt1"]
}
ATC {
manifestPlaceholders = [supportedTexture: "GL_AMD_compressed_ATC_texture"]
}
//...and list goes on...
}
What this means in practice is that I have a Copy task which checks the current flavor and copies the correct texture pack into the APK. The getCurrentFlavor() function is copied from How to get current flavor in gradle:
task copyTexSD(type: Copy) {
def currentFlavor = getCurrentFlavor()
if(currentFlavor == "etc2") {
from 'bin/tex/ETC2.bin'
}
else if(currentFlavor == "dxt") {
from 'bin/tex/DXT.bin'
}
else if(currentFlavor == "atc") {
from 'bin/tex/ATC.bin'
}
//...
into 'src/main/assets/tex'
}
To build averything I use the following batch command:
call gradlew clean
call gradlew assembleETC2Release
call gradlew assembleDXTRelease
call gradlew assembleATCRelease
This works otherwise well, but for some reason the texture packets which are copied to previous APKs are also included in the subsequent APKs like this:
app-ETC2-release.apk contains only ETC2.bin file
app-DXT-release.apk contains DXT.bin and ETC2.bin
app-ATC-release.apk contains ATC.bin, DXT.bin and ETC2.bin
Why the build process includes assets from previous Gradle task? How can I make the build process to have only one texture file per APK?
Somehow I had a wrong assumption that each gradle task would be independent. Of course all the file(s) that have been copied in the assets folder in previous tasks are still there if they are not explicitly deleted.
So, there seems to be two possibilities to get this to work:
1) Modify the batch file to delete data from the texture folder before calling next gradle task.
OR
2) Create delete task in gradle file which runs before copy task. Examples can be found here: Gradle - Delete files with certain extension .

Canonical way to write gradle task for adding new Android product flavors

I want to automate work with flavors in Android project, using custom Gradle tasks.
For example, implement addFlavor task with 2 parameters: flavorName and flavorLogo. The task should add new flavor, by making next steps:
Update productFlavors section in build.gradle file, by adding a new flavor to it:
flavorName {
buildConfigField 'String', 'PARTNER', '"flavorName"'
packageName 'com.stackoverflow.askquestion.flavorname'
}
Add flavor folder and copy flavorLogo .png file to flavorName/res/drawable-xxhdpi/ic_launcher.png
What is the correct and canonical way to do it with custom a Gradle task?
You cannot use tasks to create Flavors. Flavors describe the model used to create tasks, so it won't work the other way around.
However, you can diretly put code in your build.gradle that will create this for you everytime the build.gradle is executed (basically everytime you run a Gradle command). You could write something like:
String[] customFlavors = ['flavor1', 'flavor2']
customFlavors.each { name ->
def flavor = android.productFlavors.create(name)
flavor.buildConfigField('String', 'PARTNER', "\"${name}\""
flavor.packageName("com.so.question.${name}")
}

Categories

Resources