Jack, Jill & building Android apps by hand

Jack, Jill & building Android apps by hand

This seems to be the overarching theme of my posts. I renounce modern IDEs, go command-line and claim it’s good for you. But this time it’s serious.

I’m scared of magic.

Mysterious background processes performing tasks I’m unaware of and don’t understand. IDEs reading my mind and generating code behind my back. “Here, try some”, they’ll say. And I will try some indeed. Heck, I will even like it. But the mystery of it all will make it hard to swallow.

One such mystery for me has been the Android build process. Even outside the IDE, turning Java and XML into APK via Gradle was just enough magic to make me uncomfortable.

Whenever possible, I like to strip away the magic and do things by hand. Even if only as an exercise.

This was one such exercise.

The goal

To build and deploy an Android app by hand, using only the SDK command-line tools.

The rules

  • No IDEs
  • No Gradle

And for extra credit:

The project

For this task, I created a simple, single-activity application. When run, it will look like this:

Not a paragon of Android good practices mind you, but just enough to walk us through the major steps of the build:

For demonstration purposes I’ve adopted a simple, flat project folder structure:

handbuilt-android-project/
├── AndroidManifest.xml
├── gen/
├── lib/
│   └── picasso-2.5.2.jar
├── out/
├── res/
│   ├── drawable-xhdpi/
│   │   └── icon.png
│   └── layout/
│       └── activity_main.xml
└── src/
    └── pl/
        └── czak/
            └── handbuilt/
                └── MainActivity.java

When we’re done:

  • gen/ will contain the generated R.java class.
  • out/ will contain the products of our build

The complete source code (with a slightly more advanced folder structure and simple dependency management built in) is available on GitHub. For the tl;dr version, just check out the Makefile.

If you wish to follow along, we need to do some setup beforehand.

Prerequisites

Download the latest SDK tools package (24.4.1 as of this writing), unzip it and set $ANDROID_HOME to the root folder. Then launch the SDK Manager using:

$ $ANDROID_HOME/tools/android

Before the SDK is fully usable, we need to download additional components:

  • Tools
  • Platform tools
  • Build tools
  • Android N Preview SDK
  • Android N System Image (if you want to test on an emulator)

I live on the edge ;) so I go with preview versions of everything:

Once everything is installed, make sure the following commands are available in your $PATH:

  • $JAVA_HOME/bin/java
  • $JAVA_HOME/bin/jarsigner
  • $ANDROID_HOME/platform-tools/adb
  • $ANDROID_HOME/tools/build-tools/24.0.0-preview/aapt
  • $ANDROID_HOME/tools/build-tools/24.0.0-preview/zipalign

The Jack compiler is contained in the build tools under $ANDROID_HOME/build-tools/24.0.0-preview/jack.jar. For convenience, I use the following alias to invoke it:

$ alias jack='java -jar "$ANDROID_HOME/build-tools/24.0.0-preview/jack.jar"'

The build

The complete build consists of 7 distinct steps, described in the following subsections:

  1. Generate R.java
  2. Compile
  3. Package
  4. Sign
  5. Zipalign
  6. Upload
  7. Run

Step 1. Generate R.java

In Android Studio, this happens continuously behind the scenes and is perhaps the most magical part of the process. You add a new layout XML and a second later you can reference it in your code as R.layout.activity_main. This is because the IDE regenerates the R.java source file whenever your resources change.

In our exercise, we need to do it manually. This is the first job for aapt:

$ aapt package -f \
               -M AndroidManifest.xml \
               -I "$ANDROID_HOME/platforms/android-N/android.jar" \
               -S res/ \
               -J gen/ \
               -m

This invocation takes our AndroidManifest.xml and the resources under res/ and generates the following source file:

gen/pl/czak/handbuilt/R.java
/* AUTO-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */

package pl.czak.handbuilt;

public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int icon=0x7f020000;
    }
    public static final class id {
        public static final int image=0x7f040000;
    }
    public static final class layout {
        public static final int activity_main=0x7f030000;
    }
}

Step 2. Compile

The complete source code now consists of MainActivity.java and the newly generated R.java. We can now compile it using Jack:

