In the first part of this series, I mentioned a way to call C/C++ code from Java, and viceversa, on the Android platform. That method allows you to run performance-critical native code inside a Java-based Android project. Now I’ll show you a way to create an Android app without writing a single line of Java code.
The API level 9. This means you can only run them on devices with Gingerbread or greater (Android 2.3+).
How-To
There are actually two ways we can create a native activity. The first is to start with a blank slate and write all of the code to initialize and update the activity. The other is to use the “glue” code provided in the NDK. This code defines several important enums and callbacks. I don’t want to reinvent the wheel right now, so I’ll use the second approach.
To use the NDK glue code, you’ll need to add it to your Android.mk file. You can either use it as a static library, by importing the module from its current location (android-ndk/sources/android/native_app_glue), or you can copy-paste the source files to your project’s jni subfolder. Here’s my Android.mk file:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := NativeActivitySimpleExample LOCAL_SRC_FILES := main.cpp LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv1_CM LOCAL_STATIC_LIBRARIES := android_native_app_glue include $(BUILD_SHARED_LIBRARY) $(call import-module,android/native_app_glue)
Native Activity Hello World
Once we have the makefile, it’s time to create a main source file.
#include <android/log.h> #include <android_native_app_glue.h> #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "NativeActivitySimpleExample", __VA_ARGS__)) void android_main(struct android_app* state) { while(1) { LOGI("Tick!"); } }
The native activity’s main entry point is “ANativeActivity_onCreate()”, which is implemented in the glue code. After initialization, it calls “android_main()”, which you can think of as your own entry point. If you build this code, you’ll get a nice libNativeActivitySimpleExample.so file.
At this point you’re probably wondering, how will Android know it should run my native code, instead of looking for a normal Activity class? This is handled inside the AndroidManifest.xml file. As usual, you declare the properties of your activity with an <activity> tag. But inside it, we’ll pass along some metadata to reference our C/C++ library. Here is what my AndroidManifest.xml looks like:
<?xml version="1.0" encoding="utf-8"?> package="com.gsamour.adbad" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <!-- Our activity is the built-in NativeActivity framework class. This will take care of integrating with our NDK code. --> <activity android:name="android.app.NativeActivity" android:label="@string/app_name" android:configChanges="orientation|keyboardHidden"> <!-- Tell NativeActivity the name of or .so --> <meta-data android:name="android.app.lib_name" android:value="NativeActivitySimpleExample" /> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
We pass along the name of our native code library via a <meta-data> tag. Keep in mind that the compiled library will have a name like “libNativeActivitySimpleExample.so”, but the name to write here is simply “NativeActivitySimpleExample” (lose the “lib” and “.so”).
We can now package the app and run it, but if we do, it’ll throw a RuntimeException, with a message like “…unable to start activity… unable to load native library…”. The problem is the linker decided to strip the glue module, because none of its functions were being called directly in our code. After all, the glue code is made up of callbacks, mostly. The decision was made because the –gc-sections linker flag is enabled automatically in default-build-commands.mk. You can read a brief paragraph about this optimization here.
The NDK’s way of solving this problem is by adding a direct call to a dummy function declared in native_activity_app_glue.h. If you look at the NDK’s native activity sample, you’ll notice the following code at the top of “android_main ()”:
// Make sure glue isn't stripped. app_dummy();
This ensures android_native_app_glue.o is linked. If you add these lines, rebuild, and run, you should now see the “Tick!” message appear as log output. We now have a “hello world” app which is free of Java code!
Touch Input
As long as you leave the previous application alone, it will probably keep running just fine. However, we game developers like to mess with things. So let’s try tapping on the screen several times to send input to our app. It may take a while but, at some point in time, the OS will bring up a “app not responding” popup and will ask you to either keep waiting or force close the app. If we look at the log, we’ll see lines like these:
I/InputReader(XXXX): dispatchTouch::touch event’s action is 1, pending(waiting finished signal)=0
I/InputDispatcher(XXXX): Delivering touch to current input target: action: 1, channel ‘XXXXXXXX Sorry! (server)’
The glue module implements input handling, but we’re not using it explicitly in our code. So the app will not respond to touch and the OS will wait for a period of time, eventually asking you to force close. The way to enable input handling is by enabling general event processing. Here’s an updated main.cpp:
#include <android/log.h> #include <android_native_app_glue.h> #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "NativeActivitySimpleExample", __VA_ARGS__)) void android_main(struct android_app* state) { // Make sure glue isn't stripped. app_dummy(); while(1) { int ident; int fdesc; int events; struct android_poll_source* source; while((ident = ALooper_pollAll(0, &fdesc, &events, (void**)&source)) >= 0) { // process this event if (source) source->process(state, source); } } }
The “ALooper_pollAll()” function waits for events to be available. Its signature is:
int ALooper_pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData);
The return value is an identifier which, if greater than or equal to 0, means there is an event for us to process. The first argument is an optional timeout which will wait a desired amount of milliseconds for an event to appear. If the value is set to zero, the function returns immediately without blocking. If it is a negative number, it will block until an event is available. In game development we don’t want to wait indefinitely, so let’s set it to zero. The second, third, and fourth arguments are the file descriptor, events, and source data. These are all set to NULL if we get a negative identifier. For more information on this function and the android_poll_source structure, check the looper.h and android_native_app_glue.h files in the Android NDK.
In the case of a native activity, ALooper_pollAll() can give us two types of events: lifecycle events and input events. To execute your own logic, you’ll need to set the appropriate callbacks. The state variable passed into android_main is an android_app instance and has two member function pointers for this purpose: “onAppCmd” and “onInputEvent”. Let’s set the input callback and do something simple in it.
static int32_t handle_input(struct android_app* app, AInputEvent* event) { if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { size_t pointerCount = AMotionEvent_getPointerCount(event); for (size_t i = 0; i < pointerCount; ++i) { LOGI("Received motion event from pointer %zu: (%.2f, %.2f)", i, AMotionEvent_getX(event, i), AMotionEvent_getY(event, i)); } return 1; } else if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY) { LOGI("Received key event: %d", AKeyEvent_getKeyCode(event)); return 1; } return 0; }
When touching the screen, we should see the touch coordinates in the log output. If a key is pressed, the key code should appear. If we handled the event, we should return 1.
Lifecycle Events
To execute our own logic for a lifecycle event, such as pause/resume, we need to set the onAppCmd callback:
static void handle_cmd(struct android_app* app, int32_t cmd) { switch (cmd) { case APP_CMD_SAVE_STATE: // the OS asked us to save the state of the app break; case APP_CMD_INIT_WINDOW: // get the window ready for showing break; case APP_CMD_TERM_WINDOW: // clean up the window because it is being hidden/closed break; case APP_CMD_LOST_FOCUS: // if the app lost focus, avoid unnecessary processing (like monitoring the accelerometer) break; case APP_CMD_GAINED_FOCUS: // bring back a certain functionality, like monitoring the accelerometer break; } }
The full list of commands can be found in android_native_app_glue.h.
That’s it for part 2! In the next post, I want to write about drawing to the screen and getting input from sensors.