Overview
Native
activities – create an application based only on native code,
without a single line of Java.
Use
cases:
- Android
sensor polling – more accurate than Java events
- Cross
platform 3D – include a common OpenGL static lib that can also be
linked in say an ObjectiveC app. (If
writing purely for Android,
just use the Java API and
import
android.opengl.GLES20.*
package, adapting
a TextureView
to perform EGL init like a GLSurfaceView
-
the
performance leverages the same OpenGL
ES calls via JNI wrappers, and shaders run on the GPU
anyway, yielding
same performance. Or you
could use LibGDX...)
- Image
processing – eg leverage OpenCV C++ libs for Augmented Reality
apps (see my next post).
You
can pretty much call any POSIX API or included C/C++ lib that is
POSIX compliant !
The native_app_glue
Module API
NativeActivity
class leverages JNI to handle application lifecycle events from the
ART JVM and pass them to native code in a separate thread.
${ANDROID_NDK}/sources/android/native_app_glue
– hides details of synchronization with mutexes,
IPC pipes
different
events set android_app.activityState with the appropriate
APP_CMD_* value:
- onStart,
onResume, onPause, onStop
- onSaveInstance - wait for native application callback to save state.
- OnDestroy
- notify native thread that destruction is pending, and free
memory when acknowledged.
- onConfigChanged,
onWindowFocusedChanged, onLowMemory
- onNativeWindowCreated
/ onNativeWindowDestroyed - call android_app_set_window()
which provides and requests the native thread to change its display
window.
- onInputQueueCreated
/ onInputQueueDestoyed - call android_app_set_input() to
register an input queue
- Note:
screen orientation changes require activity recreation, and raises
an APP_CMD_ SAVE_STATE event
ANativeActivity_onCreate()
- application
entry
point - forks
the
native thread
- Executed
on the UI thread, communicates to native thread via callback event
wrappers
over Linux pipes and synchronization mutexes
- allocates
memory and initializes android_app
application context
- android_app_entry()
- executed on the native thread - reads data incoming from the
pipe and passes it to ALooper event queue processor
- ALooper_prepare()
- creates ALooper
- Alooper_addFd()
- attaches ALooper to the pipe (as a POSIX file
descriptor)
android_poll_source::process – called from custom event loop. Calls process_cmd() / process_input()
- process_cmd()
- process command queue and
calls back to custom android_app::onAppCmd callback
pointer
- process_input()
- process input queue
and calls back to custom
android_app::onAppCmd callback
pointer
thread
entry point attaches the Command Queue to the ALooper; From my
understanding, the Input Queue communicates to the ALooper via a pipe
but can be manually attached to the ALooper via
AInputQueue_attach/detachLooper()
Custom
user queue can also be attached to the ALooper to listen to
additional file-descriptors.
app_dummy()
- ensures
native glue is not stripped by the linker.
android_app
structure - contains native activity context info: state, window,
event queue, etc
- onAppCmd:
callback pointer - triggered each time an activity event occurs from
command queue.
- onInputEvent:
callback pointer -
triggered each time an
input event
occurs from input queue.
- void*
userData: pointer to data structure accessible to callback
function
- int
destroyRequested flag to notify custom event loop that an
application is being terminated, and that is should gracefully save
state & free resources
- int
activityState -
current activity state APP_CMD_*
- ANativeActivity*
activity - the JNI NativeActivity
-
AConfiguration* config - info about host hardware and
system state: locale, screen orientation & size etc
- void*
savedState, size_t savedStateSize - save data when an activity
thread is destroyed for later restoration
- AInputQueue*
inputQueue handles input events
- ALooper*
looper – manually
attach /detach event listeners against custom event sources
exposed via POSIX file descriptors
-
ANativeWindow* window, ARect contentRect: - graphics
drawable area, declared in native_window.h
Alooper_pollAll()
– custom native event loop should poll the queue for pending
android_poll_source events.
ANativeActivity_finish()
- request activity termination
Create a native Android project:
- create
a new project > Android project and set
settings: project name, Build target, Application name, Package
name (com.domain.MyApp), Min SDK Version. Uncheck Create
Activity.
- remove
res/layout/main.xml, src directory (its for Java
code).
- add
NativeActivity to the AndroidManifest.xml file:
Instead of a new Java Activity child class
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.MyNS.MyApp" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="21"/>
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name="android.app.NativeActivity" android:label="@string/app_name">
<meta-data android:name="android.app.lib_name" android:value="MyApp"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
set
project to compile native code:
- Convert
the project to a hybrid C++ project (not C) using Convert C/C++
Project wizard.
- Project
>Properties > C/C++ Build - set
default build command to ndk-build.
- Project
>Properties > C/C++ Build >
Path and Symbols/Includes - add Android NDK & native app
glue include directories:
${env_var:ANDROID_NDK}/platforms/android-21/arch-arm/usr/include
${env_var:ANDROID_NDK}/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9/include
${env_var:ANDROID_NDK}/sources/android/native_app_glue
Create
directory jni
under
project root – add
all
hpp,
cpp , mk
files here.
Make File
Build
script – I'll cover the details of Make (and Cmake) in a future
post …
define
a Make macro LS_CPP
that automatically lists all .cpp
files in jni
directory
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LS_CPP=$(subst $(1)/,,$(wildcard $(1)/*.cpp))
LOCAL_MODULE := MyApp
LOCAL_SRC_FILES := $(call LS_CPP,$(LOCAL_PATH))
LOCAL_LDLIBS := -landroid -llog
LOCAL_STATIC_LIBRARIES := android_native_app_glue
include $(BUILD_SHARED_LIBRARY)
$(call import-module, android/native_app_glue)
Main
app
entry point android_main() method runs the event loop
Main.cpp:
#include “Timer.hpp”
#include "EventHandlerImpl.hpp"
#include "EventLoop.hpp"
using namespace MyNS;
void android_main (android_app* pApp){
EventLoop eventLoop(pApp);
Timer timer;
EventHandlerImpl eventHandler (timer, pApp);
eventLoop.run(eventHandler);
}
EventHandler
Abstract
base – interface to observe
and
handle all native
activity events passed
from EventLoop
-
each
event has its own handler method. it
is important to
handle
3 core events in the
activity lifecycle:
-
onActivate(): activity is resumed and window is available
and focused.
-
onDeactivate(): activity is paused or display window loses
focus or is destroyed.
-
onStep(): - idle: computations can take place.
EventHandler.hpp:
#ifndef _EVENTHANDLER_HPP_
#define _EVENTHANDLER_HPP_
namespace MyNS {
class EventHandler {
public:
virtual int32_t onActivate() = 0;
virtual void onDeactivate() = 0;
virtual int32_t onStep() = 0;
virtual void onStart() {};
virtual void onResume() {};
virtual void onPause() {};
virtual void onStop() {};
virtual void onDestroy() {};
virtual void onSaveState(void** pData, int32_t* pSize) {};
virtual void onConfigChanged() {};
virtual void onLowMemory() {};
virtual void onInitWindow() {};
virtual void onTerminateWindow() {};
virtual void onGainFocus() {};
virtual void onLostFocus() {};
};
}
#endif
EventLoop
EventLoop.hpp:
#ifndef _EVENTLOOP_HPP_
#define _EVENTLOOP_HPP_
#include <android_native_app_glue.h>
#include "EventHandler.hpp"
namespace MyNS {
class EventLoop {
public:
EventLoop(android_app* pApp);
void
run(EventHandler&
pEventHandler);
protected:
void activate();
void deactivate();
void processEvent(int32_t appCmdCode);
private:
const
int32_t SUCCESS;
static void eventCallback(android_app* pApp, int32_t appCmdCode);
int isBlocking() const;
bool
isEnabled_;
bool
isTerminating_;
EventHandler*
eventHandler_;
android_app*
app_;
};
}
#endif
EventLoop.cpp
#include "EventLoop.hpp"
#include "Log.hpp"
namespace MyNS {
EventLoop::EventLoop(android_app* pApp) :
SUCCESS(0),
isEnabled_(false),
isTerminating_(false), app_(pApp),
eventHandler_(nullptr) {
app_->onAppCmd
= eventCallback;
app_->userData
= this;
}
run()
method polls application events in an event loop - during
activity lifetime, loops continuously over events until it is
requested to terminate.
void EventLoop::run() {
int32_t events;
android_poll_source* pPollSource;
app_dummy();
while (true) {
while (Alooper_pollAll( isBlocking(), nullptr, &events, (void**) &pPollSource) >= 0) {
if (pPollSource) {
Log::info("Processing an event");
pPollSource->process(app_, pPollSource);
}
if (app_->destroyRequested) {
Log::info("Exiting event loop");
return;
}
}
if
((isEnabled_) && (!isTerminating_)) {
if (eventHandler_->onStep() != SUCCESS) {
isTerminating_ = true;
ANativeActivity_finish(app_->activity);
}
}
}
}
int
EventLoop::isBlocking() {
//
return blocking timeout param:
//
-1 block until event is received -
avoid consuming battery / CPU
time uselessly
//
0 non-blocking: empty event queue
→ inner while loop will be terminated
//
note: > 0 → block until
event is received or timeout duration elapsed
return
isEnabled_ ? 0 : -1;
}
eventCallback()
calls processEvent()
Parameter
appCmdCode contains an enumeration value (APP_CMD_*)
which describes the occurring event.
void EventLoop::eventCallback(android_app* pApp, int32_t appCmdCode) {
EventLoop& eventLoop = *(EventLoop*) pApp->userData;
eventLoop.processEvent(appCmdCode);
}
void EventLoop::processEvent(int32_t appCmdCode) {
switch (appCmdCode) {
case APP_CMD_CONFIG_CHANGED:
eventHandler_->onConfigChanged();
break;
case APP_CMD_INIT_WINDOW:
eventHandler_->onInitWindow();
break;
case APP_CMD_START:
eventHandler_->onStart();
break;
case APP_CMD_STOP:
eventHandler_->onStop();
break;
case APP_CMD_TERM_WINDOW:
eventHandler_->onTerminateWindow();
deactivate();
break;
case APP_CMD_DESTROY:
eventHandler_->onDestroy();
break;
case APP_CMD_GAINED_FOCUS:
activate();
eventHandler_->onGainFocus();
break;
case APP_CMD_LOST_FOCUS:
eventHandler_->onLostFocus();
deactivate();
break;
case APP_CMD_LOW_MEMORY:
eventHandler_->onLowMemory();
break;
case APP_CMD_PAUSE:
eventHandler_->onPause();
deactivate();
break;
case APP_CMD_RESUME:
eventHandler_->onResume();
break;
case APP_CMD_SAVE_STATE:
eventHandler_->onSaveState(&app_->savedState, &app_->savedStateSize);
break;
default:
break;
}
}
activate()
/ deactivate() -
check activity state &
notify the observer
Activation
occurs when activity gains focus - can receive input events.
Deactivation
occurs when window loses focus , application is paused or window is
destroyed - cannot receive input events.
void EventLoop::activate() {
if ((!isEnabled_) && (app_->window != nullptr)) {
isTerminating_ = false;
isEnabled_ = true;
if (eventHandler_->onActivate() != SUCCESS) {
isTerminating_
= true;
ANativeActivity_finish(app_->activity);
}
}
}
void EventLoop::deactivate() {
if (isEnabled_) {
eventHandler_->onDeactivate();
isEnabled_ = false;
}
}
}
EventHandlerImpl
implements
EventHandler
interface - place
application-specific code here.
EventHandlerImpl.hpp:
#ifndef _EVENTHANDLERIMPL_HPP_
#define _EVENTHANDLERIMPL_HPP_
#include <android_native_app_glue.h>
#include "EventHandler.hpp"
#include “Timer.hpp”
namespace MyNS {
class EventHandlerImpl : public EventHandler {
public:
EventHandlerImpl(Timer&
pTimer, android_app* pApp);
protected:
int32_t onActivate();
void onDeactivate();
int32_t onStep();
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
void onSaveState(void** pData; int32_t* pSize);
void onConfigChanged();
void onLowMemory();
void onInitWindow();
void onTerminateWindow();
void onGainFocus();
void onLostFocus();
private:
const int32_t SUCCESS;
const int32_t FAIL;
void clear();
void draw();
android_app* app_;
ANativeWindow_Buffer windowBuffer_;
Timer* timer_;
};
}
#endif
EventHandlerImpl.cpp
#include "EventHandlerImpl.hpp"
#include "Log.hpp"
#include <math.h>
namespace
MyNS {
EventHandlerImpl::EventHandlerImpl(Timer&
pTimer, android_app* pApp) :
timer_(pTimer),
app_(pApp), SUCCESS(0), FAIL(-1) {}
the
ANativeWindow_* API gives native access to the display window
and allow manipulating its surface like a bitmap - requires locking
and unlocking before and after processing.
set
window format as 32-bit with ANativeWindow_setBuffersGeometry().
Query
window information in an ANativeWindow_Buffer structure
int32_t EventHandlerImpl::onActivate() {
timer_->reset();
if (ANativeWindow_setBuffersGeometry(
app_->window, 0,0, WINDOW_FORMAT_RGBX_8888 /* 32bit */ ) < 0)
{
return FAIL;
}
// Need to lock the window buffer to get its properties.
if (ANativeWindow_lock (app_->window, &windowBuffer_, nullptr) >= 0) {
ANativeWindow_unlockAndPost(app_->window);
} else {
return FAIL;
}
return SUCCESS;
}
step
the application by moving the cursor at a constant rate –
call
ANativeWindow_lock()
to
lock window
buffer for drawing and
call
ANativeWindow_unlockAndPost()
to
unlock it
when drawing is finished
int32_t EventHandlerImpl::onStep() {
timer_->update();
if (ANativeWindow_lock(app_->window, &windowBuffer_, nullptr) >= 0) {
clear();
draw();
ANativeWindow_unlockAndPost(app_->window);
return SUCCESS;
} else {
return FAIL;
}
}
implement
clear and draw methods
The
display window surface which is a continuous memory buffer -
directly accessible via the bits field and can be modified
pixel by pixel
Clear
the screen with a brute-force approach using memset().
void EventHandlerImpl::clear() {
memset( windowBuffer_.bits, 0, windowBuffer_.stride * windowBuffer_.height * sizeof(uint32_t*));
}
void EventHandlerImpl::draw() {
// todo: transform windowBuffer_.bits
}
}
Log
Log.hpp
define
_Log_debug macro - activates debug messages if NDEBUG flag
is set:
#ifndef _LOG_HPP
#define _LOG_HPP
namespace MyNS {
class Log {
public:
static void error(const char* pMessage, ...);
static void warn(const char* pMessage, ...);
static void info(const char* pMessage, ...);
static void debug(const char* pMessage, ...);
};
}
#ifndef NDEBUG
#define _Log_debug(...) Log::debug(__VA_ARGS__)
#else
#define _Log_debug(...)
#endif
#endif
Log.cpp
file - implement method info().
NDK
provides a dedicated logging API in header android/log.h to
write messages to Android logs.
log
methods differ in their
level macro: ANDROID_LOG_INFO/ERROR,/WARN/DEBUG
.
NDEBUG
macro is defined by the NDK compilation toolchain. To undefined it,
in the manifest, set:<application android:debuggable="true"
...>
run
adb logcat to
see emitted log messages.
#include <stdarg.h>
#include <android/log.h>
#include "Log.hpp"
namespace MyNS {
void Log::info(const char* pMessage, ...) {
va_list
varargs;
va_start(varargs,
pMessage);
__android_log_vprint(ANDROID_LOG_INFO,
"MyNS", pMessage, varargs);
__android_log_print(ANDROID_LOG_INFO, "MyNS", "\n");
va_end(varargs);
}
}
Timer
adapt
FPS according to device speed.
Use
POSIX time primitives from time.h
clock_gettime()
- retrieve current time from monotonic
system clock
work
with doubles when manipulating absolute time to avoid losing
accuracy - resulting delay can be converted back to float
Timer.hpp
#ifndef _TIMer_HPP_
#define _TIMer_HPP_
#include <time.h>
namespace MyNS {
class Timer {
public:
Timer();
void reset();
void update();
double now();
float elapsed();
private:
float elapsed_;
double lastTime_;
};
}
#endif
Timer.cpp
#include "Timer.hpp"
namespace MyNS {
Timer::Timer() : elapsed_(0.0f), lastTime_(0.0f) {}
void Timer::reset() {
elapsed_ = 0.0f;
lastTime_ = now();
}
void Timer::update() {
double currentTime = now();
elapsed_ = (currentTime - lastTime_);
lastTime_ = currentTime;
}
double Timer::now() {
timespec timeVal;
clock_gettime(CLOCK_MONOTONIC, &timeVal);
return timeVal.tv_sec + (timeVal.tv_nsec * 1.0e-9);
}
float Timer::elapsed() {
return elapsed_;
}
}