diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..08fbed3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+bin
+lib
+obj
+libmain.so
+.keystore
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..70a729e
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e58b88c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+This is a basic Android native example written completely in Odin. It adapts and rewrites mmozeiko's [C example](https://github.com/mmozeiko/android-native-example) to work with Odin.
+
+This requires that you write some basic bindings to the Android NDK in order to compile and run.
+
+# Building
+
+First you must adjust `build.sh` and `launch.sh` to have correct paths to Android SDK, Android NDK and the Jetbrains RT.
+Then set `APK` variable to the desired apk package name. Use Android SDK to install build-tools and platform SDK.
+
+Now you can use `build.sh` to build and install the app, and `launch.sh` to run the application:
diff --git a/assets/fragment.glsl b/assets/fragment.glsl
new file mode 100644
index 0000000..57f8190
--- /dev/null
+++ b/assets/fragment.glsl
@@ -0,0 +1,9 @@
+//
+
+precision mediump float;
+varying vec3 color;
+
+void main()
+{
+ gl_FragColor = vec4(color, 1.0);
+}
diff --git a/assets/vertex.glsl b/assets/vertex.glsl
new file mode 100644
index 0000000..678f8a6
--- /dev/null
+++ b/assets/vertex.glsl
@@ -0,0 +1,11 @@
+//
+
+attribute vec2 vPosition;
+attribute vec3 vColor;
+varying vec3 color;
+
+void main()
+{
+ color = vColor;
+ gl_Position = vec4(vPosition, 0.0, 1.0);
+}
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..c701676
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+## TODO: handle different ABIs, currently only builds for arm64
+
+APK=native_example.apk
+ANDROID_SDK_ROOT=/home/illusion/Android/Sdk
+ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125
+ANDROID_TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64
+ANDROID_JBR=/home/illusion/android-studio/jbr
+BUILD_TOOLS=34.0.0
+PLATFORM=34
+
+# Create required dirs
+mkdir -p lib/lib/arm64-v8a
+mkdir -p bin
+
+# Compile the glue code that comes with the NDK to an object using the aarch64 crosscompiler.
+$ANDROID_TOOLCHAIN/bin/aarch64-linux-android24-clang -c $ANDROID_NDK/sources/android/native_app_glue/android_native_app_glue.c -o android_native_app_glue.o -I $ANDROID_TOOLCHAIN/sysroot/usr/include/
+
+# Create a static lib from the glue code object
+$ANDROID_TOOLCHAIN/bin/llvm-ar rcs libandroid_native_app_glue.a android_native_app_glue.o
+
+# Compile our odin code to an object. relocation mode is set to PIC because the linker complains.
+# freestanding_arm64 should probably be preferred but raylib projects need linux_arm64, otherwise the android dynamic linker fails to find our main function.
+odin build jni/ -build-mode:obj -target:linux_arm64 -reloc-mode:pic || exit 1
+
+# Link the static library with our object and create the shared library that will hold all our native code.
+## We pass "-u ANativeActivity_onCreate" to the linker here because that symbol gets stripped by the linker even though it's required
+## This is exactly what the official ndk-build building process does, see: https://github.com/android/ndk/issues/381
+$ANDROID_TOOLCHAIN/bin/aarch64-linux-android24-clang -shared -o libmain.so jni.o libandroid_native_app_glue.a -L $ANDROID_TOOLCHAIN/sysroot/usr/lib/aarch64-linux-android/$PLATFORM/ -landroid -llog -lEGL -lGLESv3 -u ANativeActivity_onCreate
+
+# Move the shared lib to the lib directory
+cp libmain.so lib/lib/arm64-v8a
+
+# Pack the assets of the app using a basic android jar that comes with the SDK
+## If there are asset files that need to be loaded during runtime then they should be put in an `assets` directory
+## and "-A assets" should be appended after the -I option.
+$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/aapt package -f -M AndroidManifest.xml -S res -I $ANDROID_SDK_ROOT/platforms/android-$PLATFORM/android.jar -A assets -F "bin/$APK.build" lib
+
+# Create a debug signing key to sign our application.
+if [ ! -f .keystore ]; then
+ "$ANDROID_JBR/bin/keytool" -genkey -dname "CN=Android Debug, O=Android, C=US" -keystore .keystore -alias androiddebugkey -storepass android -keypass android -keyalg RSA -validity 30000
+ if [ $? -ne 0 ]; then exit 1; fi
+fi
+
+# Sign the jar
+$ANDROID_JBR/bin/jarsigner -storepass android -keystore .keystore "bin/$APK.build" androiddebugkey > /dev/null
+
+# Make it into an apk
+$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/zipalign -f 4 "bin/$APK.build" "bin/$APK"
+
+
+cleanup() {
+ rm jni.o android_native_app_glue.o libandroid_native_app_glue.a bin/$APK.build
+}
+
+cleanup
diff --git a/jni/main.odin b/jni/main.odin
new file mode 100644
index 0000000..2707c06
--- /dev/null
+++ b/jni/main.odin
@@ -0,0 +1,275 @@
+package android_native_example
+
+import "base:runtime"
+
+import "shared:android"
+
+import "vendor:egl"
+import gl "vendor:OpenGL"
+
+LOG :: android.__android_log_print
+
+engine :: struct {
+ app: ^android.android_app,
+
+ active: bool,
+ display: egl.Display,
+ surface: egl.Surface,
+ ctx: egl.Context,
+ width: i32,
+ height: i32,
+
+ buffer: u32,
+ shader: u32,
+}
+
+engine_init_display :: proc(engine: ^engine) {
+ attribs := []i32{
+ egl.SURFACE_TYPE, egl.WINDOW_BIT,
+ egl.RENDERABLE_TYPE, egl.OPENGL_ES3_BIT,
+ egl.BLUE_SIZE, 8,
+ egl.GREEN_SIZE, 8,
+ egl.RED_SIZE, 8,
+ egl.NONE,
+ }
+
+ display := egl.GetDisplay(egl.DEFAULT_DISPLAY)
+ if display == egl.NO_DISPLAY {
+ LOG(.INFO, "NativeExampleOdin", "Naterror with eglGetDisplay")
+ return
+ }
+
+ if !egl.Initialize(display, nil, nil) {
+ LOG(.INFO, "NativeExampleOdin", "error with eglInitialize")
+ return
+ }
+
+ // NOTE: I think the major and minor versions don't matter here, we just want the gl package to load all available
+ // gl procs.
+ gl.load_up_to(
+ 4,
+ 6,
+ proc(p: rawptr, name: cstring) {(cast(^rawptr)p)^ = egl.GetProcAddress(name)}
+ )
+
+ config: egl.Config
+ numConfigs: i32
+ if !egl.ChooseConfig(display, raw_data(attribs), &config, 1, &numConfigs) {
+ LOG(.INFO, "NativeExampleOdin", "error with eglChooseConfig")
+ return
+ }
+
+ format: i32
+ if !egl.GetConfigAttrib(display, config, egl.NATIVE_VISUAL_ID, &format) {
+ LOG(.INFO, "NativeExampleOdin", "error with eglGetConfigAttrib")
+ return
+ }
+
+ android.ANativeWindow_setBuffersGeometry(engine.app.window, 0, 0, format)
+
+ surface := egl.CreateWindowSurface(display, config, cast(egl.NativeWindowType)engine.app.window, nil)
+ if surface == nil {
+ LOG(.INFO, "NativeExampleOdin", "error with eglCreateWindowSurface")
+ return
+ }
+
+ ctx_attrib := []i32{ egl.CONTEXT_CLIENT_VERSION, 2, egl.NONE }
+ ctx := egl.CreateContext(display, config, nil, raw_data(ctx_attrib))
+ if ctx == nil {
+ LOG(.INFO, "NativeExampleOdin", "error with eglCreateContext")
+ return
+ }
+
+ if !egl.MakeCurrent(display, surface, surface, ctx) {
+ LOG(.INFO, "NativeExampleOdin", "error with eglMakeCurrent")
+ return
+ }
+
+ LOG(.INFO, "NativeExampleOdin", "GL_VENDOR = %s", gl.GetString(gl.VENDOR))
+ LOG(.INFO, "NativeExampleOdin", "GL_RENDERER = %s", gl.GetString(gl.RENDERER))
+ LOG(.INFO, "NativeExampleOdin", "GL_VERSION = %s", gl.GetString(gl.VERSION))
+
+ w: i32
+ h: i32
+ egl.QuerySurface(display, surface, egl.WIDTH, &w)
+ egl.QuerySurface(display, surface, egl.HEIGHT, &h)
+
+ LOG(.INFO, "NativeExampleOdin", "initial width: %d", w)
+ LOG(.INFO, "NativeExampleOdin", "initial height: %d", h)
+
+ gl.Viewport(0, 0, w, h)
+
+ engine.display = display
+ engine.ctx = ctx
+ engine.surface = surface
+ engine.width = w
+ engine.height = h
+
+ vasset := android.AAssetManager_open(engine.app.activity.assetManager, "vertex.glsl", .BUFFER)
+ defer android.AAsset_close(vasset)
+ if vasset == nil {
+ LOG(.INFO, "NativeExampleOdin", "error opening vertex.glsl")
+ return
+ }
+ vsrc := cstring(android.AAsset_getBuffer(vasset))
+ vlen := i32(android.AAsset_getLength(vasset))
+
+ v := gl.CreateShader(gl.VERTEX_SHADER)
+ gl.ShaderSource(v, 1, &vsrc, &vlen)
+ gl.CompileShader(v)
+
+ fasset := android.AAssetManager_open(engine.app.activity.assetManager, "fragment.glsl", .BUFFER)
+ defer android.AAsset_close(fasset)
+ if fasset == nil {
+ LOG(.INFO, "NativeExampleOdin", "error opening fragment.glsl")
+ return
+ }
+ fsrc := cstring(android.AAsset_getBuffer(fasset))
+ flen := i32(android.AAsset_getLength(fasset))
+
+ f := gl.CreateShader(gl.FRAGMENT_SHADER)
+ gl.ShaderSource(f, 1, &fsrc, &flen)
+ gl.CompileShader(f)
+
+
+ p := gl.CreateProgram()
+ gl.AttachShader(p, v)
+ gl.AttachShader(p, f)
+
+ gl.BindAttribLocation(p, 0, "vPosition")
+ gl.BindAttribLocation(p, 1, "vColor")
+ gl.LinkProgram(p)
+
+ gl.DeleteShader(v)
+ gl.DeleteShader(f)
+ gl.UseProgram(p)
+
+ buf := [?]f32 {
+ 0.0, 0.5, 1, 0, 0,
+ -0.5, -0.5, 0, 1, 0,
+ 0.5, -0.5, 0, 0, 1,
+ }
+
+ b: u32
+ gl.GenBuffers(1, &b)
+ gl.BindBuffer(gl.ARRAY_BUFFER, b)
+ gl.BufferData(gl.ARRAY_BUFFER, size_of(buf), &buf, gl.STATIC_DRAW)
+
+ engine.buffer = b
+ engine.shader = p
+
+ return
+}
+
+engine_draw_frame :: proc(engine: ^engine) {
+ if engine.display == nil {
+ return
+ }
+
+ gl.ClearColor(0.258824, 0.258824, 0.435294, 1)
+ gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
+
+ gl.UseProgram(engine.shader)
+
+ gl.BindBuffer(gl.ARRAY_BUFFER, engine.buffer)
+ gl.VertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, (2+3)*size_of(f32), 0)
+ gl.VertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, (2+3)*size_of(f32), (2*size_of(f32)))
+ gl.EnableVertexAttribArray(0)
+ gl.EnableVertexAttribArray(1)
+
+ gl.DrawArrays(gl.TRIANGLES, 0, 3)
+
+ egl.SwapBuffers(engine.display, engine.surface)
+}
+
+engine_term_display :: proc(engine: ^engine) {
+ if engine.display != egl.NO_DISPLAY {
+ gl.DeleteProgram(engine.shader)
+ gl.DeleteBuffers(1, &engine.buffer)
+
+ egl.MakeCurrent(engine.display, egl.NO_SURFACE, egl.NO_SURFACE, egl.NO_CONTEXT)
+ if engine.ctx != egl.NO_CONTEXT {
+ egl.DestroyContext(engine.display, engine.ctx)
+ }
+ if engine.surface != egl.NO_SURFACE {
+ egl.DestroySurface(engine.display, engine.surface)
+ }
+ egl.Terminate(engine.display)
+ }
+ engine.active = false
+ engine.display = egl.NO_DISPLAY
+ engine.ctx = egl.NO_CONTEXT
+ engine.surface = egl.NO_SURFACE
+}
+
+engine_handle_input :: proc(app: ^android.android_app, event: ^android.AInputEvent) -> i32 {
+ return 0
+}
+
+engine_handle_cmd :: proc(app: ^android.android_app, cmd: android.AppCmd) {
+ engine := cast(^engine)app.userData
+
+ #partial switch (cmd) {
+ case .INIT_WINDOW:
+ if engine.app.window != nil {
+ LOG(.INFO, "NativeExampleOdin", "init window event")
+ engine_init_display(engine)
+ engine_draw_frame(engine)
+ }
+
+ case .TERM_WINDOW:
+ LOG(.INFO, "NativeExampleOdin", "term window event")
+ engine_term_display(engine)
+ case .WINDOW_RESIZED:
+ LOG(.INFO, "NativeExampleOdin", "window resized")
+ h := android.ANativeWindow_getHeight(app.window)
+ w := android.ANativeWindow_getWidth(app.window)
+ LOG(.INFO, "NativeExampleOdin", "w: %d", w)
+ LOG(.INFO, "NativeExampleOdin", "h: %d", h)
+ gl.Viewport(0, 0, w, h)
+
+ case .GAINED_FOCUS:
+ LOG(.INFO, "NativeExampleOdin", "gained focus event")
+ engine.active = true
+
+ case .LOST_FOCUS:
+ LOG(.INFO, "NativeExampleOdin", "lost focus event")
+ engine.active = false
+ engine_draw_frame(engine)
+ }
+}
+
+@export
+android_main :: proc "contextless" (state: ^android.android_app) {
+ context = runtime.default_context()
+
+ engine: engine
+
+ state.userData = &engine
+ state.onAppCmd = engine_handle_cmd
+ state.onInputEvent = engine_handle_input
+ engine.app = state
+
+ for {
+ events: i32
+ source: ^android.android_poll_source
+
+ ident := android.ALooper_pollAll(engine.active ? 0 : -1, nil, &events, cast(^rawptr)&source)
+ for ident >= 0 {
+ if source != nil {
+ source.process(state, source)
+ }
+
+ if state.destroyRequested != 0 {
+ engine_term_display(&engine)
+ return
+ }
+
+ ident = android.ALooper_pollAll(engine.active ? 0 : -1, nil, &events, cast(^rawptr)&source)
+ }
+
+ if (engine.active) {
+ engine_draw_frame(&engine)
+ }
+ }
+}
diff --git a/launch.sh b/launch.sh
new file mode 100755
index 0000000..8aa843d
--- /dev/null
+++ b/launch.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+APK=native_example.apk
+ANDROID_SDK_ROOT=/home/illusion/Android/Sdk
+ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125
+ANDROID_TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64
+ANDROID_JBR=/home/illusion/android-studio/jbr
+BUILD_TOOLS=34.0.0
+
+get_package_activity() {
+ PACKAGE=$("$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/aapt" dump badging "bin/$APK" | grep package | awk -F"'" '{print $2}')
+ ACTIVITY=$("$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/aapt" dump badging "bin/$APK" | grep launchable-activity | awk -F"'" '{print $2}')
+}
+
+launch() {
+ get_package_activity
+ "$ANDROID_SDK_ROOT/platform-tools/adb" shell am start -n "$PACKAGE/$ACTIVITY"
+ if [ $? -ne 0 ]; then exit 1; fi
+}
+
+launch
diff --git a/res/drawable-v24/ic_launcher_background.xml b/res/drawable-v24/ic_launcher_background.xml
new file mode 100644
index 0000000..bae8060
--- /dev/null
+++ b/res/drawable-v24/ic_launcher_background.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable-v24/ic_launcher_foreground.xml b/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..7cb66ad
--- /dev/null
+++ b/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/res/mipmap-anydpi-v26/ic_launcher.xml b/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/mipmap-anydpi-v26/ic_launcher_round.xml b/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/mipmap-hdpi/ic_launcher.png b/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cb0e65c
Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/res/mipmap-hdpi/ic_launcher_round.png b/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..5ffd225
Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher_round.png differ