Tutorial 82: Android Build and Deployment
What you’ll learn
- Setting up the NDK and the SDL3 Android project template.
- Wiring CNA into the Gradle/CMake build for an ARM64-v8a target.
- Choosing a renderer (
SDL_RENDERERby default, OpenGL ES 3.0 optionally), and packaging the APK. - Where Android support genuinely stands, and what is untested.
Before you start — Tutorial 80: Cross-Platform Build Guide (the cross-platform build baseline), Tutorial 49: Touch Input and Gestures and Tutorial 50: Accelerometer and Sensors (the input paths a phone actually uses).
Where Android support actually stands. The code paths and NDK sensor backends exist and the build wiring is real, but no CI workflow builds or runs anything on Android. Everything on this page is a documented route, not a machine-verified one — budget time for hardware testing and expect to be the person who finds the problems.
CNA’s commit history at this snapshot does record emulator runs, found through downstream games: Android lifecycle events are now actually delivered and IsActive follows the application lifecycle rather than window focus; .cnb content loads from inside the APK; a portrait game runs in portrait; the Back button reaches the game once as Keys::Escape and, for a game that polls pads, as player one’s Back; and on the emulator EasyGL rebases base-vertex draws on the CPU (CNA_EASYGL_CPU_BASE_VERTEX=1 forces that anywhere). These are emulator observations recorded in commit messages, not device evidence and not a CI result.
Two concrete gaps to plan around. Video does not work on Android: FFmpeg is never built for Android, so although Video and VideoPlayer link, opening a Video or playing one throws System::NotSupportedException (guard it with #ifdef CNA_VIDEO_AVAILABLE). This snapshot selects SDL_RENDERER by default on Android, but has no automatic Android build or runtime lane and no CMake preset; do not generalize source wiring into per-device graphics evidence.
Android NDK setup
# Install an NDK through Android Studio's SDK Manager, then point at that exact install.
# The one Android project in CNA's tree (the Devices demo) pins ndkVersion 30.0.14904198;
# nothing in CMake checks the NDK version, and no other version is verified. Do not assume a path.
export ANDROID_NDK=/absolute/path/to/Android/Sdk/ndk/your-installed-version
SDL3 Android project template
SDL3 provides a complete Android project template in SDL/android-project/. Copy it and add
your CNA sources to the JNI layer:
cp -r $CNA_ROOT/third_party/SDL/android-project ./MyAndroidGame
cd MyAndroidGame
# Add your C++ sources to app/jni/src/
CNA CMake integration
# app/jni/CMakeLists.txt — follow the shape of the Devices demo APK in CNA's own tree.
cmake_minimum_required(VERSION 3.20)
project(MyGame LANGUAGES CXX)
set(CNA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CNA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(CNA_GRAPHICS_RENDERER SDL_RENDERER CACHE STRING "" FORCE)
add_subdirectory(${CNA_SOURCE_DIR} cna-build)
# SDLActivity loads a shared object named main.
add_library(main SHARED main.cpp)
target_compile_features(main PRIVATE cxx_std_23)
target_link_libraries(main PRIVATE CNA SHARP_RUNTIME SDL3::SDL3)
CNA_SOURCE_DIR above is an application-local path variable pointing at your CNA source checkout (the apple/m4-stabilization branch); it is not a CNA cache option. CNA's own packaged example nests this pattern under modules/devices/examples/demo_devices/android/: its app/jni/CMakeLists.txt adds the CNA root with CNA_BUILD_TESTS and CNA_BUILD_EXAMPLES off, so CNA’s configure-time SDL3 cross-build is reused instead of building SDL twice, and its app/jni/src/CMakeLists.txt re-imports SDL3::SDL3 with find_package(SDL3 CONFIG REQUIRED) because that imported target is scoped to the CNA directory. Gradle uses the ndkBuild route by default and this CMake route when you pass -PBUILD_WITH_CMAKE. A copied SDL template still needs matching Gradle, manifest and SDLActivity wiring.
ARM64-v8a target
Build for arm64-v8a (AArch64) as the primary Android ABI. Add another ABI only after
validating CNA and all native dependencies for it; there is no Android CI matrix, and the Devices demo builds arm64-v8a only.
cmake -S . -B build-android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-24 \
-DCNA_BUILD_TESTS=OFF \
-DCNA_BUILD_EXAMPLES=OFF \
-DCNA_GRAPHICS_RENDERER=SDL_RENDERER
OpenGL ES 3.0 on Android
The default renderer on Android is SDL_RENDERER (only Linux and Emscripten have other defaults). If your separately validated application chooses
OPENGLES3, it also needs the EasyGL/meta-gl sibling checkouts and a device that advertises
OpenGL ES 3.0. Declare that requirement in AndroidManifest.xml so package tooling can filter
incompatible devices:
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
TouchPanel (needs hardware validation)
CNA exposes TouchPanel for multi-touch input on Android. The API mirrors XNA's
TouchCollection but requires a real device for proper validation — the emulator's touch
simulation is unreliable for multi-finger gestures.
#include "Microsoft/Xna/Framework/Input/Touch/TouchPanel.hpp"
#include "Microsoft/Xna/Framework/Input/Touch/TouchLocation.hpp"
using namespace Microsoft::Xna::Framework::Input::Touch;
TouchCollection touches = TouchPanel::GetState();
for (const TouchLocation& touch : touches) {
// getPositionProperty() is a Vector2 in screen pixels;
// getStateProperty() is Pressed / Moved / Released / Invalid
HandleTouch(touch.getPositionProperty(), touch.getStateProperty());
}
See Tutorial 49 for the full TouchLocationState lifecycle and gestures.
Accelerometer on Android
Accelerometer and Gyroscope are not Android-only. Both reach a real SDL3 hardware probe on desktop Linux, Windows and macOS too, so you can develop against them without a phone in hand. Compass and Motion are Android-only. See Tutorial 50.
SDL3 exposes the device accelerometer through sensor events. CNA wraps this in the
Accelerometer class, mirroring the XNA API.
#include "Microsoft/Devices/Sensors/Accelerometer.hpp"
using namespace Microsoft::Devices::Sensors;
if (Accelerometer::getIsSupportedProperty()) { // true on a device with a real sensor
Accelerometer accelerometer;
accelerometer.Start();
AccelerometerReading reading = accelerometer.getCurrentValueProperty();
Vector3 g = reading.getAccelerationProperty(); // m/s²
// X: left/right tilt, Y: forward/back tilt, Z: gravity (~9.8 face-up)
}
See Tutorial 50 for the complete pattern (keep the Accelerometer alive as a member and call Stop() when you are done).
APK packaging
# Build debug APK
cd MyAndroidGame
./gradlew assembleDebug
# Install on connected device
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.example.mygame/.MainActivity
# Build release APK (requires signing key)
./gradlew assembleRelease
Gradle configuration
// app/build.gradle -- values from the Devices demo in CNA's tree
android {
namespace = "com.example.mygame"
compileSdkVersion 35
ndkVersion = "30.0.14904198"
defaultConfig {
applicationId "com.example.mygame"
minSdkVersion 24
targetSdkVersion 35
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
arguments "-DANDROID_PLATFORM=android-24"
abiFilters 'arm64-v8a'
}
}
}
externalNativeBuild {
cmake {
path 'jni/CMakeLists.txt'
}
}
}
The renderer is chosen in jni/CMakeLists.txt (the CNA_GRAPHICS_RENDERER cache line above) rather than through Gradle arguments, and the C++23 requirement reaches your target through the cxx_std_23 compile feature that CNA publishes (the CMakeLists.txt snippet above requests it explicitly too). The Devices demo uses arm64-v8a only; add another ABI only after building and testing CNA and every native dependency for it.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Android and Apple targets: structure, lifecycle, assets and evidence — How CNA's Android application and NDK code are structured, how Game handles mobile lifecycle events, how assets and saves are found on a device, and what the Mac mini M4 qualification, the iOS Simulator probe and the Apple workflows show.