My Android app communicates with a server defined in a buildConfigField in app/build.gradle:
buildConfigField "android.net.Uri", "server", 'android.net.Uri.parse("https://app.example.com")'
(We use a buildConfigField because this allows different "flavors" of the app to point to different servers.)
Sometimes developers need to change this value to point to a local copy of the server software running on a different server.
The problem I have is that these developers regularly forget they have made these changes and then blindly commit all of their changes to the git repository, thus breaking the app for everyone else until someone else changes it back to the original value.
Is there a way to allow developers to override the buildConfigField from a separate (.gitingored) file so they cannot commit their changes?
Inspired by a comment from #TTT, here is how I am now generating BuildConfig fields from a .env file (or Environment Variables). To prevent sharing unwanted variables (such as other system variables that might have sensitive data), it only handles variables with names that begin with ANDROID_.
This requires the co.uzzu.dotenv.gradle Gradle plugin to be set up in the project.
androidComponents {
onVariants(selector().all(), { variant ->
env.allVariables.each {
if (it.key.startsWith("ANDROID_")) {
def key = it.key.substring(8)
.split("_")
.collect {
it.substring(0,1).toUpperCase(Locale.ROOT) +
it.substring(1).toLowerCase(Locale.ROOT)
}
.join("")
key = key.substring(0,1).toLowerCase(Locale.ROOT)+key.substring(1)
def type = "String"
def wrapper = '"%s"'
def value = it.value
if (value.isEmpty()) {
wrapper = '%s'
value = null
} else if (value.matches("-?\\d+")) {
type = "int"
wrapper = '%s'
} else if (value.matches("-?\\d+(.\\d+)")) {
type = "double"
wrapper = '%s'
} else if (value.matches("https?://.*")) {
type = "android.net.Uri"
wrapper = 'android.net.Uri.parse("%s")'
}
variant.buildConfigFields.put(key, new BuildConfigField(type, String.format(wrapper, value), "Field from .env"))
}
}
})
}
It supports variables that are boolean, int, double, String, or android.net.Uri (though only String can be null since it cannot determine the type without a value).
Related
I am trying to test the behavior of different stages in an Android app. Considering the official docs, I use different Gradle BuildTypes in the module's build.gradle to define the settings needed for each stage.
Each buildType block contains a String with a similar key STAGE_NAME and a different value for each stage for example test_stage, and uat_stage.
The value of those is being accessed in the app using BuildConfig.STAGE_NAME to change some of the parameters like API endpoints accordingly.
But the problem is, there seems to be no official way to unit test the resulting behavior for each stage.
The Android docs, mention a way to change the test buildType which explicitly defines which buildType is being used for testing, but I was wondering if there is another approach to test the resulting behaviors.
The module's build.gradle file:
android {
buildTypes {
release {
...
}
debug {
applicationIdSuffix ".debug"
buildConfigField "String", "STAGE_NAME", "\"debugStage\""
...
}
testStage {
initWith(debug)
buildConfigField "String", "STAGE_NAME", "\"testStage\""
...
}
uatStage {
initWith(debug)
buildConfigField "String", "STAGE_NAME", "\"uatStage\""
...
}
}
}
Example Retrofit interface which is supposed to use different API URLs for each stage by accessing the STAGE_NAME and find out which of the URLs should be used as API_URL:
interface FakeService {
#GET("stuff")
suspend fun getStuff(): List<Stuff>?
...
companion object {
// Different urls which should be used for each stage:
private const val URL_DEBUG = "https://debug_api.example.com/"
private const val URL_TEST = "https://test_api.example.com/
private const val URL_UAT = "https://uat_api.example.com/"
private const val URL_PROD = "https://prod_api.example.com/"
// This is where we access the STAGE_NAME to figure out which stage url should be used
val API_URL_TO_USE = getUrl(BuildConfig.STAGE_NAME)
fun getUrl(stageName: String?): String {
return when (stageName) {
"debugStage" -> URL_DEBUG
"testStage" -> URL_TEST
"uatStage" -> URL_QA
else -> URL_PROD
}
}
}
}
For example in this retrofit interface, I would like to test the behavior of getUrl function to make sure it returns the right URL for each stage.
UPDATE:
After looking at this question, I was wondering maybe one way of testing this, could be mocking the BuildConfig. But since the STAGE_NAME is only available after a successful build, it can not be mocked normally.
I define a variable in order to add different plugins to project, but it seems not working at all.
variantFilter { variant ->
def names = variant.flavors*.name
println names
if (names.contains("someFlavor")) {
google_service = false
}else{
google_service = true
}
}
Answered here.
I have more than one flavor in my app and I want to use the same google-service.json for all of them, so I've thought about set the value of the attribute package_name as a regular expression and replace it using a task in my build.gradle (app module).
My flavors are defined on this way:
android {
productFlavors {
FirstFlavor {
applicationId "com.thisapp.first"
versionCode = 1
versionName "1.0"
}
SecondFlavor {
applicationId "com.myapp.second"
versionCode = 1
versionName "1.0"
}
}
}
My idea was something like:
task runBeforeBuild(type: Exec) {
def google_json = file('./google-services.json')
google_json.getText().replace('${package_name_value}', myPackageName)
}
The problem is I don't know how to access to the PackageName (myPackageName in the code) or if is even possible.
Maybe I have to use another task instead of runBeforeBuild, I'm not very familiar with Gradle.
I've found another way to do it, and wanted to share it.
Please note that this is my first time writing some tasks using gradle, so the code is not optimal at all (and I can't spend more time on it to improve it for now).
Explanation
What I'm doing is pretty simple.
1) Just before the task processFlavorBuildTypeGoogleServices, that is the task from Google Services that will read the google-services.json file, I trigger some code that will update the google-services.json file.
In order to do that :
gradle.taskGraph.beforeTask { Task task ->
if (task.name.startsWith("process") && task.name.endsWith("GoogleServices")) {
}
}
2) Retrieve the current flavor and buildType from the task name (example of a task name: processProdReleaseGoogleServices in the form of process'Flavor''BuildType'GoogleServices)
String currentFlavor = task.name.replace("process", "").replace("GoogleServices", "")
currentFlavor = currentFlavor.toLowerCase()
3) Remove the buildType from the currentFlavor variable. In order to do that, I simply loop through all the buildTypes in my project, and remove them from the currentFlavor variable
android.applicationVariants.all { variant ->
currentFlavor = currentFlavor.replace(variant.buildType.name, "")
}
At this point, the variable currentFlavor has the currentFlavor (for example "prod")
4) Retrieve the package name from the flavors defined in my build.gradle
In my build.gradle, I specify the packageName for each flavor:
productFlavors {
prod {
applicationId 'packageName1'
}
rec {
applicationId 'packageName2'
}
}
And I retrieve it like this:
(The package name is returned with [], so I have to remove them. For example I would retrieve [packageName1])
String currentApplicationId;
android.applicationVariants.all { variant ->
if (variant.flavorName == currentFlavor) {
currentApplicationId = variant.productFlavors.applicationId.toString().replace("[", "").replace("]", "")
}
}
5) Now that I have the package name of the current build, I just have to open the current google-services.json file, and update the package name inside. For that I added a method updateGoogleServicesJsonFile.
Be careful to change the filePath on the second line to point to your location.
def updateGoogleServicesJsonFile(applicationId) {
File file = new File(getProjectDir(), "/google-services.json")
if (!file.exists())
{
project.logger.log(LogLevel.ERROR, "Error updating the google-services.json because the file doesn't exists...")
return
}
List<String> lineList = file.readLines()
for (int i = 0; i < lineList.size(); i++)
{
if (lineList.get(i).trim().startsWith("\"package_name\": \""))
{
String line = lineList.get(i)
line = line.substring(0, line.indexOf(":") + 1)
line += " \"" + applicationId + "\""
lineList.set(i, line)
}
}
file.write(lineList.join("\n"))
}
And there you have it, some code to update the google-services.json file just before the task to read it is executed.
Code
def updateGoogleServicesJsonFile(applicationId) {
File file = new File(getProjectDir(), "/google-services.json")
if (!file.exists())
{
project.logger.log(LogLevel.ERROR, "Error updating the google-services.json because the file doesn't exists...")
return
}
List<String> lineList = file.readLines()
for (int i = 0; i < lineList.size(); i++)
{
if (lineList.get(i).trim().startsWith("\"package_name\": \""))
{
String line = lineList.get(i)
line = line.substring(0, line.indexOf(":") + 1)
line += " \"" + applicationId + "\""
lineList.set(i, line)
}
}
file.write(lineList.join("\n"))
}
gradle.taskGraph.beforeTask { Task task ->
// Before the task processFlavorBuildTypeGoogleServices (such as processProdReleaseGoogleServices), we update the google-services.json
if (task.name.startsWith("process") && task.name.endsWith("GoogleServices")) {
// Getting current flavor name out of the task name
String currentFlavor = task.name.replace("process", "").replace("GoogleServices", "")
currentFlavor = currentFlavor.toLowerCase()
android.applicationVariants.all { variant ->
currentFlavor = currentFlavor.replace(variant.buildType.name, "")
}
// Getting current application id that are defined in the productFlavors
String currentApplicationId;
android.applicationVariants.all { variant ->
if (variant.flavorName == currentFlavor) {
currentApplicationId = variant.productFlavors.applicationId.toString().replace("[", "").replace("]", "")
}
}
updateGoogleServicesJsonFile(currentApplicationId)
}
}
Answer updated
First of all I must explain I'm using Jenkins to compile my application, so the build process is not exactly the same than in Android Studio. In my case Jenkins only build the release version and is not getting the flavors on the same way than the IDE. I'll explain both solutions:
In the build.gradle (Module: app)
Mine
buildscript{
...
}
android {
...
}
afterEvaluate {
android.applicationVariants.all { variant ->
preBuild.doLast {
setGoogleServicesJson(variant)
}
}
// Only for Jenkins
assembleRelease.doFirst {
deleteGoogleServicesJson()
}
}
def setGoogleServicesJson(variant) {
def originalFileName = "google-services.bak"
def newFileName = "google-services.json"
def originalFile = "./$originalFileName"
def newFile = "./$newFileName"
def applicationId = variant.applicationId
def regularExpression = "\\\"package_name\\\" : \\\"(\\w(\\.\\w)?)+\\\""
def packageName = "\\\"package_name\\\" : \\\"$applicationId\\\""
copy {
from (originalFile)
into ("./")
rename (originalFileName, newFileName)
}
ant.replaceregexp(
file: newFile,
match: regularExpression,
replace: packageName,
byLine: true)
}
def deleteGoogleServicesJson() {
file("./google-services.json").delete()
}
apply plugin: 'com.google.gms.google-services'
Jenkins is getting the google-services.json located in the 'Project/app/' folder and it doesn't use the flavor ones, so for each variant and as soon as possible (after the preBuild task) I'm creating a new JSON from my *.bak file, overriding the package_name and letting Gradle continues with the building.
When everything is done and before it release the app (assembleRelease.doFirst) I delete the google-services.json and I keep the *.bak.
In my case I only want to change the package_name value of my JSON, but this solution won't work if I want to change another value as the project_number, the client_id or whatever else depending on the flavor.
Alternative solution (using flavors)
afterEvaluate {
android.applicationVariants.all { variant ->
def fileName = "google-services.json"
def originalFile = "./$fileName"
def flavorName = variant.flavorName
def destinationPath = "."
// If there is no flavor we use the original path
if (!flavorName.empty) {
destinationPath = "$destinationPath/src/$flavorName/"
copy {
from file(originalFile)
into destinationPath
}
}
def regularExpression = "\\\"package_name\\\" : \\\"(\\w(\\.\\w)?)+\\\""
def packageName = "\\\"package_name\\\" : \\\"$variant.applicationId\\\""
ant.replaceregexp(
file: "./$destinationPath/$fileName",
match: regularExpression,
replace: packageName,
byLine: true)
}
}
In this solution I have the google-services.json in the 'Project/app/' folder and I make a copy of it in each flavor folder. Then I override the package_name. In case you are working without flavors, the app will use the original JSON to compile.
You can check if another JSON exists in the flavor folder before override it, in case you have different values for the rest of the values.
Old solution
I've found a solution mixing this and this answers.
This is my build.gradle (Module: app) right now:
afterEvaluate {
android.applicationVariants.all { variant ->
def applicationId = variant.applicationId
ant.replaceregexp(file: './google-services.json', match:'package_name_value', replace: applicationId, byLine: true)
}
}
where package_name_value is the "regular expression" I've defined to be replaced.
The location of the google-services.json is "MyProject/ppp/google-services.json", and I've tested that if you put another googler-services.json inside your flavor folder, it overrides the first one.
*There is (at least) one problem when you have more than one flavor defined at the same time, because this task is always overriding the same file, so the final application id will be the last you have defined.
If you have another way, feel free to post it.
Thanks for the advice, in the doc it says:
TextResource ruleSetConfig
Note: This property is incubating and may change in a future version of Gradle.
The custom rule set to be used (if any). Replaces ruleSetFiles, except that it does not currently support multiple rule sets. See the official documentation for how to author a rule set. Example: ruleSetConfig = resources.text.fromFile(resources.file("config/pmd/myRuleSets.xml"))
But if I try
setRuleSetConfig(project.resources.text.fromFile(project.resources.file("pmd.xml")))
it says:
Caused by: groovy.lang.MissingMethodException: No signature of method: org.gradle.api.internal.resources.DefaultResourceHandler.file() is applicable for argument types: (java.lang.String) values: [pmd.xml]
Possible solutions: find(), find(groovy.lang.Closure), use([Ljava.lang.Object;), is(java.lang.Object), with(groovy.lang.Closure), wait()
at com.barista_v.android_quality.MyPmdTask.<init>(MyPmdTask.groovy:20)
at com.barista_v.android_quality.MyPmdTask_Decorated.<init>(Unknown Source)
at org.gradle.api.internal.DependencyInjectingInstantiator.newInstance(DependencyInjectingInstantiator.java:48)
at org.gradle.api.internal.ClassGeneratorBackedInstantiator.newInstance(ClassGeneratorBackedInstantiator.java:36)
at org.gradle.api.internal.project.taskfactory.TaskFactory$1.call(TaskFactory.java:121)
... 82 more
Also I am trying in this way::smile:
class MyPmdTask extends Pmd {
MyPmdTask() {
project.extensions.pmd.with {
reportsDir = project.file("$project.buildDir/outputs/")
}
source = project.fileTree('src/main/java').toList()
ruleSets = []
reports {
xml.enabled = false
html.enabled = true
}
setIgnoreFailures(true)
setRuleSetConfig(ResourceUtils.readTextResource(project, getClass().getClassLoader(), "pmd.xml"))
}
}
class ResourceUtils {
static TextResource readTextResource(Project project, ClassLoader classLoader, String fileName) {
println "reading config file $fileName"
def inputStream = classLoader.getResourceAsStream(fileName)
project.resources.text.fromString inputStream.text
}
}
and it set the config file but it dont use my rules, dont crash but dont use my rules.
I tried setting in
project.extensions.pmd.with {
// HERE
}
when I try getRuleSetConfig() it returns null but with setRuleSetConfig() appears to be setted but not used.
I dont want to create another repo just for configurations like here https://github.com/MovingBlocks/TeraConfig.
Related to: https://discuss.gradle.org/t/how-can-i-read-setup-resources-from-plugin-jar/13274/4
what can I do?
I was able to create a custom plugin that adds a PMD task of the built-in type, using a custom ruleset packaged in the plugin's src/main/resources directory; I'm using kotlin, but you can probably get the idea:
class BuildPlugin: Plugin<Project> {
override fun apply(project: Project) {
val android = project.extensions.getByType(BaseExtension::class.java)
project.pluginManager.apply(PmdPlugin::class.java)
project.extensions.getByType(PmdExtension::class.java).toolVersion = "5.5.2"
project.tasks.register("pmd", Pmd::class.java) {
it.source(android.sourceSets.getByName("main").java.sourceFiles,
android.sourceSets.getByName("test").java.sourceFiles,
android.sourceSets.getByName("androidTest").java.sourceFiles)
it.ruleSetConfig = project.resources.text.fromUri(javaClass.classLoader.getResource("config/pmd.xml"))
}
}
}
It's still a work in progress (for instance, I'm unsure if there are implications to resolving the android extension directly in my apply), but it seems to use my ruleset as expected!
I just moved to the android gradle build system and it is not clear for me how to set constant values.
e.g. I have
API.java which contains
BASE_URL = "http://debug.server.com"
but when gradle builds release I need BASE_URL to be "http://release.server.com"
How to replace this value at the build time?
Gradle generates a class called BuildConfig which contains static members (such as the boolean DEBUG, which is set to true for debug variants).
You could either query this in java like so:
if (BuildConfig.DEBUG) {
BASE_URL = "http://debug.server.com"
} else {
BASE_URL = "http://release.server.com"
}
or the same thing as a one-liner:
BASE_URL = BuildConfig.DEBUG ? "http://debug.server.com" : "http://release.server.com"
OR
You could actually set the BASE_URL inside the the BuildConfig class using gradle like so:
android {
buildTypes {
debug {
buildConfigField "String", "BASE_URL", '"http://debug.server.com"'
}
release {
buildConfigField "String", "BASE_URL", '"http://release.server.com"'
}
}
}
Note the single and double quotations around the value in gradle, as others have mentioned in the comments. This way, the double quotes become part of the value.
As a result, the static reference BuildConfig.BASE_URL would point to the corresponding URL (i.e. "debug.server.com" for debug, "release.server.com" for release)
Answer of fifer-sheep is correct. Just wanted to leave a solution for more than two environments.
Two flavors for staging and live.
productFlavors {
staging {
...
}
production {
...
}
}
Whole app config relies on the current ENV. Using:
public static String ENV_DEVELOPMENT = "development";
public static String ENV_STAGING = "staging";
public static String ENV_PRODUCTION = "production";
ENV = BuildConfig.DEBUG ? ENV_DEVELOPMENT : BuildConfig.FLAVOR;
I can switch between all different ENVs while testing locally but force staging/live settings if released.