Developer Blog

Android and CI and Gradle - a How-To

Updated 10/31/15: Since writing this, things have changed. As a result, some of the information below may be outdated and no longer applicable. For more up to date information on how to use CircleCI for your Android projects, please see the Continuous Integration section of our Android Stack Guidelines.

There are tech stacks in this world that make it dead simple to integrate a CI build system.
The Android platform is not one of them.

Although Gradle is getting better, it’s still a bit non-deterministic, and some of the fixes you’ll need will start to feel more like black magic than any sort of programming.

But fear not! It can be done!

Before we embark on our journey, you’ll need a few things to run locally:

  1. A (working) Gradle build
  2. Automated tests (JUnit, Espresso, etc.)

If you don’t have Gradle set up for your build system, it is highly recommend that you move your projects over. Android Studio has a built-in migration tool, and the Android Dev Tools website has an excellent guide on how to migrate over to the Gradle build system, whether you’re on Maven, Ant, or some unholy combination of all three.

A very general example of a build.gradle file follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//build.gradle in /app
apply plugin: 'com.android.application'

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:1.0.1'
  }
}
//See note below
task wrapper(type: Wrapper) {
    gradleVersion = '2.1'
}

android {
  compileSdkVersion 19
  buildToolsVersion "21.1.2"

  defaultConfig {
    applicationId "com.example.originate"
    minSdkVersion 14
    targetSdkVersion 19
    versionCode 1
    versionName "1.0"

    testApplicationId "com.example.originate.tests"
    testInstrumentationRunner "android.test.InstrumentationTestRunner"
  }

  buildTypes {
    debug {
        debuggable true
    }
    release {
        debuggable false
    }
  }
}

dependencies {
  compile project(':libProject')
  compile com.android.support:support-v4:21.0.+
}

(NOTE: the Gradle Wrapper task isn’t strictly necessary, but a highly recommended way of ensuring you always know what version of Gradle you’re using – both for futureproofing and for regressions)

Check out the Android Developers website for some good explanations and samples.

Choose your weapon

At Originate, we are big fans of CircleCI. They sport a clean, easy-to-use interface and support more languages than you could possibly care about. Plus, they are free for open source Github projects!
(Other options include TravisCI, Jenkins, and Bamboo)

In this guide, we’ll be using CircleCI, but these instructions should translate readily to TravisCI as well.

Configure all the things!

In order to use CircleCI to build/test your Android library, there’s some configuration necessary. Below are some snippets of some of the basic configurations you might use. About half of this comes from the CircleCI docs and half of it comes from my blood, sweat, and tears.

At the end of this section, I’ll include a complete circle.yml file. (The complete docs for the circle.yml file is here)

Machine

First, the code:

1
2
3
4
5
machine:
  environment:
    ANDROID_HOME: /home/ubuntu/android
  java:
    version: oraclejdk6
  1. The setting of the ANDROID_HOME environment variable is necessary for the Android SDKs to function properly. It’ll also be useful for booting up the emulator in later steps.
  2. Although setting the JDK version isn’t strictly necessary, it’s nice to ensure that it doesn’t change behind-the-scenes and possibly surprise-bork your build.

Dependencies + Caching

1
2
3
4
5
6
dependencies:
  cache_directories:
    - ~/.android
    - ~/android
  override:
    - (source scripts/environmentSetup.sh && getAndroidSDK)
  1. By default, CircleCI will cache nothing. You might think this a non-issue right now, but you’ll reconsider when each build takes 10+ minutes to inform you that you dropped a semicolon in your log statement.

By caching `~/.android` and `~/android`, you can shave precious minutes off of your build time.
  1. Android provides us with a nifty command-line utility called…android (inventive!). We can use this in a little Bash script that we’ll write in just a second. For now, just know that scripts/environmentSetup.sh can be whatever you want, as can the Bash function getAndroidSDK.

Bash Scripts – a Jaunt into the CLI

Gradle is good at a lot of things, but it isn’t yet a complete build system. Sometimes, you just need some good ol’fashioned bash scripting.

In this section, we’ll download Android API 19 (Android 4.4 Jelly Bean) and create a hardware-accelerated Android AVD (Android Virtual Device – aka “emulator”) image.

