Build a Lightweight Docker Container For Android Testing
Tags: android, docker, productivity, testing
Published: 2019-12-06 05:21:58


Testing helps us identify any defects or errors that may have been made during development. But it takes time and resources, especially in Android where it requires the installation of many dependencies and a device to perform UI tests.

By using a Docker container, we can build and run tests for multiple feature branches, speeding up the development and increasing productivity.

In this tutorial, we’re going to learn how to build a lightweight Android container to isolate the testing process.

* No Android Studio/GUI applications required.

* Android emulator runs on a Docker container.

* Has ability to cache dependencies for later build.

* Wipe out everything after the process.

## 1. Start docker container

The image that we build on top of is: ubuntu:latest

Assuming you have docker installed (if not, please follow this [link](https://docs.docker.com/docker-for-mac/install/)), you can run this to start docker container:

    $ docker run --privileged -dit --name android-container ubuntu

* — `privileged`: grant permission to launch VM on container.

* — `it`: interactively execute shell cmd.

* — `d`: run container in the background.

* — `name android-container`: container’s name, will use later to attach and commit docker image.

(Optional) To run docker as non-root, the simplest way is adding the current user to group docker

    $ sudo groupadd docker // Add group docker if it doesn't already exist
    $ sudo gpasswd -a $USER docker // Add current user to group docker
    $ newgrp docker // reload (or you can re-login to reload)
    $ docker run hello-world // check if it works

## 2. Install SDK packages

### **Prerequisites**:

Make sure you install the following dependencies. Otherwise, you may notice No such file or directory when installing android SDK or start emulator

*Please double-check the redundant dependencies.*🙇 (ex: vim — if you’re not a fan)

    $ apt update && apt install -y openjdk-8-jdk vim git unzip libglu1 libpulse-dev libasound2 libc6  libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxi6  libxtst6 libnss3 wget

### **Gradle**

Install `gradle` and `gradle-wrapper` . We’ll use version `5.4.1` in this tutorial. No worries, it can be updated with `build-arg` later 😊

Download `gradle-5.4.1` to `/tmp/gradle-5.4.1` and unzip the content within `/opt/gradle`

    $ wget https://services.gradle.org/distributions/gradle-5.4.1-bin.zip -P /tmp \
    && unzip -d /opt/gradle /tmp/gradle-5.4.1-bin.zip 

Make a new directory `/opt/gradlew` and install `gradle-wrapper` there. (The directory names can be anything, but save the files to somewhere that easy to find)

    $ mkdir /opt/gradlew \
    && /opt/gradle/gradle-5.4.1/bin/gradle wrapper --gradle-version 5.4.1 --distribution-type all -p /opt/gradlew \
    && /opt/gradle/gradle-5.4.1/bin/gradle wrapper -p /opt/gradlew

### **Android SDK**

You will need to download SDK manually without Android Studio bundled, SDK tools only, check the link [here](https://developer.android.com/studio) to get the URL.

Like Gradle, we’ll save it to `/tmp` and get it extracted in `/opt`

    $ wget 'https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip' -P /tmp \
    && unzip -d /opt/android /tmp/sdk-tools-linux-4333796.zip \
> The Android software development kit (SDK) includes different components, including SDK Tools, Build Tools, and Platform Tools. The SDK Tools primarily includes the stock Android emulator, [hierarchy viewer](https://developer.android.com/studio/profile/hierarchy-viewer.html), [SDK manager](https://developer.android.com/studio/intro/update.html), and [ProGuard](https://guides.codepath.com/android/Configuring-ProGuard). The Build Tools primarily include aapt (Android packaging tool to create .APK), dx (Android tool that converts .java files to .dex files). Platform Tools include the [Android debug shell](https://developer.android.com/studio/command-line/adb.html), [sqlite3](https://developer.android.com/studio/command-line/sqlite3.html) and [Systrace](https://developer.android.com/studio/profile/systrace-commandline.html).

Most important packages are `platform-tools`, `tools` and `emulator`. Run this to install them quickly:

    $ yes Y | /opt/android/tools/bin/sdkmanager --install "platform-tools" "system-images;android-*28*;google_apis;x86" "platforms;android-28" "build-tools;28.0.3" "emulator"

* `platform-tools` contains `adb`

* `tools` contains `avdmanager` and `sdkamanager`

* `emulator` : run emulator

* `system-images;android-*28*;google_apis;x86` : use to create avd

Accept all licenses of Android SDK

    $ yes Y | /opt/android/tools/bin/sdkmanager --licenses

Seems good, now create an avd **`test`**

    $ echo "no" | /opt/android/tools/bin/avdmanager --verbose create avd --force --name "test" --device "pixel" --package "system-images;android-28;google_apis;x86" --tag "google_apis" --abi "x86"

* — name: device’s name.

* — abi: CPU architecture.

* — tag `google_api`: support Google API.

Check one more time to see if it works!

    $ /opt/android/emulator/emulator -list-avds 
    # Expected Result: test

## 3. Set up environment variables

Edit your `.bashrc` or any config files that you’re familiar with:

* GRADLE_HOME=/opt/gradle/gradle-5.4.1

* *ANDROID_HOME*=/opt/android

* *PATH=*$*PATH*:$*GRADLE_HOME*/bin:/opt/gradlew:$*ANDROID_HOME*/emulator:$*ANDROID_HOME*/tools/bin:$*ANDROID_HOME*/platform-tools

* *LD_LIBRARY_PATH= $ANDROID_HOME*/emulator/lib64:$*ANDROID_HOME*/emulator/lib64/qt/lib

Then reflect the changes with `source ~/.bashrc`

***Note***: The order of PATH variable is important because android provides 2 executed file for `emulator`, one is packaged in `/android/emulator` and another is in `/android/tools/bin`. We’ll use `/android/emulator` to run android device.

More detail is this [SO answer](https://stackoverflow.com/a/49511666/5460066).

## 4. Run emulator

Here are some minor steps before running emulator:

* Stop any running emulators with ADB.

* Start emulator in the background with flag `-no-window` and `-gpu off`

* Turn off animation to avoid flaky tests.

Some test cases will perform an action on a view that might be not visible on a small screen — so we set the emulator up to have a high resolution (1440x2880)

    function wait_emulator_to_be_ready() {
      adb devices | grep emulator | cut -f1 | while read line; do adb -s $line emu kill; done
      emulator -avd test -no-audio -no-boot-anim -no-window -accel on -gpu off -skin 1440x2880 &

    boot_completed=false
      while [ "$boot_completed" == false ]; do
        status=$(adb wait-for-device shell getprop sys.boot_completed | tr -d '\r')
        echo "Boot Status: $status"
    
        if [ "$status" == "1" ]; then
          boot_completed=true
        else
          sleep 1
        fi
      done
    }
    
    function disable_animation() {
      adb shell "settings put global window_animation_scale 0.0"
      adb shell "settings put global transition_animation_scale 0.0"
      adb shell "settings put global animator_duration_scale 0.0"
    }
    
    wait_emulator_to_be_ready
    sleep 1
    disable_animation

Start the emulator with sh `start.sh` or `./start.sh` and wait until `Boot Status: 1` which means the device is fully loaded and ready to use.

Check again by enter `bg` to see the background jobs.

    $ ./start.sh
    emulator: WARNING: Your AVD has been configured with an in-guest renderer, but the system image does not support guest rendering.Falling back to 'swiftshader_indirect' mode.
    WARNING: change of renderer detected.
    checkValid: hw configs not eq
    emulator: Cold boot: different AVD configuration
    Your emulator is out of date, please update by launching Android Studio:
     - Start Android Studio
     - Select menu "Tools > Android > SDK Manager"
     - Click "SDK Tools" tab
     - Check "Android Emulator" checkbox
     - Click "OK"

    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 
    Boot Status: 1
    emulator: INFO: boot completed
    emulator: INFO: boot time 13200 ms
    emulator: Increasing screen off timeout, logcat buffer size to 2M.
    emulator: Revoking microphone permissions for Google App.

Check running emulators

    $ adb devices
    List of devices attached
    emulator-5554   device

We have an Android emulator running in the container successfully!

## 5. Build docker image

Open a new terminal tab, stop `android-container` and commit your changes to create a new Docker image:

    $ docker stop android-container && docker commit android-container android-container:v1

Test once again:

    $ docker images
    # Expected result: REPOSITORY          TAG
                       android-container   v1

For further extending with more configurations (ex: Flutter), let’s wrap things up and finalize the `Dockerfile`

    FROM ubuntu

    LABEL maintainer "codecaigicungduoc@gmail"

    WORKDIR /

    SHELL ["/bin/bash", "-c"]

    RUN apt update && apt install -y openjdk-8-jdk vim git unzip libglu1 libpulse-dev libasound2 libc6  libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxi6  libxtst6 libnss3 wget

    ARG GRADLE_VERSION=5.4.1
    ARG ANDROID_API_LEVEL=28
    ARG ANDROID_BUILD_TOOLS_LEVEL=28.0.3
    ARG EMULATOR_NAME='test'

    RUN wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip -P /tmp \
    && unzip -d /opt/gradle /tmp/gradle-${GRADLE_VERSION}-bin.zip \
    && mkdir /opt/gradlew \
    && /opt/gradle/gradle-${GRADLE_VERSION}/bin/gradle wrapper --gradle-version ${GRADLE_VERSION} --distribution-type all -p /opt/gradlew  \
    && /opt/gradle/gradle-${GRADLE_VERSION}/bin/gradle wrapper -p /opt/gradlew

    RUN wget 'https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip' -P /tmp \
    && unzip -d /opt/android /tmp/sdk-tools-linux-4333796.zip \
    && yes Y | /opt/android/tools/bin/sdkmanager --install "platform-tools" "system-images;android-${ANDROID_API_LEVEL};google_apis;x86" "platforms;android-${ANDROID_API_LEVEL}" "build-tools;${ANDROID_BUILD_TOOLS_LEVEL}" "emulator" \
    && yes Y | /opt/android/tools/bin/sdkmanager --licenses \
    && echo "no" | /opt/android/tools/bin/avdmanager --verbose create avd --force --name "test" --device "pixel" --package "system-images;android-${ANDROID_API_LEVEL};google_apis;x86" --tag "google_apis" --abi "x86"

    ENV GRADLE_HOME=/opt/gradle/gradle-$GRADLE_VERSION \
    ANDROID_HOME=/opt/android
    ENV PATH "$PATH:$GRADLE_HOME/bin:/opt/gradlew:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools"
    ENV LD_LIBRARY_PATH "$ANDROID_HOME/emulator/lib64:$ANDROID_HOME/emulator/lib64/qt/lib"

    ADD start.sh /

    RUN chmod +x start.sh

## 6. Build project and run tests

Check out your project configurations and build the ocker image with appropriate arguments:

    $ docker build \
    --build-arg GRADLE_VERSION=5.4.1 \
    --build-arg ANDROID_API_LEVEL=28 \
    --build-arg ANDROID_BUILD_TOOLS_LEVEL=28.0.3 \
    --build-arg EMULATOR_NAME=test \
    -t android-container .

Enter the top level of your project directory and run:

    docker run --privileged -it --rm -v $PWD:/data android-container bash -c ". /start.sh && gradlew build -p /data"

* -`it`: interactive mode.

* — `rm`: remove volume after the process is completed.

* -v `$PWD:/data`: mount your directory into the container:/data

* bash `-c “ . /start.sh && gradlew build -p /data”`: start the emulator and build project.

Voilà! We have the whole testing process run independently on docker container. Let’s get back to other stuff while waiting for the report. 😝 😝

For more detailed about build steps and sample, please check out this [GitHub repo](https://github.com/fastphat/android-container). I’ve made a cool sample with [Sunflower](https://github.com/android/sunflower) project.



If you have any questions, feel free to ask, reach out to me via *codecaigicungduoc@gmail.com* or creating a new issue on my github repo.

Thanks for reading 🙇 🙇 🙇