From 58be68acd5c7c9ea57a670361e05da771d72e3ed Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 23 Sep 2025 13:48:41 -0700 Subject: [PATCH 1/2] Remove obsolete screenshot. This screenshot is no longer accurate, and I don't think it's useful anyway, so I'm deleting it rather than updating it. --- native-activity/README.md | 4 ---- native-activity/screenshot.png | Bin 12784 -> 0 bytes 2 files changed, 4 deletions(-) delete mode 100644 native-activity/screenshot.png diff --git a/native-activity/README.md b/native-activity/README.md index 5e4073763..447daba9b 100644 --- a/native-activity/README.md +++ b/native-activity/README.md @@ -19,7 +19,3 @@ clear. [GameActivity]: https://developer.android.com/games/agdk/game-activity [NativeActivity]: http://developer.android.com/reference/android/app/NativeActivity.html - -## Screenshots - -![screenshot](screenshot.png) diff --git a/native-activity/screenshot.png b/native-activity/screenshot.png deleted file mode 100644 index 92f2bfcdc0e06dcb497f40c3f887f148af01af06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12784 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

k=JZjWM6XS#~U{C8;>o;v-ayn8Ub?*xz;5O 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 Yj#G=rtoB!dfG=Zxi<@QF@BH$A0OiB^?f?J) From 755346189f4f6f476f661138cc3c735f278af6b5 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 25 Sep 2025 15:08:23 -0700 Subject: [PATCH 2/2] 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. --- native-activity/README.md | 54 ++++++- .../app/src/main/AndroidManifest.xml | 68 +++++---- .../app/src/main/cpp/CMakeLists.txt | 37 ++--- .../src/main/cpp/libnative-activity.map.txt | 5 + native-activity/app/src/main/cpp/main.cpp | 134 ++++++++++++++---- 5 files changed, 221 insertions(+), 77 deletions(-) diff --git a/native-activity/README.md b/native-activity/README.md index 447daba9b..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,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 `` 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. }