Note: If android commands confuse/scare you, check out the d.android documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# Fix the CircleCI path
function getAndroidSDK(){
  export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH"

  DEPS="$ANDROID_HOME/installed-dependencies"

  if [ ! -e $DEPS ]; then
    cp -r /usr/local/android-sdk-linux $ANDROID_HOME &&
    echo y | android update sdk -u -a -t android-19 &&
    echo y | android update sdk -u -a -t platform-tools &&
    echo y | android update sdk -u -a -t build-tools-21.1.2 &&
    echo y | android update sdk -u -a -t sys-img-x86-android-19 &&
    #echo y | android update sdk -u -a -t addon-google_apis-google-19 &&
    echo no | android create avd -n testAVD -f -t android-19 --abi default/x86 &&
    touch $DEPS
  fi
}
  1. The export PATH line is to ensure we have access to all of the Android CLI tools we’ll need later in the script.
  2. The DEPS=... is used in the if/then block to determine if CircleCI has already provided us with cached dependencies. If so, there’s no need to download anything!
  3. Note that we’re explicitly requesting the x86 version of the Android 19 emulator image (sys-img-x86-android-19). The ARM-based emulator is notoriously slow, and we should use the hardware-accelerated version if at all possible.
  4. We create the Android Virtual Device (AVD) with the line android create avd ..., with a target of Android 19 and a name of testAVD.
  5. If you need the Google APIs (e.g., Maps, Play Store, etc.), you can uncomment out the line addon-google_apis-google-19.
  6. Even though Google has released an API 21 HAXM emulator, I still recommend using an API 19 AVD. API 21’s emulator doesn’t always play nice with CircleCI.

CAVEAT – Because of the way this caching works, if you ever change which version of Android you compile/run against, you need to click the “Rebuild & Clear Cache” button in CircleCI (or use the CircleCI API). If you don’t, you’ll never actually start compiling against the new SDK. You have been warned.

You shall not pass! (until your tests have run)

This section will vary greatly depending on your testing setup, so YMMV – moreso than with the rest of this post.
This section is assuming you’re using a plain vanilla Android JUnit test suite.

1
2
3
4
5
6
7
8
9
10
11
test:
  pre:
    - $ANDROID_HOME/tools/emulator -avd testAVD -no-skin -no-audio -no-window:
      background: true
    - (./gradlew assembleDebug):
      timeout: 1200
    - (./gradlew assembleDebugTest):
      timeout: 1200
    - (source scripts/environmentSetup.sh && waitForAVD)
  override:
    - (./gradlew connectedAndroidTest)
  1. The $ANDROID_HOME/tools/emulator starts a “headless” emulator – more specifically, the one we just created.
    1a. Running the emulator from the terminal is a blocking command. That’s why we are setting the background: true attribute on the emulator command. Without this, we would have to wait anywhere between 2-7 minutes for the emulator to start and THEN build the APK, etc. This way, we kick off the emulator and can get back to building.
  2. The two subsequent ./gradlew commands use the Gradle wrapper (gradle +wrapper) to build the code from your /app and androidTest directories, respectively.
  3. See below for environmentSetup.sh Part II. Essentially, after building both the app and the test suite, we cannot continue without the emulator being ready. And so we wait.
  4. Once the emulator is up and running, we run gradlew connectedAndroidTest, which, as its name suggests, runs the tests on the connected Android device. If you’re using Espresso or other test libraries, those commands would go here.
    4a. The CircleCI Android docs say that the “standard” way to run your tests is through ADB – ignore them. Gradle is the future and it elides all of those thorny problems that ADB tests have.

Bash Round 2

As mentioned above, after Gradle has finished building your app and test suite, you’ll kind of need the emulator to…y’know…run your tests.

This script relies on the currently-booting AVD’s init.svc.bootanim property, which essentially tells us whether the boot animation has finished. Sometimes, it seems like it’ll go on forever…


Android AVD Boot


*will the madness never stop?!*

This snippet can go in the same file as your previous bash script – in that case, you only need one #!/bin/bash – at the top of your file.

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

function waitAVD {
    (
    local bootanim=""
    export PATH=$(dirname $(dirname $(which android)))/platform-tools:$PATH
    until [[ "$bootanim" =~ "stopped" ]]; do
      sleep 5
      bootanim=$(adb -e shell getprop init.svc.bootanim 2>&1)
      echo "emulator status=$bootanim"
    done
    )
}

Note: This script was adapted from this busy-wait script.

Results

By default, CircleCI will be fairly vague regarding your tests’ successes and/or failures. You’ll have to go hunting through the very chatty verbose Gradle loggings in order to determine exactly which tests failed. Fortunately, there’s a better way – thanks to Gradle!

