JetBrains Space Help

Android

A typical publishing cycle of an Android application includes the following major steps:

  1. Building the application.

  2. Signing the application.

  3. Uploading the application to the Google Play Console.

There is a number of ways to automate these steps with Space Automation:

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 Running 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 may 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

Building and publishing the app using Gradle and Gradle Play Publisher

Prerequisites

  • Your project must use Gradle 6.5 or later and Android Gradle Plugin 4.1.0 or later.

  • The app's APK or AAB must be published to Google Play Console at least once.

Eligible images

  • A custom image that includes JRE, Gradle 6.5 or later, Android SDK (the same version that is used by the application), and the xxd tool. You can build your own image using the following sample Dockerfile:

    FROM gradle:6.8.3-jre15 USER root ENV TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip" \ ANDROID_SDK_ROOT="/usr/local/android-sdk" \ ANDROID_SDK=30 \ ANDROID_BUILD_TOOLS=30.0.3 \ PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin:${PATH}" # Download Android SDK RUN mkdir "$ANDROID_SDK_ROOT" .android "$ANDROID_SDK_ROOT/cmdline-tools" && \ cd "$ANDROID_SDK_ROOT/cmdline-tools" && \ curl -o sdk.zip $TOOLS_URL && \ unzip sdk.zip && \ rm sdk.zip && \ mv cmdline-tools tools && \ yes | $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager --licenses # Install Android Build Tools RUN $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager --update RUN $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" \ "platforms;android-${ANDROID_SDK}" \ "platform-tools" # Install xxd RUN apt-get update && \ apt-get install xxd

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.

  1. 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: Google Play Console | Users and Permissions | Invite new users. In our example, we will publish the app to the internal testing track, therefore the account needs the Manage testing track releases permission.

  2. Generate 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.

  3. 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:

    1. 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 the xxd tool (a part of the xxd package):

      xxd -plain upload_key.jks > upload_key.hex

      If you don't have the xxd tool, run sudo apt get install xxd first.

    2. In Space, open the required project.

    3. Open Settings | Secrets & Parameters and create a new secret.

    4. Open the upload_key.hex file, copy its contents, and paste them as a secret value.

    5. 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.hex

      Actually, 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.

    6. Create secrets for the key store password and the key password.

    7. Create a parameter for the key alias (it doesn't require secure storage).

    8. As a result, there will be four secrets and one parameter in the project's Secrets & Parameters:

      Secrets for Android automation
  4. 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 ... } } }
  5. 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) } ...
  6. If you want to automatically change the app version depending on the build run number, change the versionCode and versionName parameters in the project-level build.gradle. For example, if we want the versionCode to be equal to the build number and versionName to be 1.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 may 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' }
  7. 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 pulbish 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.

  8. Run the job. After the job successfully finishes, check Google Play Console. It must contain the uploaded application draft:

    Published application draft
  9. 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.

Building and publishing the app using fastlane

Prerequisites

  • Your project must be configured to use fastlane. Learn more

  • The app's APK or AAB must be published to Google Play Console at least once.

Eligible images

  • A custom image that includes fastlane, JRE, Android SDK (the same version that is used by the application), and the xxd tool. You can build your own image using the following sample Dockerfile:

    FROM fastlanetools/ci:0.3.0 USER root ENV TOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip" \ ANDROID_SDK_ROOT="/usr/local/android-sdk" \ ANDROID_SDK=30 \ ANDROID_BUILD_TOOLS=30.0.3 \ PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin:${PATH}" # Install OpenJDK-11 RUN apt-get -y -o Acquire::Check-Valid-Until=false update && \ apt-get install -y openjdk-11-jdk -o Acquire::Check-Valid-Until=false && \ apt-get install -y ant -o Acquire::Check-Valid-Until=false && \ apt-get clean; ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk-amd64/ RUN export JAVA_HOME # Install Android SDK RUN mkdir "$ANDROID_SDK_ROOT" .android "$ANDROID_SDK_ROOT/cmdline-tools" && \ cd "$ANDROID_SDK_ROOT/cmdline-tools" && \ curl -o sdk.zip $TOOLS_URL && \ unzip sdk.zip && \ rm sdk.zip && \ mv cmdline-tools tools && \ yes | $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager --licenses RUN $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager --update RUN $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" \ "platforms;android-${ANDROID_SDK}" \ "platform-tools" # Install xxd RUN apt-get install xxd -o Acquire::Check-Valid-Until=false # Install Fastlane RUN apt-get install --no-install-recommends -y --allow-unauthenticated build-essential git ruby-full -o Acquire::Check-Valid-Until=false && \ gem install fastlane && \ gem install bundler

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.

  1. 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: Google Play Console | Users and Permissions | Invite new users. In our example, we will publish the app to the internal testing track, therefore the account needs the Manage testing track releases permission.

  2. Generate 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.

  3. 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:

    1. 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 the xxd tool (a part of the xxd package):

      xxd -plain upload_key.jks > upload_key.hex

      If you don't have the xxd tool, run sudo apt get install xxd first.

    2. In Space, open the required project.

    3. Open Settings | Secrets & Parameters and create a new secret.

    4. Open the upload_key.hex file, copy its contents, and paste them as a secret value.

    5. 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.hex

      Actually, 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.

    6. Create secrets for the key store password and the key password.

    7. Create a parameter for the key alias (it doesn't require secure storage).

    8. As a result, there will be four secrets and one parameter in the project's Secrets & Parameters:

      Secrets for Android automation
  4. 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 ... } } }
  5. If you want to automatically change the app version depending on the build run number, change the versionCode and versionName parameters in the project-level build.gradle. For example, if we want the versionCode to be equal to the build number and versionName to be 1.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 may 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' }
  6. 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' ) end

    This lane first builds an application AAB and then uploads it to the internal testing track.

  7. 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")
  8. 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 pulbish 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).

  9. Run the job. After the job successfully finishes, check Google Play Console. It must contain the uploaded application draft:

    Published application draft
  10. 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.

Running 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

Last modified: 13 April 2021