summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2025-12-17 00:35:47 +0800
committerruki <[email protected]>2025-12-17 00:35:47 +0800
commit838e5387f94de362bbb58cff2d93ae193b72689f (patch)
treedb88434daab1cd053bb4f9af38f7c87c99d08734
parente9cc5c5f4627b2df658310371e43557ae00c2e1e (diff)
add custom glue test
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/android/AndroidManifest.xml31
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/android/debug.jksbin0 -> 2148 bytes
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap-mdpi/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/android/res/values/strings.xml4
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/android/res/values/styles.xml9
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.c418
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.h234
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/src/main.cpp32
-rw-r--r--tests/projects/android/native_app/raylib_custom_glue/xmake.lua20
-rw-r--r--xmake/rules/platform/android/package.lua25
11 files changed, 771 insertions, 2 deletions
diff --git a/tests/projects/android/native_app/raylib_custom_glue/android/AndroidManifest.xml b/tests/projects/android/native_app/raylib_custom_glue/android/AndroidManifest.xml
new file mode 100644
index 000000000..2c521f2ed
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/android/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.raylib.custom_glue"
+
+ android:versionCode="15"
+ android:versionName="1.5">
+
+ <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
+
+ <uses-permission android:name="android.permission.SET_RELEASE_APP"/>
+
+ <application
+ android:label="RaylibCustomGlue" android:debuggable="true" android:hasCode="false"
+ tools:replace="android:icon,android:theme,android:allowBackup,label"
+ android:icon="@mipmap/icon">
+
+ <activity android:name="android.app.NativeActivity"
+ android:label="RaylibCustomGlue"
+ android:configChanges="orientation|keyboardHidden|screenSize|screenLayout"
+ android:screenOrientation="portrait"
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+ <meta-data android:name="android.app.lib_name"
+ android:value="raydemo_custom_glue" />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest> \ No newline at end of file
diff --git a/tests/projects/android/native_app/raylib_custom_glue/android/debug.jks b/tests/projects/android/native_app/raylib_custom_glue/android/debug.jks
new file mode 100644
index 000000000..0a18aa012
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/android/debug.jks
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap-mdpi/icon.png b/tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap-mdpi/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap/icon.png b/tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/android/res/mipmap/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_custom_glue/android/res/values/strings.xml b/tests/projects/android/native_app/raylib_custom_glue/android/res/values/strings.xml
new file mode 100644
index 000000000..bc0a9bfbf
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Raylib Basic</string>
+</resources> \ No newline at end of file
diff --git a/tests/projects/android/native_app/raylib_custom_glue/android/res/values/styles.xml b/tests/projects/android/native_app/raylib_custom_glue/android/res/values/styles.xml
new file mode 100644
index 000000000..621547380
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/android/res/values/styles.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="AppTheme" parent="android:Theme.NoTitleBar.Fullscreen">
+ <item name="android:windowBackground">@android:color/black</item>
+ <item name="android:colorPrimary">#FF6200EE</item>
+ <item name="android:colorPrimaryDark">#FF3700B3</item>
+ <item name="android:colorAccent">#FF03DAC5</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.c b/tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.c
new file mode 100644
index 000000000..bdf856b39
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.c
@@ -0,0 +1,418 @@
+#include "android_native_app_glue.h"
+#include <android/log.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "raydemo_custom_glue", __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "raydemo_custom_glue", __VA_ARGS__))
+
+/* For debug builds, always enable the debug traces in this library */
+#ifndef NDEBUG
+# define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "raydemo_custom_glue", __VA_ARGS__))
+#else
+# define LOGV(...) ((void)0)
+#endif
+
+static void free_saved_state(struct android_app* android_app) {
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->stateSaved) {
+ android_app->stateSaved = 0;
+ pthread_cond_broadcast(&android_app->cond);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+int8_t android_app_read_cmd(struct android_app* android_app) {
+ int8_t cmd;
+ if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) {
+ switch (cmd) {
+ case APP_CMD_SAVE_STATE:
+ free_saved_state(android_app);
+ break;
+ }
+ return cmd;
+ }
+ LOGE("No data on command pipe!");
+ return -1;
+}
+
+static void print_cur_config(struct android_app* android_app) {
+ char lang[2], country[2];
+ AConfiguration_getLanguage(android_app->config, lang);
+ AConfiguration_getCountry(android_app->config, country);
+
+ LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
+ "keys=%d nav=%d keyshid=%d navhid=%d sdk=%d size=%d long=%d "
+ "modetype=%d modenight=%d",
+ AConfiguration_getMcc(android_app->config),
+ AConfiguration_getMnc(android_app->config),
+ lang[0], lang[1], country[0], country[1],
+ AConfiguration_getOrientation(android_app->config),
+ AConfiguration_getTouchscreen(android_app->config),
+ AConfiguration_getDensity(android_app->config),
+ AConfiguration_getKeyboard(android_app->config),
+ AConfiguration_getNavigation(android_app->config),
+ AConfiguration_getKeysHidden(android_app->config),
+ AConfiguration_getNavHidden(android_app->config),
+ AConfiguration_getSdkVersion(android_app->config),
+ AConfiguration_getScreenSize(android_app->config),
+ AConfiguration_getScreenLong(android_app->config),
+ AConfiguration_getUiModeType(android_app->config),
+ AConfiguration_getUiModeNight(android_app->config));
+}
+
+void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
+ switch (cmd) {
+ case APP_CMD_INPUT_CHANGED:
+ LOGV("APP_CMD_INPUT_CHANGED\n");
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->inputQueue != NULL) {
+ AInputQueue_detachLooper(android_app->inputQueue);
+ }
+ android_app->inputQueue = android_app->pendingInputQueue;
+ if (android_app->inputQueue != NULL) {
+ LOGV("Attaching input queue to looper");
+ AInputQueue_attachLooper(android_app->inputQueue,
+ android_app->looper, LOOPER_ID_EVENT, NULL,
+ &android_app->inputPollSource);
+ }
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_INIT_WINDOW:
+ LOGV("APP_CMD_INIT_WINDOW\n");
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->window = android_app->pendingWindow;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_TERM_WINDOW:
+ LOGV("APP_CMD_TERM_WINDOW\n");
+ pthread_cond_broadcast(&android_app->cond);
+ break;
+
+ case APP_CMD_RESUME:
+ case APP_CMD_START:
+ case APP_CMD_PAUSE:
+ case APP_CMD_STOP:
+ LOGV("activityState=%d\n", cmd);
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->activityState = cmd;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_CONFIG_CHANGED:
+ LOGV("APP_CMD_CONFIG_CHANGED\n");
+ AConfiguration_fromAssetManager(android_app->config,
+ android_app->activity->assetManager);
+ print_cur_config(android_app);
+ break;
+
+ case APP_CMD_DESTROY:
+ LOGV("APP_CMD_DESTROY\n");
+ android_app->destroyRequested = 1;
+ break;
+ }
+}
+
+void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
+ switch (cmd) {
+ case APP_CMD_TERM_WINDOW:
+ LOGV("APP_CMD_TERM_WINDOW\n");
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->window = NULL;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_SAVE_STATE:
+ LOGV("APP_CMD_SAVE_STATE\n");
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->stateSaved = 1;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_RESUME:
+ free_saved_state(android_app);
+ break;
+ }
+}
+
+void app_dummy() {
+
+}
+
+static void android_app_destroy(struct android_app* android_app) {
+ LOGV("android_app_destroy!");
+ free_saved_state(android_app);
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->inputQueue != NULL) {
+ AInputQueue_detachLooper(android_app->inputQueue);
+ }
+ AConfiguration_delete(android_app->config);
+ android_app->destroyed = 1;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ // Can't touch android_app object after this.
+}
+
+static void process_input(struct android_app* app, struct android_poll_source* source) {
+ AInputEvent* event = NULL;
+ while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
+ LOGV("New input event: type=%d\n", AInputEvent_getType(event));
+ if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
+ continue;
+ }
+ int32_t handled = 0;
+ if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
+ AInputQueue_finishEvent(app->inputQueue, event, handled);
+ }
+}
+
+static void process_cmd(struct android_app* app, struct android_poll_source* source) {
+ int8_t cmd = android_app_read_cmd(app);
+ android_app_pre_exec_cmd(app, cmd);
+ if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
+ android_app_post_exec_cmd(app, cmd);
+}
+
+static void* android_app_entry(void* param) {
+ struct android_app* android_app = (struct android_app*)param;
+
+ android_app->config = AConfiguration_new();
+ AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager);
+
+ print_cur_config(android_app);
+
+ android_app->cmdPollSource.id = LOOPER_ID_MAIN;
+ android_app->cmdPollSource.app = android_app;
+ android_app->cmdPollSource.process = process_cmd;
+ android_app->inputPollSource.id = LOOPER_ID_EVENT;
+ android_app->inputPollSource.app = android_app;
+ android_app->inputPollSource.process = process_input;
+
+ ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
+ ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
+ &android_app->cmdPollSource);
+ android_app->looper = looper;
+
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->running = 1;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+
+ android_main(android_app);
+
+ android_app_destroy(android_app);
+ return NULL;
+}
+
+// --------------------------------------------------------------------
+// Native activity interaction (called from main thread)
+// --------------------------------------------------------------------
+
+static struct android_app* android_app_create(ANativeActivity* activity,
+ void* savedState, size_t savedStateSize) {
+ struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
+ memset(android_app, 0, sizeof(struct android_app));
+ android_app->activity = activity;
+
+ pthread_mutex_init(&android_app->mutex, NULL);
+ pthread_cond_init(&android_app->cond, NULL);
+
+ if (savedState != NULL) {
+ android_app->savedState = malloc(savedStateSize);
+ android_app->savedStateSize = savedStateSize;
+ memcpy(android_app->savedState, savedState, savedStateSize);
+ }
+
+ int msgpipe[2];
+ if (pipe(msgpipe)) {
+ LOGE("could not create pipe: %s", strerror(errno));
+ return NULL;
+ }
+ android_app->msgread = msgpipe[0];
+ android_app->msgwrite = msgpipe[1];
+
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
+
+ // Wait for thread to start.
+ pthread_mutex_lock(&android_app->mutex);
+ while (!android_app->running) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+
+ return android_app;
+}
+
+static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
+ if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
+ LOGE("Failure writing android_app cmd: %s\n", strerror(errno));
+ }
+}
+
+static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) {
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->pendingInputQueue = inputQueue;
+ android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED);
+ while (android_app->inputQueue != android_app->pendingInputQueue) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) {
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->pendingWindow != NULL) {
+ android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
+ }
+ android_app->pendingWindow = window;
+ if (window != NULL) {
+ android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
+ }
+ while (android_app->window != android_app->pendingWindow) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) {
+ pthread_mutex_lock(&android_app->mutex);
+ android_app_write_cmd(android_app, cmd);
+ while (android_app->activityState != cmd) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_free(struct android_app* android_app) {
+ pthread_mutex_lock(&android_app->mutex);
+ android_app_write_cmd(android_app, APP_CMD_DESTROY);
+ while (!android_app->destroyed) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+
+ close(android_app->msgread);
+ close(android_app->msgwrite);
+ pthread_cond_destroy(&android_app->cond);
+ pthread_mutex_destroy(&android_app->mutex);
+ free(android_app);
+}
+
+static void onDestroy(ANativeActivity* activity) {
+ LOGV("Destroy: %p\n", activity);
+ android_app_free((struct android_app*)activity->instance);
+}
+
+static void onStart(ANativeActivity* activity) {
+ LOGV("Start: %p\n", activity);
+ android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START);
+}
+
+static void onResume(ANativeActivity* activity) {
+ LOGV("Resume: %p\n", activity);
+ android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME);
+}
+
+static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
+ struct android_app* android_app = (struct android_app*)activity->instance;
+ void* savedState = NULL;
+
+ LOGV("SaveInstanceState: %p\n", activity);
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->stateSaved = 0;
+ android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
+ while (!android_app->stateSaved) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+
+ if (android_app->savedState != NULL) {
+ savedState = android_app->savedState;
+ *outLen = android_app->savedStateSize;
+ android_app->savedState = NULL;
+ android_app->savedStateSize = 0;
+ }
+
+ pthread_mutex_unlock(&android_app->mutex);
+
+ return savedState;
+}
+
+static void onPause(ANativeActivity* activity) {
+ LOGV("Pause: %p\n", activity);
+ android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE);
+}
+
+static void onStop(ANativeActivity* activity) {
+ LOGV("Stop: %p\n", activity);
+ android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP);
+}
+
+static void onConfigurationChanged(ANativeActivity* activity) {
+ struct android_app* android_app = (struct android_app*)activity->instance;
+ LOGV("ConfigurationChanged: %p\n", activity);
+ android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED);
+}
+
+static void onLowMemory(ANativeActivity* activity) {
+ struct android_app* android_app = (struct android_app*)activity->instance;
+ LOGV("LowMemory: %p\n", activity);
+ android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY);
+}
+
+static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
+ LOGV("WindowFocusChanged: %p -- %d\n", activity, focused);
+ android_app_write_cmd((struct android_app*)activity->instance,
+ focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
+}
+
+static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
+ LOGV("NativeWindowCreated: %p -- %p\n", activity, window);
+ android_app_set_window((struct android_app*)activity->instance, window);
+}
+
+static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
+ LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window);
+ android_app_set_window((struct android_app*)activity->instance, NULL);
+}
+
+static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
+ LOGV("InputQueueCreated: %p -- %p\n", activity, queue);
+ android_app_set_input((struct android_app*)activity->instance, queue);
+}
+
+static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
+ LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue);
+ android_app_set_input((struct android_app*)activity->instance, NULL);
+}
+
+void ANativeActivity_onCreate(ANativeActivity* activity,
+ void* savedState, size_t savedStateSize) {
+ LOGV("Creating: %p\n", activity);
+ activity->callbacks->onDestroy = onDestroy;
+ activity->callbacks->onStart = onStart;
+ activity->callbacks->onResume = onResume;
+ activity->callbacks->onSaveInstanceState = onSaveInstanceState;
+ activity->callbacks->onPause = onPause;
+ activity->callbacks->onStop = onStop;
+ activity->callbacks->onConfigurationChanged = onConfigurationChanged;
+ activity->callbacks->onLowMemory = onLowMemory;
+ activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
+ activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
+ activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
+ activity->callbacks->onInputQueueCreated = onInputQueueCreated;
+ activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
+
+ activity->instance = android_app_create(activity, savedState, savedStateSize);
+}
diff --git a/tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.h b/tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.h
new file mode 100644
index 000000000..a13034f43
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/src/android_native_app_glue.h
@@ -0,0 +1,234 @@
+#ifndef _ANDROID_NATIVE_APP_GLUE_H
+#define _ANDROID_NATIVE_APP_GLUE_H
+
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+
+#include <android/native_activity.h>
+#include <android/looper.h>
+#include <android/configuration.h>
+#include <android/rect.h>
+#include <android/native_window.h>
+#include <android/input.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The native activity interface provided by <android/native_activity.h>
+ * is based on a set of application-provided callbacks that will be called
+ * by the Activity's main thread when certain events occur.
+ *
+ * This means that each one of this callbacks _should_ _not_ block, or they
+ * risk having the system force-close the application. This programming
+ * model is direct, lightweight, but constraining.
+ *
+ * The 'threaded_native_app' static library is used to provide a different
+ * execution model where the application can implement its own main event
+ * loop in a different thread instead. Here's how it works:
+ *
+ * 1/ The application must provide a function named "android_main()" that
+ * will be called when the activity is created, in a new thread that is
+ * distinct from the activity's main thread.
+ *
+ * 2/ android_main() receives a pointer to a valid "android_app" structure
+ * that contains references to other important objects, e.g. the
+ * ANativeActivity obejct instance the application is running in.
+ *
+ * 3/ the "android_app" object holds an ALooper instance that already
+ * listens to two important things:
+ *
+ * - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX
+ * declarations below.
+ *
+ * - input events coming from the AInputQueue attached to the activity.
+ *
+ * Each of these correspond to an ALooper callback that returns a "data"
+ * value of LOOPER_ID_MAIN and LOOPER_ID_EVENT, respectively.
+ *
+ * Your application can use the same ALooper to listen to additionnal
+ * file-descriptors.
+ *
+ * 4/ Whenever you receive a LOOPER_ID_MAIN event from the ALooper, your
+ * code should call the function android_app_read_cmd() to read the
+ * command value and act upon it. This is normally done by calling
+ * android_app_exec_cmd() directly.
+ *
+ * XXX: MAKE THIS STUFF MORE CLEAR !!
+ *
+ * 5/ Whenever you receive a LOOPER_ID_EVENT event from the ALooper, you
+ * should read one event from the AInputQueue with AInputQueue_getEvent().
+ *
+ * See the sample named "native-activity" that comes with the NDK with a
+ * full usage example.
+ *
+ */
+
+/**
+ * Data associated with an ALooper fd that will be returned as the "outData"
+ * when that source has data ready.
+ */
+struct android_poll_source {
+ // The identifier of this source. May be LOOPER_ID_MAIN or
+ // LOOPER_ID_EVENT.
+ int32_t id;
+
+ // The android_app structure associated with this source.
+ struct android_app* app;
+
+ // Function to call to process this source when data is available.
+ void (*process)(struct android_app* app, struct android_poll_source* source);
+};
+
+struct android_app {
+ // The application can place a pointer to its own state object
+ // here if it likes.
+ void* userData;
+
+ // Fill this in with the function to process main app commands (APP_CMD_*)
+ void (*onAppCmd)(struct android_app* app, int32_t cmd);
+
+ // Fill this in with the function to process input events. At this point
+ // the event has already been pre-dispatched, and it will be finished upon
+ // return. Return 1 if you have handled the event, 0 for any default
+ // dispatching.
+ int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
+
+ // The ANativeActivity object instance that this app is running in.
+ ANativeActivity* activity;
+
+ // The current configuration the app is running in.
+ AConfiguration* config;
+
+ // The last saved state that was given at creation time.
+ void* savedState;
+ size_t savedStateSize;
+
+ // The ALooper associated with the app's thread.
+ ALooper* looper;
+
+ // When non-NULL, this is the input queue from which the app will
+ // receive user input events.
+ AInputQueue* inputQueue;
+
+ // When non-NULL, this is the window surface that the app can draw in.
+ ANativeWindow* window;
+
+ // Current content rectangle of the window; this is the area where the
+ // window's content should be placed to be seen by the user.
+ ARect contentRect;
+
+ // Current state of the app's activity. May be either APP_CMD_START,
+ // APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below.
+ int activityState;
+
+ // This is non-zero when the application's NativeActivity is being
+ // destroyed and waiting for the app thread to complete.
+ int destroyRequested;
+
+ // -------------------------------------------------
+ // Below are "private" implementation of the glue code.
+
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+
+ int msgread;
+ int msgwrite;
+
+ pthread_t thread;
+
+ struct android_poll_source cmdPollSource;
+ struct android_poll_source inputPollSource;
+
+ int running;
+ int stateSaved;
+ int destroyed;
+ int redrawNeeded;
+ AInputQueue* pendingInputQueue;
+ ANativeWindow* pendingWindow;
+ ARect pendingContentRect;
+};
+
+enum {
+ /**
+ * Looper data ID of commands coming from the app's main thread.
+ * These can be retrieved and processed with android_app_read_cmd()
+ * and android_app_exec_cmd().
+ */
+ LOOPER_ID_MAIN = 1,
+
+ /**
+ * Looper data ID of events coming from the AInputQueue of the
+ * application's window. These can be read via the inputQueue
+ * object of android_app.
+ */
+ LOOPER_ID_EVENT = 2,
+
+ /**
+ * Looper data ID of events coming from the AInputQueue of the
+ * application's window. These can be read via the inputQueue
+ * object of android_app.
+ */
+ LOOPER_ID_USER = 3,
+};
+
+enum {
+ APP_CMD_INPUT_CHANGED,
+ APP_CMD_INIT_WINDOW,
+ APP_CMD_TERM_WINDOW,
+ APP_CMD_WINDOW_RESIZED,
+ APP_CMD_WINDOW_REDRAW_NEEDED,
+ APP_CMD_CONTENT_RECT_CHANGED,
+ APP_CMD_GAINED_FOCUS,
+ APP_CMD_LOST_FOCUS,
+ APP_CMD_CONFIG_CHANGED,
+ APP_CMD_LOW_MEMORY,
+ APP_CMD_START,
+ APP_CMD_RESUME,
+ APP_CMD_SAVE_STATE,
+ APP_CMD_PAUSE,
+ APP_CMD_STOP,
+ APP_CMD_DESTROY,
+};
+
+/**
+ * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
+ * app command message.
+ */
+int8_t android_app_read_cmd(struct android_app* android_app);
+
+/**
+ * Call with the command returned by android_app_read_cmd() to do the
+ * initial pre-processing of the given command. You can use this for
+ * your own normal default behavior for the command, and then inspect
+ * it yourself to do your own processing. This function may block for
+ * certain commands (e.g. to wait for the app's graphics context to be
+ * initialized) and will returns true if it blocked.
+ */
+void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
+
+/**
+ * Call with the command returned by android_app_read_cmd() to do the
+ * final post-processing of the given command. You must have first called
+ * android_app_pre_exec_cmd() before calling this function.
+ */
+void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
+
+/**
+ * Dummy function you can call to ensure glue code isn't stripped.
+ */
+void app_dummy();
+
+/**
+ * This is the function that application code must implement, representing
+ * the main entry to the app.
+ */
+extern void android_main(struct android_app* app);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _ANDROID_NATIVE_APP_GLUE_H */
diff --git a/tests/projects/android/native_app/raylib_custom_glue/src/main.cpp b/tests/projects/android/native_app/raylib_custom_glue/src/main.cpp
new file mode 100644
index 000000000..0094d9e3c
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/src/main.cpp
@@ -0,0 +1,32 @@
+#include <raylib.h>
+#include <android/log.h>
+
+#define LOG_TAG "raydemo_basic"
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+int main(int argc, char** argv) {
+ InitWindow(0, 0, "Hello, xmake for raylib android!");
+ SetTargetFPS(60);
+ LOGI("raylib application is running!");
+
+ while (!WindowShouldClose()) {
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+ // Draw FPS
+ const char* fpsText = TextFormat("%2i FPS", GetFPS());
+ int fpsWidth = MeasureText(fpsText, 40);
+ DrawText(fpsText, GetScreenWidth() / 2 - fpsWidth / 2, 30, 40, GREEN);
+
+ // Draw centered text
+ int fontSize = 50;
+ const char* text = "Hello, xmake for raylib android!";
+ int textWidth = MeasureText(text, fontSize);
+ DrawText(text, GetScreenWidth() / 2 - textWidth / 2, 100, fontSize, BLUE);
+
+ EndDrawing();
+ }
+
+ CloseWindow();
+ return 0;
+}
diff --git a/tests/projects/android/native_app/raylib_custom_glue/xmake.lua b/tests/projects/android/native_app/raylib_custom_glue/xmake.lua
new file mode 100644
index 000000000..3b112f8fe
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_custom_glue/xmake.lua
@@ -0,0 +1,20 @@
+add_rules("mode.debug", "mode.release")
+
+add_requires("raylib 5.5.0")
+
+target("raydemo_custom_glue")
+ set_kind("binary")
+ set_languages("c++17")
+ add_files("src/main.cpp", "src/android_native_app_glue.c")
+ add_syslinks("log")
+ add_packages("raylib")
+ add_rules("android.native_app", {
+ android_sdk_version = "35",
+ android_manifest = "android/AndroidManifest.xml",
+ android_res = "android/res",
+ keystore = "android/debug.jks",
+ keystore_pass = "123456",
+ package_name = "com.raylib.custom_glue",
+ native_app_glue = false,
+ logcat_filters = {"raydemo_custom_glue", "raylib"}
+ })
diff --git a/xmake/rules/platform/android/package.lua b/xmake/rules/platform/android/package.lua
index 00142a85f..42e4ac80e 100644
--- a/xmake/rules/platform/android/package.lua
+++ b/xmake/rules/platform/android/package.lua
@@ -35,6 +35,19 @@ function main(target, opt)
assert(android_sdk_version, "android sdk version not set")
assert(android_manifest, "android manifest not set")
+ if not path.is_absolute(android_manifest) then
+ android_manifest = path.join(target:scriptdir(), android_manifest)
+ end
+ if keystore and not path.is_absolute(keystore) then
+ keystore = path.join(target:scriptdir(), keystore)
+ end
+ if android_res and not path.is_absolute(android_res) then
+ android_res = path.join(target:scriptdir(), android_res)
+ end
+ if android_assets and not path.is_absolute(android_assets) then
+ android_assets = path.join(target:scriptdir(), android_assets)
+ end
+
local final_apk = path.join(target:targetdir(), target:basename() .. ".apk")
-- add dependencies
@@ -55,7 +68,15 @@ function main(target, opt)
os.mkdir(path.join(tmp_path, "lib", target:arch()))
-- copy the target library to temp folder with name libmain.so
- local libfile = path.join(tmp_path, "lib", target:arch(), "libmain.so")
+ local libname = "libmain.so"
+ local manifest_xml = io.readfile(android_manifest)
+ if manifest_xml then
+ local lib_name_meta = manifest_xml:match('android:name="android.app.lib_name"%s+android:value="([^"]+)"')
+ if lib_name_meta then
+ libname = "lib" .. lib_name_meta .. ".so"
+ end
+ end
+ local libfile = path.join(tmp_path, "lib", target:arch(), libname)
os.cp(target:targetfile(), libfile)
-- get android tool path
@@ -87,7 +108,7 @@ function main(target, opt)
os.vrunv(aapt, aapt_argv)
-- pack libs
- os.vrunv(aapt, {"add", "res_only.apk", "lib/" .. target:arch() .."/libmain.so"}, {curdir = tmp_path})
+ os.vrunv(aapt, {"add", "res_only.apk", "lib/" .. target:arch() .."/" .. libname}, {curdir = tmp_path})
-- align apk
local aligned_apk = path.join(tmp_path, "unsigned.apk")