So, I have this code in my build.gradle:
defaultConfig {
manifestPlaceholders = [fabricKey: getFabricKey()]
}
def getFabricKey() {
String path = System.getenv("HOME") + "/.apikeys/fabric_key.gradle"
if (file(path).exists()) {
return new File(path).text;
} else {
return "null";
}
}
While in AndroidManifest.xml I have this:
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabricKey}" />
~/.apikeys/fabric_key.gradle exist on my machine. However, when I'm trying to build the project, "Crashalytics developer tools error" pops up.
If I just place my key instead of ${fabricKey}, it works just fine.
What am I doing wrong?
Also, just to clarify things: What I want to achieve is, while having apikey removed from publically-accessible git repo, still be able to produce builds on my Jenkins server.
EDIT:
After running gradlew assembleDebug by-hand instead of IDE, I've found out this:
java.lang.IllegalArgumentException: Crashlytics found an invalid API key: myValidApiKey .
(where myValidApiKey is a key that works when replacing ${fabricKey} with itself).
Related
What I'm trying to achieve
I'm trying to generate my REST API client for Android using OpenAPI Generator from the build.gradle script. That way, I wouldn't have to run the generator command line every time the specs change. Ideally, this would be generated when I build/assemble my app, and the sources would end up in the java (generated) folder, where generated sources are then accessible from the code (this is what happens with the BuildConfig.java file for example).
What I've tried so far
Following this link from their official GitHub, here's the build.gradle file I ended up with:
apply plugin: 'com.android.application'
apply plugin: 'org.openapi.generator'
...
openApiValidate {
inputSpec = "$rootDir/app/src/main/openapi/my-api.yaml"
recommend = true
}
openApiGenerate {
generatorName = "java"
inputSpec = "$rootDir/app/src/main/openapi/my-api.yaml"
outputDir = "$buildDir/generated/openapi"
groupId = "$project.group"
id = "$project.name-openapi"
version = "$project.version"
apiPackage = "com.example.mypackage.api"
invokerPackage = "com.example.mypackage.invoker"
modelPackage = "com.example.mypackage.model"
configOptions = [
java8 : "true",
dateLibrary : "java8",
library : "retrofit2"
]
}
...
First, I've never managed to get the API generated with the build/assemble task, even when I tried adding:
compileJava.dependsOn tasks.openApiGenerate
or
assemble.dependsOn tasks.openApiGenerate
The only way I could generate the sources was by manually triggering the openApiGenerate task:
Then, when I do generate my sources this way, they end up in the build folder but aren't accessible from my code, and aren't visible in the java (generated) folder:
I then have to manually copy/paste the generated source files to my project sources in order to use the API.
Even though I'm able to work around these issues by adding manual procedures, it would be way more maintainable if the whole process was simply automatic. I was able to achieve a similar result with another tool, Protobuf. Indeed, my gradle task gets triggered every time I build the app, and the sources end up in the java (generated) folder, so I don't have to do any additional work. The task is much simpler though, so I assume the main work that I'm not able to replicate with OpenAPI Generator is handled by the Protobuf plugin itself.
You have to specify path to the generated sources as a custom source set for your Gradle module, which is app in this case, as described here – https://developer.android.com/studio/build/build-variants#configure-sourcesets. That way Gradle will treat your sources as accessible from your code.
Something like this:
android {
...
sourceSets {
main {
java.srcDirs = ['build/generated/openapi/src/main/java']
}
}
...
}
I solved the issue you described like this, I'm using gradle.kts however.
See my build.gradle.kts
plugins {
// Your other plugins
id("org.openapi.generator") version "5.3.0"
}
openApiGenerate {
generatorName.set("kotlin")
inputSpec.set("$rootDir/app/src/main/openapi/my-api.yaml")
outputDir.set("$buildDir/generated/api")
// Your other specification
}
application {
// Your other code
sourceSets {
main {
java {
// TODO: Set this path according to what was generated for you
srcDir("$buildDir/generated/api/src/main/kotlin")
}
}
}
}
tasks.compileKotlin {
dependsOn(tasks.openApiGenerate)
}
You need to build the application at least once for the IDE to detect the library (at least this is the case for me in Intellij)
Your build should automatically generate the open api classes , to refer the generated classes in your java project you should add the generated class path to your source directory like it was mentioned in the other answers
https://developer.android.com/studio/build/build-variants#configure-sourcesets
As far as the task dependency goes , in android tasks are generated after configuration thus for gradle to recognize the task , wrap it inside afterEvaluate block like
afterEvaluate {
tasks.compileDebugJavaWithJavac.dependsOn(tasks.openApiGenerate)
}
I had this issue, and this answer https://stackoverflow.com/a/55646891/14111809 led me to a more informative error:
error: incompatible types: Object cannot be converted to Annotation
#java.lang.Object()
Taking a look at the generated files that were causing this error, noticed:
import com.squareup.moshi.Json;
After including a Moshi in the app build.gradle, the build succeeded and the generated code was accessible.
implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
Since update to Android Studio 3.3 we get a weird warning with some legacy code about encrypted gradle parameters.
In build.gradle we have this line:
apply from: "encryption.gradle"
In encryption.gradle we have this content:
afterEvaluate {
android.applicationVariants.all { variant ->
def pwd = "";
variant.productFlavors.each { flavor ->
if (flavor.ext.has("pwd1")) {
pwd = flavor.ext.pwd1
}
}
if (pwd.isEmpty() && variant.buildType.ext.has("pwd2")) {
pwd = variant.buildType.ext.pwd2
}
variant.resValue 'string', 'pwd', encryptPassword(pwd, variant.signingConfig, variant.applicationId)
}
}
def String encryptPassword(String password, signingConfig, String applicationId) {
...
}
In the code we use it like this:
getString(R.string.pwd)
And since the AS update we get the following error:
Cannot resolve symbol 'pwd'
When compiling/building the project everything runs fine because it can find the parameter. But when working in the IDE, all files that try to use R.string.pwd are marked red and show the error message, which is pretty annoying.
Any way to make this go away easily? SuppressWarnings("all") and SuppressLint("all") are not helping.
Is afterEvaluate the right place to do this or would it be better somehow in the defaultConfig section of build.gradle?
Even i was facing similar issues. so i reverted my gradle to 3.2.1 and it works as expected, it might be a technical glitch/issue with gradle 3.3
While i was on gradle 3.3 i tried invalidating cache for android studio, restarted it etc, but nothing solved the issue.
Currently I have a react native app and the issue that I have is that is very time consuming to update the version on every build or commit.
Also, I have Sentry enabled so every time I build, some builds get the same version so some crashes are hard to determine where they came from.
Lastly, updating the version manually is error prone.
How can I setup my builds to generate an automatic version every time I build and forget about all of this manual task?
While the currently accepted answer will work, there is a much simpler, and therefore more reliable way to do it.
You can actually read the value set in package.json right from build.gradle.
Modify your android/app/build.gradle:
// On top of your file import a JSON parser
import groovy.json.JsonSlurper
// Create an easy to use function
def getVersionFromNpm() {
// Read and parse package.json file from project root
def inputFile = new File("$rootDir/../package.json")
def packageJson = new JsonSlurper().parseText(inputFile.text)
// Return the version, you can get any value this way
return packageJson["version"]
}
android {
defaultConfig {
applicationId "your.app.id"
versionName getVersionFromNpm()
}
}
This way you won't need a pre-build script or anything, it will just work.
Since I was working with this for several days, I decided to share with everyone how I did it, because it could help others.
Tools used:
GitVersion: We will use GitVersion to generate a semantic version automatically depending on many factors like current branch, tags, commits, etc. The toold does an excellent job and you can forget about naming your versions. Of course, if you set a tag to a commit, it will use that tag as name.
PowerShell: This command line OS built by Microsoft has the ability to be run from Mac, Linux or Windows, and I chose it because the builds can be agnostic of the OS version. For example I develop on Windows but the build machine has MacOS.
Edit App build.gradle
The app gradle only needs one line added at the end of it. In my case I have the Google Play Services gradle and I added it after that.
apply from: 'version.gradle'
version.gradle
This file should be in the same folder as your app gradle and this is the content:
task updatePackage(type: Exec, description: 'Updating package.json') {
commandLine 'powershell', ' -command ' , '$semver=(gitversion /showvariable Semver); Set-Content -path version.properties -value semver=$semver; npm version --no-git-tag-version --allow-same-version $semver'
}
preBuild.dependsOn updatePackage
task setVariantVersion {
doLast {
if (plugins.hasPlugin('android') || plugins.hasPlugin('android-library')) {
def autoIncrementVariant = { variant ->
variant.mergedFlavor.versionName = calculateVersionName()
}
if (plugins.hasPlugin('android')){
//Fails without putting android. first
android.applicationVariants.all { variant -> autoIncrementVariant(variant) }
}
if (plugins.hasPlugin('android-library')) {
//Probably needs android-library before libraryVariants. Needs testing
libraryVariants.all { variant -> autoIncrementVariant(variant) }
}
}
}
}
preBuild.dependsOn setVariantVersion
setVariantVersion.mustRunAfter updatePackage
ext {
versionFile = new File('version.properties')
calculateVersionName = {
def version = readVersion()
def semver = "Unknown"
if (version != null){
semver = version.getProperty('semver')
}
return semver
}
}
Properties readVersion() {
//It gets called once for every variant but all get the same version
def version = new Properties()
try {
file(versionFile).withInputStream { version.load(it) }
} catch (Exception error) {
version = null
}
return version
}
Now, let's review what the script is actually doing:
updatePackage: This task runs at the very beginning of your build (actually before preBuild) and it executes gitversion to get the current version and then creates a version.properties file which later be read by gradle to take the version.
setVariantVersion: This is called afterEvaluate on every variant. Meaning that if you have multiple builds like debug, release, qa, staging, etc, all will get the same version. For my use case this is fine, but you might want to tweak this.
Task Order: One thing that bothered me was that the version was being run before the file was generated. This is fixed by using the mustRunAfter tag.
PowerShell Script Explained
This is the script that gets run first. Let's review what is doing:
$semver=(gitversion /showvariable Semver);
Set-Content -path props.properties -value semver=$semver;
npm version --no-git-tag-version --allow-same-version $semver
Line 1: gitversion has multiple type of versions. If you run it without any parameter you will get a json file with many variants. Here we are saying that we only want the SemVer. (See also FullSemVer)
Line 2: PowerShell way to create a file and save the contents to it. This can be also made with > but I had encoding issues and the properties file was not being read.
Line 3: This line updates your package.json version. By default it saves a commit to git with the new version. --no-git-tag-version makes sure you don't override it.
And that is it. Now every time you make a build, the version should be generated automatically, your package.json updated and your build should have that specific version name.
App Center
Since I am using App Center to make the builds, I will tell you how you can use this in a Build machine. You only need to use a custom script.
app-center-pre-build.sh
#!/usr/bin/env sh
#Installing GitVersion
OS=$(uname -s)
if [[ $OS == *"W64"* ]]; then
echo "Installing GitVersion with Choco"
choco install GitVersion.Portable -y
else
echo "Installing GitVersion with Homebrew"
brew install --ignore-dependencies gitversion
fi
This is needed because GitVersion is not currently a part of the build machines. Also, you need to ignore the mono dependency when installing, otherwise you get an error when brew tries to link the files.
The #MacRusher version was fine for me. Just for further readers, I had to add .toInteger() to make it work. Since I'm using yarn version --patch to automatically upgrade the version in package.json I also had to take only the two first characters.
Here is the new version:
// On top of your file import a JSON parser
import groovy.json.JsonSlurper
def getVersionFromPackageJson() {
// Read and parse package.json file from project root
def inputFile = new File("$rootDir/../package.json")
def packageJson = new JsonSlurper().parseText(inputFile.text)
// Return the version, you can get any value this way
return packageJson["version"].substring(0,2).toInteger()
}
android {
defaultConfig {
applicationId "your.app.id"
versionName getVersionFromPackageJson()
}
}
When I am trying to build project with value of meta-data tag as a string reference, crashlytics fail with following error:
Crashlytics found an invalid API key: #string/crashlytics.
Check the Crashlytics plugin to make sure that the application has been added successfully!
Contact support#crashlytics.com for assistance.
Doesn't work
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="#string/crashlytics"/>
Works
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="1234567890..."/>
I am want to define different keys inside string.xml for different productFlavors of my android project.
Update
After writing to crashlytics support:
Currently we only are able to evaluate the AndroidManifest.xml at build time so we don't look at any strings resources so we only support a hard coded string. I'll definitely share this with the team that you're interested so we can look into supporting this in a future release.
Edit: The solution accepted is working only if you are using an old version of Crashlytics (I was using v1.1.11). If you are using Fabric SDK you will notice
the tasks of the plugin have changed considerably and the script below
will not work. Also the API secret is not needed anymore, therefore
you can just use the <meta> in the manifest to specify the API key along with a manifest placeholder defined in your flavor:
in build.gradle:
flavor1 {
...
manifestPlaceholders = [crashlyticsApiKey: CRASHLYTICS_API_SECRET_HERE]
...
}
in AndroidManifest.xml:
...
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="${crashlyticsApiKey}" />
...
There is another undocumented way to specify the Crashlytics key as noted here, and it is to use the crashlytics.properties (in the root of your project) to specify that value along with the API secret:
apiKey=YOUR_API_KEY
apiSecret=YOUR_API_SECRET
Unfortuntately this will not allow you to simply specify a different crashlytics.properties for each flavor, because it needs to be in the root of your project in order to be picked correctly by the gradle plugin. That means you need to generate that file dynamically.
The idea is to add the key/secret values in your flavor as custom properties, and generate the crashlytics.properties at buildtime, using the values from the current flavor to fill the file.
The build.gradle inside your android module should look like this:
...
productFlavors {
flavor1 {
...
set("crashlyticsApiKey", CRASHLYTICS_API_KEY_HERE)
set("crashlyticsApiSecret", CRASHLYTICS_API_SECRET_HERE)
...
}
...
}
File crashlyticsProperties = new File("${project.projectDir.absolutePath}/crashlytics.properties")
applicationVariants.all { variant ->
variant.productFlavors.each { flavor ->
def variantSuffix = variant.name.capitalize()
def generateResourcesTask = project.tasks.getByName("crashlyticsGenerateResources${variantSuffix}")
def generatePropertiesTask = task("crashlyticsGenerateProperties${variantSuffix}") << {
Properties properties = new Properties()
println "...copying apiSecret for ${variant.name}"
properties.put("apiSecret", flavor.crashlyticsApiSecret)
println "...copying apiKey for ${variant.name}"
properties.put("apiKey", flavor.crashlyticsApiKey)
properties.store(new FileWriter(crashlyticsProperties), "")
}
generateResourcesTask.dependsOn generatePropertiesTask
def cleanResourcesTask = project.tasks.getByName("crashlyticsCleanupResourcesAfterUpload${variantSuffix}")
cleanResourcesTask.doLast {
println "...removing crashlytics.properties"
crashlyticsProperties.delete()
}
}
}
...
Basically the script hooks in the building process and generates/fills the properties file just before the Crashlytics gradle plugin does its magic.
With Fabric's Crashlytics 2.6.6. I'm able to simply select a Build Variant from the menu (usually located on the left in Android Studio) and run the app. It takes a minute or so to propagate through to the Fabric dashboard, but I didn't have any need for a workaround.
I'm currently working on an Android project, and learning how to use git.
I'm blocked because of a problem with git : I have my Google Maps api key declared in my android-manifest file :
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="HEREISMYKEY"/>
Now, I'd like to push my code in github, but I can't push my AndroidManifest, because it contains my api key (which is supposed to remain secret).
I'd like to know if there is a way to modify it before every push, or maybe modify it each time I compile my application?
Thank you for your help !
Create a new *.xml file in your res/values (call it api-keys.xml or something similar).
Change your manifest to point to this string:
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="#string/GoogleMapsKey"/>
When you first push your changes to the public, put in a dummy key.
Then edit the file with your real API key.
Then add the file to your .gitignore file so it doesn't get pushed again.
I suggest to use Gradle to dynamically load the API key for different build types and load a dummy key if none is found. This has the advantage that also a CI can build your application without the need of a real API key, but every new developer in your team has to set up the API key.
create a properties file google-maps-api.properties in your root gradle folder. Ignore the file by adding this line gradle/google-maps-api.properties to your .gitignore and put in the following 2 properties:
googleMapsApiKeyDebug=[YOUR_DEBUG_GOOGLE_MAPS_API_KEY_HERE]
googleMapsApiKeyRelease=[YOUR_RELEASE_GOOGLE_MAPS_API_KEY_HERE]
Next create the following google-maps-api-config.gradle file in your root gradle folder. This Gradle file will load the property file you created in step 1.
ext {
googleMapsApiConfig = new GoogleMapsApiConfigLogic(project)
}
/*
Before you run your application, you need a Google Maps API key.
Using these values:
Package name:
at.ict4d.covid19map
SHA-1 certificate fingerprint:
DE:4A:F0:B1:F5:F4:00:88:B6:E0:03:3E:31:1F:11:88:3F:43:0D:8B
Follow the directions here:
https://developers.google.com/maps/documentation/android/start#get-key
Once you have your key (it starts with "AIza") create a file with the name "google-maps-api.properties" in the gradle folder:
gradle/google-maps-api.properties
Put in 2 properties:
googleMapsApiKeyDebug:[THE KEY YOU JUST CREATED]
googleMapsApiKeyRelease:[YOUR RELEASE GOOGLE MAPS KEY] (if you are just developing and not planning to publish then fill in "YOUR_RELEASE_KEY_HERE")
"gradle/google-maps-api.properties" example:
googleMapsApiKeyDebug=AIzaFJKLJKsdLFDDsjlkdfDFJKLdjkf
googleMapsApiKeyRelease=YOUR_RELEASE_KEY_HERE
Sync your project and build.
*/
class GoogleMapsApiConfigLogic {
public final keyGoogleMapsApiKeyDebug = "googleMapsApiKeyDebug"
public final keyGoogleMapsApiKeyRelease = "googleMapsApiKeyRelease"
private final configGoogleMapsApiPropsFileName = "gradle/google-maps-api.properties"
public final props
private final project
GoogleMapsApiConfigLogic(project) {
this.project = project
this.props = loadGoogleMapsApiProperties()
}
def loadGoogleMapsApiProperties() {
Properties propertiesObj
if ((propertiesObj = loadFromProperties("$project.rootDir/$configGoogleMapsApiPropsFileName")) != null) {
project.logger.quiet("google-maps-api-config: use local properties file for Google Maps API")
return propertiesObj
} else {
propertiesObj = new Properties()
propertiesObj.put(keyGoogleMapsApiKeyDebug, "YOUR_KEY_HERE")
propertiesObj.put(keyGoogleMapsApiKeyRelease, "YOUR_KEY_HERE")
}
project.logger.quiet("google-maps-api-config: no API key found for Google Maps")
return propertiesObj
}
def loadFromProperties(fileName) {
Properties props = new Properties()
File propFile = new File(fileName)
if (propFile.exists()) {
props.load(new FileInputStream(propFile))
if (props.get(keyGoogleMapsApiKeyDebug) != null &&
props.get(keyGoogleMapsApiKeyRelease) != null) {
project.logger.quiet("google-maps-api-config: use $fileName for Google Maps API")
return props
}
} else {
project.logger.quiet("google-maps-api-config: $propFile does not exist for Google Maps API")
}
return null
}
}
Add the the following to your app/build.gradle file:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
// ...
}
apply from: "$rootDir/gradle/google-maps-api-config.gradle" // handles Google Maps API key
android {
// ...
buildTypes {
release {
// other config
resValue "string", "google_maps_key", "\"${googleMapsApiConfig.props.getProperty(googleMapsApiConfig.keyGoogleMapsApiKeyRelease)}\""
}
debug {
// other config
resValue "string", "google_maps_key", "\"${googleMapsApiConfig.props.getProperty(googleMapsApiConfig.keyGoogleMapsApiKeyDebug)}\""
}
}
This will create a String Resource with the key google_maps_key and the value of your API key. You can delete any other API key references, e.g. Android Studio generate the following files: app/src/debug/res/values/google_maps_api.xml and app/src/release/res/values/google_maps_api.xml - you can delete them.
Add the Google Maps API Key to your manifest:
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="#string/google_maps_key" />
The way I have approached similar issues in the past is by using specific branches in my git repo for pubic pushes.
Say you have a local master branch with your keys in the manifest. When you are ready to push to github (or wherever else) you can make a new "release" branch with no history. You can see a bit more about that here: How to push new branch without history git branch --orphan release. Once you do that remove all private information, commit all files and push only that branch to github git push origin release.
The issue with this is you will not have a commit history, maybe someone else can come up with something better.
If you create Google Maps Activity from Android Studio as a template it will add needed plugin in your build.gradle file, and then you can put your API key in local.properties file and use it as variable in Manifest file as for example: "${API_KEY}"
For more information visit https://developers.google.com/maps/documentation/places/android-sdk/secrets-gradle-plugin