Build a Lightweight Docker Container For Android Testing

Speed up your mobile development

Phat Nguyen
Better Programming

--

Source: Xmodulo Flickr

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 ), you can run this to start the 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. (example: 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 contents to /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 somewhere 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 need to download the SDK manually without Android Studio bundled, using SDK tools only. Check the link here 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, SDK manager, and ProGuard. The Build Tools primarily include aapt (an Android packaging tool to create .APK), dx (an Android tool that converts .java files to .dex files). Platform Tools include the Android debug shell, sqlite3 and Systrace.

The 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

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 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 two executed files for emulator. One is packaged in /android/emulator and the other is in /android/tools/bin. We’ll use /android/emulator to run the Android device.

There is more detail in this SO answer.

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 entering 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 ubuntuLABEL 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 wgetARG 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 the 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 in a Docker container. Let’s get back to other stuff while waiting for the report.

For more detailed about build steps and samples, check out the GitHub repo. I’ve made a cool sample with a 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!

--

--

Mobile Lead Engineer at Lazada, Alibaba. I write about Android and software engineering in general.