Android App Dev in C on Windows. No Java, No Gradle, No Android Studio.

This is my setup process for developing Android Apps in C on Windows. This method is as of yet, untested. I plan on starting a project using this setup, but I wanted to write this as I was going through it myself. The process should be the same for macOS and Linux. I'll update for Linux once I get around to installing it on my laptop. There are no prerequisites to follow this guide. At the end you'll be setup for native Android development.
No .java, no Kotlin(I don't know the file extension), no classes.dex, whatever that is.

Step 1: Install the JDK

The apps you'll write won't contain any Java, but the tooling is written in Java, so we still need the Java Development Kit.
The easiest install on Windows is:
winget install Microsoft.OpenJDK.21
Feel free to use the package manager of your choice.
You can verify the install using java --version

Step 2: Install the Android Command-Line Tools

Download "Command line tools only" from the bottom of https://developer.android.com/studio Extract the downloaded tools and create an SDK folder named android-sdk.
I create mine at C:\android-sdk, but place yours wherever you'd like.
By default, the extracted tools will nest like: /cmdline-tools/other folders, But we need to insert a folder like: /cmdline-tools/latest/other folders.
That was confusing, more information can be found in the docs

Step 3: Install SDK Packages

Now that the command line tools are in the proper place, we can grab the sdk packages we'll need for development. Open a terminal and cd to the directory where you placed cmdline-tools\latest\bin, in my case that's
cd C:\android-sdk\cmdline-tools\latest\bin and run
sdkmanager --list | findstr /i "build-tools ndk platforms;android"
We're looking for the newest version of 4 packages.

  • platform-tools - which provides adb, the tool that lets us talk to our android phone docs
  • platforms;android-XX - provides android.jar, the Android library the packager compiles against as its build target. There's one SDK Platform per Android version. The official reference is the SDK Platform release notes.
  • ndk;XX.X.XXXXXX - the Native Development Kit provides a clang cross compiler to run our c code on Android, plus c headers.https://developer.android.com/ndk
  • build-tools;XX.X.X - the tools that turn our code into an app:


All 4 can be installed, substituting the versions below for the versions found in the last cmd.
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865"

Then you'll need to accept the licenses.
sdkmanager --licenses
Answer y to each prompt
And verify everything worked
dir C:\android-sdk
There should be 4 new folders, platform-tools, platforms build-tools, and ndk.

Step 4: Environment Variables


Run these two commands in cmd to setup your environment variables.
setx ANDROID_HOME "C:\android-sdk"
setx PATH "%PATH%;C:\android-sdk\platform-tools"
Make sure to open a new cmd so it will see the variable we just set.

Step 5: Phone Setup


On the phone:

  1. Settings -> About Phone -> tap Build number 7 times to unlock Developer options.
  2. Settings -> Developer options -> enable USB debugging
  3. Connect phone to pc via USB
  4. Accept the "Allow USB debugging?" prompt on the phone


Type adb devices into cmd. The output should say,
List of devices attached
XXXXXXXXXXXXXX device
If it says unauthorized, run adb kill-server, replug and check again
It's also possible to setup wireless debugging, but I haven't tried this myself. It's clearly explained in the docs

Step 6: Project Files


Create a project folder with three files.
AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.notes"
          android:versionCode="1" android:versionName="0.1">
        <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
        <application android:label="Notes" android:hasCode="false">
        <activity android:name="android.app.NativeActivity"
                android:exported="true"
                android:configChanges="orientation|screenSize|keyboardHidden">
          <meta-data android:name="android.app.lib_name" android:value="notes"/>
          <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
          </intent-filter>
        </activity>
      </application>
    </manifest>

I took most of this from the rawdrawandroid github page rawdrawandroid
You can find more options on developer.android.com
main.c
Uses native_app_glue from the NDK to run c code on its own thread.
This demo app fills the screen with a color and cycles colors on tap. Right now, I'm just setting a global called g_color_index and changing the index when the user taps.
This is obviously not a great long term practice, but it's good enough to get things started.
Hopefully I'll be able to provide a better example in future blogs.
I'm watching Handmade Hero again and I really just wanted to write the equivalent of StretchDIBits on Android.

#include <android_native_app_glue.h>
#include <android/native_window.h>
#include <android/log.h>
#include <stdint.h>

// WINDOW_FORMAT_RGBX_8888 is byte order R,G,B,X in memory,
// which on little-endian ARM makes the uint32 0xXXBBGGRR.
#define COLOR(r, g, b) (0xFF000000u | ((uint32_t)(b) << 16) | ((uint32_t)(g) << 8) | (uint32_t)(r))

static uint32_t g_palette[] = {
    COLOR(0x28, 0x2C, 0x34),  // dark slate
    COLOR(0x3A, 0x6E, 0xA5),  // blue
    COLOR(0x98, 0xC3, 0x79),  // green
    COLOR(0xE0, 0x6C, 0x75),  // red
};
static int g_color_index = 0;

static void draw_frame(struct android_app *app)
{
    if (!app->window) return;

    ANativeWindow_setBuffersGeometry(app->window, 0, 0, WINDOW_FORMAT_RGBX_8888);

    ANativeWindow_Buffer buffer;
    if (ANativeWindow_lock(app->window, &buffer, 0) != 0) return;

    uint32_t color = g_palette[g_color_index];
    uint32_t *row = (uint32_t *)buffer.bits;
    for (int y = 0; y < buffer.height; ++y) {
        for (int x = 0; x < buffer.width; ++x) {
            row[x] = color;
        }
        row += buffer.stride;  // stride is in pixels, not bytes
    }

    ANativeWindow_unlockAndPost(app->window);
}

static void handle_cmd(struct android_app *app, int32_t cmd)
{
    switch (cmd) {
    case APP_CMD_INIT_WINDOW:
        __android_log_print(ANDROID_LOG_INFO, "notes", "window ready: drawing first frame");
        draw_frame(app);
        break;
    case APP_CMD_WINDOW_RESIZED:
    case APP_CMD_WINDOW_REDRAW_NEEDED:
        draw_frame(app);
        break;
    }
}

static int32_t handle_input(struct android_app *app, AInputEvent *event)
{
    if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION &&
        (AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK) == AMOTION_EVENT_ACTION_DOWN)
    {
        g_color_index = (g_color_index + 1) % 4;
        __android_log_print(ANDROID_LOG_INFO, "notes", "tap at (%.0f, %.0f), color %d",
            AMotionEvent_getX(event, 0), AMotionEvent_getY(event, 0), g_color_index);
        draw_frame(app);
        return 1;  // consumed
    }
    return 0;
}