When you run gradlew connectedAndroidTests, Gradle will create a folder called /build/outputs/reports/**testFolderName**/connected in whichever folder you have a build.gradle script in.

So, for example, if your repo was in ~/username/awesomerepo, with a local library in awesome_repo/lib and an app in /awesome_repo/app, the Gradle test artifacts should be in /awesome_repo/app/build/outputs/reports/**testFolderName**/connected.

In this directory, you’ll find a little website that Gradle has generated, showing you which test packages and specific tests passed/failed. If you like, you can tell CircleCI to grab this by placing the following at the top of your circle.yml file:

1
2
3
general:
  artifacts:
    -/home/ubuntu/**repo_name**/build/outputs/reports/**testFolderName**/connected

You can then peruse your overwhelming success under the Artifacts tab for your CircleCI build – just click on index.html. It should pull up something like this:


Example Artifact

Security, Signing, and Keystores

The astute among you will notice that I haven’t gone much into the process of signing an Android app. This is mainly for the reason that people trying to set up APK signing fall into 2 categories – Enterprise and Simple.

Enterprise: If you’re programming Android for a company, you probably have some protocol regarding where your keystores/passwords can and cannot live – so a general guide such as this won’t be much help for you. Sorry.

Simple: You’re not Enterprise, so your security protocol is probably a little more flexible – i.e., you feel moderately comfortable with checking your keystore files into your respository.

In either case, Google and StackOverflow are your friends.

My final word of advice is that CircleCI can encrypt things like keystore passphrases – stuff you might consider passing in plain-text in your buildscript files. Check out CircleCI’s Environment Variables doc.

Finally

Go into your CircleCI settings, add a hook for your Github repo, and then do a git push origin branchName. If the Gradle Gods have smiled upon you, Circle should detect your config files and start building and testing!

Depending on your test suite, tests can take as little as a few minutes or as much as a half-hour to run. Try not to slack off in the meanwhile, but rejoice in having some solid continuous integration!

Stay tuned for a future blog post about using CircleCI to automagically deploy to MavenCentral!

Flipping to the back of the book…

Below is the full circle.yml as well as environmentSetup.sh for your viewing/copying pleasure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Build configuration file for Circle CI
# needs to be named `circle.yml` and should be in the top level dir of the repo

general:
  artifacts:
    -/home/ubuntu/**repo_name**/build/outputs/reports/**testFolderName**/connected

machine:
  environment:
    ANDROID_HOME: /home/ubuntu/android
  java:
    version: oraclejdk6

dependencies:
  cache_directories:
    - ~/.android
    - ~/android
  override:
    - (echo "Downloading Android SDK v19 now!")
    - (source scripts/environmentSetup.sh && getAndroidSDK)

test:
  pre:
    - $ANDROID_HOME/tools/emulator -avd testAVD -no-skin -no-audio -no-window:
      background: true
    - (./gradlew assembleDebug):
      timeout: 1200
    - (./gradlew assembleDebugTest):
      timeout: 1200
    - (source scripts/environmentSetup.sh && waitForAVD)
  override:
    - (echo "Running JUnit tests!")
    - (./gradlew connectedAndroidTest)

And the accompanying shell scripts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash

# Fix the CircleCI path
function getAndroidSDK(){
  export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH"

  DEPS="$ANDROID_HOME/installed-dependencies"

  if [ ! -e $DEPS ]; then
    cp -r /usr/local/android-sdk-linux $ANDROID_HOME &&
    echo y | android update sdk -u -a -t android-19 &&
    echo y | android update sdk -u -a -t platform-tools &&
    echo y | android update sdk -u -a -t build-tools-21.1.2 &&
    echo y | android update sdk -u -a -t sys-img-x86-android-19 &&
    #echo y | android update sdk -u -a -t addon-google_apis-google-18 &&
    echo no | android create avd -n testAVD -f -t android-19 --abi default/x86 &&
    touch $DEPS
  fi
}

function waitForAVD {
    (
    local bootanim=""
    export PATH=$(dirname $(dirname $(which android)))/platform-tools:$PATH
    until [[ "$bootanim" =~ "stopped" ]]; do
      sleep 5
      bootanim=$(adb -e shell getprop init.svc.bootanim 2>&1)
      echo "emulator status=$bootanim"
    done
    )
}

References

Comments