Platform Apps in Android Studio

Even with all the current and upcoming changes in the Android compiler toolchains and build systems used for both applications and the platform, one common issue continues to come up amongst Android platform developer community: What is the best way to develop platform applications given the current state of the tools?

The Problem

With the introduction of the Gradle build system (used by Android Studio), the default project structure for an application has deviated from the platform’s internal build system (based on Android.mk files). This creates a problem if you are working on internal applications destined to be packaged with a custom system image.

Which build system do you choose? Either way you go, some mapping is required. The default project structure for a platform package (application) looks something like this:

 PlatformAppPackage/
   aidl/
   assets/
   AndroidManifest.xml
   Android.mk
   java/
   jni/
   res/

Where the contents of the Android.mk file might include:

Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional
# Reference java/ and aidl/ source files
# res/ and assets/ are discovered automatically
LOCAL_SRC_FILES := \
    $(call all-java-files-under, java) \
    $(call all-named-files-under, *.aidl, aidl)

LOCAL_PACKAGE_NAME := MySystemApplication
LOCAL_SDK_VERSION := current
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_CERTIFICATE := platform

include $(BUILD_PACKAGE)

#######################################

include $(CLEAR_VARS)
LOCAL_SRC_FILES:= \
    $(call all-cpp-files-under, jni)

LOCAL_MODULE := mypackage_jni

include $(BUILD_SHARED_LIBRARY)

Hacking Android.mk

Most folks are typically inclined to migrate the entire project structure to a Gradle-based project, so that Android Studio will recognize the sources and the developers can more easily work with the code. Then the necessary changes are made to the Android.mk files to support building with the platform. This would take the above project and make it look more like this:

 GradleAppPackage/
   Android.mk                  (1)
   app/
     Android.mk                (1)
     build.gradle              (4)
     src/
       Android.mk              (1)
       main/
         aidl/
         assets/
         Android.mk            (2)
         AndroidManifest.xml
         java/
         jni/
         res/
   build.gradle                (3)
   settings.gradle             (5)
1 Required intermediate makefile to allow build to find real makefile
2 Real makefile used to compile the package
3 Top-level Gradle build file to declare project dependencies
4 Gradle module file used to compile the package
5 Gradle project file allowing top-level to reference the module

The first thing you may notice is that it’s makefiles all the way down! What’s worse, all those intermediate makefiles look like this:

include $(call all-subdir-makefiles)

Their only purpose is to allow the build system to find the actual makefile that lives in a subdirectory. Without these, the module might be skipped by the build system depending on what other directories are around it. The reason for this is that the build implicitly looks for the res/ and assets/ directories in the local path.

The build.gradle files in this case are standard fare that Android Studio would generate:

build.gradle
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'

        ...
    }
}
app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "..."
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    ...
}

dependencies {
    ...
}

Alternate Android.mk

One alternative to remove the excess is to map the implicit dependencies in the Android.mk file. We can replace the above Gradle structure with the following:

 GradleAppPackage/
   Android.mk
   app/
     build.gradle
     src/
       main/
         aidl/
         assets/
         AndroidManifest.xml
         java/
         jni/
         res/
   build.gradle
   settings.gradle

If we modify the base Android.mk file to look more like this:

Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional
# Reference java/ and aidl/ source files
LOCAL_SRC_FILES := \
    $(call all-java-files-under, app/src/main/java) \
    $(call all-named-files-under, *.aidl, app/src/main/aidl)
# Re-map res/ and assets/ directly
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/src/main/res
LOCAL_ASSET_DIR := $(LOCAL_PATH)/app/src/main/assets

LOCAL_PACKAGE_NAME := MySystemApplication
LOCAL_SDK_VERSION := current
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_CERTIFICATE := platform

include $(BUILD_PACKAGE)

#######################################

include $(CLEAR_VARS)
# Re-map native code path
LOCAL_SRC_FILES:= \
    $(call all-cpp-files-under, app/src/main/jni)

LOCAL_MODULE := mypackage_jni

include $(BUILD_SHARED_LIBRARY)

One advantage to this approach is that you could leverage the product flavors feature of a Gradle build, and potentially map different build targets to each of the variants.

Hacking Gradle

The other option is to keep the source files in the structure that Android.mk prefers and create a build.gradle that maps the appropriate directories using source sets.

This was a common technique in the early days of migrating ADT-based projects to the Gradle build system before Android Studio came with an importer that moved all the files around automatically for you. Since then, using the sourceSets attribute has become somewhat of a lost art amongst Android folks.

In this case, the source tree would look like the following:

 PlatformAppPackage/
   aidl/
   assets/
   AndroidManifest.xml
   Android.mk
   build.gradle
   java/
   jni/
   res/

Awesome! We only had to add one file! Let’s see what it looks like:

build.gradle
//Top-level buildscript information
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
    }
}

//App module build information
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
      targetSdkVersion 23
      ...
    }

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['java']
            resources.srcDirs = ['java']
            aidl.srcDirs = ['aidl']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }
    }
}

You may notice that this has the contents of both previous build.gradle files condensed into one. If we have no intention of supporting multiple modules in the same build project, this is perfectly legal.

The key here, though, is the sourceSets attribute. This maps the source files and directories from their current locations into the Gradle build without requiring us to move them to the locations where Gradle would implicitly search.

The advantage to this approach is that it requires the least amount of change to map an existing platform project into allowing Gradle builds.

Summary

So, which method should you choose? I suppose it depends on where you are coming from. If you are integrating an existing Studio project into platform builds, the first option may be less work for you. If the reverse is true, the second approach may make more sense. If you are starting from scratch, pick your favorite.

If you were to corner me at a conference and ask that question, my personal preference would be to use sourceSets in Gradle to map a standard project. If your application is intended for platform builds, this creates consistency between your project and the rest of the source tree. Gradle functionality is only provided as an extension to make developer lives easier.

 

Dave

Dave Smith is an embedded software developer based in Denver, CO and head geek Wireless Designs, LLC. He has been focused on the Android platform since 2009. If you would like to hear more from Dave, you can follow him on Twitter @devunwired. You can also find him on Google+.