void android_main(struct android_app *app)
{
    app->onAppCmd = handle_cmd;
    app->onInputEvent = handle_input;
    __android_log_print(ANDROID_LOG_INFO, "notes", "android_main started");

    while (1) {
        int events;
        struct android_poll_source *source;

        // Block until something happens; we only redraw on events.
        while (ALooper_pollOnce(-1, NULL, &events, (void **)&source) >= 0) {
            if (source) source->process(app, source);
            if (app->destroyRequested) {
                __android_log_print(ANDROID_LOG_INFO, "notes", "destroy requested, exiting");
                return;
            }
        }
    }
}

build.bat
Compiles, packages, aligns, signs, installs, and launches our program. You'll have to adjust the version numbers at the top to match the installed packages.
This is a collection of commands/flags from all of the documentation linked up to this point. There are really 7 smaller build steps, each one is echo'd to cmd.

@echo off
setlocal

set SDK=C:\android-sdk
set NDK=%SDK%\ndk\29.0.14206865
set BT=%SDK%\build-tools\36.0.0
set PLATFORM=%SDK%\platforms\android-36\android.jar
set TOOLCHAIN=%NDK%\toolchains\llvm\prebuilt\windows-x86_64\bin
set GLUE=%NDK%\sources\android\native_app_glue

if not exist lib\arm64-v8a mkdir lib\arm64-v8a

echo === Compiling ===
call %TOOLCHAIN%\aarch64-linux-android26-clang.cmd -shared -O0 -g ^
  -o lib\arm64-v8a\libnotes.so ^
  main.c %GLUE%\android_native_app_glue.c ^
  -I%GLUE% ^
  -landroid -llog ^
  -u ANativeActivity_onCreate
if errorlevel 1 (echo COMPILE FAILED & exit /b 1)

echo === Packaging ===
call %BT%\aapt2.exe link -o notes.unaligned.apk ^
  -I %PLATFORM% ^
  --manifest AndroidManifest.xml ^
  --min-sdk-version 26 ^
  --target-sdk-version 34
if errorlevel 1 (echo AAPT2 LINK FAILED & exit /b 1)

echo === Adding native library ===
call %BT%\aapt.exe add notes.unaligned.apk lib/arm64-v8a/libnotes.so
if errorlevel 1 (echo ADD SO FAILED & exit /b 1)

echo === Aligning ===
%BT%\zipalign -f 4 notes.unaligned.apk notes.apk
if errorlevel 1 (echo ZIPALIGN FAILED & exit /b 1)

if not exist debug.keystore (
  echo === Creating debug keystore ===
  keytool -genkeypair -keystore debug.keystore -alias debug -keyalg RSA ^
    -validity 10000 -storepass android -keypass android -dname "CN=Debug"
)

echo === Signing ===
call %BT%\apksigner.bat sign --ks debug.keystore --ks-pass pass:android notes.apk
if errorlevel 1 (echo SIGN FAILED & exit /b 1)

echo === Installing ===
adb install -r notes.apk
if errorlevel 1 (echo INSTALL FAILED & exit /b 1)

echo === Launching ===
adb shell am force-stop com.example.notes
adb shell am start -n com.example.notes/android.app.NativeActivity
  

Build and run


Run build.bat with your phone plugged in and the app will install and launch.
Run adb logcat -s notes in cmd to see a live log from the app.

ClangD Setup

compile_flags.txt


--target=aarch64-linux-android26
--sysroot=C:/android-sdk/ndk/29.0.14206865/toolchains/llvm/prebuilt/windows-x86_64/sysroot
-IC:/android-sdk/ndk/29.0.14206865/sources/android/native_app_glue
  

← Back to all posts