Android Development from the Ground Up: Project Skeleton

Posted on Thu, 2 Nov 2017 at 19:38:51 EST

Tagged as tutorial, programming, and android.

The Android ecosystem is actually pretty cool; it's comprised (mostly) of free software, running atop the Linux kernel, with its own tricked out Java runtime. The ecosystem can frustrating to get into, though. Google expects that all Android developers use their in-house IDE, Android Studio, and they choose to provide only minimal documentation for the command-line tools included in the SDK. Now, this is fine if you're alright using the IDE, but I'd like to point out just one of many issues I have with it:

Botnet

I know I'm in love with text editor that comes with a web browser and tetris, but at least it doesn't ask me to log ingo my FSF Associate Member account for this vague promise of "cloud integration." That's just absurd.

I'll tell you how I really feel

In this series of blog posts, I'll try to be documenting enough that you can start developing for Android without Android Studio, but also cover the knowledge required to reverse engineer Android apps.

So far, the only tasks I've found Android Studio to be useful for so far are creating the directory hierarchy for the project, and designing the UI. Luckily for you, I've started development on a few scripts to aid in that, one of which will give you the minimal project skeleton for an app written in either Java or Kotlin.

Using the scripts should be straightforward. For the rest of the blog post, I'll be describing the anatomy of an app written in Java. So let's create one of those. The only argument for "skeleton_java.py" is the name of the package - which follows the reverse domain name notation you see often with Java.

[jakob@Epsilon ~]$ mkdir test_app && cd test_app
[jakob@Epsilon test_app]$ skeleton_java.py "space.jakob.testapp"
[jakob@Epsilon test_app]$ ls
app  build.gradle  gradle  gradle.properties  gradlew  gradlew.bat  settings.gradle

A lot of files were created, but it's mostly boilerplate. When you're developing for Android, you're going to want to use Gradle. Fortunately, you don't actually need to install anything - there are "gradlew" and "gradlew.bat" scripts which will bootstrap the build system and allow you to use it to build the app. Instead of focusing on the garbage related to configuring Gradle, direct your attention to the "app/src/main/" directory. That's where the meat and potatoes of our app is located.

[jakob@Epsilon test_app]$ cd app/src/main/
[jakob@Epsilon main]$ ls
AndroidManifest.xml  java  res

"java" is the default location of our source code. This can be changed in the Gradle configuration, but there isn't much of a point. The skeleton script for Kotlin still names the directory "java". "res" are any resources that get included with the APK. That's images, and XML resources, mostly. I'll get back to that XML part soon enough, don't worry. "AndroidManifest.xml" is metadata about our app. Let's take a peek:

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="space.jakob.testapp">
  <application
      android:label="@string/app_name">
    <activity
        android:name=".MainActivity"
        android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>

Not super interesting. You can read more about the manifest file here if you want. The key things to notice are that "MainActivity" thing, and "@string/app_name". MainActivity is the entry point to our app - not unlike "main" in C. Android organizes apps into "activities," which are essentially different view that the app can have. The documentation describes it as "a single, focused thing that the user can do." An intent is "an abstract description of an operation to be performed." In this case, we're specifying that MainActivity should be shown when the app is run from the launcher via intent hinting.

"@string/app_name" is your first taste of modularizing resources using XML resources. It's considered good practice to avoid using bare string constants, and instead using a reference to a string value. This makes localization easier. So, what is the value of "@string/app_name" anyway? Well, you can find out if you take a look at the string resources file, which for US English is located at "app/src/main/res/values" in "strings.xml".

<resources>
  <string name="app_name">Project Skeleton</string>
  <string name="hello_world">Hello world!</string>
</resources>

So it's "Project Skeleton." Cool. Now we can take a look at the app's boilerplate code. Unfortunately, the Java convention carries through, and the code is deep in a nest of directories forming the name of our package. In our case, "app/src/main/java/space/jakob/testapp". Yeah, gross, I know. The source file is "MainActivity.java".

package space.jakob.testapp;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

If you already know Java, this should be familiar. The only thing that might stand out is the use of "R". What's that? Well, if you want to reference files in the "res/" directory, that's how you do it. In this case, "R.layout.activity_main" refers to "res/layout/activity_main.xml", which is a description of our UI.

<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="@string/hello_world"
    tools:context=".MainActivity"/>

User interfaces are declared through XML, which is where the IDE does become useful. This is pretty basic, though - our activity is merely a TextView that contains a string from our resource file. It would probably be better to wrap it in something like a LinearLayout, but that's a topic for a future post.

Our app is going to show "Hello world!", but just for fun, I'd like to introduce at least one feature that will be useful in development. Let's import the logging facility for Android:

import android.util.Log;

and add some logging to onCreate:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.d("space.jakob.testapp", "Hello, world!");
}

This is going to get logged at the level of DEBUG. There are other methods for logging with higher and lower significance, which you can view here. That message won't be visible unless you run logcat (after the app's started, obviously).

[jakob@Epsilon test_app]$ adb logcat
...
11-02 16:20:16.321  4405  4405 D space.jakob.testapp: Hello, world!
...

And we're actually ready to build the app. First things first, you'll need to install the Android SDK. Don't worry, you can obtain it without messing with Android Studio. It will vary from distribution to distribution, but on Arch Linux, there are packages for it in the AUR. When that's installed, you're still not done. Assuming you've added the build-tools directory to your PATH:

[jakob@Epsilon test_app]$ sdkmanager --licenses

and accept the licenses. Otherwise Gradle will complain when you try to build. Now you can build the app.

[jakob@Epsilon test_app]$ ./gradlew build

And you should have file in "app/build/outputs/apk" named "app-release-unsigned.apk". APK files are signed in Android as a security measure, and your phone probably won't let you install an unsigned package. So we'll have to self-sign, which fortunately isn't hard. You'll have to generate a keypair, first:

[jakob@Epsilon test_app]$ keytool -genkey -v -keystore my-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key

Adjust the values as needed. You can sign it now, but I would suggest aligning the APK before you do that:

[jakob@Epsilon test_app]$ zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk

And finally, sign the package.

[jakob@Epsilon test_app]$ apksigner sign --ks my-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk

You can now install the APK to your phone and test it out.

Useful app

Here are a few reference materials, which I'll probably end up going back in the next few posts.