Expand documentation for native-activity sample.

This expands the docs and comments in the native-activity sample to be
something closer to a proper guide. There isn't a very clear flow to it
since the documentation is necessarily spread out into multiple files,
and sometimes the order of the code requires explaining things out of
order, but it's better than nothing.

If we wanted to turn this into an actual guide that could be written in
a clear order without duplicating code snippets into the docs, which
would surely be quickly out of date, we could use something like sphinx
(possibly through readthedocs) to generate something closer to a
literate code sample.
This commit is contained in:
Dan Albert
2025-09-25 15:08:23 -07:00
parent 58be68acd5
commit 755346189f
5 changed files with 221 additions and 77 deletions

View File

@@ -1,15 +1,30 @@
# Native Activity
> [!WARNING]
> **Most apps should not use the app development model shown in this sample**.
> Instead, use a Java or Kotlin `AppCompatActivity` and connect your native code
> using JNI like the other samples in this repository. `NativeActivity` and
> `GameActivity` attempt to translate the Android [activity lifecycle] into a
> desktop style `main()` function with a polled event loop. That is not how
> Android apps work, and while it may help you get your prototype running more
> quickly, as your app matures you will likely end up retranslating the
> `native_app_glue` model to again look like `Activity`.
This is an Android sample that uses [NativeActivity] with `native_app_glue`,
which enables building NDK apps without having to write any Java code. In
practice most apps, even games which are predominantly native code, will need to
call some Java APIs or customize their app's activity further.
call some Java APIs or customize their app's activity further. While it may save
you a small amount of effort during prototyping, it may result in a difficult
migration later. It's also worth noting that some of the code in this sample is
spent undoing the work of `native_app_glue` to create a class very similar to
`Activity`.
The more modern approach to this is to use [GameActivity], which has all the
same benefits as `NativeActivity` and `native_app_glue`, while also making it
easier to include Java code in your app without a rewrite later. It's also
source compatible. This sample will likely migrate to `GameActivity` in the
future.
source compatible. However, it still has all the problems explained in the
warning above, and in practice neither `NativeActivity` nor `GameActivity` is
the recommended app development model.
The app here is intentionally quite simple, aiming to show the core event and
draw loop necessary for an app using `native_app_glue` without any extra
@@ -17,5 +32,38 @@ clutter. It uses `AChoreographer` to manage the update/render loop, and uses
`ANativeWindow` and `AHardwareBuffer` to update the screen with a simple color
clear.
[activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle
[GameActivity]: https://developer.android.com/games/agdk/game-activity
[NativeActivity]: http://developer.android.com/reference/android/app/NativeActivity.html
## Walkthrough
The interesting sections of code in this sample are in three files:
[AndroidManifest.xml], [CMakeLists.txt], and [main.cpp]. Each of those files has
code comments explaining the portions relevant to using `NativeActivity`, but
the high level details of the app are explained here.
This app uses `NativeActivity` rather than its own child class of `Activity` or
`AppCompatActivity`. This is specified in the `<activity />` declaration in [the
manifest].
Apps which use `NativeActivity` are typically written using `native_app_glue`,
which adapts the Android activity lifecycle code to look more like a desktop
program with a `main()` function and an event loop. This is set up in the app's
[CMakeLists.txt file].
When using `native_app_glue` with a [version script], you must export
`ANativeActivity_onCreate`. This sample does this in
[libnative-activity.map.txt].
This is a fairly simple application, so all of the code is in a single file,
[main.cpp]. The entry point for an app using `native_app_glue` is
`android_main()`. That function is the best place to start reading in this file
to learn how the sample works, then follow through to the definition of
`engine_handle_cmd` and `Engine`.
[CMakeLists.txt file]: app/src/main/cpp/CMakeLists.txt
[libnative-activity.map.txt]: app/src/main/cpp/libnative-activity.map.txt
[main.cpp]: app/src/main/cpp/main.cpp
[the manifest]: app/src/main/AndroidManifest.xml
[version script]: https://developer.android.com/ndk/guides/symbol-visibility

View File

@@ -1,38 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0">
android:versionName="1.0">
<!--
This .apk has no Java/Kotlin code, so set hasCode to false.
<!--
This .apk has no Java/Kotlin code, so set hasCode to false.
If you copy from this sample and later add Java/Kotlin code, or add a
dependency on a library that does (such as androidx), be sure to set
`android:hasCode` to `true` (or just remove it, since that's the default).
-->
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hasCode="false">
<!-- 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"
android:exported="true">
<!-- Tell NativeActivity the name of our .so -->
<meta-data android:name="android.app.lib_name"
android:value="native-activity" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
If you copy from this sample and later add Java/Kotlin code, or add a
dependency on a library that does (such as androidx), be sure to set
`android:hasCode` to `true` (or just remove it, since that's the default).
-->
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hasCode="false">
<!--
This app uses android.app.NativeActivity rather than its own child class of
Activity or AppCompatActivity. The advantage of this is that we do not have
to write any Java code of our own. The Java portion of the app is provided
by the OS.
-->
<activity
android:name="android.app.NativeActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true">
<!--
This property tells NativeActivity which of our app's libraries
provide the definition of ANativeActivity_onCreate, which is the
entry point for apps using ANativeActivity.
-->
<meta-data
android:name="android.app.lib_name"
android:value="native-activity" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<!-- END_INCLUDE(manifest) -->

View File

@@ -1,31 +1,34 @@
#
# Copyright (C) The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright (C) 2010 The Android Open Source Project
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 4.1.0)
project(NativeActivity LANGUAGES C CXX)
include(AppLibrary)
include(AndroidNdkModules)
# This includes the AndroidNdkModules.cmake file, which is shipped with the
# NDK's CMake distribution. Including this file defines
# android_ndk_import_module_native_app_glue(), which defines the
# native_app_glue target when called.
include(AndroidNdkModules)
android_ndk_import_module_native_app_glue()
add_app_library(native-activity SHARED main.cpp)
# Linking the native_app_glue target with our native-activity target (the
# library which contains the main app code) includes native_app_glue in the app.
target_link_libraries(native-activity
android
# We have to use $<LINK_LIBRARY:WHOLE_ARCHIVE,native_app_glue> rather than
# the simpler native_app_glue spelling to instruct the linker that the
# entire native_app_glue static library should be included in
# libnative-activity.so, even if the linker does not find any calls in our
# code to native_app_glue. This is because native_app_glue is a static
# library rather than a shared library, and normally the linker will only
# include code from static libraries when it has found a call to that code.
# This is usually a good thing because it reduces the size of the app, but
# in this case the calls to native_app_glue, specifically the
# ANativeActivity_onCreate function, don't come from us, but instead come
# from ANativeActivity, which the linker cannot detect.
$<LINK_LIBRARY:WHOLE_ARCHIVE,native_app_glue>
log
)

View File

@@ -1,5 +1,10 @@
LIBNATIVEACTIVITY {
global:
# When using NativeActivity and you don't need any of your own JNI
# functions this is the only symbol that should be exported. If your app
# needs additional JNI functions (and most apps will), then you'll need to
# include JNI_OnLoad (or your individual Java_... functions if you're not
# using RegisterNatives) here.
ANativeActivity_onCreate;
local:
*;

View File

@@ -1,19 +1,5 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Copyright (C) 2010 The Android Open Source Project
// SPDX-License-Identifier: Apache-2.0
#include <android/choreographer.h>
#include <android/hardware_buffer.h>
@@ -71,13 +57,30 @@ enum class Color : uint32_t {
};
/**
* Shared state for our app.
* The implementation for our app.
*
* This class implements the activity lifecycle behaviors akin to how Activity
* would in a Java app. With native_app_glue, those lifecycle events are instead
* communicated to this class from engine_handle_cmd, which is in turned called
* by looper (see the description below in android_main).
*
* The comments here will briefly explain some aspects of the Android activity
* lifecycle, but they cannot explain it fully. See
* https://developer.android.com/guide/components/activities/activity-lifecycle
* and the other docs in that section for more information.
*/
class Engine {
public:
explicit Engine(android_app* app) : app_(app) {}
void AttachWindow() {
// This is called whenever a new native window is created for our app, so we
// need to reinitialize the buffer format to the format our render loop
// expects.
//
// Attaching the window will not cause the app to start running its update
// and render loop. The app's update cycle is separately enabled by
// Engine::Resume.
if (ANativeWindow_setBuffersGeometry(
app_->window, 0, 0, AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM) < 0) {
LOGE("Unable to set window buffer geometry");
@@ -89,10 +92,29 @@ class Engine {
last_update_ = std::chrono::steady_clock::now();
}
void DetachWindow() { window_initialized = false; }
void DetachWindow() {
// This is called whenever the native window for our app is destroyed. That
// does not necessarily mean that the app is being killed, as it is also
// done when the screen rotates.
//
// For a more typical app where the rendering is done with OpenGL or Vulkan,
// this is where you'd perform any window cleanup needed by those
// frameworks. For our app, it's sufficient to just set a flag to disable
// our render loop.
window_initialized = false;
}
/// Resumes ticking the application.
void Resume() {
// This is called whenever the activity is resumed (brought into the
// foreground). When that happens, we schedule our next update tick with
// Choreographer. Choreographer is the Android system that paces app render
// loops. If you instead render new frames in a loop without frame pacing,
// you risk rendering more quickly than the display pipeline is able to
// present new frames. This will increase the latency between frame
// submission and presentation.
// https://developer.android.com/ndk/reference/group/choreographer
// Checked to make sure we don't double schedule Choreographer.
if (!running_) {
running_ = true;
@@ -104,7 +126,12 @@ class Engine {
///
/// When paused, sensor and input events will still be processed, but the
/// update and render parts of the loop will not run.
void Pause() { running_ = false; }
void Pause() {
// This is called whenever something interrupts the activity and moves it
// into the background. In multiwindow mode the app might still be visible,
// but it is no longer the focused app and should pause accordingly,
running_ = false;
}
private:
android_app* app_;
@@ -172,6 +199,7 @@ class Engine {
return;
}
// Lock the native window's buffer so we can write to it.
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(app_->window, &buffer, nullptr) < 0) {
LOGE("Unable to lock window buffer");
@@ -186,34 +214,50 @@ class Engine {
return;
}
// Write a solid color to the window buffer.
for (auto y = 0; y < buffer.height; y++) {
for (auto x = 0; x < buffer.width; x++) {
// Note that we index the row by the buffers stride, not its width. The
// buffer itself may be wider than the render area.
size_t pixel_idx = y * buffer.stride + x;
reinterpret_cast<uint32_t*>(buffer.bits)[pixel_idx] =
static_cast<uint32_t>(color_);
}
}
// Now unlock the buffer, causing the display to update.
ANativeWindow_unlockAndPost(app_->window);
}
};
/**
* Process the next main command.
* The callback for native_app_glue's Activity lifecycle event queue.
*/
static void engine_handle_cmd(android_app* app, int32_t cmd) {
auto* engine = (Engine*)app->userData;
// There are a lot of lifecycle events that we're ignoring here. See
// android_native_app_glue.h for the complete list that native_app_glue
// handles (which may not be complete if Activity adds new lifecycle
// methods!)
//
// Most applications will need to handle many more of than just this set.
// We're getting away with ignoring most events because this app doesn't do
// anything interesting.
switch (cmd) {
case APP_CMD_INIT_WINDOW:
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onnativewindowcreated
engine->AttachWindow();
break;
case APP_CMD_TERM_WINDOW:
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onnativewindowdestroyed
engine->DetachWindow();
break;
case APP_CMD_GAINED_FOCUS:
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onwindowfocuschanged
engine->Resume();
break;
case APP_CMD_LOST_FOCUS:
// https://developer.android.com/ndk/reference/struct/a-native-activity-callbacks#onwindowfocuschanged
engine->Pause();
break;
default:
@@ -222,19 +266,53 @@ static void engine_handle_cmd(android_app* app, int32_t cmd) {
}
/**
* This is the main entry point of a native application that is using
* android_native_app_glue. It runs in its own thread, with its own
* event loop for receiving input events and doing other things.
* `android_main()` is the entry point for an app using `native_app_glue`.
*
* This function is called from a separate thread spawned from
* `ANativeActivity_onCreate`, which is the native equivalent of the
* `onCreate` stage in the activity lifecycle:
* https://developer.android.com/guide/components/activities/activity-lifecycle
*
* The `android_main()` implementation typically will perform application setup,
* enter the main event loop, and shut down if necessary.
*/
void android_main(android_app* state) {
Engine engine{state};
state->userData = &engine;
// onAppCmd is called whenever native_app_glue receives one of the activity
// lifecycle events from the framework:
// https://developer.android.com/guide/components/activities/activity-lifecycle
//
// Typical native Android applications would implement the various
// onPause(), onResume(), etc in JNI methods. native_app_glue handles that
// for us and instead presents those method calls as if they were a pollable
// event queue. Our engine_handle_cmd callback is the function that will
// respond to new events in that queue.
state->onAppCmd = engine_handle_cmd;
// The userData property will be passed to the callback we registered with
// onAppCmd.
state->userData = &engine;
// destroyRequested will be set when onDestroy() is called:
// https://developer.android.com/guide/components/activities/activity-lifecycle#ondestroy
while (!state->destroyRequested) {
// Our input, sensor, and update/render logic is all driven by callbacks, so
// we don't need to use the non-blocking poll.
// native_app_glue communicates events to the app using Looper rather than
// method calls like a Java activity would use. Looper is an Android API
// similar to POSIX's select(2):
// https://developer.android.com/ndk/reference/group/looper#alooper
//
// Whenever an activity lifecycle method is called on our ANativeActivity,
// or an input event is received, native_app_glue will forward that to our
// app as a looper event.
//
// Polling looper can be done in either a blocking or non-blocking manner.
// If your app needs to wake periodically on this thread, pass a value for
// the poll timeout. Most of the things you'd normally do during this loop
// (respond to input or sensor updates, or even render the next frame of
// your game) should be driven by callbacks registered with those
// subsystems though rather than done here. Our main loop doesn't need to
// do anything other than process looper events.
android_poll_source* source = nullptr;
auto result = ALooper_pollOnce(-1, nullptr, nullptr,
reinterpret_cast<void**>(&source));
@@ -246,4 +324,8 @@ void android_main(android_app* state) {
source->process(state, source);
}
}
// Most cleanup code should actually run in response to activity lifecycle
// events that are processed in engine_handle_cmd rather than after the main
// loop exits, as would be more typical of main loops on desktop platforms.
}