chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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.
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user