$ jack --classpath "$ANDROID_HOME/platforms/android-N/android.jar" \
       --import lib/picasso-2.5.2.jar \
       --output-dex out/ \
       src/ gen/

We pass in N’s android.jar for classpath and import the Picasso JAR. We also specify both folders containing our Java files.

As a result, we get out/classes.dex ready for packaging.

Step 3. Package

We’re ready to start building the APK. I split the packaging process into two steps. First, we create the initial package file with the manifest and resources:

$ aapt package -f \
               -M AndroidManifest.xml \
               -I "$ANDROID_HOME/platforms/android-N/android.jar" \
               -S res/ \
               -F out/handbuilt.apk

Once again, this is performed with the Android Asset Packaging Tool (aapt). This time however, we’re using the -F switch to create out/handbuilt.apk.

Now we add our compiled classes.dex. Note that aapt is sensitive to relative paths and classes.dex needs to reside in root of the package. Thus, we perform this step from within the out/ folder:

$ cd out/
$ aapt add handbuilt.apk classes.dex

We’ve provided all the required contents of the APK. Before we install it however, it needs to be signed.

Step 4. Sign

Every Android APK needs to be signed before it can be installed on a device or an emulator. This is true whether you’re just trying out a debug build in development or preparing your final APK for public release. When working in the IDE this is kept out of sight and there’s no mention of any keys until you want to publish your app in Google Play.

For development purposes, the SDK provides a standard debug key in ~/.android/debug.keystore. The key can be used as follows:

  • Keystore password: android
  • Key password: android
  • Key alias: androiddebugkey

With this in mind, we turn to JDK’s jarsigner to perform the task:

$ jarsigner -verbose \
            -keystore ~/.android/debug.keystore \
            -storepass android \
            -keypass android \
            out/handbuilt.apk \
            androiddebugkey

The APK is now ready to upload. However, let’s perform one important optimization first.

Step 5. Zipalign

Zipaligning is a process which takes an APK and ensures that all uncompressed data (e.g. images and resources.arsc) starts with a particular alignment relative to the start of the file. This way they can be mmaped without moving any bytes around. This is an important performance optimization. Luckily, it’s very simple:

$ zipalign -f 4 out/handbuilt.apk out/handbuilt-aligned.apk

Our final APK is handbuilt-aligned.apk.

Step 6. Upload

Now launch an emulator (as an exercise for the reader – do this from the command line as well) and let’s deploy:

$ adb install -r out/handbuilt-aligned.apk

The app is ready to be launched. Sure we could just tap the icon in the drawer. But where’s the fun in that?

Step 7. Run

By now we’re commandline ninjas after all. So let’s launch the app the right way:

$ adb shell am start pl.czak.handbuilt/.MainActivity

Done.

Notes

  • Jack simplifies the compilation process quite a bit. What previously took multiple steps (javac, dx, ProGuard) is now taken care of by this single tool.
  • Android Studio generates one more class behind the scenes - BuildConfig.java. For this simple exercise I’ve omitted it.
  • The Gradle build system uses a different naming scheme - the package before zipaligning would be called handbuilt-unaligned.apk and the final APK would be handbuilt.apk. This is just a convention and I’m breaking it here. Sue me. (Please don’t sue me).
  • A typical build process would also incorporate Jill – Jack’s helper tool for converting libraries into Jack’s expected jayce format. Again, I’ve omitted it in the examples here - as of version 1.2-rc2 Jack seems to accept android.jar and picasso-2.5.2.jar just fine. To speed up consecutive builds, you would want to “pre-jill” both of these just like you would “pre-dex” libraries with the old toolchain.
  • Some Java 8 features are now available to Android developers. If you want to use those, you need to specify -D jack.java.source.version=8 to Jack. Also note that Jack is the only option for using Java 8 as dx won’t accept Java 8 .class files.

Summary

Once again, I’m pleased to see the magic behind the Android build process is not that bad. Will I be using make instead of gradle from now on? Most probably not. I’m even starting to miss Android Studio a bit. And it’s not such a scary place anymore.

Łukasz Adamczak's Picture

About Łukasz Adamczak

I'm a mobile & web developer based in Warsaw, Poland. On this blog I write down notes from my programming journeys.

Warsaw, Poland http://czak.pl

Comments