diff --git a/native-activity/README.md b/native-activity/README.md
index 5e4073763..90d452e40 100644
--- a/native-activity/README.md
+++ b/native-activity/README.md
@@ -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,9 +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
-## Screenshots
+## 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 `` 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
diff --git a/native-activity/app/src/main/AndroidManifest.xml b/native-activity/app/src/main/AndroidManifest.xml
index 0626d44a9..77d2fd4f2 100644
--- a/native-activity/app/src/main/AndroidManifest.xml
+++ b/native-activity/app/src/main/AndroidManifest.xml
@@ -1,38 +1,44 @@
-
+ android:versionName="1.0">
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 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).
+ -->
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/native-activity/app/src/main/cpp/CMakeLists.txt b/native-activity/app/src/main/cpp/CMakeLists.txt
index 08c1b8829..2d28cfd4e 100644
--- a/native-activity/app/src/main/cpp/CMakeLists.txt
+++ b/native-activity/app/src/main/cpp/CMakeLists.txt
@@ -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 $ 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.
$
log
)
diff --git a/native-activity/app/src/main/cpp/libnative-activity.map.txt b/native-activity/app/src/main/cpp/libnative-activity.map.txt
index c1c1974b2..042561d7c 100644
--- a/native-activity/app/src/main/cpp/libnative-activity.map.txt
+++ b/native-activity/app/src/main/cpp/libnative-activity.map.txt
@@ -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:
*;
diff --git a/native-activity/app/src/main/cpp/main.cpp b/native-activity/app/src/main/cpp/main.cpp
index e1ae34e85..e261c35d6 100644
--- a/native-activity/app/src/main/cpp/main.cpp
+++ b/native-activity/app/src/main/cpp/main.cpp
@@ -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
#include
@@ -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(buffer.bits)[pixel_idx] =
static_cast(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(&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.
}
diff --git a/native-activity/screenshot.png b/native-activity/screenshot.png
deleted file mode 100644
index 92f2bfcdc..000000000
Binary files a/native-activity/screenshot.png and /dev/null differ