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`. -![screenshot](screenshot.png) +[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