Android
A typical publishing cycle of an Android application includes the following major steps:
Building the application.
Signing the application.
Uploading the application to the Google Play Console.
There is a number of ways to automate these steps with Space Automation:
Using the Google Play Developer API.
If you are new to the Android building and publishing workflows, we recommend that you start with the Android publishing basics topic that comes next. If you are interested in automating testing in Android projects, refer to Run tests in Android projects.
Android publishing basics
The contents of this topic are based on the information from developer.android.com/. Note that certain information might have changed since the date this topic was published. For the actual information, refer to the official Google documentation.
Build system
Typically, Android applications are built with the Gradle build tool. So, all recommendations for building Kotlin and Java applications with Gradle can be applied to Android applications as well. Basically, all you need to build an app is to run ./gradlew build
Build artifact formats
You can configure your Android build to produce artifacts in one of the following formats:
Android Application Package (APK) – an archive that includes app's binaries, screenshots, and other resources. To verify the identity of the application developer, an APK file must be digitally signed with a private key. Then you can upload the APK to Google Play or distribute it separately. For better support for multiple devices, you should create a number of APKs optimized for each device. Alternatively, you can create a single "non-optimized" APK.
Android App Bundle (AAB) – a publishing format that includes app's binaries and other resources and defers APK generation to Google Play. Pros: Google Play itself creates a number of APKs for each device configuration. As well as APK, a bundle must be signed before uploading to Google Play.
Signing basics
There are two ways to sign the app:
Private app signing key – (applicable to APK only) You directly sign the APK using a private app signing key. The signing key never changes during the lifetime of the app. To sign the APK with the key, use Android Studio, a Gradle task, or a command-line tool. If you lose the key, you lose the ability to update the app.
Play App Signing – (applicable to APK and AAB, recommended) This flow uses two keys: an upload key and a private app signing key. You store the upload key on your side and use it to sign the AAB or APK before uploading. The app signing key is stored by Google – you cannot download it. Google uses this key to sign the APKs (the uploaded ones or the ones built from the AAB). The main benefit is that if your upload key is lost or compromised, you can issue a new one. Learn more
Build and publish the app using Gradle and Gradle Play Publisher
Prerequisites
Eligible images
|
Android applications use the Gradle build tool. The default Gradle configuration of an Android project lets you automate generating and signing the app's APKs or AABs. What it is not able to do is to automate release activities: uploading of the app, promoting it, and so on. In this section, we will show how you can solve this issue with the help of the Gradle Play Publisher plugin. It's an unofficial Gradle plugin that is able to perform all release activities using the Google Play Developer API.
Create a Google Cloud Platform service account as described in the Gradle Play Publisher documentation. The plugin will use this account to perform release activities in the Google Play Console.
When creating a service account, you will get the account's private key in a
.json
file. Save the file on your computer. You will need it in the next steps.Make sure you provide the account sufficient permissions for the app: Manage testing track releases permission.
. In our example, we will publish the app to the internal testing track, therefore the account needs theGenerate an upload key for signing your application (in our example, we will use Play App Signing). If you already have the key, skip this step. To generate the key, you can use:
Android Studio. Learn more
The
keytool
command-line tool. For example:keytool -genkey -v -keystore uploadkey.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
When creating a key, you should specify the following information (you will need it in the next steps):
Key store file name: the
.jks
file that will store the key.Key store password: the password to the
.jks
key storage.Key alias: the name of the private key.
Key password: the password to the private key.
For security reasons, we will store app private signing key data and the Google service account key in the Secrets & Parameters storage.
Save all sensitive data as secrets and parameters:
First, let's convert the binary
.jks
key store file to the hex format. This way we will be able to save it as a secret. During the build, the Automation script will covert the hex secret back to the binary.jks
file. To convert the key store file you can use thexxd
tool (a part of thexxd
package):xxd -plain upload_key.jks > upload_key.hexIf you don't have the
xxd
tool, runsudo apt get install xxd
first.In Space, open the required project.
Open Settings | Secrets & Parameters and create a new secret.
Open the
upload_key.hex
file, copy its contents, and paste them as a secret value.In the same way, convert the Google service account key and create a secret for it:
xxd -plain google_sa_key.json > google_sa_key.hexActually, it's not necessary to convert
.json
to hex as it's simply plain text. Nevertheless, to avoid copy-paste encoding issues, it's better to store it as hex and convert back to plain text during the build.Create secrets for the key store password and the key password.
Create a parameter for the key alias (it doesn't require secure storage).
As a result, there will be four secrets and one parameter in the project's Secrets & Parameters:
In the project-level
build.gradle
file, configure application signing. For security reasons, we will access key data stored in the Secrets & Parameters storage using environment variables. For example:... // Key store file name // The Automation script will create the key store file in the root directory, // but this build.gradle is located one level down - on the project level. def appKeyStoreFile = "../upload_key.jks" // Key store password, key alias and password created on the prev steps. // Env vars will be assigned by the Automation script. def appKeyStorePassword = System.env.KEY_STORE_PASSWORD def appKeyAlias = System.env.KEY_ALIAS def appKeyPassword = System.env.KEY_PASSWORD android { ... signingConfigs { release { storeFile file(appKeyStoreFile) storePassword appKeyStorePassword keyAlias appKeyAlias keyPassword appKeyPassword } } buildTypes { release { signingConfig signingConfigs.release ... } } }In the project-level
build.gradle
file, configure the Gradle Play Publisher plugin. For security reasons, we will store the service account key in the Secrets & Parameters storage and create it using the Automation script. For more information on how to configure the plugin, refer to the plugin documentation.import com.github.triplet.gradle.androidpublisher.ReleaseStatus plugins { id 'com.android.application' id 'com.github.triplet.play' version '3.3.0' } // Service account key file you created on step 1 // The Automation script will create the key store file in the root directory, // but this build.gradle is located one level down - on the project level. def googleServiceAccountKeyFile = "../google_sa_key.json" ... android {...} play { // We will publish the app to the internal testing track track.set("internal") serviceAccountCredentials.set(file(googleServiceAccountKeyFile)) // Our app is not yet publicly available, // we will publish it in the draft state. releaseStatus.set(ReleaseStatus.DRAFT) } ...If you want to automatically change the app version depending on the build run number, change the
versionCode
andversionName
parameters in the project-levelbuild.gradle
. For example, if we want theversionCode
to be equal to the build number andversionName
to be1.0.$build_number
:def appVersionCode = Integer.valueOf(System.env.JB_SPACE_EXECUTION_NUMBER ?: 0) def appVersionName = "1.0.${System.env.JB_SPACE_EXECUTION_NUMBER}" ... android { ... defaultConfig { ... versionCode appVersionCode versionName appVersionName } } ...The resulting project
build.gradle
might look like follows:import com.github.triplet.gradle.androidpublisher.ReleaseStatus plugins { id 'com.android.application' id 'com.github.triplet.play' version '3.3.0' } def appVersionCode = Integer.valueOf(System.env.JB_SPACE_EXECUTION_NUMBER ?: 0) def appVersionName = "1.1.${System.env.JB_SPACE_EXECUTION_NUMBER}" def appKeyStoreFile = "../upload_key.jks" def appKeyStorePassword = System.env.KEY_STORE_PASSWORD def appKeyAlias = System.env.KEY_ALIAS def appKeyPassword = System.env.KEY_PASSWORD def googleServiceAccountKeyFile = "../google_sa_key.json" apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.first.simpleandroidapp" minSdkVersion 28 targetSdkVersion 30 versionCode appVersionCode versionName appVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { release { storeFile file(appKeyStoreFile) storePassword appKeyStorePassword keyAlias appKeyAlias keyPassword appKeyPassword // Optional, specify signing versions used v1SigningEnabled true v2SigningEnabled true } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { viewBinding true } } play { track.set("internal") serviceAccountCredentials.set(file(googleServiceAccountKeyFile)) releaseStatus.set(ReleaseStatus.DRAFT) } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }In the project root, create the
.space.kts
automation script:job("Build and publish bundle to internal track") { // disable gitPush job trigger startOn { gitPush { enabled = false } } container("Build and publish", "mycompany.registry.jetbrains.space/p/projectkey/mydocker/automation-android:1.0.5") { env["GOOGLE_SA_KEY"] = Secrets("google_sa_key") env["KEY_STORE"] = Secrets("key_store") env["KEY_STORE_PASSWORD"] = Secrets("key_store_password") env["KEY_PASSWORD"] = Secrets("key_password") env["KEY_ALIAS"] = Params("key_alias") shellScript { content = """ echo Get private signing key... echo ${'$'}KEY_STORE > upload_key.hex xxd -plain -revert upload_key.hex upload_key.jks echo Get Google service account key... echo ${'$'}GOOGLE_SA_KEY > google_sa_key.hex xxd -plain -revert google_sa_key.hex google_sa_key.json echo Build and publish AAB... ./gradlew publishBundle """ } } }Notes:
We use a custom Docker image that meets the following requirements.
In our example, we build and publish an AAB. If you want to build and publish an APK instead, change the line
./gradlew publishBundle
to./gradlew publishApk
.
Run the job. After the job successfully finishes, check Google Play Console. It must contain the uploaded application draft:
If needed, you can configure Gradle Play Publisher to automate all other release tasks, like uploading app metadata (screenshots, descriptions, and so on), promoting app artifacts, working with product flavors, and much more. For more information, refer to the Gradle Play Publisher documentation.
Build and publish the app using fastlane
Prerequisites
Eligible images
|
Android applications use the Gradle build tool. The default Gradle configuration of an Android project lets you automate generating and signing the app's APKs or AABs. What it is not able to do is to automate release activities: uploading of the app, promoting it, and so on. In this section, we will show how you can solve this issue with the help of the fastlane platform. The fastlane tool lets you automate deployment and release routines for your iOS and Android applications. The tool is configured with a Fastfile
file which is typically stored in VCS along with the project sources.
Create a Google Cloud Platform service account as described in the fastlane documentation. The tool will use this account to perform release activities in the Google Play Console.
When creating a service account, you will get the account's private key in a
.json
file. Save the file on your computer. You will need it in the next steps.Make sure you provide the account sufficient permissions for the app: Manage testing track releases permission.
. In our example, we will publish the app to the internal testing track, therefore the account needs theGenerate an upload key for signing your application (in our example, we will use Play App Signing). If you already have the key, skip this step. To generate the key, you can use:
Android Studio. Learn more
The
keytool
command-line tool. For example:keytool -genkey -v -keystore uploadkey.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
When creating a key, you should specify the following information (you will need it in the next steps):
Key store file name: the
.jks
file that will store the key.Key store password: the password to the
.jks
key storage.Key alias: the name of the private key.
Key password: the password to the private key.
For security reasons, we will store app private signing key data and the Google service account key in the Secrets & Parameters storage.
Save all sensitive data as secrets and parameters:
First, let's convert the binary
.jks
key store file to the hex format. This way we will be able to save it as a secret. During the build, the Automation script will covert the hex secret back to the binary.jks
file. To convert the key store file you can use thexxd
tool (a part of thexxd
package):xxd -plain upload_key.jks > upload_key.hexIf you don't have the
xxd
tool, runsudo apt get install xxd
first.In Space, open the required project.
Open Settings | Secrets & Parameters and create a new secret.
Open the
upload_key.hex
file, copy its contents, and paste them as a secret value.In the same way, convert the Google service account key and create a secret for it:
xxd -plain google_sa_key.json > google_sa_key.hexActually, it's not necessary to convert
.json
to hex as it's simply plain text. Nevertheless, to avoid copy-paste encoding issues, it's better to store it as hex and convert back to plain text during the build.Create secrets for the key store password and the key password.
Create a parameter for the key alias (it doesn't require secure storage).
As a result, there will be four secrets and one parameter in the project's Secrets & Parameters:
In the project-level
build.gradle
file, configure application signing. For security reasons, we will access key data stored in the Secrets & Parameters storage using environment variables. For example:... // Key store file name // The Automation script will create the key store file in the root directory, // but this build.gradle is located one level down - on the project level. def appKeyStoreFile = "../upload_key.jks" // Key store password, key alias and password created on the prev steps. // Env vars will be assigned by the Automation script. def appKeyStorePassword = System.env.KEY_STORE_PASSWORD def appKeyAlias = System.env.KEY_ALIAS def appKeyPassword = System.env.KEY_PASSWORD android { ... signingConfigs { release { storeFile file(appKeyStoreFile) storePassword appKeyStorePassword keyAlias appKeyAlias keyPassword appKeyPassword } } buildTypes { release { signingConfig signingConfigs.release ... } } }If you want to automatically change the app version depending on the build run number, change the
versionCode
andversionName
parameters in the project-levelbuild.gradle
. For example, if we want theversionCode
to be equal to the build number andversionName
to be1.0.$build_number
:def appVersionCode = Integer.valueOf(System.env.JB_SPACE_EXECUTION_NUMBER ?: 0) def appVersionName = "1.0.${System.env.JB_SPACE_EXECUTION_NUMBER}" ... android { ... defaultConfig { ... versionCode appVersionCode versionName appVersionName } } ...The resulting project
build.gradle
might look like follows:plugins { id 'com.android.application' } def appVersionCode = Integer.valueOf(System.env.JB_SPACE_EXECUTION_NUMBER ?: 0) def appVersionName = "1.1.${System.env.JB_SPACE_EXECUTION_NUMBER}" def appKeyStoreFile = "../upload_key.jks" def appKeyStorePassword = System.env.KEY_STORE_PASSWORD def appKeyAlias = System.env.KEY_ALIAS def appKeyPassword = System.env.KEY_PASSWORD apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.first.simpleandroidapp" minSdkVersion 28 targetSdkVersion 30 versionCode appVersionCode versionName appVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { release { storeFile file(appKeyStoreFile) storePassword appKeyStorePassword keyAlias appKeyAlias keyPassword appKeyPassword // Optional, specify signing versions used v1SigningEnabled true v2SigningEnabled true } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { viewBinding true } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }Configure fastlane. In fastlane/Fastfile, add a new lane:
desc "Upload app draft to internal testing track" lane :deploy_draft_to_internal do gradle( task: 'bundle', build_type: 'Release' ) upload_to_play_store( track: 'internal', release_status: 'draft' ) endThis lane first builds an application AAB and then uploads it to the internal testing track.
In the fastlane/Appfile file, specify the path to the Google Play service account key (relative to the project root). During the build, we will get the key from a project secret and put it to the root directory. So, we can specify any name here. For example, the
Appfile
could look like follows:json_key_file("google_sa_key.json") package_name("com.first.simpleandroidapp")In the project root, create the
.space.kts
automation script:job("Build and publish bundle to internal track") { // disable gitPush job trigger startOn { gitPush { enabled = false } } container("Build and publish", "mycompany.registry.jetbrains.space/p/projectkey/mydocker/automation-android-fastlane:1.0.5") { env["GOOGLE_SA_KEY"] = Secrets("google_sa_key") env["KEY_STORE"] = Secrets("key_store") env["KEY_STORE_PASSWORD"] = Secrets("key_store_password") env["KEY_PASSWORD"] = Secrets("key_password") env["KEY_ALIAS"] = Params("key_alias") shellScript { content = """ echo Get private signing key... echo ${'$'}KEY_STORE > upload_key.hex xxd -plain -revert upload_key.hex upload_key.jks echo Get Google service account key... echo ${'$'}GOOGLE_SA_KEY > google_sa_key.hex xxd -plain -revert google_sa_key.hex google_sa_key.json echo Build and publish AAB... fastlane android deploy_draft_to_internal """ } } }Notes:
We use a custom Docker image that meets the following requirements.
We restore the service account key to the
google_sa_key.json
file in the root directory (the same file we specified in fastlane/Appfile).
Run the job. After the job successfully finishes, check Google Play Console. It must contain the uploaded application draft:
If needed, you can configure fastlane to automate all other release tasks, like uploading app metadata (screenshots, descriptions, and so on), promoting app artifacts, working with product flavors, and much more. For more information, refer to the fastlane documentation.
Run tests in Android projects
Android projects support two types of tests:
Local unit tests
These are "regular" unit tests. These tests would normally run when you build / publish your Android application. For example, you can run local unit tests with
./gradlew test
Instrumented tests
These are tests that run on a real Android device or inside an emulator. To automate running instrumented tests, you can use Firebase Test Lab – a cloud-based service provided by Google. Firebase Test Lab fully supports the
gcloud
command-line tool that you can use in your Automation scripts. Learn more