summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2025-12-16 17:57:11 +0800
committerGitHub <[email protected]>2025-12-16 17:57:11 +0800
commitd1485fa867ebb3b661a16ec043f33537a7f9f0cd (patch)
tree351a965dfb03438caf4a5a55620a11108ffabcac
parentd3acb6ca0d74d506d4a7da0fb7c055344591cb7a (diff)
parent7b542ebe22e833bd56dd1eb0271aa5aa7c0b45a8 (diff)
Merge pull request #7139 from xmake-io/android
Add android native app support
-rw-r--r--tests/projects/android/native_app/lvgl_basic/android/AndroidManifest.xml30
-rw-r--r--tests/projects/android/native_app/lvgl_basic/android/debug.jksbin0 -> 2148 bytes
-rw-r--r--tests/projects/android/native_app/lvgl_basic/android/res/mipmap-mdpi/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/lvgl_basic/android/res/mipmap/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/lvgl_basic/android/res/values/strings.xml4
-rw-r--r--tests/projects/android/native_app/lvgl_basic/android/res/values/styles.xml9
-rw-r--r--tests/projects/android/native_app/lvgl_basic/src/main.c169
-rw-r--r--tests/projects/android/native_app/lvgl_basic/xmake.lua23
-rw-r--r--tests/projects/android/native_app/lvgl_particles/android/AndroidManifest.xml30
-rw-r--r--tests/projects/android/native_app/lvgl_particles/android/debug.jksbin0 -> 2148 bytes
-rw-r--r--tests/projects/android/native_app/lvgl_particles/android/res/mipmap-mdpi/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/lvgl_particles/android/res/mipmap/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/lvgl_particles/android/res/values/strings.xml4
-rw-r--r--tests/projects/android/native_app/lvgl_particles/android/res/values/styles.xml9
-rw-r--r--tests/projects/android/native_app/lvgl_particles/src/main.c261
-rw-r--r--tests/projects/android/native_app/lvgl_particles/xmake.lua23
-rw-r--r--tests/projects/android/native_app/raylib_basic/android/AndroidManifest.xml31
-rw-r--r--tests/projects/android/native_app/raylib_basic/android/debug.jksbin0 -> 2148 bytes
-rw-r--r--tests/projects/android/native_app/raylib_basic/android/res/mipmap-mdpi/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/raylib_basic/android/res/mipmap/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/raylib_basic/android/res/values/strings.xml4
-rw-r--r--tests/projects/android/native_app/raylib_basic/android/res/values/styles.xml9
-rw-r--r--tests/projects/android/native_app/raylib_basic/src/main.cpp32
-rw-r--r--tests/projects/android/native_app/raylib_basic/xmake.lua19
-rw-r--r--tests/projects/android/native_app/raylib_particles/android/AndroidManifest.xml31
-rw-r--r--tests/projects/android/native_app/raylib_particles/android/debug.jksbin0 -> 2148 bytes
-rw-r--r--tests/projects/android/native_app/raylib_particles/android/res/mipmap-mdpi/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/raylib_particles/android/res/mipmap/icon.pngbin0 -> 2643 bytes
-rw-r--r--tests/projects/android/native_app/raylib_particles/android/res/values/strings.xml4
-rw-r--r--tests/projects/android/native_app/raylib_particles/android/res/values/styles.xml9
-rw-r--r--tests/projects/android/native_app/raylib_particles/src/main.cpp89
-rw-r--r--tests/projects/android/native_app/raylib_particles/xmake.lua19
-rw-r--r--xmake/actions/run/main.lua16
-rw-r--r--xmake/rules/platform/android/install.lua34
-rw-r--r--xmake/rules/platform/android/load.lua44
-rw-r--r--xmake/rules/platform/android/package.lua103
-rw-r--r--xmake/rules/platform/android/run.lua56
-rw-r--r--xmake/rules/platform/android/uninstall.lua32
-rw-r--r--xmake/rules/platform/android/xmake.lua53
39 files changed, 1145 insertions, 2 deletions
diff --git a/tests/projects/android/native_app/lvgl_basic/android/AndroidManifest.xml b/tests/projects/android/native_app/lvgl_basic/android/AndroidManifest.xml
new file mode 100644
index 000000000..98c3d3795
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/android/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.lvgl.basic"
+
+ 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="LvglBasic" 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="LvglBasic"
+ android:configChanges="orientation|keyboardHidden|screenSize|screenLayout"
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+ <meta-data android:name="android.app.lib_name"
+ android:value="main" />
+ <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/lvgl_basic/android/debug.jks b/tests/projects/android/native_app/lvgl_basic/android/debug.jks
new file mode 100644
index 000000000..0a18aa012
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/android/debug.jks
Binary files differ
diff --git a/tests/projects/android/native_app/lvgl_basic/android/res/mipmap-mdpi/icon.png b/tests/projects/android/native_app/lvgl_basic/android/res/mipmap-mdpi/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/android/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/lvgl_basic/android/res/mipmap/icon.png b/tests/projects/android/native_app/lvgl_basic/android/res/mipmap/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/android/res/mipmap/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/lvgl_basic/android/res/values/strings.xml b/tests/projects/android/native_app/lvgl_basic/android/res/values/strings.xml
new file mode 100644
index 000000000..7dc865540
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Lvgl Basic</string>
+</resources> \ No newline at end of file
diff --git a/tests/projects/android/native_app/lvgl_basic/android/res/values/styles.xml b/tests/projects/android/native_app/lvgl_basic/android/res/values/styles.xml
new file mode 100644
index 000000000..621547380
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/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/lvgl_basic/src/main.c b/tests/projects/android/native_app/lvgl_basic/src/main.c
new file mode 100644
index 000000000..f07036e9b
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/src/main.c
@@ -0,0 +1,169 @@
+#include <lvgl/lvgl.h>
+#include <android/native_window.h>
+#include <android/log.h>
+#include <android_native_app_glue.h>
+#include <time.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#define LOG_TAG "lvgl_basic"
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+static lv_display_t * display = NULL;
+static lv_indev_t * indev = NULL;
+static void * disp_buf = NULL;
+static ANativeWindow * native_window = NULL;
+static int32_t window_width = 0;
+static int32_t window_height = 0;
+static lv_obj_t * label_fps = NULL;
+
+static bool touch_down = false;
+static int32_t touch_x = 0;
+static int32_t touch_y = 0;
+
+static void my_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * px_map) {
+ if (!native_window) {
+ lv_display_flush_ready(disp);
+ return;
+ }
+
+ ANativeWindow_Buffer buffer;
+ if (ANativeWindow_lock(native_window, &buffer, NULL) < 0) {
+ lv_display_flush_ready(disp);
+ return;
+ }
+
+ int32_t width = lv_area_get_width(area);
+ int32_t height = lv_area_get_height(area);
+
+ // Assume 32-bit RGBA
+ uint32_t * dst_base = (uint32_t *)buffer.bits;
+ uint32_t * src = (uint32_t *)px_map;
+ int stride = buffer.stride;
+
+ for (int y = 0; y < height; y++) {
+ uint32_t * dst_line = dst_base + (area->y1 + y) * stride + area->x1;
+ memcpy(dst_line, src + y * width, width * sizeof(uint32_t));
+ }
+
+ ANativeWindow_unlockAndPost(native_window);
+ lv_display_flush_ready(disp);
+}
+
+static void my_input_read(lv_indev_t * indev, lv_indev_data_t * data) {
+ data->point.x = touch_x;
+ data->point.y = touch_y;
+ data->state = touch_down ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
+}
+
+static void create_ui(void) {
+ lv_obj_t * label = lv_label_create(lv_screen_active());
+ lv_label_set_text(label, "Hello Xmake + LVGL!");
+ lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
+ lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 50);
+
+ // FPS label
+ label_fps = lv_label_create(lv_screen_active());
+ lv_label_set_text(label_fps, "FPS: 0");
+ lv_obj_set_style_text_font(label_fps, &lv_font_montserrat_14, 0);
+ lv_obj_align(label_fps, LV_ALIGN_TOP_MID, 0, 100);
+}
+
+static void handle_cmd(struct android_app* app, int32_t cmd) {
+ switch (cmd) {
+ case APP_CMD_INIT_WINDOW:
+ LOGI("Init Window");
+ native_window = app->window;
+ window_width = ANativeWindow_getWidth(native_window);
+ window_height = ANativeWindow_getHeight(native_window);
+ ANativeWindow_setBuffersGeometry(native_window, window_width, window_height, WINDOW_FORMAT_RGBA_8888);
+
+ if (!display) {
+ display = lv_display_create(window_width, window_height);
+ lv_display_set_color_format(display, LV_COLOR_FORMAT_ARGB8888);
+ lv_display_set_flush_cb(display, my_flush_cb);
+
+ size_t buf_size = window_width * window_height * 4;
+ disp_buf = malloc(buf_size);
+ lv_display_set_buffers(display, disp_buf, NULL, buf_size, LV_DISPLAY_RENDER_MODE_FULL);
+
+ indev = lv_indev_create();
+ lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
+ lv_indev_set_read_cb(indev, my_input_read);
+
+ create_ui();
+ } else {
+ lv_display_set_resolution(display, window_width, window_height);
+ lv_obj_invalidate(lv_scr_act());
+ }
+ break;
+ case APP_CMD_TERM_WINDOW:
+ native_window = NULL;
+ break;
+ case APP_CMD_DESTROY:
+ if (display) {
+ lv_display_delete(display);
+ display = NULL;
+ }
+ if (indev) {
+ lv_indev_delete(indev);
+ indev = NULL;
+ }
+ if (disp_buf) {
+ free(disp_buf);
+ disp_buf = NULL;
+ }
+ break;
+ }
+}
+
+static int32_t handle_input(struct android_app* app, AInputEvent* event) {
+ if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
+ int action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
+ touch_x = AMotionEvent_getX(event, 0);
+ touch_y = AMotionEvent_getY(event, 0);
+ if (action == AMOTION_EVENT_ACTION_DOWN || action == AMOTION_EVENT_ACTION_MOVE) {
+ touch_down = true;
+ } else {
+ touch_down = false;
+ }
+ return 1;
+ }
+ return 0;
+}
+
+void android_main(struct android_app* app) {
+ lv_init();
+
+ app->onAppCmd = handle_cmd;
+ app->onInputEvent = handle_input;
+
+ int frame_count = 0;
+ time_t last_time = time(NULL);
+
+ while (1) {
+ int ident;
+ int events;
+ struct android_poll_source* source;
+
+ uint32_t timeout = lv_timer_handler();
+ if (timeout > 50) timeout = 50;
+
+ while ((ident = ALooper_pollAll(timeout, NULL, &events, (void**)&source)) >= 0) {
+ if (source != NULL) source->process(app, source);
+ if (app->destroyRequested != 0) return;
+ }
+
+ lv_tick_inc(timeout);
+
+ frame_count++;
+ time_t current_time = time(NULL);
+ if (current_time - last_time >= 1) {
+ if (label_fps) {
+ lv_label_set_text_fmt(label_fps, "FPS: %d", frame_count);
+ }
+ frame_count = 0;
+ last_time = current_time;
+ }
+ }
+}
diff --git a/tests/projects/android/native_app/lvgl_basic/xmake.lua b/tests/projects/android/native_app/lvgl_basic/xmake.lua
new file mode 100644
index 000000000..659d04f45
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_basic/xmake.lua
@@ -0,0 +1,23 @@
+add_rules("mode.debug", "mode.release")
+
+add_requires("lvgl 9.1.0")
+
+target("lvgl_basic")
+ set_kind("binary")
+ set_languages("c99")
+ add_files("src/main.c")
+ add_syslinks("log", "android", "EGL", "GLESv2")
+ add_packages("lvgl")
+
+ -- define LV_CONF_INCLUDE_SIMPLE to include lv_conf.h
+ add_defines("LV_CONF_INCLUDE_SIMPLE")
+
+ 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.lvgl.basic",
+ logcat_filters = {"lvgl_basic", "lvgl"}
+ })
diff --git a/tests/projects/android/native_app/lvgl_particles/android/AndroidManifest.xml b/tests/projects/android/native_app/lvgl_particles/android/AndroidManifest.xml
new file mode 100644
index 000000000..16d614e95
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/android/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.lvgl.particles"
+
+ 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="LvglParticles" 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="LvglParticles"
+ android:configChanges="orientation|keyboardHidden|screenSize|screenLayout"
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+ <meta-data android:name="android.app.lib_name"
+ android:value="main" />
+ <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/lvgl_particles/android/debug.jks b/tests/projects/android/native_app/lvgl_particles/android/debug.jks
new file mode 100644
index 000000000..0a18aa012
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/android/debug.jks
Binary files differ
diff --git a/tests/projects/android/native_app/lvgl_particles/android/res/mipmap-mdpi/icon.png b/tests/projects/android/native_app/lvgl_particles/android/res/mipmap-mdpi/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/android/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/lvgl_particles/android/res/mipmap/icon.png b/tests/projects/android/native_app/lvgl_particles/android/res/mipmap/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/android/res/mipmap/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/lvgl_particles/android/res/values/strings.xml b/tests/projects/android/native_app/lvgl_particles/android/res/values/strings.xml
new file mode 100644
index 000000000..0c86996ef
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Lvgl Particles</string>
+</resources> \ No newline at end of file
diff --git a/tests/projects/android/native_app/lvgl_particles/android/res/values/styles.xml b/tests/projects/android/native_app/lvgl_particles/android/res/values/styles.xml
new file mode 100644
index 000000000..621547380
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/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/lvgl_particles/src/main.c b/tests/projects/android/native_app/lvgl_particles/src/main.c
new file mode 100644
index 000000000..1db849fc1
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/src/main.c
@@ -0,0 +1,261 @@
+#include <lvgl/lvgl.h>
+#include <android/native_window.h>
+#include <android/log.h>
+#include <android_native_app_glue.h>
+#include <time.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#define LOG_TAG "lvgl_particles"
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+static lv_display_t * display = NULL;
+static lv_indev_t * indev = NULL;
+static void * disp_buf = NULL;
+static ANativeWindow * native_window = NULL;
+static int32_t window_width = 0;
+static int32_t window_height = 0;
+static lv_obj_t * label_fps = NULL;
+
+static bool touch_down = false;
+static int32_t touch_x = 0;
+static int32_t touch_y = 0;
+
+static void my_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * px_map) {
+ if (!native_window) {
+ lv_display_flush_ready(disp);
+ return;
+ }
+
+ ANativeWindow_Buffer buffer;
+ if (ANativeWindow_lock(native_window, &buffer, NULL) < 0) {
+ lv_display_flush_ready(disp);
+ return;
+ }
+
+ int32_t width = lv_area_get_width(area);
+ int32_t height = lv_area_get_height(area);
+
+ // Assume 32-bit RGBA
+ uint32_t * dst_base = (uint32_t *)buffer.bits;
+ uint32_t * src = (uint32_t *)px_map;
+ int stride = buffer.stride;
+
+ for (int y = 0; y < height; y++) {
+ uint32_t * dst_line = dst_base + (area->y1 + y) * stride + area->x1;
+ memcpy(dst_line, src + y * width, width * sizeof(uint32_t));
+ }
+
+ ANativeWindow_unlockAndPost(native_window);
+ lv_display_flush_ready(disp);
+}
+
+static void my_input_read(lv_indev_t * indev, lv_indev_data_t * data) {
+ data->point.x = touch_x;
+ data->point.y = touch_y;
+ data->state = touch_down ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
+}
+
+#include <math.h>
+
+typedef struct {
+ int32_t x;
+ int32_t y;
+ int32_t vx;
+ int32_t vy;
+ lv_color_t color;
+ int32_t size;
+ int32_t life;
+} Particle;
+
+#define MAX_PARTICLES 100
+static Particle particles[MAX_PARTICLES];
+static int particle_count = 0;
+
+static void create_particle(int32_t x, int32_t y) {
+ if (particle_count < MAX_PARTICLES) {
+ Particle *p = &particles[particle_count++];
+ p->x = x;
+ p->y = y;
+ p->vx = (rand() % 10) - 5;
+ p->vy = (rand() % 10) - 5;
+ p->color = lv_color_make(rand() % 255, rand() % 255, rand() % 255);
+ p->size = (rand() % 15) + 5;
+ p->life = 100;
+ }
+}
+
+static void update_particles(void) {
+ for (int i = 0; i < particle_count; i++) {
+ Particle *p = &particles[i];
+ p->x += p->vx;
+ p->y += p->vy;
+ p->life--;
+
+ if (p->life <= 0 || p->x < 0 || p->x > window_width || p->y < 0 || p->y > window_height) {
+ *p = particles[--particle_count];
+ i--;
+ }
+ }
+}
+
+static void draw_particles(lv_layer_t * layer) {
+ lv_draw_rect_dsc_t draw_dsc;
+ lv_draw_rect_dsc_init(&draw_dsc);
+ draw_dsc.radius = LV_RADIUS_CIRCLE;
+
+ for (int i = 0; i < particle_count; i++) {
+ Particle *p = &particles[i];
+ draw_dsc.bg_color = p->color;
+ draw_dsc.bg_opa = (p->life * 255) / 100;
+
+ lv_area_t area;
+ area.x1 = p->x - p->size / 2;
+ area.y1 = p->y - p->size / 2;
+ area.x2 = area.x1 + p->size;
+ area.y2 = area.y1 + p->size;
+
+ lv_draw_rect(layer, &draw_dsc, &area);
+ }
+}
+
+static void particle_timer_cb(lv_timer_t * timer) {
+ if (touch_down) {
+ for (int i = 0; i < 5; i++) {
+ create_particle(touch_x, touch_y);
+ }
+ } else {
+ // Auto emit
+ static float t = 0;
+ t += 0.1f;
+ int cx = window_width / 2 + cos(t) * (window_width / 4);
+ int cy = window_height / 2 + sin(t) * (window_height / 4);
+ create_particle(cx, cy);
+ }
+
+ update_particles();
+ lv_obj_invalidate(lv_scr_act()); // Force redraw
+}
+
+static void canvas_draw_event_cb(lv_event_t * e) {
+ lv_layer_t * layer = lv_event_get_layer(e);
+ draw_particles(layer);
+}
+
+static void create_ui(void) {
+ lv_obj_t * label = lv_label_create(lv_screen_active());
+ lv_label_set_text(label, "Touch to create particles!");
+ lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
+ lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 50);
+
+ // FPS label
+ label_fps = lv_label_create(lv_screen_active());
+ lv_label_set_text(label_fps, "FPS: 0");
+ lv_obj_set_style_text_font(label_fps, &lv_font_montserrat_14, 0);
+ lv_obj_align(label_fps, LV_ALIGN_TOP_MID, 0, 100);
+
+ // Add draw event to screen for particles
+ lv_obj_add_event_cb(lv_screen_active(), canvas_draw_event_cb, LV_EVENT_DRAW_POST, NULL);
+
+ lv_timer_create(particle_timer_cb, 16, NULL);
+}
+
+static void handle_cmd(struct android_app* app, int32_t cmd) {
+ switch (cmd) {
+ case APP_CMD_INIT_WINDOW:
+ LOGI("Init Window");
+ native_window = app->window;
+ window_width = ANativeWindow_getWidth(native_window);
+ window_height = ANativeWindow_getHeight(native_window);
+ ANativeWindow_setBuffersGeometry(native_window, window_width, window_height, WINDOW_FORMAT_RGBA_8888);
+
+ if (!display) {
+ display = lv_display_create(window_width, window_height);
+ lv_display_set_color_format(display, LV_COLOR_FORMAT_ARGB8888);
+ lv_display_set_flush_cb(display, my_flush_cb);
+
+ size_t buf_size = window_width * window_height * 4;
+ disp_buf = malloc(buf_size);
+ lv_display_set_buffers(display, disp_buf, NULL, buf_size, LV_DISPLAY_RENDER_MODE_FULL);
+
+ indev = lv_indev_create();
+ lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
+ lv_indev_set_read_cb(indev, my_input_read);
+
+ create_ui();
+ } else {
+ lv_display_set_resolution(display, window_width, window_height);
+ lv_obj_invalidate(lv_scr_act());
+ }
+ break;
+ case APP_CMD_TERM_WINDOW:
+ native_window = NULL;
+ break;
+ case APP_CMD_DESTROY:
+ if (display) {
+ lv_display_delete(display);
+ display = NULL;
+ }
+ if (indev) {
+ lv_indev_delete(indev);
+ indev = NULL;
+ }
+ if (disp_buf) {
+ free(disp_buf);
+ disp_buf = NULL;
+ }
+ break;
+ }
+}
+
+static int32_t handle_input(struct android_app* app, AInputEvent* event) {
+ if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
+ int action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
+ touch_x = AMotionEvent_getX(event, 0);
+ touch_y = AMotionEvent_getY(event, 0);
+ if (action == AMOTION_EVENT_ACTION_DOWN || action == AMOTION_EVENT_ACTION_MOVE) {
+ touch_down = true;
+ } else {
+ touch_down = false;
+ }
+ return 1;
+ }
+ return 0;
+}
+
+void android_main(struct android_app* app) {
+ srand(time(NULL));
+ lv_init();
+
+ app->onAppCmd = handle_cmd;
+ app->onInputEvent = handle_input;
+
+ int frame_count = 0;
+ time_t last_time = time(NULL);
+
+ while (1) {
+ int ident;
+ int events;
+ struct android_poll_source* source;
+
+ uint32_t timeout = lv_timer_handler();
+ if (timeout > 50) timeout = 50;
+
+ while ((ident = ALooper_pollAll(timeout, NULL, &events, (void**)&source)) >= 0) {
+ if (source != NULL) source->process(app, source);
+ if (app->destroyRequested != 0) return;
+ }
+
+ lv_tick_inc(timeout);
+
+ frame_count++;
+ time_t current_time = time(NULL);
+ if (current_time - last_time >= 1) {
+ if (label_fps) {
+ lv_label_set_text_fmt(label_fps, "FPS: %d", frame_count);
+ }
+ frame_count = 0;
+ last_time = current_time;
+ }
+ }
+}
diff --git a/tests/projects/android/native_app/lvgl_particles/xmake.lua b/tests/projects/android/native_app/lvgl_particles/xmake.lua
new file mode 100644
index 000000000..3a74e8439
--- /dev/null
+++ b/tests/projects/android/native_app/lvgl_particles/xmake.lua
@@ -0,0 +1,23 @@
+add_rules("mode.debug", "mode.release")
+
+add_requires("lvgl 9.1.0")
+
+target("lvgl_particles")
+ set_kind("binary")
+ set_languages("c99")
+ add_files("src/main.c")
+ add_syslinks("log", "android", "EGL", "GLESv2")
+ add_packages("lvgl")
+
+ -- define LV_CONF_INCLUDE_SIMPLE to include lv_conf.h
+ add_defines("LV_CONF_INCLUDE_SIMPLE")
+
+ 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.lvgl.particles",
+ logcat_filters = {"lvgl_particles", "lvgl"}
+ })
diff --git a/tests/projects/android/native_app/raylib_basic/android/AndroidManifest.xml b/tests/projects/android/native_app/raylib_basic/android/AndroidManifest.xml
new file mode 100644
index 000000000..2d6b2d67c
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/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.basic"
+
+ 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="RaylibBasic" 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="RaylibBasic"
+ 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="main" />
+ <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_basic/android/debug.jks b/tests/projects/android/native_app/raylib_basic/android/debug.jks
new file mode 100644
index 000000000..0a18aa012
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/android/debug.jks
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_basic/android/res/mipmap-mdpi/icon.png b/tests/projects/android/native_app/raylib_basic/android/res/mipmap-mdpi/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/android/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_basic/android/res/mipmap/icon.png b/tests/projects/android/native_app/raylib_basic/android/res/mipmap/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/android/res/mipmap/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_basic/android/res/values/strings.xml b/tests/projects/android/native_app/raylib_basic/android/res/values/strings.xml
new file mode 100644
index 000000000..bc0a9bfbf
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/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_basic/android/res/values/styles.xml b/tests/projects/android/native_app/raylib_basic/android/res/values/styles.xml
new file mode 100644
index 000000000..621547380
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/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_basic/src/main.cpp b/tests/projects/android/native_app/raylib_basic/src/main.cpp
new file mode 100644
index 000000000..0094d9e3c
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/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_basic/xmake.lua b/tests/projects/android/native_app/raylib_basic/xmake.lua
new file mode 100644
index 000000000..50886764d
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_basic/xmake.lua
@@ -0,0 +1,19 @@
+add_rules("mode.debug", "mode.release")
+
+add_requires("raylib 5.5.0")
+
+target("raydemo_basic")
+ set_kind("binary")
+ set_languages("c++17")
+ add_files("src/main.cpp")
+ 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.basic",
+ logcat_filters = {"raydemo_basic", "raylib"}
+ })
diff --git a/tests/projects/android/native_app/raylib_particles/android/AndroidManifest.xml b/tests/projects/android/native_app/raylib_particles/android/AndroidManifest.xml
new file mode 100644
index 000000000..70dd36933
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/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.particles"
+
+ 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="RaylibParticles" 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="RaylibParticles"
+ 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="main" />
+ <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_particles/android/debug.jks b/tests/projects/android/native_app/raylib_particles/android/debug.jks
new file mode 100644
index 000000000..0a18aa012
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/android/debug.jks
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_particles/android/res/mipmap-mdpi/icon.png b/tests/projects/android/native_app/raylib_particles/android/res/mipmap-mdpi/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/android/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_particles/android/res/mipmap/icon.png b/tests/projects/android/native_app/raylib_particles/android/res/mipmap/icon.png
new file mode 100644
index 000000000..743413831
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/android/res/mipmap/icon.png
Binary files differ
diff --git a/tests/projects/android/native_app/raylib_particles/android/res/values/strings.xml b/tests/projects/android/native_app/raylib_particles/android/res/values/strings.xml
new file mode 100644
index 000000000..3967cc312
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">Raylib Particles</string>
+</resources> \ No newline at end of file
diff --git a/tests/projects/android/native_app/raylib_particles/android/res/values/styles.xml b/tests/projects/android/native_app/raylib_particles/android/res/values/styles.xml
new file mode 100644
index 000000000..621547380
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/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_particles/src/main.cpp b/tests/projects/android/native_app/raylib_particles/src/main.cpp
new file mode 100644
index 000000000..5bbfc90e8
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/src/main.cpp
@@ -0,0 +1,89 @@
+#include <raylib.h>
+#include <android/log.h>
+#include <vector>
+#include <cmath>
+
+#define LOG_TAG "raydemo_particles"
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+
+struct Particle {
+ Vector2 position;
+ Vector2 velocity;
+ Color color;
+ float size;
+ float life;
+};
+
+int main(int argc, char** argv) {
+ InitWindow(0, 0, "Raylib Particles");
+ SetTargetFPS(60);
+ LOGD("raylib particles starting!");
+
+ std::vector<Particle> particles;
+
+ while (!WindowShouldClose()) {
+ // Update
+ bool inputActive = IsMouseButtonDown(MOUSE_BUTTON_LEFT) || GetTouchPointCount() > 0;
+ Vector2 pos;
+
+ if (inputActive) {
+ pos = GetMousePosition();
+ if (GetTouchPointCount() > 0) pos = GetTouchPosition(0);
+ } else {
+ // Auto emit when idle
+ double time = GetTime();
+ pos.x = GetScreenWidth() / 2.0f + cos(time * 3.0f) * (GetScreenWidth() / 4.0f);
+ pos.y = GetScreenHeight() / 2.0f + sin(time * 2.0f) * (GetScreenHeight() / 4.0f);
+ }
+
+ if (inputActive || GetRandomValue(0, 100) < 50) {
+ int count = inputActive ? 5 : 2;
+ for (int i = 0; i < count; i++) {
+ Particle p;
+ p.position = pos;
+ p.velocity = {(float)GetRandomValue(-200, 200) / 100.0f, (float)GetRandomValue(-200, 200) / 100.0f};
+ p.color = (Color){(unsigned char)GetRandomValue(0, 255), (unsigned char)GetRandomValue(0, 255), (unsigned char)GetRandomValue(0, 255), 255};
+ p.size = (float)GetRandomValue(5, 20);
+ p.life = 1.0f;
+ particles.push_back(p);
+ }
+ }
+
+ for (auto it = particles.begin(); it != particles.end();) {
+ it->position.x += it->velocity.x * 5.0f;
+ it->position.y += it->velocity.y * 5.0f;
+ it->life -= 0.02f;
+ it->size *= 0.99f;
+
+ if (it->life <= 0) {
+ it = particles.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ // Draw
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+ for (const auto& p : particles) {
+ DrawCircleV(p.position, p.size, Fade(p.color, p.life));
+ }
+
+ // 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 instructions
+ int fontSize = 40;
+ const char* text = "Touch to create particles!";
+ int textWidth = MeasureText(text, fontSize);
+ DrawText(text, GetScreenWidth() / 2 - textWidth / 2, 80, fontSize, WHITE);
+
+ EndDrawing();
+ }
+
+ CloseWindow();
+ return 0;
+}
diff --git a/tests/projects/android/native_app/raylib_particles/xmake.lua b/tests/projects/android/native_app/raylib_particles/xmake.lua
new file mode 100644
index 000000000..c6d9a6217
--- /dev/null
+++ b/tests/projects/android/native_app/raylib_particles/xmake.lua
@@ -0,0 +1,19 @@
+add_rules("mode.debug", "mode.release")
+
+add_requires("raylib 5.5.0")
+
+target("raydemo_particles")
+ set_kind("binary")
+ set_languages("c++17")
+ add_files("src/main.cpp")
+ 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.particles",
+ logcat_filters = {"raydemo_particles", "raylib"}
+ })
diff --git a/xmake/actions/run/main.lua b/xmake/actions/run/main.lua
index 94c7d7d6b..6cfe09d3a 100644
--- a/xmake/actions/run/main.lua
+++ b/xmake/actions/run/main.lua
@@ -145,6 +145,18 @@ function _run(target)
os.setenvs(oldenvs)
end
+-- check if the target is runnable
+function _is_runnable(target)
+ if target:is_binary() or target:script("run") then
+ return true
+ end
+ for _, r in ipairs(target:orderules()) do
+ if r:script("run") then
+ return true
+ end
+ end
+end
+
-- check targets
function _check_targets(targetname, group_pattern)
@@ -155,7 +167,7 @@ function _check_targets(targetname, group_pattern)
table.insert(targets, target)
else
for _, target in ipairs(project.ordertargets()) do
- if target:is_binary() or target:script("run") then
+ if _is_runnable(target) then
local group = target:get("group")
if (target:is_default() and not group_pattern) or option.get("all") or (group_pattern and group and group:match(group_pattern)) then
table.insert(targets, target)
@@ -216,7 +228,7 @@ function main()
else
local targets = {}
for _, target in ipairs(project.ordertargets()) do
- if target:is_binary() or target:script("run") then
+ if _is_runnable(target) then
local group = target:get("group")
if (target:is_default() and not group_pattern) or option.get("all") or (group_pattern and group and group:match(group_pattern)) then
table.insert(targets, target)
diff --git a/xmake/rules/platform/android/install.lua b/xmake/rules/platform/android/install.lua
new file mode 100644
index 000000000..ec340ac00
--- /dev/null
+++ b/xmake/rules/platform/android/install.lua
@@ -0,0 +1,34 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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) 2015-present, Xmake Open Source Community.
+--
+-- @author keosu
+-- @file install.lua
+--
+
+-- imports
+
+function main(target)
+
+ local android_sdkdir = target:toolchain("ndk"):config("android_sdk")
+ local adb = path.join(android_sdkdir, "platform-tools", "adb")
+
+ local final_apk = path.join(target:targetdir(), target:basename() .. ".apk")
+ assert(os.isfile(final_apk), "apk file %s not found!", final_apk)
+
+ cprint("installing %s ...", final_apk)
+ os.vrunv(adb, {"install", "-r", final_apk})
+ cprint("install ok")
+end
diff --git a/xmake/rules/platform/android/load.lua b/xmake/rules/platform/android/load.lua
new file mode 100644
index 000000000..3e49d9533
--- /dev/null
+++ b/xmake/rules/platform/android/load.lua
@@ -0,0 +1,44 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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) 2015-present, Xmake Open Source Community.
+--
+-- @author keosu
+-- @file load.lua
+--
+
+function main(target)
+
+ -- only for android target
+ if not target:is_plat("android") then
+ wprint("rule(android.native_app): only for android system!")
+ target:rule_enable("android.native_app", false)
+ return
+ end
+
+ local toolchain_ndk = target:toolchain("ndk")
+ local ndk_root = toolchain_ndk:config("ndk")
+ if not ndk_root then
+ raise("NDK path not set! Please set NDK path properly.")
+ end
+
+ -- set target kind
+ target:set("kind", "shared")
+
+ -- add glue file to target
+ local native_app_glue_file = path.join(ndk_root, "sources", "android", "native_app_glue", "android_native_app_glue.c")
+ local native_app_glue_dir = path.directory(native_app_glue_file)
+ target:add("files", native_app_glue_file)
+ target:add("includedirs", native_app_glue_dir)
+end
diff --git a/xmake/rules/platform/android/package.lua b/xmake/rules/platform/android/package.lua
new file mode 100644
index 000000000..00142a85f
--- /dev/null
+++ b/xmake/rules/platform/android/package.lua
@@ -0,0 +1,103 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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) 2015-present, Xmake Open Source Community.
+--
+-- @author keosu
+-- @file package.lua
+--
+
+-- imports
+import("core.project.depend")
+import("utils.progress")
+
+function main(target, opt)
+
+ local conf = target:extraconf("rules", "android.native_app")
+ local android_sdk_version = conf.android_sdk_version
+ local android_manifest = conf.android_manifest
+ local android_res = conf.android_res
+ local android_assets = conf.android_assets
+ local keystore = conf.keystore
+ local keystore_pass = conf.keystore_pass
+
+ assert(android_sdk_version, "android sdk version not set")
+ assert(android_manifest, "android manifest not set")
+
+ local final_apk = path.join(target:targetdir(), target:basename() .. ".apk")
+
+ -- add dependencies
+ local depfiles = {target:targetfile(), android_manifest, keystore}
+ if android_res and os.isdir(android_res) then
+ table.join2(depfiles, os.files(path.join(android_res, "**")))
+ end
+ if android_assets and os.isdir(android_assets) then
+ table.join2(depfiles, os.files(path.join(android_assets, "**")))
+ end
+
+ depend.on_changed(function ()
+ progress.show(opt.progress, "${color.build.target}packing.apk %s", final_apk)
+
+ local tmp_path = path.join(target:targetdir(), "temp")
+ os.mkdir(tmp_path)
+ os.mkdir(path.join(tmp_path, "lib"))
+ 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")
+ os.cp(target:targetfile(), libfile)
+
+ -- get android tool path
+ local android_sdkdir = target:toolchain("ndk"):config("android_sdk")
+ local android_build_toolver = target:toolchain("ndk"):config("build_toolver")
+ local sdk_tool_path = path.join(android_sdkdir, "build-tools", android_build_toolver)
+
+ local aapt = path.join(sdk_tool_path, "aapt" .. (is_host("windows") and ".exe" or ""))
+ local zipalign = path.join(sdk_tool_path, "zipalign" .. (is_host("windows") and ".exe" or ""))
+ local apksigner = path.join(sdk_tool_path, "apksigner" .. (is_host("windows") and ".bat" or ""))
+
+ -- pack resources
+ local resonly_apk = path.join(tmp_path, "res_only.apk")
+ local androidjar = path.join(android_sdkdir, "platforms", string.format("android-%s", android_sdk_version),
+ "android.jar")
+ assert(os.isfile(androidjar), "%s not found", androidjar)
+ local aapt_argv = {"package", "-f", "-M", android_manifest, "-I", androidjar, "-F", resonly_apk}
+
+ if android_res and not os.emptydir(android_res) then
+ table.insert(aapt_argv, "-S")
+ table.insert(aapt_argv, android_res)
+ end
+
+ if android_assets and not os.emptydir(android_assets) then
+ table.insert(aapt_argv, "-A")
+ table.insert(aapt_argv, android_assets)
+ end
+
+ os.vrunv(aapt, aapt_argv)
+
+ -- pack libs
+ os.vrunv(aapt, {"add", "res_only.apk", "lib/" .. target:arch() .."/libmain.so"}, {curdir = tmp_path})
+
+ -- align apk
+ local aligned_apk = path.join(tmp_path, "unsigned.apk")
+ local zipalign_argv = {"-f", "4", "res_only.apk", "unsigned.apk"}
+
+ os.vrunv(zipalign, zipalign_argv, {curdir = tmp_path})
+
+ -- sign apk
+ local apksigner_argv = {"sign", "--ks", keystore, "--ks-pass", string.format("pass:%s", keystore_pass), "--out",
+ final_apk, "--in", aligned_apk}
+ os.vrunv(apksigner, apksigner_argv)
+ end, {dependfile = target:dependfile(final_apk), files = depfiles, values = {android_sdk_version, keystore_pass}})
+end
diff --git a/xmake/rules/platform/android/run.lua b/xmake/rules/platform/android/run.lua
new file mode 100644
index 000000000..657dad4e8
--- /dev/null
+++ b/xmake/rules/platform/android/run.lua
@@ -0,0 +1,56 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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) 2015-present, Xmake Open Source Community.
+--
+-- @author keosu
+-- @file run.lua
+--
+
+-- imports
+import("core.base.tty")
+import("install", {alias = "install_app"})
+
+function main(target)
+
+ -- install it first
+ install_app(target)
+
+ local conf = target:extraconf("rules", "android.native_app")
+ local package_name = conf.package_name
+ local activity_name = conf.activity_name or "android.app.NativeActivity"
+
+ local android_sdkdir = target:toolchain("ndk"):config("android_sdk")
+ local adb = path.join(android_sdkdir, "platform-tools", "adb")
+
+ local run_argv = {"shell", "am", "start", "-n", package_name .. "/" .. activity_name}
+
+ cprint("running %s ...", package_name)
+ os.vrunv(adb, run_argv)
+
+ -- show logcat
+ local logcat_argv = {"logcat"}
+ if io.isatty() and (tty.has_color8() or tty.has_color256()) then
+ table.insert(logcat_argv, "-v")
+ table.insert(logcat_argv, "color")
+ end
+ local logcat_filters = conf.logcat_filters
+ if logcat_filters then
+ table.insert(logcat_argv, "-s")
+ for _, filter in ipairs(logcat_filters) do
+ table.insert(logcat_argv, filter)
+ end
+ end
+ os.execv(adb, logcat_argv)
+end
diff --git a/xmake/rules/platform/android/uninstall.lua b/xmake/rules/platform/android/uninstall.lua
new file mode 100644
index 000000000..24564d631
--- /dev/null
+++ b/xmake/rules/platform/android/uninstall.lua
@@ -0,0 +1,32 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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) 2015-present, Xmake Open Source Community.
+--
+-- @author ruki
+-- @file uninstall.lua
+--
+
+function main(target)
+
+ local conf = target:extraconf("rules", "android.native_app")
+ local package_name = conf.package_name
+
+ local android_sdkdir = target:toolchain("ndk"):config("android_sdk")
+ local adb = path.join(android_sdkdir, "platform-tools", "adb")
+
+ cprint("uninstalling %s ...", package_name)
+ os.vrunv(adb, {"uninstall", package_name})
+ cprint("uninstall ok")
+end
diff --git a/xmake/rules/platform/android/xmake.lua b/xmake/rules/platform/android/xmake.lua
new file mode 100644
index 000000000..0c58d7d09
--- /dev/null
+++ b/xmake/rules/platform/android/xmake.lua
@@ -0,0 +1,53 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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) 2015-present, Xmake Open Source Community.
+--
+-- @author keosu
+-- @file xmake.lua
+--
+
+-- define rule: build android native app with NDK
+--
+-- @code
+-- target("test")
+-- set_kind("binary")
+-- add_rules("android.native_app", {
+-- android_sdk_version = "35",
+-- android_manifest = "android/AndroidManifest.xml",
+-- android_res = "android/res",
+-- android_assets = "android/assets",
+-- keystore = "android/debug.jks",
+-- keystore_pass = "123456",
+-- package_name = "com.raylib.demo",
+-- logcat_filters = {"raydemo_android", "raylib"}
+-- })
+-- @endcode
+--
+rule("android.native_app")
+
+ -- we must set_kind and add some glue files to target
+ on_load("load")
+
+ -- generate android apk package
+ after_build("package")
+
+ -- install android package with adb
+ on_install("install")
+
+ -- uninstall android package with adb
+ on_uninstall("uninstall")
+
+ -- run android app through adb
+ on_run("run")