chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:29:49 +08:00
commit 505bac6b53
1470 changed files with 457757 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
# Android Example
A minimal Android mobile shell that embeds a Native SDK static library through JNI. The example keeps a native Android header above a WebView workspace and routes native navigation/header actions through the Native SDK command path.
Android views own the mobile shell layout: native header sizing, density-aware resize events, touch forwarding, and the `WebView` workspace. The Native SDK runtime is driven through JNI calls into the C ABI.
## Build the native library
Build or package an Android static library from the repository root, then copy it into this example:
```bash
zig build lib -Dtarget=aarch64-linux-android
mkdir -p examples/android/app/src/main/cpp/lib
cp zig-out/lib/libnative-sdk.a examples/android/app/src/main/cpp/lib/libnative-sdk.a
```
The CMake project expects the library at `app/src/main/cpp/lib/libnative-sdk.a` and the C header at `app/src/main/cpp/native_sdk.h`.
## Run
Open `examples/android` in Android Studio, or build from the command line with a configured Android SDK:
```bash
./gradlew :app:assembleDebug
```
Install on an emulator or device:
```bash
./gradlew :app:installDebug
```
## Files
- `app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt` hosts native Android chrome, a WebView workspace, a `SurfaceView`, and the JNI bridge.
- `app/src/main/cpp/native_sdk_jni.c` forwards JNI calls to the Native SDK C ABI.
- `app/src/main/cpp/CMakeLists.txt` imports `libnative-sdk.a` and builds the JNI shared library.
- `app.zon` records the mobile example metadata for Native SDK tooling.
## Host lifecycle
- `onCreate` loads the JNI library, creates the native shell, then starts the Native SDK app.
- `onResume` and `onPause` forward activation lifecycle with `native_sdk_app_activate` and `native_sdk_app_deactivate`.
- `surfaceChanged` forwards size, display density, safe-area insets, keyboard inset, and the Android `Surface`, then requests a frame.
- Orientation and screen-size changes stay in the same activity so the embedded runtime is not recreated during rotation.
- The activity uses `windowSoftInputMode="adjustResize"` so Android owns keyboard avoidance and relayouts the content area.
- The native Back and Refresh buttons call `nativeCommand` with stable mobile command IDs, update status from `native_sdk_app_last_command_count`, and request a frame.
- The Android system Back action dispatches `mobile.back` through the same command path.
- `onTouchEvent` forwards pointer id, phase, position, and pressure.
- The JNI bridge exposes hardware key, committed text, and IME composition entry points for GPU/widget text fields.
- The embedded C ABI can expose retained GPU/widget accessibility semantics by indexed snapshot and dispatch widget accessibility actions for Android accessibility providers.
- `surfaceDestroyed` and `onDestroy` stop and destroy the app.
The `app.zon` shell view tree describes this header and WebView workspace. Native mobile layout is still implemented in Kotlin so Android owns soft-keyboard relayout, Back handling, orientation changes, and activity lifecycle while the Native SDK receives the viewport metrics needed for GPU/widget layout.
+30
View File
@@ -0,0 +1,30 @@
.{
.id = "dev.native_sdk.android-example",
.name = "android-example",
.display_name = "Android Example",
.version = "0.1.0",
.platforms = .{ "android" },
.permissions = .{},
.capabilities = .{ "webview", "native_views", "native_module" },
.commands = .{
.{ .id = "mobile.back", .title = "Back" },
.{ .id = "mobile.refresh", .title = "Refresh" },
},
.shell = .{
.windows = .{
.{
.label = "main",
.title = "Mobile Shell",
.views = .{
.{ .label = "mobile-header", .kind = "toolbar", .edge = "top", .height = 104, .role = "toolbar" },
.{ .label = "mobile-title", .kind = "label", .parent = "mobile-header", .text = "Mobile Shell", .accessibility_label = "Mobile Shell" },
.{ .label = "mobile-status", .kind = "statusbar", .edge = "bottom", .height = 28, .text = "Native commands ready" },
.{ .label = "mobile-back", .kind = "button", .parent = "mobile-header", .text = "Back", .command = "mobile.back" },
.{ .label = "mobile-refresh", .kind = "button", .parent = "mobile-header", .text = "Refresh", .command = "mobile.refresh" },
.{ .label = "workspace", .kind = "webview", .url = "zero://app/index.html", .fill = true },
},
},
},
},
.web_engine = "system",
}
+29
View File
@@ -0,0 +1,29 @@
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
}
android {
namespace "dev.native_sdk.examples.android"
compileSdk 35
defaultConfig {
applicationId "dev.native_sdk.examples.android"
minSdk 26
targetSdk 35
versionCode 1
versionName "0.1.0"
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
@@ -0,0 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Android Example"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.22.1)
project(native_sdk_android_example C)
add_library(native-sdk STATIC IMPORTED)
set_target_properties(native-sdk PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/lib/libnative-sdk.a"
)
add_library(native_sdk_example SHARED native_sdk_jni.c)
target_include_directories(native_sdk_example PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(native_sdk_example native-sdk android log)
@@ -0,0 +1,277 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
enum {
NATIVE_SDK_WIDGET_ROLE_NONE = 0,
NATIVE_SDK_WIDGET_ROLE_GROUP = 1,
NATIVE_SDK_WIDGET_ROLE_TEXT = 2,
NATIVE_SDK_WIDGET_ROLE_IMAGE = 3,
NATIVE_SDK_WIDGET_ROLE_BUTTON = 4,
NATIVE_SDK_WIDGET_ROLE_TEXTBOX = 5,
NATIVE_SDK_WIDGET_ROLE_TOOLTIP = 6,
NATIVE_SDK_WIDGET_ROLE_DIALOG = 7,
NATIVE_SDK_WIDGET_ROLE_MENU = 8,
NATIVE_SDK_WIDGET_ROLE_MENUITEM = 9,
NATIVE_SDK_WIDGET_ROLE_LIST = 10,
NATIVE_SDK_WIDGET_ROLE_LISTITEM = 11,
NATIVE_SDK_WIDGET_ROLE_ROW = 12,
NATIVE_SDK_WIDGET_ROLE_GRID = 13,
NATIVE_SDK_WIDGET_ROLE_GRIDCELL = 14,
NATIVE_SDK_WIDGET_ROLE_TAB = 15,
NATIVE_SDK_WIDGET_ROLE_CHECKBOX = 16,
NATIVE_SDK_WIDGET_ROLE_SWITCH = 17,
NATIVE_SDK_WIDGET_ROLE_SLIDER = 18,
NATIVE_SDK_WIDGET_ROLE_PROGRESSBAR = 19,
};
enum {
NATIVE_SDK_WIDGET_FLAG_FOCUSED = 1u << 0,
NATIVE_SDK_WIDGET_FLAG_HOVERED = 1u << 1,
NATIVE_SDK_WIDGET_FLAG_PRESSED = 1u << 2,
NATIVE_SDK_WIDGET_FLAG_SELECTED = 1u << 3,
NATIVE_SDK_WIDGET_FLAG_DISABLED = 1u << 4,
NATIVE_SDK_WIDGET_FLAG_FOCUSABLE = 1u << 5,
NATIVE_SDK_WIDGET_FLAG_EXPANDED = 1u << 6,
NATIVE_SDK_WIDGET_FLAG_COLLAPSED = 1u << 7,
NATIVE_SDK_WIDGET_FLAG_REQUIRED = 1u << 8,
NATIVE_SDK_WIDGET_FLAG_READ_ONLY = 1u << 9,
NATIVE_SDK_WIDGET_FLAG_INVALID = 1u << 10,
};
enum {
NATIVE_SDK_WIDGET_ACTION_FOCUS = 1u << 0,
NATIVE_SDK_WIDGET_ACTION_PRESS = 1u << 1,
NATIVE_SDK_WIDGET_ACTION_TOGGLE = 1u << 2,
NATIVE_SDK_WIDGET_ACTION_INCREMENT = 1u << 3,
NATIVE_SDK_WIDGET_ACTION_DECREMENT = 1u << 4,
NATIVE_SDK_WIDGET_ACTION_SET_TEXT = 1u << 5,
NATIVE_SDK_WIDGET_ACTION_SET_SELECTION = 1u << 6,
NATIVE_SDK_WIDGET_ACTION_SELECT = 1u << 7,
NATIVE_SDK_WIDGET_ACTION_DRAG = 1u << 8,
NATIVE_SDK_WIDGET_ACTION_DROP_FILES = 1u << 9,
NATIVE_SDK_WIDGET_ACTION_DISMISS = 1u << 10,
};
enum {
NATIVE_SDK_WIDGET_ACTION_KIND_FOCUS = 0,
NATIVE_SDK_WIDGET_ACTION_KIND_PRESS = 1,
NATIVE_SDK_WIDGET_ACTION_KIND_TOGGLE = 2,
NATIVE_SDK_WIDGET_ACTION_KIND_INCREMENT = 3,
NATIVE_SDK_WIDGET_ACTION_KIND_DECREMENT = 4,
NATIVE_SDK_WIDGET_ACTION_KIND_SET_TEXT = 5,
NATIVE_SDK_WIDGET_ACTION_KIND_SET_SELECTION = 6,
NATIVE_SDK_WIDGET_ACTION_KIND_SET_COMPOSITION = 7,
NATIVE_SDK_WIDGET_ACTION_KIND_COMMIT_COMPOSITION = 8,
NATIVE_SDK_WIDGET_ACTION_KIND_CANCEL_COMPOSITION = 9,
NATIVE_SDK_WIDGET_ACTION_KIND_SELECT = 10,
NATIVE_SDK_WIDGET_ACTION_KIND_DRAG = 11,
NATIVE_SDK_WIDGET_ACTION_KIND_DROP_FILES = 12,
NATIVE_SDK_WIDGET_ACTION_KIND_DISMISS = 13,
};
enum {
NATIVE_SDK_GPU_SURFACE_STATUS_UNAVAILABLE = 0,
NATIVE_SDK_GPU_SURFACE_STATUS_INITIALIZING = 1,
NATIVE_SDK_GPU_SURFACE_STATUS_READY = 2,
NATIVE_SDK_GPU_SURFACE_STATUS_LOST = 3,
};
typedef struct native_sdk_widget_semantics {
uint64_t id;
uint64_t parent_id;
int role;
uint32_t flags;
uint32_t actions;
float x;
float y;
float width;
float height;
float value;
int has_value;
const char *label;
uintptr_t label_len;
const char *text;
uintptr_t text_len;
const char *placeholder;
uintptr_t placeholder_len;
intptr_t text_selection_start;
intptr_t text_selection_end;
intptr_t text_composition_start;
intptr_t text_composition_end;
intptr_t grid_row_index;
intptr_t grid_column_index;
intptr_t grid_row_count;
intptr_t grid_column_count;
intptr_t list_item_index;
intptr_t list_item_count;
float scroll_offset;
float scroll_viewport_extent;
float scroll_content_extent;
int has_scroll;
} native_sdk_widget_semantics_t;
typedef struct native_sdk_widget_text_geometry {
uint64_t id;
int has_caret_bounds;
float caret_x;
float caret_y;
float caret_width;
float caret_height;
int has_selection_bounds;
float selection_x;
float selection_y;
float selection_width;
float selection_height;
uintptr_t selection_rect_count;
int has_composition_bounds;
float composition_x;
float composition_y;
float composition_width;
float composition_height;
uintptr_t composition_rect_count;
} native_sdk_widget_text_geometry_t;
typedef struct native_sdk_widget_action {
uint64_t id;
int action;
const char *text;
uintptr_t text_len;
uintptr_t selection_anchor;
uintptr_t selection_focus;
int has_selection;
} native_sdk_widget_action_t;
typedef struct native_sdk_canvas_pixels {
uintptr_t width;
uintptr_t height;
uintptr_t byte_len;
} native_sdk_canvas_pixels_t;
// Result of native_sdk_app_render_pixels_damage: the surface dimensions
// plus the damaged region the call wrote into the caller's RETAINED
// buffer, in device pixels. damage_width == 0 (or damage_height == 0)
// means nothing changed since the previous call: the buffer already
// shows the current frame and the host skips its upload entirely.
typedef struct native_sdk_canvas_pixels_damage {
uintptr_t width;
uintptr_t height;
uintptr_t byte_len;
uintptr_t damage_x;
uintptr_t damage_y;
uintptr_t damage_width;
uintptr_t damage_height;
// The retained-canvas revision the buffer now REFLECTS: gate re-renders
// on canvas_revision != this value (a change whose frame has not
// presented yet reports the OLD revision with empty damage - call again
// next tick), never on your own last sighting of canvas_revision.
uint64_t revision;
} native_sdk_canvas_pixels_damage_t;
typedef struct native_sdk_text_input_state {
int active;
uint64_t widget_id;
float x;
float y;
float width;
float height;
} native_sdk_text_input_state_t;
typedef struct native_sdk_viewport_state {
float width;
float height;
float scale;
int has_surface;
float safe_top;
float safe_right;
float safe_bottom;
float safe_left;
float keyboard_top;
float keyboard_right;
float keyboard_bottom;
float keyboard_left;
float content_x;
float content_y;
float content_width;
float content_height;
} native_sdk_viewport_state_t;
typedef struct native_sdk_gpu_frame_state {
uint64_t surface_id;
uint64_t window_id;
float width;
float height;
float scale;
uint64_t frame_index;
uint64_t timestamp_ns;
uint64_t frame_interval_ns;
uint64_t input_timestamp_ns;
uint64_t input_latency_ns;
uint64_t input_latency_budget_ns;
uintptr_t input_latency_budget_exceeded_count;
int input_latency_budget_ok;
uint64_t first_frame_latency_ns;
uint64_t first_frame_latency_budget_ns;
uintptr_t first_frame_latency_budget_exceeded_count;
int first_frame_latency_budget_ok;
int nonblank;
uint32_t sample_color;
int status;
int vsync;
uint64_t canvas_revision;
uintptr_t canvas_command_count;
int canvas_frame_requires_render;
int canvas_frame_full_repaint;
uintptr_t canvas_frame_batch_count;
uintptr_t canvas_frame_budget_exceeded_count;
int canvas_frame_budget_ok;
uint64_t widget_revision;
uintptr_t widget_node_count;
uintptr_t widget_semantics_count;
} native_sdk_gpu_frame_state_t;
void *native_sdk_app_create(void);
void native_sdk_app_destroy(void *app);
void native_sdk_app_start(void *app);
void native_sdk_app_activate(void *app);
void native_sdk_app_deactivate(void *app);
void native_sdk_app_stop(void *app);
void native_sdk_app_resize(void *app, float width, float height, float scale, void *surface);
void native_sdk_app_viewport(void *app, float width, float height, float scale, void *surface, float safe_top, float safe_right, float safe_bottom, float safe_left, float keyboard_top, float keyboard_right, float keyboard_bottom, float keyboard_left);
int native_sdk_app_viewport_state(void *app, native_sdk_viewport_state_t *out);
int native_sdk_app_gpu_frame_state(void *app, native_sdk_gpu_frame_state_t *out);
void native_sdk_app_touch(void *app, uint64_t id, int phase, float x, float y, float pressure);
void native_sdk_app_scroll(void *app, uint64_t id, float x, float y, float delta_x, float delta_y);
void native_sdk_app_key(void *app, int phase, const char *key, uintptr_t key_len, const char *text, uintptr_t text_len, uint32_t modifiers_mask);
void native_sdk_app_text(void *app, const char *text, uintptr_t len);
void native_sdk_app_ime(void *app, int kind, const char *text, uintptr_t len, intptr_t cursor);
void native_sdk_app_command(void *app, const char *name, uintptr_t len);
void native_sdk_app_frame(void *app);
void native_sdk_app_set_asset_root(void *app, const char *path, uintptr_t len);
void native_sdk_app_set_asset_entry(void *app, const char *path, uintptr_t len);
uintptr_t native_sdk_app_last_command_count(void *app);
const char *native_sdk_app_last_command_name(void *app);
const char *native_sdk_app_last_error_name(void *app);
uintptr_t native_sdk_app_widget_semantics_count(void *app);
int native_sdk_app_widget_semantics_at(void *app, uintptr_t index, native_sdk_widget_semantics_t *out);
int native_sdk_app_widget_semantics_by_id(void *app, uint64_t id, native_sdk_widget_semantics_t *out);
int native_sdk_app_widget_text_geometry(void *app, uint64_t id, native_sdk_widget_text_geometry_t *out);
int native_sdk_app_widget_action(void *app, const native_sdk_widget_action_t *action);
int native_sdk_app_text_input_state(void *app, native_sdk_text_input_state_t *out);
// Platform text measurement for layout: returns the typographic width of a
// single-line UTF-8 run at `size` for `font_id` (1 = sans, 2 = mono),
// measured with the same font resolution presentation draws with. Return a
// negative value to fall back to the deterministic estimator (e.g. invalid
// UTF-8). Register before native_sdk_app_start; pass NULL to fall back to
// the estimator on the next layout.
typedef double (*native_sdk_text_measure_fn)(void *context, uint64_t font_id, double size, const char *text, uintptr_t text_len);
int native_sdk_app_set_text_measure(void *app, native_sdk_text_measure_fn measure, void *context);
int native_sdk_app_set_automation_dir(void *app, const char *path, uintptr_t len);
int native_sdk_app_render_pixel_size(void *app, float scale, native_sdk_canvas_pixels_t *out);
int native_sdk_app_render_pixels(void *app, float scale, uint8_t *pixels, uintptr_t pixels_len, native_sdk_canvas_pixels_t *out);
// Incremental sibling of native_sdk_app_render_pixels for a host that
// keeps `pixels` RETAINED across calls (one buffer, one consumer): the
// fast path copies only the pixels changed since the previous call —
// captured off the runtime's own dirty-scissored raster, no second
// render — and reports that region; the first call (and any size or
// scale change) fills the whole buffer with full damage.
int native_sdk_app_render_pixels_damage(void *app, float scale, uint8_t *pixels, uintptr_t pixels_len, native_sdk_canvas_pixels_damage_t *out);
@@ -0,0 +1,345 @@
#include <jni.h>
#include <stdint.h>
#include <string.h>
#include "native_sdk.h"
static jbyteArray native_sdk_jni_bytes(JNIEnv *env, const char *ptr, uintptr_t len) {
jbyteArray out = (*env)->NewByteArray(env, (jsize)len);
if (!out) return NULL;
if (ptr && len > 0) {
(*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte *)ptr);
}
return out;
}
JNIEXPORT jlong JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeCreate(JNIEnv *env, jobject self) {
(void)env;
(void)self;
return (jlong)native_sdk_app_create();
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeDestroy(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
native_sdk_app_destroy((void *)app);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeStart(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
native_sdk_app_start((void *)app);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeActivate(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
native_sdk_app_activate((void *)app);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeDeactivate(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
native_sdk_app_deactivate((void *)app);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeStop(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
native_sdk_app_stop((void *)app);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeResize(JNIEnv *env, jobject self, jlong app, jfloat width, jfloat height, jfloat scale, jobject surface) {
(void)env;
(void)self;
native_sdk_app_resize((void *)app, width, height, scale, surface);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeViewport(JNIEnv *env, jobject self, jlong app, jfloat width, jfloat height, jfloat scale, jobject surface, jfloat safe_top, jfloat safe_right, jfloat safe_bottom, jfloat safe_left, jfloat keyboard_top, jfloat keyboard_right, jfloat keyboard_bottom, jfloat keyboard_left) {
(void)env;
(void)self;
native_sdk_app_viewport((void *)app, width, height, scale, surface, safe_top, safe_right, safe_bottom, safe_left, keyboard_top, keyboard_right, keyboard_bottom, keyboard_left);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeTouch(JNIEnv *env, jobject self, jlong app, jlong id, jint phase, jfloat x, jfloat y, jfloat pressure) {
(void)env;
(void)self;
native_sdk_app_touch((void *)app, (uint64_t)id, phase, x, y, pressure);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeScroll(JNIEnv *env, jobject self, jlong app, jlong id, jfloat x, jfloat y, jfloat delta_x, jfloat delta_y) {
(void)env;
(void)self;
native_sdk_app_scroll((void *)app, (uint64_t)id, x, y, delta_x, delta_y);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeKey(JNIEnv *env, jobject self, jlong app, jint phase, jstring key, jstring text, jint modifiers) {
(void)self;
const char *key_chars = key ? (*env)->GetStringUTFChars(env, key, NULL) : NULL;
const char *text_chars = text ? (*env)->GetStringUTFChars(env, text, NULL) : NULL;
native_sdk_app_key((void *)app, phase, key_chars, key_chars ? strlen(key_chars) : 0, text_chars, text_chars ? strlen(text_chars) : 0, (uint32_t)modifiers);
if (key_chars) (*env)->ReleaseStringUTFChars(env, key, key_chars);
if (text_chars) (*env)->ReleaseStringUTFChars(env, text, text_chars);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeText(JNIEnv *env, jobject self, jlong app, jstring text) {
(void)self;
const char *text_chars = (*env)->GetStringUTFChars(env, text, NULL);
if (!text_chars) return;
native_sdk_app_text((void *)app, text_chars, strlen(text_chars));
(*env)->ReleaseStringUTFChars(env, text, text_chars);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeIme(JNIEnv *env, jobject self, jlong app, jint kind, jstring text, jlong cursor) {
(void)self;
const char *text_chars = text ? (*env)->GetStringUTFChars(env, text, NULL) : NULL;
native_sdk_app_ime((void *)app, kind, text_chars, text_chars ? strlen(text_chars) : 0, (intptr_t)cursor);
if (text_chars) (*env)->ReleaseStringUTFChars(env, text, text_chars);
}
JNIEXPORT jint JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeCommand(JNIEnv *env, jobject self, jlong app, jstring command) {
(void)self;
const char *command_chars = (*env)->GetStringUTFChars(env, command, NULL);
if (!command_chars) return 0;
native_sdk_app_command((void *)app, command_chars, strlen(command_chars));
(*env)->ReleaseStringUTFChars(env, command, command_chars);
return (jint)native_sdk_app_last_command_count((void *)app);
}
JNIEXPORT void JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeFrame(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
native_sdk_app_frame((void *)app);
}
JNIEXPORT jboolean JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeGpuFrameState(JNIEnv *env, jobject self, jlong app, jlongArray longs, jintArray ints, jfloatArray floats) {
(void)self;
if (!longs || !ints || !floats) return JNI_FALSE;
if ((*env)->GetArrayLength(env, longs) < 19 || (*env)->GetArrayLength(env, ints) < 9 || (*env)->GetArrayLength(env, floats) < 3) return JNI_FALSE;
native_sdk_gpu_frame_state_t state;
memset(&state, 0, sizeof(state));
if (!native_sdk_app_gpu_frame_state((void *)app, &state)) return JNI_FALSE;
const jlong long_values[19] = {
(jlong)state.surface_id,
(jlong)state.window_id,
(jlong)state.frame_index,
(jlong)state.timestamp_ns,
(jlong)state.frame_interval_ns,
(jlong)state.input_timestamp_ns,
(jlong)state.input_latency_ns,
(jlong)state.input_latency_budget_ns,
(jlong)state.input_latency_budget_exceeded_count,
(jlong)state.first_frame_latency_ns,
(jlong)state.first_frame_latency_budget_ns,
(jlong)state.first_frame_latency_budget_exceeded_count,
(jlong)state.canvas_revision,
(jlong)state.canvas_command_count,
(jlong)state.canvas_frame_batch_count,
(jlong)state.canvas_frame_budget_exceeded_count,
(jlong)state.widget_revision,
(jlong)state.widget_node_count,
(jlong)state.widget_semantics_count,
};
const jint int_values[9] = {
(jint)state.input_latency_budget_ok,
(jint)state.first_frame_latency_budget_ok,
(jint)state.nonblank,
(jint)state.sample_color,
(jint)state.status,
(jint)state.vsync,
(jint)state.canvas_frame_requires_render,
(jint)state.canvas_frame_full_repaint,
(jint)state.canvas_frame_budget_ok,
};
const jfloat float_values[3] = {
(jfloat)state.width,
(jfloat)state.height,
(jfloat)state.scale,
};
(*env)->SetLongArrayRegion(env, longs, 0, 19, long_values);
(*env)->SetIntArrayRegion(env, ints, 0, 9, int_values);
(*env)->SetFloatArrayRegion(env, floats, 0, 3, float_values);
return JNI_TRUE;
}
JNIEXPORT jint JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsCount(JNIEnv *env, jobject self, jlong app) {
(void)env;
(void)self;
return (jint)native_sdk_app_widget_semantics_count((void *)app);
}
JNIEXPORT jboolean JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsFields(JNIEnv *env, jobject self, jlong app, jint index, jlongArray ids, jintArray ints, jfloatArray floats) {
(void)self;
if (!ids || !ints || !floats) return JNI_FALSE;
if ((*env)->GetArrayLength(env, ids) < 12 || (*env)->GetArrayLength(env, ints) < 5 || (*env)->GetArrayLength(env, floats) < 8) return JNI_FALSE;
native_sdk_widget_semantics_t node;
memset(&node, 0, sizeof(node));
if (!native_sdk_app_widget_semantics_at((void *)app, (uintptr_t)index, &node)) return JNI_FALSE;
const jlong id_values[12] = {
(jlong)node.id,
(jlong)node.parent_id,
(jlong)node.text_selection_start,
(jlong)node.text_selection_end,
(jlong)node.text_composition_start,
(jlong)node.text_composition_end,
(jlong)node.grid_row_index,
(jlong)node.grid_column_index,
(jlong)node.grid_row_count,
(jlong)node.grid_column_count,
(jlong)node.list_item_index,
(jlong)node.list_item_count,
};
const jint int_values[5] = {
(jint)node.role,
(jint)node.flags,
(jint)node.actions,
(jint)node.has_value,
(jint)node.has_scroll,
};
const jfloat float_values[8] = {
(jfloat)node.x,
(jfloat)node.y,
(jfloat)node.width,
(jfloat)node.height,
(jfloat)node.value,
(jfloat)node.scroll_offset,
(jfloat)node.scroll_viewport_extent,
(jfloat)node.scroll_content_extent,
};
(*env)->SetLongArrayRegion(env, ids, 0, 12, id_values);
(*env)->SetIntArrayRegion(env, ints, 0, 5, int_values);
(*env)->SetFloatArrayRegion(env, floats, 0, 8, float_values);
return JNI_TRUE;
}
JNIEXPORT jbyteArray JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsLabel(JNIEnv *env, jobject self, jlong app, jint index) {
(void)self;
native_sdk_widget_semantics_t node;
memset(&node, 0, sizeof(node));
if (!native_sdk_app_widget_semantics_at((void *)app, (uintptr_t)index, &node)) return native_sdk_jni_bytes(env, "", 0);
return native_sdk_jni_bytes(env, node.label, node.label_len);
}
JNIEXPORT jbyteArray JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsText(JNIEnv *env, jobject self, jlong app, jint index) {
(void)self;
native_sdk_widget_semantics_t node;
memset(&node, 0, sizeof(node));
if (!native_sdk_app_widget_semantics_at((void *)app, (uintptr_t)index, &node)) return native_sdk_jni_bytes(env, "", 0);
return native_sdk_jni_bytes(env, node.text, node.text_len);
}
JNIEXPORT jboolean JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsByIdFields(JNIEnv *env, jobject self, jlong app, jlong id, jlongArray ids, jintArray ints, jfloatArray floats) {
(void)self;
if (!ids || !ints || !floats) return JNI_FALSE;
if ((*env)->GetArrayLength(env, ids) < 12 || (*env)->GetArrayLength(env, ints) < 5 || (*env)->GetArrayLength(env, floats) < 8) return JNI_FALSE;
native_sdk_widget_semantics_t node;
memset(&node, 0, sizeof(node));
if (!native_sdk_app_widget_semantics_by_id((void *)app, (uint64_t)id, &node)) return JNI_FALSE;
const jlong id_values[12] = {
(jlong)node.id,
(jlong)node.parent_id,
(jlong)node.text_selection_start,
(jlong)node.text_selection_end,
(jlong)node.text_composition_start,
(jlong)node.text_composition_end,
(jlong)node.grid_row_index,
(jlong)node.grid_column_index,
(jlong)node.grid_row_count,
(jlong)node.grid_column_count,
(jlong)node.list_item_index,
(jlong)node.list_item_count,
};
const jint int_values[5] = {
(jint)node.role,
(jint)node.flags,
(jint)node.actions,
(jint)node.has_value,
(jint)node.has_scroll,
};
const jfloat float_values[8] = {
(jfloat)node.x,
(jfloat)node.y,
(jfloat)node.width,
(jfloat)node.height,
(jfloat)node.value,
(jfloat)node.scroll_offset,
(jfloat)node.scroll_viewport_extent,
(jfloat)node.scroll_content_extent,
};
(*env)->SetLongArrayRegion(env, ids, 0, 12, id_values);
(*env)->SetIntArrayRegion(env, ints, 0, 5, int_values);
(*env)->SetFloatArrayRegion(env, floats, 0, 8, float_values);
return JNI_TRUE;
}
JNIEXPORT jbyteArray JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsByIdLabel(JNIEnv *env, jobject self, jlong app, jlong id) {
(void)self;
native_sdk_widget_semantics_t node;
memset(&node, 0, sizeof(node));
if (!native_sdk_app_widget_semantics_by_id((void *)app, (uint64_t)id, &node)) return native_sdk_jni_bytes(env, "", 0);
return native_sdk_jni_bytes(env, node.label, node.label_len);
}
JNIEXPORT jbyteArray JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetSemanticsByIdText(JNIEnv *env, jobject self, jlong app, jlong id) {
(void)self;
native_sdk_widget_semantics_t node;
memset(&node, 0, sizeof(node));
if (!native_sdk_app_widget_semantics_by_id((void *)app, (uint64_t)id, &node)) return native_sdk_jni_bytes(env, "", 0);
return native_sdk_jni_bytes(env, node.text, node.text_len);
}
JNIEXPORT jboolean JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetTextGeometry(JNIEnv *env, jobject self, jlong app, jlong id, jintArray ints, jfloatArray floats) {
(void)self;
if (!ints || !floats) return JNI_FALSE;
if ((*env)->GetArrayLength(env, ints) < 5 || (*env)->GetArrayLength(env, floats) < 12) return JNI_FALSE;
native_sdk_widget_text_geometry_t geometry;
memset(&geometry, 0, sizeof(geometry));
if (!native_sdk_app_widget_text_geometry((void *)app, (uint64_t)id, &geometry)) return JNI_FALSE;
const jint int_values[5] = {
(jint)geometry.has_caret_bounds,
(jint)geometry.has_selection_bounds,
(jint)geometry.selection_rect_count,
(jint)geometry.has_composition_bounds,
(jint)geometry.composition_rect_count,
};
const jfloat float_values[12] = {
(jfloat)geometry.caret_x,
(jfloat)geometry.caret_y,
(jfloat)geometry.caret_width,
(jfloat)geometry.caret_height,
(jfloat)geometry.selection_x,
(jfloat)geometry.selection_y,
(jfloat)geometry.selection_width,
(jfloat)geometry.selection_height,
(jfloat)geometry.composition_x,
(jfloat)geometry.composition_y,
(jfloat)geometry.composition_width,
(jfloat)geometry.composition_height,
};
(*env)->SetIntArrayRegion(env, ints, 0, 5, int_values);
(*env)->SetFloatArrayRegion(env, floats, 0, 12, float_values);
return JNI_TRUE;
}
JNIEXPORT jboolean JNICALL Java_dev_native_1sdk_examples_android_MainActivity_nativeWidgetAction(JNIEnv *env, jobject self, jlong app, jlong id, jint action, jstring text, jlong selection_anchor, jlong selection_focus, jboolean has_selection) {
(void)self;
const char *text_chars = text ? (*env)->GetStringUTFChars(env, text, NULL) : NULL;
native_sdk_widget_action_t request;
memset(&request, 0, sizeof(request));
request.id = (uint64_t)id;
request.action = (int)action;
request.text = text_chars;
request.text_len = text_chars ? strlen(text_chars) : 0;
request.selection_anchor = (uintptr_t)selection_anchor;
request.selection_focus = (uintptr_t)selection_focus;
request.has_selection = has_selection ? 1 : 0;
const int ok = native_sdk_app_widget_action((void *)app, &request);
if (text_chars) (*env)->ReleaseStringUTFChars(env, text, text_chars);
return ok ? JNI_TRUE : JNI_FALSE;
}
@@ -0,0 +1,840 @@
package dev.native_sdk.examples.android
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.webkit.WebView
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
class MainActivity : Activity(), SurfaceHolder.Callback {
private var nativeApp: Long = 0
private lateinit var statusLabel: TextView
private lateinit var widgetSurface: WidgetSurfaceView
private var currentSurfaceHolder: SurfaceHolder? = null
private var lastTouchX: Float = 0f
private var lastTouchY: Float = 0f
private var lastTouchActive: Boolean = false
data class WidgetSemantics(
val id: Long,
val parentId: Long,
val role: Int,
val flags: Int,
val actions: Int,
val x: Float,
val y: Float,
val width: Float,
val height: Float,
val value: Float?,
val label: String,
val text: String,
val textSelectionStart: Long,
val textSelectionEnd: Long,
val textCompositionStart: Long,
val textCompositionEnd: Long,
val gridRowIndex: Long,
val gridColumnIndex: Long,
val gridRowCount: Long,
val gridColumnCount: Long,
val listItemIndex: Long,
val listItemCount: Long,
val scrollOffset: Float,
val scrollViewportExtent: Float,
val scrollContentExtent: Float,
val hasScroll: Boolean,
)
data class WidgetTextGeometry(
val id: Long,
val hasCaretBounds: Boolean,
val caretX: Float,
val caretY: Float,
val caretWidth: Float,
val caretHeight: Float,
val hasSelectionBounds: Boolean,
val selectionX: Float,
val selectionY: Float,
val selectionWidth: Float,
val selectionHeight: Float,
val selectionRectCount: Int,
val hasCompositionBounds: Boolean,
val compositionX: Float,
val compositionY: Float,
val compositionWidth: Float,
val compositionHeight: Float,
val compositionRectCount: Int,
)
private inner class WidgetSurfaceView : SurfaceView(this@MainActivity) {
private val provider = WidgetAccessibilityProvider(this)
init {
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
isFocusable = true
}
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider = provider
override fun onCheckIsTextEditor(): Boolean = true
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
outAttrs.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE
return object : BaseInputConnection(this, true) {
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
return dispatchCommittedWidgetText(text?.toString().orEmpty())
}
override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
return dispatchWidgetIme(WIDGET_IME_SET_COMPOSITION, text?.toString().orEmpty(), newCursorPosition.toLong())
}
override fun finishComposingText(): Boolean {
return dispatchWidgetIme(WIDGET_IME_COMMIT_COMPOSITION, "", 0)
}
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
if (beforeLength > 0 && afterLength == 0) return dispatchWidgetKey("backspace")
if (afterLength > 0 && beforeLength == 0) return dispatchWidgetKey("delete")
return super.deleteSurroundingText(beforeLength, afterLength)
}
override fun sendKeyEvent(event: KeyEvent): Boolean {
if (event.action != KeyEvent.ACTION_DOWN) return super.sendKeyEvent(event)
return when (event.keyCode) {
KeyEvent.KEYCODE_DEL -> dispatchWidgetKey("backspace")
KeyEvent.KEYCODE_FORWARD_DEL -> dispatchWidgetKey("delete")
else -> super.sendKeyEvent(event)
}
}
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return handleWidgetTouch(event)
}
fun notifyWidgetSemanticsChanged() {
invalidate()
provider.notifyWidgetSemanticsChanged()
}
}
private inner class WidgetAccessibilityProvider(private val host: View) : AccessibilityNodeProvider() {
private var accessibilityFocusedId: Long = 0
override fun createAccessibilityNodeInfo(virtualViewId: Int): AccessibilityNodeInfo? {
val nodes = widgetSemanticsSnapshot()
return if (virtualViewId == View.NO_ID) {
createHostNode(nodes)
} else {
(widgetSemanticsById(virtualViewId.toLong()) ?: nodes.firstOrNull { it.id.toInt() == virtualViewId })?.let { createWidgetNode(it, nodes) }
}
}
override fun performAction(virtualViewId: Int, action: Int, arguments: Bundle?): Boolean {
if (virtualViewId == View.NO_ID) return false
val node = widgetSemanticsById(virtualViewId.toLong()) ?: return false
val id = node.id
val handled = when (action) {
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS -> {
accessibilityFocusedId = id
host.invalidate()
sendVirtualEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
true
}
AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS -> {
if (accessibilityFocusedId == id) accessibilityFocusedId = 0
host.invalidate()
sendVirtualEvent(id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)
true
}
AccessibilityNodeInfo.ACTION_FOCUS -> {
if (widgetSupportsAction(node, WIDGET_ACTION_FOCUS)) dispatchWidgetAction(id, WIDGET_ACTION_KIND_FOCUS) else false
}
AccessibilityNodeInfo.ACTION_CLICK -> performWidgetClick(id)
AccessibilityNodeInfo.ACTION_SELECT -> {
if (widgetSupportsAction(node, WIDGET_ACTION_SELECT)) dispatchWidgetAction(id, WIDGET_ACTION_KIND_SELECT) else false
}
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> {
if (widgetSupportsAction(node, WIDGET_ACTION_INCREMENT)) dispatchWidgetAction(id, WIDGET_ACTION_KIND_INCREMENT) else false
}
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> {
if (widgetSupportsAction(node, WIDGET_ACTION_DECREMENT)) dispatchWidgetAction(id, WIDGET_ACTION_KIND_DECREMENT) else false
}
AccessibilityNodeInfo.ACTION_DISMISS -> {
if (widgetSupportsAction(node, WIDGET_ACTION_DISMISS)) dispatchWidgetAction(id, WIDGET_ACTION_KIND_DISMISS) else false
}
AccessibilityNodeInfo.ACTION_SET_TEXT -> {
val text = arguments?.getCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE)?.toString()
if (text != null && widgetSupportsAction(node, WIDGET_ACTION_SET_TEXT)) dispatchWidgetAction(id, WIDGET_ACTION_KIND_SET_TEXT, text) else false
}
AccessibilityNodeInfo.ACTION_SET_SELECTION -> {
val start = arguments?.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) ?: -1
val end = arguments?.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) ?: -1
if (start >= 0 && end >= 0 && widgetSupportsAction(node, WIDGET_ACTION_SET_SELECTION)) {
dispatchWidgetAction(id, WIDGET_ACTION_KIND_SET_SELECTION, selectionAnchor = start.toLong(), selectionFocus = end.toLong(), hasSelection = true)
} else {
false
}
}
else -> false
}
if (handled) host.invalidate()
if (handled) updateSoftKeyboardForFocusedWidget()
return handled
}
fun notifyWidgetSemanticsChanged() {
val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
event.setSource(host)
event.packageName = packageName
event.contentChangeTypes = AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
host.parent?.requestSendAccessibilityEvent(host, event)
}
private fun createHostNode(nodes: List<WidgetSemantics>): AccessibilityNodeInfo {
val info = AccessibilityNodeInfo.obtain(host)
host.onInitializeAccessibilityNodeInfo(info)
info.className = SurfaceView::class.java.name
for (node in nodes.filter { it.parentId == 0L }) {
info.addChild(host, node.id.toInt())
}
return info
}
private fun createWidgetNode(node: WidgetSemantics, nodes: List<WidgetSemantics>): AccessibilityNodeInfo {
val info = AccessibilityNodeInfo.obtain()
val virtualId = node.id.toInt()
val parentNode = nodes.firstOrNull { it.id == node.parentId }
info.setSource(host, virtualId)
if (parentNode != null) {
info.setParent(host, parentNode.id.toInt())
} else {
info.setParent(host)
}
for (child in nodes.filter { it.parentId == node.id }) {
info.addChild(host, child.id.toInt())
}
info.packageName = packageName
info.className = widgetAccessibilityClassName(node)
info.contentDescription = node.label.ifEmpty { node.text }
if (node.text.isNotEmpty()) info.text = node.text
info.isVisibleToUser = host.isShown
info.isEnabled = (node.flags and WIDGET_FLAG_DISABLED) == 0
info.isFocusable = (node.flags and WIDGET_FLAG_FOCUSABLE) != 0
info.isFocused = (node.flags and WIDGET_FLAG_FOCUSED) != 0
info.isAccessibilityFocused = accessibilityFocusedId == node.id
info.isSelected = (node.flags and WIDGET_FLAG_SELECTED) != 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
info.stateDescription = widgetStateDescription(node)
}
info.isCheckable = node.role == WIDGET_ROLE_CHECKBOX || node.role == WIDGET_ROLE_SWITCH
info.isChecked = info.isCheckable && widgetValueSelected(node)
info.isClickable = widgetSupportsAnyAction(node, WIDGET_ACTION_PRESS or WIDGET_ACTION_TOGGLE or WIDGET_ACTION_SELECT)
info.isEditable = node.role == WIDGET_ROLE_TEXTBOX && (node.flags and WIDGET_FLAG_READ_ONLY) == 0
info.isScrollable = node.hasScroll
if (node.value != null) {
info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT, 0f, 1f, node.value))
}
setCollectionInfo(info, node, nodes)
setCollectionItemInfo(info, node)
if (node.textSelectionStart >= 0 && node.textSelectionEnd >= 0) {
info.setTextSelection(node.textSelectionStart.toInt(), node.textSelectionEnd.toInt())
}
if (accessibilityFocusedId == node.id) {
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS)
} else {
info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}
if (info.isFocusable) info.addAction(AccessibilityNodeInfo.ACTION_FOCUS)
if (info.isClickable) info.addAction(AccessibilityNodeInfo.ACTION_CLICK)
if (widgetSupportsAction(node, WIDGET_ACTION_SELECT)) info.addAction(AccessibilityNodeInfo.ACTION_SELECT)
if (widgetSupportsAction(node, WIDGET_ACTION_INCREMENT)) info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
if (widgetSupportsAction(node, WIDGET_ACTION_DECREMENT)) info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
if (widgetSupportsAction(node, WIDGET_ACTION_DISMISS)) info.addAction(AccessibilityNodeInfo.ACTION_DISMISS)
if (widgetSupportsAction(node, WIDGET_ACTION_SET_TEXT)) info.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT)
if (widgetSupportsAction(node, WIDGET_ACTION_SET_SELECTION)) info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION)
info.setBoundsInParent(boundsInParent(node, parentNode))
val location = IntArray(2)
host.getLocationOnScreen(location)
val screenBounds = Rect(node.x.toInt(), node.y.toInt(), (node.x + node.width).toInt(), (node.y + node.height).toInt())
info.setBoundsInScreen(Rect(screenBounds.left + location[0], screenBounds.top + location[1], screenBounds.right + location[0], screenBounds.bottom + location[1]))
return info
}
private fun performWidgetClick(id: Long): Boolean {
val node = widgetSemanticsById(id) ?: return false
return when {
widgetSupportsAction(node, WIDGET_ACTION_TOGGLE) -> dispatchWidgetAction(id, WIDGET_ACTION_KIND_TOGGLE)
widgetSupportsAction(node, WIDGET_ACTION_PRESS) -> dispatchWidgetAction(id, WIDGET_ACTION_KIND_PRESS)
widgetSupportsAction(node, WIDGET_ACTION_SELECT) -> dispatchWidgetAction(id, WIDGET_ACTION_KIND_SELECT)
else -> false
}
}
private fun setCollectionInfo(info: AccessibilityNodeInfo, node: WidgetSemantics, nodes: List<WidgetSemantics>) {
if (node.role == WIDGET_ROLE_GRID && node.gridRowCount >= 0 && node.gridColumnCount >= 0) {
info.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(node.gridRowCount.toInt(), node.gridColumnCount.toInt(), false))
} else if (node.role == WIDGET_ROLE_LIST) {
val childCount = nodes.count { it.parentId == node.id && it.role == WIDGET_ROLE_LISTITEM }
val itemCount = if (node.listItemCount >= 0) node.listItemCount.toInt() else childCount
if (itemCount > 0) info.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(itemCount, 1, false))
}
}
private fun setCollectionItemInfo(info: AccessibilityNodeInfo, node: WidgetSemantics) {
if (node.gridRowIndex >= 0 && node.gridColumnIndex >= 0) {
info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(node.gridRowIndex.toInt(), 1, node.gridColumnIndex.toInt(), 1, false, info.isSelected))
} else if (node.listItemIndex >= 0) {
info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(node.listItemIndex.toInt(), 1, 0, 1, false, info.isSelected))
}
}
private fun boundsInParent(node: WidgetSemantics, parent: WidgetSemantics?): Rect {
val parentX = parent?.x ?: 0f
val parentY = parent?.y ?: 0f
return Rect(
(node.x - parentX).toInt(),
(node.y - parentY).toInt(),
(node.x - parentX + node.width).toInt(),
(node.y - parentY + node.height).toInt(),
)
}
private fun widgetValueSelected(node: WidgetSemantics): Boolean {
return node.value != null && node.value >= 0.5f
}
private fun widgetStateDescription(node: WidgetSemantics): String? {
val states = ArrayList<String>()
if ((node.flags and WIDGET_FLAG_EXPANDED) != 0) states.add("Expanded")
if ((node.flags and WIDGET_FLAG_COLLAPSED) != 0) states.add("Collapsed")
if ((node.flags and WIDGET_FLAG_REQUIRED) != 0) states.add("Required")
if ((node.flags and WIDGET_FLAG_READ_ONLY) != 0) states.add("Read only")
if ((node.flags and WIDGET_FLAG_INVALID) != 0) states.add("Invalid")
return if (states.isEmpty()) null else states.joinToString(", ")
}
private fun sendVirtualEvent(id: Long, type: Int) {
val event = AccessibilityEvent.obtain(type)
event.setSource(host, id.toInt())
event.packageName = packageName
host.parent?.requestSendAccessibilityEvent(host, event)
}
private fun widgetAccessibilityClassName(node: WidgetSemantics): String {
return when (node.role) {
WIDGET_ROLE_BUTTON, WIDGET_ROLE_MENUITEM -> "android.widget.Button"
WIDGET_ROLE_TEXTBOX -> "android.widget.EditText"
WIDGET_ROLE_CHECKBOX -> "android.widget.CheckBox"
WIDGET_ROLE_SWITCH -> "android.widget.Switch"
WIDGET_ROLE_SLIDER -> "android.widget.SeekBar"
WIDGET_ROLE_PROGRESSBAR -> "android.widget.ProgressBar"
WIDGET_ROLE_IMAGE -> "android.widget.ImageView"
WIDGET_ROLE_LIST -> "android.widget.ListView"
else -> "android.view.View"
}
}
private fun widgetSupportsAction(node: WidgetSemantics, action: Int): Boolean {
return (node.actions and action) != 0
}
private fun widgetSupportsAnyAction(node: WidgetSemantics, actions: Int): Boolean {
return (node.actions and actions) != 0
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
System.loadLibrary("native_sdk_example")
widgetSurface = WidgetSurfaceView()
widgetSurface.holder.addCallback(this)
val header = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(Color.rgb(245, 246, 248))
setPadding(32, 28, 32, 24)
}
val title = TextView(this).apply {
text = "Mobile Shell"
textSize = 24f
setTextColor(Color.rgb(24, 24, 27))
}
val subtitle = TextView(this).apply {
text = "Native header with WebView workspace"
textSize = 14f
setTextColor(Color.rgb(95, 102, 114))
setPadding(0, 6, 0, 0)
}
statusLabel = TextView(this).apply {
text = "Native commands ready"
textSize = 13f
setTextColor(Color.rgb(95, 102, 114))
setPadding(0, 12, 0, 0)
}
val actions = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 12, 0, 0)
}
val back = Button(this).apply {
text = "Back"
setOnClickListener {
dispatchNativeCommand("mobile.back")
}
}
val refresh = Button(this).apply {
text = "Refresh"
setOnClickListener {
dispatchNativeCommand("mobile.refresh")
}
}
actions.addView(back, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
actions.addView(refresh, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
header.addView(title)
header.addView(subtitle)
header.addView(statusLabel)
header.addView(actions)
val webView = WebView(this).apply {
settings.javaScriptEnabled = false
loadDataWithBaseURL(null, html, "text/html", "UTF-8", null)
}
val content = FrameLayout(this)
content.addView(widgetSurface, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
))
content.addView(webView, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
))
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(Color.WHITE)
}
root.addView(header, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
))
root.addView(content, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1f,
))
setContentView(root)
nativeApp = nativeCreate()
nativeStart(nativeApp)
refreshWidgetSemanticsStatus()
}
private fun dispatchNativeCommand(command: String) {
if (nativeApp == 0L) return
val count = nativeCommand(nativeApp, command)
if (::statusLabel.isInitialized) {
statusLabel.text = "Command $count: $command"
}
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
}
override fun onResume() {
super.onResume()
if (nativeApp != 0L) {
nativeActivate(nativeApp)
}
}
override fun onPause() {
if (nativeApp != 0L) {
nativeDeactivate(nativeApp)
}
super.onPause()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
currentSurfaceHolder = holder
sendViewport(width, height, holder.surface)
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
}
override fun surfaceCreated(holder: SurfaceHolder) = Unit
override fun surfaceDestroyed(holder: SurfaceHolder) {
if (currentSurfaceHolder == holder) currentSurfaceHolder = null
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (nativeApp != 0L) {
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
}
}
private fun widgetSemanticsSnapshot(): List<WidgetSemantics> {
if (nativeApp == 0L) return emptyList()
val count = nativeWidgetSemanticsCount(nativeApp)
val items = mutableListOf<WidgetSemantics>()
for (index in 0 until count) {
widgetSemanticsAt(index)?.let { items.add(it) }
}
return items
}
private fun widgetSemanticsAt(index: Int): WidgetSemantics? {
val ids = LongArray(12)
val ints = IntArray(5)
val floats = FloatArray(8)
if (!nativeWidgetSemanticsFields(nativeApp, index, ids, ints, floats)) return null
return widgetSemanticsFromNative(
ids,
ints,
floats,
String(nativeWidgetSemanticsLabel(nativeApp, index), Charsets.UTF_8),
String(nativeWidgetSemanticsText(nativeApp, index), Charsets.UTF_8),
)
}
private fun widgetSemanticsById(id: Long): WidgetSemantics? {
val ids = LongArray(12)
val ints = IntArray(5)
val floats = FloatArray(8)
if (!nativeWidgetSemanticsByIdFields(nativeApp, id, ids, ints, floats)) return null
return widgetSemanticsFromNative(
ids,
ints,
floats,
String(nativeWidgetSemanticsByIdLabel(nativeApp, id), Charsets.UTF_8),
String(nativeWidgetSemanticsByIdText(nativeApp, id), Charsets.UTF_8),
)
}
private fun widgetSemanticsFromNative(ids: LongArray, ints: IntArray, floats: FloatArray, label: String, text: String): WidgetSemantics {
return WidgetSemantics(
id = ids[0],
parentId = ids[1],
role = ints[0],
flags = ints[1],
actions = ints[2],
x = floats[0],
y = floats[1],
width = floats[2],
height = floats[3],
value = if (ints[3] != 0) floats[4] else null,
label = label,
text = text,
textSelectionStart = ids[2],
textSelectionEnd = ids[3],
textCompositionStart = ids[4],
textCompositionEnd = ids[5],
gridRowIndex = ids[6],
gridColumnIndex = ids[7],
gridRowCount = ids[8],
gridColumnCount = ids[9],
listItemIndex = ids[10],
listItemCount = ids[11],
scrollOffset = floats[5],
scrollViewportExtent = floats[6],
scrollContentExtent = floats[7],
hasScroll = ints[4] != 0,
)
}
private fun widgetTextGeometry(id: Long): WidgetTextGeometry? {
val ints = IntArray(5)
val floats = FloatArray(12)
if (!nativeWidgetTextGeometry(nativeApp, id, ints, floats)) return null
return WidgetTextGeometry(
id = id,
hasCaretBounds = ints[0] != 0,
caretX = floats[0],
caretY = floats[1],
caretWidth = floats[2],
caretHeight = floats[3],
hasSelectionBounds = ints[1] != 0,
selectionX = floats[4],
selectionY = floats[5],
selectionWidth = floats[6],
selectionHeight = floats[7],
selectionRectCount = ints[2],
hasCompositionBounds = ints[3] != 0,
compositionX = floats[8],
compositionY = floats[9],
compositionWidth = floats[10],
compositionHeight = floats[11],
compositionRectCount = ints[4],
)
}
private fun dispatchWidgetAction(
id: Long,
action: Int,
text: String? = null,
selectionAnchor: Long = 0,
selectionFocus: Long = 0,
hasSelection: Boolean = false,
): Boolean {
if (nativeApp == 0L) return false
val ok = nativeWidgetAction(nativeApp, id, action, text, selectionAnchor, selectionFocus, hasSelection)
if (ok) {
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
}
return ok
}
private fun refreshWidgetSemanticsStatus() {
if (nativeApp == 0L || !::statusLabel.isInitialized) return
statusLabel.contentDescription = "Accessible items: ${widgetSemanticsSnapshot().size}"
if (::widgetSurface.isInitialized) widgetSurface.notifyWidgetSemanticsChanged()
}
private fun sendViewport(width: Int, height: Int, surface: Any) {
if (nativeApp == 0L) return
val density = resources.displayMetrics.density
val insets = window.decorView.rootWindowInsets
val safeTop = ((insets?.systemWindowInsetTop ?: 0).toFloat()) / density
val safeRight = ((insets?.systemWindowInsetRight ?: 0).toFloat()) / density
val safeBottom = ((insets?.systemWindowInsetBottom ?: 0).toFloat()) / density
val safeLeft = ((insets?.systemWindowInsetLeft ?: 0).toFloat()) / density
nativeViewport(
nativeApp,
width.toFloat(),
height.toFloat(),
density,
surface,
safeTop,
safeRight,
safeBottom,
safeLeft,
0f,
0f,
keyboardBottomInset(density),
0f,
)
}
private fun keyboardBottomInset(density: Float): Float {
val visibleFrame = Rect()
window.decorView.getWindowVisibleDisplayFrame(visibleFrame)
val hiddenBottom = (window.decorView.rootView.height - visibleFrame.bottom).coerceAtLeast(0)
return if (hiddenBottom > (100 * density).toInt()) hiddenBottom.toFloat() / density else 0f
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return handleWidgetTouch(event)
}
private fun handleWidgetTouch(event: MotionEvent): Boolean {
if (nativeApp == 0L || event.pointerCount == 0) return false
val pointerIndex = event.actionIndex.coerceIn(0, event.pointerCount - 1)
val pointerId = event.getPointerId(pointerIndex).toLong()
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = x
lastTouchY = y
lastTouchActive = true
}
MotionEvent.ACTION_MOVE -> {
if (lastTouchActive) nativeScroll(nativeApp, pointerId, x, y, lastTouchX - x, lastTouchY - y)
lastTouchX = x
lastTouchY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
lastTouchActive = false
}
}
nativeTouch(
nativeApp,
pointerId,
event.actionMasked,
x,
y,
event.getPressure(pointerIndex),
)
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
updateSoftKeyboardForFocusedWidget()
return true
}
private fun dispatchCommittedWidgetText(text: String): Boolean {
if (nativeApp == 0L) return false
if (text.isEmpty()) return true
nativeText(nativeApp, text)
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
return true
}
private fun dispatchWidgetIme(kind: Int, text: String, cursor: Long): Boolean {
if (nativeApp == 0L) return false
nativeIme(nativeApp, kind, text, cursor)
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
return true
}
private fun dispatchWidgetKey(key: String): Boolean {
if (nativeApp == 0L) return false
nativeKey(nativeApp, 0, key, "", 0)
nativeKey(nativeApp, 1, key, "", 0)
nativeFrame(nativeApp)
refreshWidgetSemanticsStatus()
return true
}
private fun focusedTextWidget(): WidgetSemantics? {
return widgetSemanticsSnapshot().firstOrNull {
it.role == WIDGET_ROLE_TEXTBOX && (it.flags and WIDGET_FLAG_FOCUSED) != 0
}
}
private fun updateSoftKeyboardForFocusedWidget() {
if (!::widgetSurface.isInitialized) return
val input = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (focusedTextWidget() != null) {
widgetSurface.requestFocus()
input.showSoftInput(widgetSurface, InputMethodManager.SHOW_IMPLICIT)
} else {
input.hideSoftInputFromWindow(widgetSurface.windowToken, 0)
}
}
override fun onBackPressed() {
if (nativeApp != 0L) {
dispatchNativeCommand("mobile.back")
return
}
super.onBackPressed()
}
override fun onDestroy() {
if (nativeApp != 0L) {
nativeStop(nativeApp)
nativeDestroy(nativeApp)
nativeApp = 0
}
super.onDestroy()
}
external fun nativeCreate(): Long
external fun nativeDestroy(app: Long)
external fun nativeStart(app: Long)
external fun nativeActivate(app: Long)
external fun nativeDeactivate(app: Long)
external fun nativeStop(app: Long)
external fun nativeResize(app: Long, width: Float, height: Float, scale: Float, surface: Any)
external fun nativeViewport(app: Long, width: Float, height: Float, scale: Float, surface: Any, safeTop: Float, safeRight: Float, safeBottom: Float, safeLeft: Float, keyboardTop: Float, keyboardRight: Float, keyboardBottom: Float, keyboardLeft: Float)
external fun nativeTouch(app: Long, id: Long, phase: Int, x: Float, y: Float, pressure: Float)
external fun nativeScroll(app: Long, id: Long, x: Float, y: Float, deltaX: Float, deltaY: Float)
external fun nativeKey(app: Long, phase: Int, key: String, text: String, modifiers: Int)
external fun nativeText(app: Long, text: String)
external fun nativeIme(app: Long, kind: Int, text: String, cursor: Long)
external fun nativeCommand(app: Long, command: String): Int
external fun nativeFrame(app: Long)
external fun nativeGpuFrameState(app: Long, longs: LongArray, ints: IntArray, floats: FloatArray): Boolean
external fun nativeWidgetSemanticsCount(app: Long): Int
external fun nativeWidgetSemanticsFields(app: Long, index: Int, ids: LongArray, ints: IntArray, floats: FloatArray): Boolean
external fun nativeWidgetSemanticsLabel(app: Long, index: Int): ByteArray
external fun nativeWidgetSemanticsText(app: Long, index: Int): ByteArray
external fun nativeWidgetSemanticsByIdFields(app: Long, id: Long, ids: LongArray, ints: IntArray, floats: FloatArray): Boolean
external fun nativeWidgetSemanticsByIdLabel(app: Long, id: Long): ByteArray
external fun nativeWidgetSemanticsByIdText(app: Long, id: Long): ByteArray
external fun nativeWidgetTextGeometry(app: Long, id: Long, ints: IntArray, floats: FloatArray): Boolean
external fun nativeWidgetAction(app: Long, id: Long, action: Int, text: String?, selectionAnchor: Long, selectionFocus: Long, hasSelection: Boolean): Boolean
companion object {
private const val WIDGET_ROLE_BUTTON = 4
private const val WIDGET_ROLE_TEXTBOX = 5
private const val WIDGET_ROLE_MENUITEM = 9
private const val WIDGET_ROLE_LIST = 10
private const val WIDGET_ROLE_LISTITEM = 11
private const val WIDGET_ROLE_GRID = 13
private const val WIDGET_ROLE_IMAGE = 3
private const val WIDGET_ROLE_CHECKBOX = 16
private const val WIDGET_ROLE_SWITCH = 17
private const val WIDGET_ROLE_SLIDER = 18
private const val WIDGET_ROLE_PROGRESSBAR = 19
private const val WIDGET_FLAG_FOCUSED = 1 shl 0
private const val WIDGET_FLAG_SELECTED = 1 shl 3
private const val WIDGET_FLAG_DISABLED = 1 shl 4
private const val WIDGET_FLAG_FOCUSABLE = 1 shl 5
private const val WIDGET_FLAG_EXPANDED = 1 shl 6
private const val WIDGET_FLAG_COLLAPSED = 1 shl 7
private const val WIDGET_FLAG_REQUIRED = 1 shl 8
private const val WIDGET_FLAG_READ_ONLY = 1 shl 9
private const val WIDGET_FLAG_INVALID = 1 shl 10
private const val WIDGET_ACTION_FOCUS = 1 shl 0
private const val WIDGET_ACTION_PRESS = 1 shl 1
private const val WIDGET_ACTION_TOGGLE = 1 shl 2
private const val WIDGET_ACTION_INCREMENT = 1 shl 3
private const val WIDGET_ACTION_DECREMENT = 1 shl 4
private const val WIDGET_ACTION_SET_TEXT = 1 shl 5
private const val WIDGET_ACTION_SET_SELECTION = 1 shl 6
private const val WIDGET_ACTION_SELECT = 1 shl 7
private const val WIDGET_ACTION_DRAG = 1 shl 8
private const val WIDGET_ACTION_DROP_FILES = 1 shl 9
private const val WIDGET_ACTION_DISMISS = 1 shl 10
private const val WIDGET_ACTION_KIND_FOCUS = 0
private const val WIDGET_ACTION_KIND_PRESS = 1
private const val WIDGET_ACTION_KIND_TOGGLE = 2
private const val WIDGET_ACTION_KIND_INCREMENT = 3
private const val WIDGET_ACTION_KIND_DECREMENT = 4
private const val WIDGET_ACTION_KIND_SET_TEXT = 5
private const val WIDGET_ACTION_KIND_SET_SELECTION = 6
private const val WIDGET_ACTION_KIND_SET_COMPOSITION = 7
private const val WIDGET_ACTION_KIND_COMMIT_COMPOSITION = 8
private const val WIDGET_ACTION_KIND_CANCEL_COMPOSITION = 9
private const val WIDGET_ACTION_KIND_SELECT = 10
private const val WIDGET_ACTION_KIND_DRAG = 11
private const val WIDGET_ACTION_KIND_DROP_FILES = 12
private const val WIDGET_ACTION_KIND_DISMISS = 13
private const val WIDGET_IME_SET_COMPOSITION = 0
private const val WIDGET_IME_COMMIT_COMPOSITION = 1
private const val WIDGET_IME_CANCEL_COMPOSITION = 2
private const val html = """
<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<body style="margin:0;font-family:system-ui,sans-serif;background:#f7f8fa;color:#18181b">
<main style="padding:28px 22px;display:grid;gap:16px">
<h1 style="margin:0;font-size:30px">Workspace</h1>
<p style="margin:0;color:#5f6672;line-height:1.5">This content is rendered by Android WebView while the header remains native Android UI.</p>
<section style="display:grid;gap:10px">
<div style="padding:14px;border:1px solid #e1e5ea;border-radius:8px;background:white">Inbox review</div>
<div style="padding:14px;border:1px solid #e1e5ea;border-radius:8px;background:white">Sync queue</div>
<div style="padding:14px;border:1px solid #e1e5ea;border-radius:8px;background:white">Offline cache</div>
</section>
</main>
</body>
"""
}
}
@@ -0,0 +1,6 @@
<resources>
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:colorAccent">#2563EB</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
plugins {
id "com.android.application" version "8.5.0" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
}
+18
View File
@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "NativeSdkAndroidExample"
include ":app"