Packages
mob
0.5.4
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
android/jni/mob_nif.c
// mob_nif.c — Mob UI NIF for Android (Jetpack Compose backend).
//
// NIF functions:
// platform/0 — returns :android
// log/1, log/2 — Android logcat
// set_root/1 — pass JSON node tree to Compose
// register_tap/1 — register ErlNifPid, get integer handle back
// clear_taps/0 — clear tap registry before each render
#include <jni.h>
#include <android/log.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "erl_nif.h"
#include "mob_beam.h"
#define LOG_TAG "MobNIF"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// ── Cached JNI method IDs ────────────────────────────────────────────────────
static struct {
jclass cls;
jmethodID set_root;
jmethodID move_to_back;
jmethodID get_safe_area;
jmethodID haptic;
jmethodID clipboard_put;
jmethodID clipboard_get;
jmethodID share_text;
jmethodID request_permission;
jmethodID biometric_authenticate;
jmethodID location_get_once;
jmethodID location_start;
jmethodID location_stop;
jmethodID camera_capture_photo;
jmethodID camera_capture_video;
jmethodID camera_start_preview;
jmethodID camera_stop_preview;
jmethodID alert_show;
jmethodID action_sheet_show;
jmethodID toast_show;
jmethodID webview_eval_js;
jmethodID webview_post_message;
jmethodID webview_can_go_back;
jmethodID webview_go_back;
jmethodID photos_pick;
jmethodID files_pick;
jmethodID audio_start_recording;
jmethodID audio_stop_recording;
jmethodID audio_play;
jmethodID audio_stop_playback;
jmethodID audio_set_volume;
jmethodID motion_start;
jmethodID motion_stop;
jmethodID scanner_scan;
jmethodID notify_schedule;
jmethodID notify_cancel;
jmethodID notify_register_push;
jmethodID take_launch_notification;
jmethodID storage_dir;
jmethodID storage_save_to_media_store;
jmethodID storage_external_files_dir;
jmethodID background_keep_alive;
jmethodID background_stop;
// Cached before nif_load (used during BEAM startup before NIFs are loaded)
jmethodID set_startup_phase;
jmethodID set_startup_error;
// ── Test harness ──────────────────────────────────────────────────────────
jmethodID ui_tree;
jmethodID tap_xy;
jmethodID tap_by_label;
jmethodID type_text;
jmethodID delete_backward;
jmethodID clear_text;
jmethodID long_press_xy;
jmethodID swipe_xy;
} Bridge;
// ── Tap handle registry ───────────────────────────────────────────────────────
// Cleared before every render. Max 256 tappable elements per frame.
//
// Each handle stores a pid and an optional tag term (copied into a persistent
// NIF env). When tapped, sends {:tap, tag} to pid.
// Backwards compat: register_tap(pid) stores tag = :ok.
#define MAX_TAP_HANDLES 256
typedef struct {
ErlNifPid pid;
ErlNifEnv* tag_env; // persistent env owning tag; NULL when not in use
ERL_NIF_TERM tag; // the term sent as the second element of {:tap, tag}
// ── Batch 5 throttle state — populated by mob_set_throttle_config ──
int throttle_ms;
int debounce_ms;
double delta_threshold;
int leading;
int trailing;
long long last_emit_ns; // CLOCK_MONOTONIC ns
double last_x;
double last_y;
unsigned long long seq;
} TapHandle;
static TapHandle tap_handles[MAX_TAP_HANDLES];
static int tap_handle_next = 0;
static ErlNifMutex* tap_mutex = NULL;
static char g_transition[16] = "none"; // set by set_transition/1, read+reset by set_root/1
// ── Component handle registry ─────────────────────────────────────────────────
// Persistent (not cleared between renders). Each slot maps an integer handle to
// a component process pid. register_component/1 allocates; deregister_component/1 frees.
#define MAX_COMPONENT_HANDLES 64
typedef struct {
ErlNifPid pid;
int active;
} ComponentHandle;
static ComponentHandle component_handles[MAX_COMPONENT_HANDLES];
static ErlNifMutex* component_mutex = NULL;
void mob_send_component_event(int handle, const char* event, const char* payload_json) {
if (handle < 0 || handle >= MAX_COMPONENT_HANDLES) return;
enif_mutex_lock(component_mutex);
if (!component_handles[handle].active) {
enif_mutex_unlock(component_mutex);
return;
}
ErlNifPid pid = component_handles[handle].pid;
enif_mutex_unlock(component_mutex);
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "component_event"),
enif_make_string(env, event, ERL_NIF_LATIN1),
enif_make_string(env, payload_json, ERL_NIF_LATIN1));
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
// Called from the app's Java_..._MobBridge_nativeSendTap JNI stub
// (declared in mob_beam.h, defined here).
void mob_send_tap(int handle) {
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ErlNifEnv* tag_env = tap_handles[handle].tag_env;
ERL_NIF_TERM tag = tap_handles[handle].tag;
enif_mutex_unlock(tap_mutex);
ErlNifEnv* msg_env = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple2(msg_env,
enif_make_atom(msg_env, "tap"),
enif_make_copy(msg_env, tag));
enif_send(NULL, &pid, msg_env, msg);
enif_free_env(msg_env);
(void)tag_env; // owned by tap_handles; freed in clear_taps
}
// ── Change senders ────────────────────────────────────────────────────────────
// Called from beam_jni.c JNI stubs when an input widget fires an onChange event.
// Each builds {:change, tag, value} and sends it to the registered pid.
static void send_change(int handle, ERL_NIF_TERM value_term) {
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
enif_mutex_unlock(tap_mutex);
ErlNifEnv* msg_env = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple3(msg_env,
enif_make_atom(msg_env, "change"),
enif_make_copy(msg_env, tag),
enif_make_copy(msg_env, value_term));
enif_send(NULL, &pid, msg_env, msg);
enif_free_env(msg_env);
}
void mob_send_change_str(int handle, const char* utf8) {
ErlNifEnv* tmp = enif_alloc_env();
ErlNifBinary bin;
size_t len = strlen(utf8);
enif_alloc_binary(len, &bin);
memcpy(bin.data, utf8, len);
ERL_NIF_TERM term = enif_make_binary(tmp, &bin);
send_change(handle, term);
enif_free_env(tmp);
}
void mob_send_change_bool(int handle, int bool_val) {
ErlNifEnv* tmp = enif_alloc_env();
ERL_NIF_TERM term = enif_make_atom(tmp, bool_val ? "true" : "false");
send_change(handle, term);
enif_free_env(tmp);
}
void mob_send_change_float(int handle, double value) {
ErlNifEnv* tmp = enif_alloc_env();
ERL_NIF_TERM term = enif_make_double(tmp, value);
send_change(handle, term);
enif_free_env(tmp);
}
// ── Focus / blur / submit senders ────────────────────────────────────────────
// Called from beam_jni.c JNI stubs when a text field gains/loses focus or
// the return key is pressed. Sends a {:event, tag} 2-tuple to the registered pid.
static void send_event(int handle, const char* atom) {
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
enif_mutex_unlock(tap_mutex);
ErlNifEnv* msg_env = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple2(msg_env,
enif_make_atom(msg_env, atom),
enif_make_copy(msg_env, tag));
enif_send(NULL, &pid, msg_env, msg);
enif_free_env(msg_env);
}
void mob_send_focus(int handle) { send_event(handle, "focus"); }
void mob_send_blur(int handle) { send_event(handle, "blur"); }
void mob_send_submit(int handle) { send_event(handle, "submit"); }
void mob_send_select(int handle) { send_event(handle, "select"); }
// IME composition. Sends {compose, tag, %{text, phase}} where phase is
// began/updating/committed/cancelled. Called from beam_jni.c when the
// Compose TextField's TextFieldValue.composition range changes, or from
// an InputConnection observer in legacy view-system text fields.
void mob_send_compose(int handle, const char* text, const char* phase) {
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
enif_mutex_unlock(tap_mutex);
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM keys[2] = {
enif_make_atom(env, "text"),
enif_make_atom(env, "phase"),
};
ERL_NIF_TERM vals[2] = {
enif_make_string(env, text ? text : "", ERL_NIF_LATIN1),
enif_make_atom(env, phase),
};
ERL_NIF_TERM payload;
enif_make_map_from_arrays(env, keys, vals, 2, &payload);
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "compose"),
enif_make_copy(env, tag),
payload);
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
// ── Gesture senders (Batch 4) ───────────────────────────────────────────────
// Called from beam_jni.c when the Compose gesture detector fires. Each is
// per-widget opt-in — only registered handles emit. Direction-aware swipes
// use mob_send_swipe_with_direction.
void mob_send_long_press(int handle) { send_event(handle, "long_press"); }
void mob_send_double_tap(int handle) { send_event(handle, "double_tap"); }
void mob_send_swipe_left(int handle) { send_event(handle, "swipe_left"); }
void mob_send_swipe_right(int handle) { send_event(handle, "swipe_right"); }
void mob_send_swipe_up(int handle) { send_event(handle, "swipe_up"); }
void mob_send_swipe_down(int handle) { send_event(handle, "swipe_down"); }
void mob_send_swipe_with_direction(int handle, const char* direction) {
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
enif_mutex_unlock(tap_mutex);
ErlNifEnv* msg_env = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple3(msg_env,
enif_make_atom(msg_env, "swipe"),
enif_make_copy(msg_env, tag),
enif_make_atom(msg_env, direction));
enif_send(NULL, &pid, msg_env, msg);
enif_free_env(msg_env);
}
// ── Batch 5 Tier 1: high-frequency events with throttling ─────────────────
// Mirrors the iOS implementation in mob_nif.m. Throttle state lives on each
// TapHandle (above). JNI stubs in beam_jni.c are pending — these C functions
// are the bridge target.
static long long mob_now_ns_android(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec;
}
void mob_set_throttle_config(int handle,
int throttle_ms, int debounce_ms,
double delta_threshold,
int leading, int trailing) {
enif_mutex_lock(tap_mutex);
if (handle >= 0 && handle < tap_handle_next && tap_handles[handle].tag_env) {
tap_handles[handle].throttle_ms = throttle_ms;
tap_handles[handle].debounce_ms = debounce_ms;
tap_handles[handle].delta_threshold = delta_threshold;
tap_handles[handle].leading = leading;
tap_handles[handle].trailing = trailing;
}
enif_mutex_unlock(tap_mutex);
}
static int mob_throttle_check_a(int handle, double x, double y,
int default_throttle_ms, double default_delta) {
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return 0;
}
TapHandle* h = &tap_handles[handle];
int throttle_ms = h->throttle_ms ? h->throttle_ms : default_throttle_ms;
double delta_threshold = h->delta_threshold > 0 ? h->delta_threshold : default_delta;
long long now_ns = mob_now_ns_android();
double dx = x - h->last_x;
double dy = y - h->last_y;
double dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
if (h->last_emit_ns > 0 && throttle_ms > 0) {
long long elapsed_ms = (now_ns - h->last_emit_ns) / 1000000LL;
if (elapsed_ms < throttle_ms) {
enif_mutex_unlock(tap_mutex);
return 0;
}
}
if (h->last_emit_ns > 0 && dist < delta_threshold) {
enif_mutex_unlock(tap_mutex);
return 0;
}
h->last_emit_ns = now_ns;
h->last_x = x;
h->last_y = y;
h->seq++;
enif_mutex_unlock(tap_mutex);
return 1;
}
// Build payload map for scroll/drag/etc. Caller owns msg_env.
static ERL_NIF_TERM mob_build_scroll_map(ErlNifEnv* env,
double x, double y,
double dx, double dy,
double vx, double vy,
const char* phase,
long long ts_ms,
unsigned long long seq) {
ERL_NIF_TERM keys[9] = {
enif_make_atom(env, "x"), enif_make_atom(env, "y"),
enif_make_atom(env, "dx"), enif_make_atom(env, "dy"),
enif_make_atom(env, "velocity_x"), enif_make_atom(env, "velocity_y"),
enif_make_atom(env, "phase"),
enif_make_atom(env, "ts"), enif_make_atom(env, "seq"),
};
ERL_NIF_TERM vals[9] = {
enif_make_double(env, x), enif_make_double(env, y),
enif_make_double(env, dx), enif_make_double(env, dy),
enif_make_double(env, vx), enif_make_double(env, vy),
enif_make_atom(env, phase),
enif_make_int64(env, ts_ms),
enif_make_uint64(env, seq),
};
ERL_NIF_TERM map;
enif_make_map_from_arrays(env, keys, vals, 9, &map);
return map;
}
void mob_send_scroll(int handle,
double x, double y,
double dx, double dy,
double vx, double vy,
const char* phase) {
int phase_boundary = (strcmp(phase, "began") == 0) || (strcmp(phase, "ended") == 0);
if (!phase_boundary && !mob_throttle_check_a(handle, x, y, 33, 1.0)) return;
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
unsigned long long seq = tap_handles[handle].seq;
enif_mutex_unlock(tap_mutex);
long long ts_ms = mob_now_ns_android() / 1000000LL;
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM payload = mob_build_scroll_map(env, x, y, dx, dy, vx, vy, phase, ts_ms, seq);
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "scroll"),
enif_make_copy(env, tag),
payload);
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
void mob_send_drag(int handle,
double x, double y,
double dx, double dy,
const char* phase) {
int phase_boundary = (strcmp(phase, "began") == 0) || (strcmp(phase, "ended") == 0);
if (!phase_boundary && !mob_throttle_check_a(handle, x, y, 16, 1.0)) return;
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
unsigned long long seq = tap_handles[handle].seq;
enif_mutex_unlock(tap_mutex);
long long ts_ms = mob_now_ns_android() / 1000000LL;
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM keys[7] = {
enif_make_atom(env, "x"), enif_make_atom(env, "y"),
enif_make_atom(env, "dx"), enif_make_atom(env, "dy"),
enif_make_atom(env, "phase"),
enif_make_atom(env, "ts"), enif_make_atom(env, "seq"),
};
ERL_NIF_TERM vals[7] = {
enif_make_double(env, x), enif_make_double(env, y),
enif_make_double(env, dx), enif_make_double(env, dy),
enif_make_atom(env, phase),
enif_make_int64(env, ts_ms), enif_make_uint64(env, seq),
};
ERL_NIF_TERM payload;
enif_make_map_from_arrays(env, keys, vals, 7, &payload);
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "drag"),
enif_make_copy(env, tag),
payload);
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
void mob_send_pinch(int handle, double scale, double velocity, const char* phase) {
int phase_boundary = (strcmp(phase, "began") == 0) || (strcmp(phase, "ended") == 0);
if (!phase_boundary && !mob_throttle_check_a(handle, scale, 0, 16, 0.01)) return;
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
unsigned long long seq = tap_handles[handle].seq;
enif_mutex_unlock(tap_mutex);
long long ts_ms = mob_now_ns_android() / 1000000LL;
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM keys[5] = {
enif_make_atom(env, "scale"), enif_make_atom(env, "velocity"),
enif_make_atom(env, "phase"),
enif_make_atom(env, "ts"), enif_make_atom(env, "seq"),
};
ERL_NIF_TERM vals[5] = {
enif_make_double(env, scale), enif_make_double(env, velocity),
enif_make_atom(env, phase),
enif_make_int64(env, ts_ms), enif_make_uint64(env, seq),
};
ERL_NIF_TERM payload;
enif_make_map_from_arrays(env, keys, vals, 5, &payload);
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "pinch"),
enif_make_copy(env, tag),
payload);
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
void mob_send_rotate(int handle, double degrees, double velocity, const char* phase) {
int phase_boundary = (strcmp(phase, "began") == 0) || (strcmp(phase, "ended") == 0);
if (!phase_boundary && !mob_throttle_check_a(handle, degrees, 0, 16, 1.0)) return;
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
unsigned long long seq = tap_handles[handle].seq;
enif_mutex_unlock(tap_mutex);
long long ts_ms = mob_now_ns_android() / 1000000LL;
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM keys[5] = {
enif_make_atom(env, "degrees"), enif_make_atom(env, "velocity"),
enif_make_atom(env, "phase"),
enif_make_atom(env, "ts"), enif_make_atom(env, "seq"),
};
ERL_NIF_TERM vals[5] = {
enif_make_double(env, degrees), enif_make_double(env, velocity),
enif_make_atom(env, phase),
enif_make_int64(env, ts_ms), enif_make_uint64(env, seq),
};
ERL_NIF_TERM payload;
enif_make_map_from_arrays(env, keys, vals, 5, &payload);
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "rotate"),
enif_make_copy(env, tag),
payload);
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
void mob_send_pointer_move(int handle, double x, double y) {
if (!mob_throttle_check_a(handle, x, y, 33, 4.0)) return;
enif_mutex_lock(tap_mutex);
if (handle < 0 || handle >= tap_handle_next || !tap_handles[handle].tag_env) {
enif_mutex_unlock(tap_mutex);
return;
}
ErlNifPid pid = tap_handles[handle].pid;
ERL_NIF_TERM tag = tap_handles[handle].tag;
unsigned long long seq = tap_handles[handle].seq;
enif_mutex_unlock(tap_mutex);
long long ts_ms = mob_now_ns_android() / 1000000LL;
ErlNifEnv* env = enif_alloc_env();
ERL_NIF_TERM keys[4] = {
enif_make_atom(env, "x"), enif_make_atom(env, "y"),
enif_make_atom(env, "ts"), enif_make_atom(env, "seq"),
};
ERL_NIF_TERM vals[4] = {
enif_make_double(env, x), enif_make_double(env, y),
enif_make_int64(env, ts_ms), enif_make_uint64(env, seq),
};
ERL_NIF_TERM payload;
enif_make_map_from_arrays(env, keys, vals, 4, &payload);
ERL_NIF_TERM msg = enif_make_tuple3(env,
enif_make_atom(env, "pointer_move"),
enif_make_copy(env, tag),
payload);
enif_send(NULL, &pid, env, msg);
enif_free_env(env);
}
// ── Batch 5 Tier 2: semantic single-fire scroll events ──
void mob_send_scroll_began(int handle) { send_event(handle, "scroll_began"); }
void mob_send_scroll_ended(int handle) { send_event(handle, "scroll_ended"); }
void mob_send_scroll_settled(int handle) { send_event(handle, "scroll_settled"); }
void mob_send_top_reached(int handle) { send_event(handle, "top_reached"); }
void mob_send_scrolled_past(int handle) { send_event(handle, "scrolled_past"); }
// ── Back gesture sender ───────────────────────────────────────────────────────
// Called from beam_jni.c's nativeHandleBack JNI stub when the Android back
// gesture fires. Looks up the :mob_screen registered process and sends
// {:mob, :back} — Mob.Screen.handle_info/2 handles popping or exiting.
void mob_handle_back(void) {
ErlNifEnv* env = enif_alloc_env();
ErlNifPid pid;
if (enif_whereis_pid(env, enif_make_atom(env, "mob_screen"), &pid)) {
ERL_NIF_TERM msg = enif_make_tuple2(env,
enif_make_atom(env, "mob"),
enif_make_atom(env, "back"));
enif_send(NULL, &pid, env, msg);
}
enif_free_env(env);
}
// ── JNI helpers ──────────────────────────────────────────────────────────────
static JNIEnv* get_jenv(int* attached) {
JNIEnv* env = NULL;
*attached = 0;
if ((*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6) == JNI_EDETACHED) {
(*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL);
*attached = 1;
}
return env;
}
// ── Cache MobBridge class (called from mob_beam.c) ───────────────────────────
void _mob_ui_cache_class_impl(JNIEnv* jenv, const char* bridge_class) {
LOGI("mob_ui_cache_class: looking up %s", bridge_class);
jclass cls = (*jenv)->FindClass(jenv, bridge_class);
if (!cls) { LOGE("mob_ui_cache_class: %s not found", bridge_class); return; }
Bridge.cls = (*jenv)->NewGlobalRef(jenv, cls);
(*jenv)->DeleteLocalRef(jenv, cls);
// Cache startup status methods now — they're needed before nif_load runs.
// These are optional (older MobBridge versions may not have them); clear
// any pending exception rather than aborting.
Bridge.set_startup_phase = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "setStartupPhase", "(Ljava/lang/String;)V");
if (!Bridge.set_startup_phase) (*jenv)->ExceptionClear(jenv);
Bridge.set_startup_error = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "setStartupError", "(Ljava/lang/String;)V");
if (!Bridge.set_startup_error) (*jenv)->ExceptionClear(jenv);
LOGI("mob_ui_cache_class: %s cached OK", bridge_class);
}
void mob_set_startup_phase(const char* phase) {
if (!g_jvm || !Bridge.cls || !Bridge.set_startup_phase) return;
int att; JNIEnv* env = get_jenv(&att);
jstring js = (*env)->NewStringUTF(env, phase);
(*env)->CallStaticVoidMethod(env, Bridge.cls, Bridge.set_startup_phase, js);
(*env)->DeleteLocalRef(env, js);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
LOGI("startup: %s", phase);
}
void mob_set_startup_error(const char* error) {
if (!g_jvm || !Bridge.cls || !Bridge.set_startup_error) return;
int att; JNIEnv* env = get_jenv(&att);
jstring js = (*env)->NewStringUTF(env, error);
(*env)->CallStaticVoidMethod(env, Bridge.cls, Bridge.set_startup_error, js);
(*env)->DeleteLocalRef(env, js);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
LOGE("startup ERROR: %s", error);
}
// ── Initialize bridge with Activity (called from mob_beam.c) ─────────────────
void _mob_bridge_init_activity(JNIEnv* env, jobject activity) {
if (!Bridge.cls) { LOGE("_mob_bridge_init_activity: Bridge.cls not cached"); return; }
jmethodID init = (*env)->GetStaticMethodID(env, Bridge.cls, "init",
"(Landroid/app/Activity;)V");
(*env)->CallStaticVoidMethod(env, Bridge.cls, init, activity);
LOGI("_mob_bridge_init_activity: MobBridge.init called");
}
// ── NIF: platform/0 ──────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_platform(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
return enif_make_atom(env, "android");
}
// ── NIF: log/1 ───────────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_log(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char buf[4096] = {0};
ErlNifBinary bin;
if (enif_inspect_binary(env, argv[0], &bin)) {
size_t len = bin.size < sizeof(buf) - 1 ? bin.size : sizeof(buf) - 1;
memcpy(buf, bin.data, len);
buf[len] = 0;
} else if (!enif_get_string(env, argv[0], buf, sizeof(buf), ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
__android_log_print(ANDROID_LOG_INFO, "Elixir", "%s", buf);
return enif_make_atom(env, "ok");
}
// ── NIF: log/2 ───────────────────────────────────────────────────────────────
static int atom_to_android_priority(ErlNifEnv* env, ERL_NIF_TERM level_atom) {
char level[16];
if (!enif_get_atom(env, level_atom, level, sizeof(level), ERL_NIF_LATIN1))
return ANDROID_LOG_INFO;
if (strcmp(level, "debug") == 0) return ANDROID_LOG_DEBUG;
if (strcmp(level, "warning") == 0) return ANDROID_LOG_WARN;
if (strcmp(level, "error") == 0) return ANDROID_LOG_ERROR;
return ANDROID_LOG_INFO;
}
static ERL_NIF_TERM nif_log2(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char buf[4096] = {0};
int priority = atom_to_android_priority(env, argv[0]);
ErlNifBinary bin;
if (enif_inspect_binary(env, argv[1], &bin)) {
size_t len = bin.size < sizeof(buf) - 1 ? bin.size : sizeof(buf) - 1;
memcpy(buf, bin.data, len);
buf[len] = 0;
} else if (!enif_get_string(env, argv[1], buf, sizeof(buf), ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
__android_log_print(priority, "Elixir", "%s", buf);
return enif_make_atom(env, "ok");
}
// ── NIF: set_root/1 ──────────────────────────────────────────────────────────
// Accepts a JSON binary and passes it to MobBridge.setRootJson(String) on the
// Kotlin side. Compose state update is thread-safe — no main-thread hop needed.
static ERL_NIF_TERM nif_set_root(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
// Null-terminate for NewStringUTF
char* json = (char*)malloc(bin.size + 1);
if (!json) return enif_make_atom(env, "error");
memcpy(json, bin.data, bin.size);
json[bin.size] = 0;
// Snapshot the current transition (set by set_transition/1 before this call)
enif_mutex_lock(tap_mutex);
char transition[16];
strncpy(transition, g_transition, sizeof(transition) - 1);
transition[sizeof(transition) - 1] = 0;
strncpy(g_transition, "none", sizeof(g_transition)); // reset to none
enif_mutex_unlock(tap_mutex);
int att; JNIEnv* jenv = get_jenv(&att);
jstring jjson = (*jenv)->NewStringUTF(jenv, json);
jstring jtransition = (*jenv)->NewStringUTF(jenv, transition);
free(json);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.set_root, jjson, jtransition);
(*jenv)->DeleteLocalRef(jenv, jjson);
(*jenv)->DeleteLocalRef(jenv, jtransition);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── NIF: register_tap/1 ──────────────────────────────────────────────────────
// Accepts pid (tag = :ok) or {pid, tag} (any Erlang term used as the tag).
static ERL_NIF_TERM nif_register_tap(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifPid pid;
ERL_NIF_TERM tag_term;
// Try plain pid first
if (enif_get_local_pid(env, argv[0], &pid)) {
// No explicit tag — use :ok
tag_term = enif_make_atom(env, "ok");
} else {
// Try {pid, tag} 2-tuple
int arity;
const ERL_NIF_TERM* elems;
if (!enif_get_tuple(env, argv[0], &arity, &elems) || arity != 2)
return enif_make_badarg(env);
if (!enif_get_local_pid(env, elems[0], &pid))
return enif_make_badarg(env);
tag_term = elems[1];
}
enif_mutex_lock(tap_mutex);
if (tap_handle_next >= MAX_TAP_HANDLES) {
enif_mutex_unlock(tap_mutex);
return enif_make_badarg(env);
}
int handle = tap_handle_next++;
tap_handles[handle].pid = pid;
tap_handles[handle].tag_env = enif_alloc_env();
tap_handles[handle].tag = enif_make_copy(tap_handles[handle].tag_env, tag_term);
enif_mutex_unlock(tap_mutex);
return enif_make_int(env, handle);
}
// ── NIF: clear_taps/0 ────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_clear_taps(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
enif_mutex_lock(tap_mutex);
for (int i = 0; i < tap_handle_next; i++) {
if (tap_handles[i].tag_env) {
enif_free_env(tap_handles[i].tag_env);
tap_handles[i].tag_env = NULL;
}
// Reset throttle state — slots get reused across renders.
tap_handles[i].throttle_ms = 0;
tap_handles[i].debounce_ms = 0;
tap_handles[i].delta_threshold = 0;
tap_handles[i].leading = 1;
tap_handles[i].trailing = 1;
tap_handles[i].last_emit_ns = 0;
tap_handles[i].last_x = 0;
tap_handles[i].last_y = 0;
tap_handles[i].seq = 0;
}
tap_handle_next = 0;
enif_mutex_unlock(tap_mutex);
return enif_make_atom(env, "ok");
}
// ── NIF: exit_app/0 ──────────────────────────────────────────────────────────
// Backgrounds the app via MobBridge.moveToBack() → activity.moveTaskToBack(true).
// Called by Mob.Screen when the back gesture fires at the root of the nav stack.
static ERL_NIF_TERM nif_exit_app(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.move_to_back);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── NIF: set_transition/1 ────────────────────────────────────────────────────
// Stores the transition type atom (push/pop/reset/none) to be passed to
// setRootJson on the next set_root call. Must be called before set_root.
static ERL_NIF_TERM nif_set_transition(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
enif_mutex_lock(tap_mutex);
if (!enif_get_atom(env, argv[0], g_transition, sizeof(g_transition), ERL_NIF_LATIN1)) {
enif_mutex_unlock(tap_mutex);
return enif_make_badarg(env);
}
enif_mutex_unlock(tap_mutex);
return enif_make_atom(env, "ok");
}
// ── NIF: safe_area/0 ─────────────────────────────────────────────────────────
// Returns {Top, Right, Bottom, Left} in dp via MobBridge.getSafeArea().
static ERL_NIF_TERM nif_safe_area(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
jfloatArray arr = (jfloatArray)(*jenv)->CallStaticObjectMethod(
jenv, Bridge.cls, Bridge.get_safe_area);
float vals[4] = {0.0f, 0.0f, 0.0f, 0.0f};
if (arr) {
(*jenv)->GetFloatArrayRegion(jenv, arr, 0, 4, vals);
(*jenv)->DeleteLocalRef(jenv, arr);
}
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_tuple4(env,
enif_make_double(env, (double)vals[0]),
enif_make_double(env, (double)vals[1]),
enif_make_double(env, (double)vals[2]),
enif_make_double(env, (double)vals[3])
);
}
// ── NIF: haptic/1 ─────────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_haptic(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char type[32] = {0};
enif_get_atom(env, argv[0], type, sizeof(type), ERL_NIF_LATIN1);
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtype = (*jenv)->NewStringUTF(jenv, type);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.haptic, jtype);
(*jenv)->DeleteLocalRef(jenv, jtype);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── NIF: clipboard_put/1 ──────────────────────────────────────────────────────
static ERL_NIF_TERM nif_clipboard_put(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* text = (char*)malloc(bin.size + 1);
if (!text) return enif_make_atom(env, "error");
memcpy(text, bin.data, bin.size);
text[bin.size] = 0;
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtext = (*jenv)->NewStringUTF(jenv, text);
free(text);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.clipboard_put, jtext);
(*jenv)->DeleteLocalRef(jenv, jtext);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── NIF: clipboard_get/0 ──────────────────────────────────────────────────────
// Returns {:ok, Binary} or :empty.
static ERL_NIF_TERM nif_clipboard_get(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
jstring result = (jstring)(*jenv)->CallStaticObjectMethod(jenv, Bridge.cls, Bridge.clipboard_get);
ERL_NIF_TERM ret;
if (result) {
const char* utf8 = (*jenv)->GetStringUTFChars(jenv, result, NULL);
ErlNifBinary bin;
size_t len = strlen(utf8);
enif_alloc_binary(len, &bin);
memcpy(bin.data, utf8, len);
(*jenv)->ReleaseStringUTFChars(jenv, result, utf8);
(*jenv)->DeleteLocalRef(jenv, result);
ret = enif_make_tuple2(env, enif_make_atom(env, "ok"), enif_make_binary(env, &bin));
} else {
ret = enif_make_atom(env, "empty");
}
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ret;
}
// ── NIF: share_text/1 ─────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_share_text(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* text = (char*)malloc(bin.size + 1);
if (!text) return enif_make_atom(env, "error");
memcpy(text, bin.data, bin.size);
text[bin.size] = 0;
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtext = (*jenv)->NewStringUTF(jenv, text);
free(text);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.share_text, jtext);
(*jenv)->DeleteLocalRef(jenv, jtext);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ════════════════════════════════════════════════════════════════════════════
// Device capability NIFs (Android JNI bridge)
// Each calls a static method on MobBridge with the PID encoded as a long so
// Kotlin can call mob_nif_deliver_event() with the result.
// ════════════════════════════════════════════════════════════════════════════
// Launch notification global (written by MobBridge.setLaunchNotification, read once)
static char* g_launch_notif_json = NULL;
static ErlNifMutex* g_launch_notif_mutex = NULL;
// Called from MobBridge.setLaunchNotification(json)
void mob_set_launch_notification(const char* json) {
if (!g_launch_notif_mutex) return;
enif_mutex_lock(g_launch_notif_mutex);
free(g_launch_notif_json);
g_launch_notif_json = json ? strdup(json) : NULL;
enif_mutex_unlock(g_launch_notif_mutex);
}
static ERL_NIF_TERM nif_take_launch_notification(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!g_launch_notif_mutex) return enif_make_atom(env, "none");
enif_mutex_lock(g_launch_notif_mutex);
char* json = g_launch_notif_json;
g_launch_notif_json = NULL;
enif_mutex_unlock(g_launch_notif_mutex);
if (!json) return enif_make_atom(env, "none");
ErlNifBinary bin;
enif_alloc_binary(strlen(json), &bin);
memcpy(bin.data, json, strlen(json));
free(json);
return enif_make_binary(env, &bin);
}
// Generic helper: call Kotlin static method(pid_long, string_arg)
static ERL_NIF_TERM call_bridge_pid_str(ErlNifEnv* env, jmethodID method,
ErlNifPid pid, const char* arg) {
int att; JNIEnv* jenv = get_jenv(&att);
jlong jpid;
memcpy(&jpid, &pid, sizeof(ErlNifPid) < sizeof(jlong) ? sizeof(ErlNifPid) : sizeof(jlong));
jstring jarg = arg ? (*jenv)->NewStringUTF(jenv, arg) : NULL;
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, method, jpid, jarg);
if (jarg) (*jenv)->DeleteLocalRef(jenv, jarg);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM call_bridge_pid_str2(ErlNifEnv* env, jmethodID method,
ErlNifPid pid, const char* a1, const char* a2) {
int att; JNIEnv* jenv = get_jenv(&att);
jlong jpid;
memcpy(&jpid, &pid, sizeof(ErlNifPid) < sizeof(jlong) ? sizeof(ErlNifPid) : sizeof(jlong));
jstring j1 = a1 ? (*jenv)->NewStringUTF(jenv, a1) : NULL;
jstring j2 = a2 ? (*jenv)->NewStringUTF(jenv, a2) : NULL;
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, method, jpid, j1, j2);
if (j1) (*jenv)->DeleteLocalRef(jenv, j1);
if (j2) (*jenv)->DeleteLocalRef(jenv, j2);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// mob_nif_deliver_event — called from Kotlin with a JSON string result.
// Decodes the JSON and sends the appropriate BEAM message to the pid stored in it.
// JSON format: {"pid": <long>, "event": [...erlang term json...]}
// We use a simpler approach: Kotlin encodes the event as a JSON array describing the term.
// Actually, simplest approach: Kotlin constructs a binary JSON string, and we route
// to pre-stored PIDs in a simple table. But since we pass the PID as a long to Kotlin,
// Kotlin passes it back to us and we reconstruct the ErlNifPid.
//
// mob_nif_deliver_json(pid_long, json_cstr) — send pre-formed JSON event to pid
// This is declared in mob_beam.h for Kotlin to call via JNI.
void mob_nif_deliver_json(jlong pid_long, const char* json_str) {
// We don't send JSON to the BEAM — we need to build proper Erlang terms.
// Instead, we use a set of typed delivery functions called from Kotlin.
// See mob_beam.h for the full set.
}
// Typed event delivery functions called from Kotlin/JNI
// These are declared in mob_beam.h and implemented here.
static ErlNifPid pid_from_long(jlong jpid) {
ErlNifPid pid;
memset(&pid, 0, sizeof(pid));
memcpy(&pid, &jpid, sizeof(ErlNifPid) < sizeof(jlong) ? sizeof(ErlNifPid) : sizeof(jlong));
return pid;
}
void mob_deliver_atom2(jlong jpid, const char* a1, const char* a2) {
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e,a1), enif_make_atom(e,a2));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
void mob_deliver_atom3(jlong jpid, const char* a1, const char* a2, const char* a3) {
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple3(e,
enif_make_atom(e,a1), enif_make_atom(e,a2), enif_make_atom(e,a3));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
void mob_deliver_location(jlong jpid, double lat, double lon, double acc, double alt) {
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ERL_NIF_TERM keys[4] = {
enif_make_atom(e,"lat"), enif_make_atom(e,"lon"),
enif_make_atom(e,"accuracy"), enif_make_atom(e,"altitude")
};
ERL_NIF_TERM vals[4] = {
enif_make_double(e,lat), enif_make_double(e,lon),
enif_make_double(e,acc), enif_make_double(e,alt)
};
ERL_NIF_TERM map; enif_make_map_from_arrays(e, keys, vals, 4, &map);
ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e,"location"), map);
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
void mob_deliver_motion(jlong jpid, double ax, double ay, double az,
double gx, double gy, double gz, long long ts) {
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ERL_NIF_TERM accel = enif_make_tuple3(e,
enif_make_double(e,ax), enif_make_double(e,ay), enif_make_double(e,az));
ERL_NIF_TERM gyro = enif_make_tuple3(e,
enif_make_double(e,gx), enif_make_double(e,gy), enif_make_double(e,gz));
ERL_NIF_TERM keys[3] = {
enif_make_atom(e,"accel"), enif_make_atom(e,"gyro"), enif_make_atom(e,"timestamp")
};
ERL_NIF_TERM vals[3] = {accel, gyro, enif_make_int64(e,ts)};
ERL_NIF_TERM map; enif_make_map_from_arrays(e, keys, vals, 3, &map);
ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e,"motion"), map);
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
// Deliver a {:webview, tag, binary} message. When jpid==0, looks up :mob_screen.
static void deliver_webview_binary(jlong jpid, const char* tag, const char* utf8) {
ErlNifEnv* e = enif_alloc_env();
ErlNifPid pid;
if (jpid != 0) {
pid = pid_from_long(jpid);
} else if (!enif_whereis_pid(e, enif_make_atom(e, "mob_screen"), &pid)) {
enif_free_env(e); return;
}
size_t len = strlen(utf8);
ErlNifBinary bin;
enif_alloc_binary(len, &bin);
memcpy(bin.data, utf8, len);
ERL_NIF_TERM msg = enif_make_tuple3(e,
enif_make_atom(e, "webview"),
enif_make_atom(e, tag),
enif_make_binary(e, &bin));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
void mob_deliver_webview_message(jlong jpid, const char* json) {
deliver_webview_binary(jpid, "message", json);
}
void mob_deliver_webview_blocked(jlong jpid, const char* url) {
deliver_webview_binary(jpid, "blocked", url);
}
void mob_deliver_file_result(jlong jpid, const char* event, // "camera","photos","files","audio","scan"
const char* sub, // "photo","video","picked","recorded","result","cancelled"
const char* json_items) { // JSON array of item maps, or NULL for cancelled
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ERL_NIF_TERM msg;
if (!json_items || strcmp(json_items, "cancelled") == 0) {
msg = enif_make_tuple2(e, enif_make_atom(e,event), enif_make_atom(e,"cancelled"));
} else {
// Parse JSON array of maps and build Erlang list
// Simple approach: pass the raw JSON binary as a string; the BEAM can decode it if needed.
// Better: build proper terms here.
// For now, pass as binary; Elixir side can use :json.decode.
// But we want typed data, so let's build a simple list of maps.
// We'll use a JSON-like binary approach: send the raw JSON and let the BEAM decode it.
ErlNifBinary jb;
size_t jlen = strlen(json_items);
enif_alloc_binary(jlen, &jb);
memcpy(jb.data, json_items, jlen);
// Build: {event_atom, sub_atom, json_binary}
// The Elixir Mob.Screen will need to decode it. Actually, let's send the JSON
// and have Mob.Screen decode it — but screen doesn't do that for file results.
// Better: send as a tagged binary that Elixir wrappers decode.
// We'll send {:mob_file_result, event, sub, json_binary} and add a handler.
ErlNifBinary eb; size_t el = strlen(event); enif_alloc_binary(el,&eb); memcpy(eb.data,event,el);
ErlNifBinary sb; size_t sl = strlen(sub); enif_alloc_binary(sl,&sb); memcpy(sb.data,sub,sl);
msg = enif_make_tuple4(e,
enif_make_atom(e,"mob_file_result"),
enif_make_binary(e,&eb),
enif_make_binary(e,&sb),
enif_make_binary(e,&jb));
}
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
void mob_deliver_push_token(jlong jpid, const char* token) {
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ErlNifBinary tb; size_t tl = strlen(token); enif_alloc_binary(tl,&tb); memcpy(tb.data,token,tl);
ERL_NIF_TERM msg = enif_make_tuple3(e,
enif_make_atom(e,"push_token"), enif_make_atom(e,"android"), enif_make_binary(e,&tb));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
void mob_deliver_notification(jlong jpid, const char* json) {
ErlNifPid pid = pid_from_long(jpid);
ErlNifEnv* e = enif_alloc_env();
ErlNifBinary jb; size_t jl = strlen(json); enif_alloc_binary(jl,&jb); memcpy(jb.data,json,jl);
ERL_NIF_TERM msg = enif_make_tuple2(e,
enif_make_atom(e,"mob_launch_notification"), enif_make_binary(e,&jb));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
// NIF implementations — thin wrappers that pass work to Kotlin
static ERL_NIF_TERM nif_request_permission(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char cap[32]; enif_get_atom(env, argv[0], cap, sizeof(cap), ERL_NIF_LATIN1);
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.request_permission, pid, cap);
}
static ERL_NIF_TERM nif_biometric_authenticate(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char reason[256] = "Authenticate";
if (bin.size < sizeof(reason)) { memcpy(reason, bin.data, bin.size); reason[bin.size] = 0; }
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.biometric_authenticate, pid, reason);
}
static ERL_NIF_TERM nif_location_get_once(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.location_get_once, pid, "balanced");
}
static ERL_NIF_TERM nif_location_start(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char acc[16] = "balanced"; enif_get_atom(env, argv[0], acc, sizeof(acc), ERL_NIF_LATIN1);
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.location_start, pid, acc);
}
static ERL_NIF_TERM nif_location_stop(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.location_stop);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_camera_capture_photo(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char qual[16] = "high"; enif_get_atom(env, argv[0], qual, sizeof(qual), ERL_NIF_LATIN1);
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.camera_capture_photo, pid, qual);
}
static ERL_NIF_TERM nif_camera_capture_video(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int max_dur = 60; enif_get_int(env, argv[0], &max_dur);
ErlNifPid pid; enif_self(env, &pid);
char dur_str[16]; snprintf(dur_str, sizeof(dur_str), "%d", max_dur);
return call_bridge_pid_str(env, Bridge.camera_capture_video, pid, dur_str);
}
static ERL_NIF_TERM nif_camera_start_preview(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* json = malloc(bin.size + 1);
memcpy(json, bin.data, bin.size); json[bin.size] = 0;
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str(env, Bridge.camera_start_preview, pid, json);
free(json);
return result;
}
static ERL_NIF_TERM nif_camera_stop_preview(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.camera_stop_preview);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_photos_pick(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int max = 1; enif_get_int(env, argv[0], &max);
ErlNifPid pid; enif_self(env, &pid);
char max_str[16]; snprintf(max_str, sizeof(max_str), "%d", max);
return call_bridge_pid_str(env, Bridge.photos_pick, pid, max_str);
}
static ERL_NIF_TERM nif_files_pick(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* json = malloc(bin.size + 1);
memcpy(json, bin.data, bin.size); json[bin.size] = 0;
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str(env, Bridge.files_pick, pid, json);
free(json);
return result;
}
static ERL_NIF_TERM nif_audio_start_recording(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* json = malloc(bin.size + 1);
memcpy(json, bin.data, bin.size); json[bin.size] = 0;
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str(env, Bridge.audio_start_recording, pid, json);
free(json);
return result;
}
static ERL_NIF_TERM nif_audio_stop_recording(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.audio_stop_recording);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_audio_play(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary path_bin, opts_bin;
if (!enif_inspect_binary(env, argv[0], &path_bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &path_bin)) return enif_make_badarg(env);
if (!enif_inspect_binary(env, argv[1], &opts_bin) &&
!enif_inspect_iolist_as_binary(env, argv[1], &opts_bin)) return enif_make_badarg(env);
char* path = malloc(path_bin.size + 1);
memcpy(path, path_bin.data, path_bin.size); path[path_bin.size] = 0;
char* opts = malloc(opts_bin.size + 1);
memcpy(opts, opts_bin.data, opts_bin.size); opts[opts_bin.size] = 0;
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str2(env, Bridge.audio_play, pid, path, opts);
free(path); free(opts);
return result;
}
static ERL_NIF_TERM nif_audio_stop_playback(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.audio_stop_playback);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_audio_set_volume(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
double vol = 1.0;
enif_get_double(env, argv[0], &vol);
char vol_str[32]; snprintf(vol_str, sizeof(vol_str), "%.6f", vol);
int att; JNIEnv* jenv = get_jenv(&att);
jstring jvol = (*jenv)->NewStringUTF(jenv, vol_str);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.audio_set_volume, jvol);
(*jenv)->DeleteLocalRef(jenv, jvol);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_motion_start(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int interval_ms = 100; enif_get_int(env, argv[1], &interval_ms);
char interval_str[16]; snprintf(interval_str, sizeof(interval_str), "%d", interval_ms);
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.motion_start, pid, interval_str);
}
static ERL_NIF_TERM nif_motion_stop(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.motion_stop);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_scanner_scan(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* json = malloc(bin.size + 1);
memcpy(json, bin.data, bin.size); json[bin.size] = 0;
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str(env, Bridge.scanner_scan, pid, json);
free(json);
return result;
}
static ERL_NIF_TERM nif_notify_schedule(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* json = malloc(bin.size + 1);
memcpy(json, bin.data, bin.size); json[bin.size] = 0;
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str(env, Bridge.notify_schedule, pid, json);
free(json);
return result;
}
static ERL_NIF_TERM nif_notify_cancel(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char nid[256] = "";
if (bin.size < sizeof(nid)) { memcpy(nid, bin.data, bin.size); nid[bin.size] = 0; }
int att; JNIEnv* jenv = get_jenv(&att);
jstring js = (*jenv)->NewStringUTF(jenv, nid);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.notify_cancel, js);
(*jenv)->DeleteLocalRef(jenv, js);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_notify_register_push(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifPid pid; enif_self(env, &pid);
return call_bridge_pid_str(env, Bridge.notify_register_push, pid, NULL);
}
// ── NIF table & load ─────────────────────────────────────────────────────────
// ── Test harness NIFs ─────────────────────────────────────────────────────────
//
// Android implementation notes vs iOS:
// - View tree walk uses android.view.View hierarchy (Compose exposes Views)
// - Touch injection via DecorView.dispatchTouchEvent — no INJECT_EVENTS needed
// - Text input via InputConnection.commitText — works for Compose TextField
// - All blocking operations use CountDownLatch on the Kotlin side;
// from C we just call the JNI method which blocks until the latch fires
// - Coordinates in dp (density-independent pixels), matching iOS convention
// Helper: jstring → ERL_NIF_TERM binary (UTF-8). Deletes local ref.
static ERL_NIF_TERM jstring_to_bin(ErlNifEnv* env, JNIEnv* jenv, jstring js) {
if (!js) return enif_make_atom(env, "nil");
const char* utf = (*jenv)->GetStringUTFChars(jenv, js, NULL);
if (!utf) return enif_make_atom(env, "nil");
size_t len = strlen(utf);
ErlNifBinary bin;
enif_alloc_binary(len, &bin);
memcpy(bin.data, utf, len);
(*jenv)->ReleaseStringUTFChars(jenv, js, utf);
(*jenv)->DeleteLocalRef(jenv, js);
return enif_make_binary(env, &bin);
}
// Helper: make a binary term from a C string (does NOT delete jstring).
static ERL_NIF_TERM cstr_to_bin(ErlNifEnv* env, const char* s, size_t len) {
ErlNifBinary bin;
enif_alloc_binary(len, &bin);
memcpy(bin.data, s, len);
return enif_make_binary(env, &bin);
}
// nif_ui_tree/0 — returns [{type_atom, label_binary, value_binary, {x,y,w,h}}, ...]
//
// Calls MobBridge.uiTree() which returns a newline-separated string:
// type|label|value|x|y|w|h\n...
// Parses that into a list of 4-tuples matching the iOS ui_tree format.
static ERL_NIF_TERM nif_ui_tree(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.ui_tree) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
int att; JNIEnv* jenv = get_jenv(&att);
jstring jresult = (jstring)(*jenv)->CallStaticObjectMethod(jenv, Bridge.cls, Bridge.ui_tree);
if (!jresult) {
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_list(env, 0);
}
const char* raw = (*jenv)->GetStringUTFChars(jenv, jresult, NULL);
ERL_NIF_TERM list = enif_make_list(env, 0);
// Parse lines in reverse (we'll reverse the list at the end)
// Format per line: type|label|value|x|y|w|h
const char* p = raw;
// Collect all lines into a temp array first (we build list in reverse for efficiency)
// Simple approach: walk forward, build list, reverse at end
ERL_NIF_TERM items[512];
int count = 0;
while (*p && count < 512) {
// Find end of line
const char* nl = strchr(p, '\n');
if (!nl) break;
size_t line_len = nl - p;
char line[512];
if (line_len >= sizeof(line)) { p = nl + 1; continue; }
memcpy(line, p, line_len);
line[line_len] = 0;
p = nl + 1;
// Split on '|': type, label, value, x, y, w, h
char* fields[7];
int nf = 0;
char* tok = line;
for (int i = 0; i < 7; i++) {
fields[i] = tok;
char* sep = (i < 6) ? strchr(tok, '|') : NULL;
if (sep) { *sep = 0; tok = sep + 1; nf++; }
else { nf = i + 1; break; }
}
if (nf < 7) continue;
double x = atof(fields[3]);
double y = atof(fields[4]);
double w = atof(fields[5]);
double h = atof(fields[6]);
ERL_NIF_TERM frame = enif_make_tuple4(env,
enif_make_double(env, x), enif_make_double(env, y),
enif_make_double(env, w), enif_make_double(env, h));
// label and value: non-empty → binary, empty → atom nil
size_t llen = strlen(fields[1]);
size_t vlen = strlen(fields[2]);
ERL_NIF_TERM label = llen > 0 ? cstr_to_bin(env, fields[1], llen)
: enif_make_atom(env, "nil");
ERL_NIF_TERM value = vlen > 0 ? cstr_to_bin(env, fields[2], vlen)
: enif_make_atom(env, "nil");
items[count++] = enif_make_tuple4(env,
enif_make_atom(env, fields[0]),
label, value, frame);
}
(*jenv)->ReleaseStringUTFChars(jenv, jresult, raw);
(*jenv)->DeleteLocalRef(jenv, jresult);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
// Build list from items array (forward order)
list = enif_make_list(env, 0);
for (int i = count - 1; i >= 0; i--)
list = enif_make_list_cell(env, items[i], list);
return list;
}
// nif_ui_debug/0 — returns raw uiTree string as a binary (for debugging)
static ERL_NIF_TERM nif_ui_debug(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.ui_tree) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
int att; JNIEnv* jenv = get_jenv(&att);
jstring jresult = (jstring)(*jenv)->CallStaticObjectMethod(jenv, Bridge.cls, Bridge.ui_tree);
ERL_NIF_TERM result = jstring_to_bin(env, jenv, jresult);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return result;
}
// nif_tap/1 — tap by accessibility label binary
static ERL_NIF_TERM nif_tap(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.tap_by_label) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin)) return enif_make_badarg(env);
char* label = (char*)malloc(bin.size + 1);
if (!label) return enif_make_atom(env, "error");
memcpy(label, bin.data, bin.size);
label[bin.size] = 0;
int att; JNIEnv* jenv = get_jenv(&att);
jstring jlabel = (*jenv)->NewStringUTF(jenv, label);
free(label);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.tap_by_label, jlabel);
(*jenv)->DeleteLocalRef(jenv, jlabel);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "no_element_with_label"));
}
// nif_tap_xy/2 — tap at (x, y) dp coordinates
static ERL_NIF_TERM nif_tap_xy(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.tap_xy) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
double x, y;
if (!enif_get_double(env, argv[0], &x)) { int ix; if (!enif_get_int(env, argv[0], &ix)) return enif_make_badarg(env); x = ix; }
if (!enif_get_double(env, argv[1], &y)) { int iy; if (!enif_get_int(env, argv[1], &iy)) return enif_make_badarg(env); y = iy; }
int att; JNIEnv* jenv = get_jenv(&att);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.tap_xy, (jfloat)x, (jfloat)y);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "dispatch_failed"));
}
// nif_type_text/1 — type text into the focused view
static ERL_NIF_TERM nif_type_text(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.type_text) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin)) return enif_make_badarg(env);
char* text = (char*)malloc(bin.size + 1);
if (!text) return enif_make_atom(env, "error");
memcpy(text, bin.data, bin.size);
text[bin.size] = 0;
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtext = (*jenv)->NewStringUTF(jenv, text);
free(text);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.type_text, jtext);
(*jenv)->DeleteLocalRef(jenv, jtext);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "no_first_responder"));
}
// nif_delete_backward/0 — delete one character backward
static ERL_NIF_TERM nif_delete_backward(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.delete_backward) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
int att; JNIEnv* jenv = get_jenv(&att);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.delete_backward);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "no_first_responder"));
}
// nif_key_press/1 — not yet implemented on Android (no KeyCharacterMap lookup)
static ERL_NIF_TERM nif_key_press(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_implemented"));
}
// nif_clear_text/0 — select-all + delete in the focused view
static ERL_NIF_TERM nif_clear_text(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.clear_text) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
int att; JNIEnv* jenv = get_jenv(&att);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.clear_text);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "no_first_responder"));
}
// nif_long_press_xy/3 — long press at (x, y) for duration_ms milliseconds
static ERL_NIF_TERM nif_long_press_xy(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.long_press_xy) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
double x, y; int dur;
if (!enif_get_double(env, argv[0], &x)) { int ix; if (!enif_get_int(env, argv[0], &ix)) return enif_make_badarg(env); x = ix; }
if (!enif_get_double(env, argv[1], &y)) { int iy; if (!enif_get_int(env, argv[1], &iy)) return enif_make_badarg(env); y = iy; }
if (!enif_get_int(env, argv[2], &dur)) return enif_make_badarg(env);
int att; JNIEnv* jenv = get_jenv(&att);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.long_press_xy,
(jfloat)x, (jfloat)y, (jlong)dur);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "dispatch_failed"));
}
// nif_swipe_xy/4 — swipe from (x1,y1) to (x2,y2) in dp
static ERL_NIF_TERM nif_swipe_xy(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (!Bridge.swipe_xy) return enif_make_tuple2(env,
enif_make_atom(env, "error"), enif_make_atom(env, "not_loaded"));
double x1, y1, x2, y2;
if (!enif_get_double(env, argv[0], &x1)) { int i; if (!enif_get_int(env, argv[0], &i)) return enif_make_badarg(env); x1 = i; }
if (!enif_get_double(env, argv[1], &y1)) { int i; if (!enif_get_int(env, argv[1], &i)) return enif_make_badarg(env); y1 = i; }
if (!enif_get_double(env, argv[2], &x2)) { int i; if (!enif_get_int(env, argv[2], &i)) return enif_make_badarg(env); x2 = i; }
if (!enif_get_double(env, argv[3], &y2)) { int i; if (!enif_get_int(env, argv[3], &i)) return enif_make_badarg(env); y2 = i; }
int att; JNIEnv* jenv = get_jenv(&att);
jboolean ok = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.swipe_xy,
(jfloat)x1, (jfloat)y1, (jfloat)x2, (jfloat)y2);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ok ? enif_make_atom(env, "ok")
: enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "dispatch_failed"));
}
// ── Storage ───────────────────────────────────────────────────────────────────
static ERL_NIF_TERM nif_storage_dir(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char loc[32]; enif_get_atom(env, argv[0], loc, sizeof(loc), ERL_NIF_LATIN1);
int att; JNIEnv* jenv = get_jenv(&att);
jstring jloc = (*jenv)->NewStringUTF(jenv, loc);
jstring result = (jstring)(*jenv)->CallStaticObjectMethod(jenv, Bridge.cls, Bridge.storage_dir, jloc);
(*jenv)->DeleteLocalRef(jenv, jloc);
ERL_NIF_TERM ret;
if (result) {
const char* utf8 = (*jenv)->GetStringUTFChars(jenv, result, NULL);
ErlNifBinary bin; size_t len = strlen(utf8);
enif_alloc_binary(len, &bin); memcpy(bin.data, utf8, len);
(*jenv)->ReleaseStringUTFChars(jenv, result, utf8);
(*jenv)->DeleteLocalRef(jenv, result);
ret = enif_make_binary(env, &bin);
} else {
ret = enif_make_atom(env, "nil");
}
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ret;
}
static ERL_NIF_TERM nif_storage_save_to_media_store(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin)) return enif_make_badarg(env);
char* path = malloc(bin.size + 1);
memcpy(path, bin.data, bin.size); path[bin.size] = 0;
char type[16] = "auto"; enif_get_atom(env, argv[1], type, sizeof(type), ERL_NIF_LATIN1);
ErlNifPid pid; enif_self(env, &pid);
ERL_NIF_TERM result = call_bridge_pid_str2(env, Bridge.storage_save_to_media_store, pid, path, type);
free(path);
return result;
}
static ERL_NIF_TERM nif_storage_external_files_dir(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
char type[32] = {0}; enif_get_atom(env, argv[0], type, sizeof(type), ERL_NIF_LATIN1);
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtype = (*jenv)->NewStringUTF(jenv, type);
jstring result = (jstring)(*jenv)->CallStaticObjectMethod(jenv, Bridge.cls,
Bridge.storage_external_files_dir, jtype);
(*jenv)->DeleteLocalRef(jenv, jtype);
ERL_NIF_TERM ret;
if (result) {
const char* utf8 = (*jenv)->GetStringUTFChars(jenv, result, NULL);
ErlNifBinary bin; size_t len = strlen(utf8);
enif_alloc_binary(len, &bin); memcpy(bin.data, utf8, len);
(*jenv)->ReleaseStringUTFChars(jenv, result, utf8);
(*jenv)->DeleteLocalRef(jenv, result);
ret = enif_make_binary(env, &bin);
} else {
ret = enif_make_atom(env, "nil");
}
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return ret;
}
static ERL_NIF_TERM nif_storage_save_to_photo_library(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "not_supported"));
}
// ── WebView ────────────────────────────────────────────────────────────────────
// ── Alert delivery (called from beam_jni.c when a dialog button is tapped) ──
void mob_deliver_alert_action(const char* action) {
ErlNifEnv* e = enif_alloc_env();
ErlNifPid pid;
if (!enif_whereis_pid(e, enif_make_atom(e, "mob_screen"), &pid)) {
enif_free_env(e); return;
}
ERL_NIF_TERM msg = enif_make_tuple2(e,
enif_make_atom(e, "alert"),
enif_make_atom(e, action));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
}
// ── NIF: alert_show/3 ─────────────────────────────────────────────────────
static ERL_NIF_TERM nif_alert_show(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary title_bin, msg_bin, btns_bin;
if (!enif_inspect_binary(env, argv[0], &title_bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &title_bin))
return enif_make_badarg(env);
if (!enif_inspect_binary(env, argv[1], &msg_bin) &&
!enif_inspect_iolist_as_binary(env, argv[1], &msg_bin))
return enif_make_badarg(env);
if (!enif_inspect_binary(env, argv[2], &btns_bin) &&
!enif_inspect_iolist_as_binary(env, argv[2], &btns_bin))
return enif_make_badarg(env);
char* title = malloc(title_bin.size + 1);
memcpy(title, title_bin.data, title_bin.size);
title[title_bin.size] = '\0';
char* message = malloc(msg_bin.size + 1);
memcpy(message, msg_bin.data, msg_bin.size);
message[msg_bin.size] = '\0';
char* btns = malloc(btns_bin.size + 1);
memcpy(btns, btns_bin.data, btns_bin.size);
btns[btns_bin.size] = '\0';
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtitle = (*jenv)->NewStringUTF(jenv, title);
jstring jmessage = (*jenv)->NewStringUTF(jenv, message);
jstring jbtns = (*jenv)->NewStringUTF(jenv, btns);
free(title); free(message); free(btns);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.alert_show, jtitle, jmessage, jbtns);
(*jenv)->DeleteLocalRef(jenv, jtitle);
(*jenv)->DeleteLocalRef(jenv, jmessage);
(*jenv)->DeleteLocalRef(jenv, jbtns);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── NIF: action_sheet_show/2 ──────────────────────────────────────────────
static ERL_NIF_TERM nif_action_sheet_show(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary title_bin, btns_bin;
if (!enif_inspect_binary(env, argv[0], &title_bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &title_bin))
return enif_make_badarg(env);
if (!enif_inspect_binary(env, argv[1], &btns_bin) &&
!enif_inspect_iolist_as_binary(env, argv[1], &btns_bin))
return enif_make_badarg(env);
char* title = malloc(title_bin.size + 1);
memcpy(title, title_bin.data, title_bin.size);
title[title_bin.size] = '\0';
char* btns = malloc(btns_bin.size + 1);
memcpy(btns, btns_bin.data, btns_bin.size);
btns[btns_bin.size] = '\0';
int att; JNIEnv* jenv = get_jenv(&att);
jstring jtitle = (*jenv)->NewStringUTF(jenv, title);
jstring jbtns = (*jenv)->NewStringUTF(jenv, btns);
free(title); free(btns);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.action_sheet_show, jtitle, jbtns);
(*jenv)->DeleteLocalRef(jenv, jtitle);
(*jenv)->DeleteLocalRef(jenv, jbtns);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── NIF: toast_show/2 ────────────────────────────────────────────────────
static ERL_NIF_TERM nif_toast_show(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary msg_bin;
char dur[8] = "short";
if (!enif_inspect_binary(env, argv[0], &msg_bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &msg_bin))
return enif_make_badarg(env);
enif_get_atom(env, argv[1], dur, sizeof(dur), ERL_NIF_LATIN1);
char* msg = malloc(msg_bin.size + 1);
memcpy(msg, msg_bin.data, msg_bin.size);
msg[msg_bin.size] = '\0';
int att; JNIEnv* jenv = get_jenv(&att);
jstring jmsg = (*jenv)->NewStringUTF(jenv, msg);
jstring jdur = (*jenv)->NewStringUTF(jenv, dur);
free(msg);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.toast_show, jmsg, jdur);
(*jenv)->DeleteLocalRef(jenv, jmsg);
(*jenv)->DeleteLocalRef(jenv, jdur);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_webview_eval_js(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* code = malloc(bin.size + 1);
memcpy(code, bin.data, bin.size);
code[bin.size] = '\0';
int att; JNIEnv* jenv = get_jenv(&att);
jstring jcode = (*jenv)->NewStringUTF(jenv, code);
free(code);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.webview_eval_js, jcode);
(*jenv)->DeleteLocalRef(jenv, jcode);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_webview_post_message(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);
char* json = malloc(bin.size + 1);
memcpy(json, bin.data, bin.size);
json[bin.size] = '\0';
int att; JNIEnv* jenv = get_jenv(&att);
jstring jjson = (*jenv)->NewStringUTF(jenv, json);
free(json);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.webview_post_message, jjson);
(*jenv)->DeleteLocalRef(jenv, jjson);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_webview_can_go_back(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
jboolean result = (*jenv)->CallStaticBooleanMethod(jenv, Bridge.cls, Bridge.webview_can_go_back);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, result ? "true" : "false");
}
static ERL_NIF_TERM nif_webview_go_back(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.webview_go_back);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── Native view component NIFs ────────────────────────────────────────────────
static ERL_NIF_TERM nif_register_component(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
ErlNifPid pid;
if (!enif_get_local_pid(env, argv[0], &pid))
return enif_make_badarg(env);
enif_mutex_lock(component_mutex);
for (int i = 0; i < MAX_COMPONENT_HANDLES; i++) {
if (!component_handles[i].active) {
component_handles[i].pid = pid;
component_handles[i].active = 1;
enif_mutex_unlock(component_mutex);
return enif_make_int(env, i);
}
}
enif_mutex_unlock(component_mutex);
return enif_make_badarg(env);
}
static ERL_NIF_TERM nif_deregister_component(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int handle;
if (!enif_get_int(env, argv[0], &handle) || handle < 0 || handle >= MAX_COMPONENT_HANDLES)
return enif_make_badarg(env);
enif_mutex_lock(component_mutex);
component_handles[handle].active = 0;
enif_mutex_unlock(component_mutex);
return enif_make_atom(env, "ok");
}
// ── NIF: background_keep_alive/0, background_stop/0 ─────────────────────────
static ERL_NIF_TERM nif_background_keep_alive(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.background_keep_alive);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_background_stop(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
int att; JNIEnv* jenv = get_jenv(&att);
(*jenv)->CallStaticVoidMethod(jenv, Bridge.cls, Bridge.background_stop);
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
return enif_make_atom(env, "ok");
}
// ── Mob.Device — lifecycle events + queries ─────────────────────────────────
//
// Android implementation pending. The Elixir-side API works (subscribe will
// succeed, no events will fire on Android until ProcessLifecycleObserver
// + ComponentCallbacks2 are wired up in MainActivity / Application).
// Query NIFs return reasonable defaults for now.
static ERL_NIF_TERM nif_device_set_dispatcher(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): wire ProcessLifecycleObserver + ComponentCallbacks2.
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM nif_device_battery_state(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): query BatteryManager. For now, unknown / -1.
return enif_make_tuple2(env,
enif_make_atom(env, "unknown"),
enif_make_int(env, -1));
}
static ERL_NIF_TERM nif_device_thermal_state(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): query PowerManager.getCurrentThermalStatus() (API 29+).
return enif_make_atom(env, "nominal");
}
static ERL_NIF_TERM nif_device_low_power_mode(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): query PowerManager.isPowerSaveMode().
return enif_make_atom(env, "false");
}
static ERL_NIF_TERM nif_device_foreground(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): track via ProcessLifecycleOwner.
return enif_make_atom(env, "true");
}
static ERL_NIF_TERM nif_device_os_version(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): Build.VERSION.RELEASE via JNI.
return enif_make_string(env, "", ERL_NIF_LATIN1);
}
static ERL_NIF_TERM nif_device_model(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
// TODO(android): Build.MODEL via JNI.
return enif_make_string(env, "Android", ERL_NIF_LATIN1);
}
// ── NIF table & load ──────────────────────────────────────────────────────────
static ErlNifFunc nif_funcs[] = {
// ── Test harness first (matches iOS nif_funcs[] ordering convention) ──────
{"ui_tree", 0, nif_ui_tree, 0},
{"ui_debug", 0, nif_ui_debug, 0},
{"tap", 1, nif_tap, 0},
{"tap_xy", 2, nif_tap_xy, 0},
{"type_text", 1, nif_type_text, 0},
{"delete_backward", 0, nif_delete_backward, 0},
{"key_press", 1, nif_key_press, 0},
{"clear_text", 0, nif_clear_text, 0},
{"long_press_xy", 3, nif_long_press_xy, 0},
{"swipe_xy", 4, nif_swipe_xy, 0},
// ── Core mob functions ────────────────────────────────────────────────────
{"platform", 0, nif_platform, 0},
{"log", 1, nif_log, 0},
{"log", 2, nif_log2, 0},
{"set_transition", 1, nif_set_transition, 0},
{"set_root", 1, nif_set_root, 0},
{"register_tap", 1, nif_register_tap, 0},
{"clear_taps", 0, nif_clear_taps, 0},
{"exit_app", 0, nif_exit_app, 0},
{"safe_area", 0, nif_safe_area, 0},
{"haptic", 1, nif_haptic, 0},
{"clipboard_put", 1, nif_clipboard_put, 0},
{"clipboard_get", 0, nif_clipboard_get, 0},
{"share_text", 1, nif_share_text, 0},
{"request_permission", 1, nif_request_permission, 0},
{"biometric_authenticate", 1, nif_biometric_authenticate, 0},
{"location_get_once", 0, nif_location_get_once, 0},
{"location_start", 1, nif_location_start, 0},
{"location_stop", 0, nif_location_stop, 0},
{"camera_capture_photo", 1, nif_camera_capture_photo, 0},
{"camera_capture_video", 1, nif_camera_capture_video, 0},
{"camera_start_preview", 1, nif_camera_start_preview, 0},
{"camera_stop_preview", 0, nif_camera_stop_preview, 0},
{"photos_pick", 2, nif_photos_pick, 0},
{"files_pick", 1, nif_files_pick, 0},
{"audio_start_recording", 1, nif_audio_start_recording, 0},
{"audio_stop_recording", 0, nif_audio_stop_recording, 0},
{"audio_play", 2, nif_audio_play, 0},
{"audio_stop_playback", 0, nif_audio_stop_playback, 0},
{"audio_set_volume", 1, nif_audio_set_volume, 0},
{"motion_start", 2, nif_motion_start, 0},
{"motion_stop", 0, nif_motion_stop, 0},
{"scanner_scan", 1, nif_scanner_scan, 0},
{"notify_schedule", 1, nif_notify_schedule, 0},
{"notify_cancel", 1, nif_notify_cancel, 0},
{"notify_register_push", 0, nif_notify_register_push, 0},
{"take_launch_notification", 0, nif_take_launch_notification, 0},
{"storage_dir", 1, nif_storage_dir, 0},
{"storage_save_to_media_store", 2, nif_storage_save_to_media_store, 0},
{"storage_external_files_dir", 1, nif_storage_external_files_dir, 0},
{"storage_save_to_photo_library", 1, nif_storage_save_to_photo_library, 0},
{"alert_show", 3, nif_alert_show, 0},
{"action_sheet_show", 2, nif_action_sheet_show, 0},
{"toast_show", 2, nif_toast_show, 0},
{"webview_eval_js", 1, nif_webview_eval_js, 0},
{"webview_post_message",1, nif_webview_post_message,0},
{"webview_can_go_back", 0, nif_webview_can_go_back, 0},
{"webview_go_back", 0, nif_webview_go_back, 0},
{"register_component", 1, nif_register_component, 0},
{"deregister_component", 1, nif_deregister_component, 0},
{"background_keep_alive", 0, nif_background_keep_alive, 0},
{"background_stop", 0, nif_background_stop, 0},
// ── Mob.Device — lifecycle events + queries (Android stubs) ───────────────
{"device_set_dispatcher", 1, nif_device_set_dispatcher, 0},
{"device_battery_state", 0, nif_device_battery_state, 0},
{"device_thermal_state", 0, nif_device_thermal_state, 0},
{"device_low_power_mode", 0, nif_device_low_power_mode, 0},
{"device_foreground", 0, nif_device_foreground, 0},
{"device_os_version", 0, nif_device_os_version, 0},
{"device_model", 0, nif_device_model, 0},
};
static int nif_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) {
LOGI("nif_load: entered, Bridge.cls=%p", (void*)Bridge.cls);
if (!Bridge.cls) { LOGE("Bridge.cls not cached — was mob_ui_cache_class called?"); return -1; }
tap_mutex = enif_mutex_create("mob_tap_mutex");
if (!tap_mutex) { LOGE("nif_load: failed to create tap mutex"); return -1; }
component_mutex = enif_mutex_create("mob_component_mutex");
if (!component_mutex) { LOGE("nif_load: failed to create component mutex"); return -1; }
int att; JNIEnv* jenv = get_jenv(&att);
Bridge.set_root = (*jenv)->GetStaticMethodID(jenv, Bridge.cls,
"setRootJson", "(Ljava/lang/String;Ljava/lang/String;)V");
if (!Bridge.set_root) { LOGE("nif_load: setRootJson(String,String) not found on MobBridge"); return -1; }
Bridge.move_to_back = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "moveToBack", "()V");
if (!Bridge.move_to_back) { LOGE("nif_load: moveToBack() not found on MobBridge"); return -1; }
Bridge.get_safe_area = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "getSafeArea", "()[F");
if (!Bridge.get_safe_area) { LOGE("nif_load: getSafeArea() not found on MobBridge"); return -1; }
Bridge.haptic = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "haptic", "(Ljava/lang/String;)V");
if (!Bridge.haptic) { LOGE("nif_load: haptic(String) not found on MobBridge"); return -1; }
Bridge.clipboard_put = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "clipboardPut", "(Ljava/lang/String;)V");
if (!Bridge.clipboard_put) { LOGE("nif_load: clipboardPut(String) not found on MobBridge"); return -1; }
Bridge.clipboard_get = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "clipboardGet", "()Ljava/lang/String;");
if (!Bridge.clipboard_get) { LOGE("nif_load: clipboardGet() not found on MobBridge"); return -1; }
Bridge.share_text = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, "shareText", "(Ljava/lang/String;)V");
if (!Bridge.share_text) { LOGE("nif_load: shareText(String) not found on MobBridge"); return -1; }
#define CACHE(name, sig) \
Bridge.name = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, #name, sig); \
if (!Bridge.name) { LOGE("nif_load: " #name " not found"); return -1; }
CACHE(request_permission, "(JLjava/lang/String;)V")
CACHE(biometric_authenticate, "(JLjava/lang/String;)V")
CACHE(location_get_once, "(JLjava/lang/String;)V")
CACHE(location_start, "(JLjava/lang/String;)V")
CACHE(location_stop, "()V")
CACHE(camera_capture_photo, "(JLjava/lang/String;)V")
CACHE(camera_capture_video, "(JLjava/lang/String;)V")
CACHE(camera_start_preview, "(JLjava/lang/String;)V")
CACHE(camera_stop_preview, "()V")
CACHE(photos_pick, "(JLjava/lang/String;)V")
CACHE(files_pick, "(JLjava/lang/String;)V")
CACHE(audio_start_recording, "(JLjava/lang/String;)V")
CACHE(audio_stop_recording, "()V")
CACHE(audio_play, "(JLjava/lang/String;Ljava/lang/String;)V")
CACHE(audio_stop_playback, "()V")
CACHE(audio_set_volume, "(Ljava/lang/String;)V")
CACHE(storage_dir, "(Ljava/lang/String;)Ljava/lang/String;")
CACHE(storage_save_to_media_store, "(JLjava/lang/String;Ljava/lang/String;)V")
CACHE(storage_external_files_dir, "(Ljava/lang/String;)Ljava/lang/String;")
CACHE(alert_show, "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")
CACHE(action_sheet_show, "(Ljava/lang/String;Ljava/lang/String;)V")
CACHE(toast_show, "(Ljava/lang/String;Ljava/lang/String;)V")
CACHE(webview_eval_js, "(Ljava/lang/String;)V")
CACHE(webview_post_message, "(Ljava/lang/String;)V")
CACHE(webview_can_go_back, "()Z")
CACHE(webview_go_back, "()V")
CACHE(motion_start, "(JLjava/lang/String;)V")
CACHE(motion_stop, "()V")
CACHE(scanner_scan, "(JLjava/lang/String;)V")
CACHE(notify_schedule, "(JLjava/lang/String;)V")
CACHE(notify_cancel, "(Ljava/lang/String;)V")
CACHE(notify_register_push, "(JLjava/lang/String;)V")
CACHE(background_keep_alive, "()V")
CACHE(background_stop, "()V")
#undef CACHE
g_launch_notif_mutex = enif_mutex_create("mob_launch_notif_mutex");
if (!g_launch_notif_mutex) { LOGE("nif_load: failed to create launch notif mutex"); return -1; }
// ── Test harness method IDs (optional — clear exception if not present) ────
#define CACHE_OPT(field, name, sig) \
Bridge.field = (*jenv)->GetStaticMethodID(jenv, Bridge.cls, name, sig); \
if (!Bridge.field) { (*jenv)->ExceptionClear(jenv); LOGI("nif_load: %s not found (optional)", name); }
CACHE_OPT(ui_tree, "uiTree", "()Ljava/lang/String;")
CACHE_OPT(tap_xy, "tapXy", "(FF)Z")
CACHE_OPT(tap_by_label, "tapByLabel", "(Ljava/lang/String;)Z")
CACHE_OPT(type_text, "typeText", "(Ljava/lang/String;)Z")
CACHE_OPT(delete_backward,"deleteBackward","()Z")
CACHE_OPT(clear_text, "clearText", "()Z")
CACHE_OPT(long_press_xy, "longPressXy", "(FFJ)Z")
CACHE_OPT(swipe_xy, "swipeXy", "(FFFF)Z")
#undef CACHE_OPT
if (att) (*g_jvm)->DetachCurrentThread(g_jvm);
LOGI("Mob NIF loaded (Compose backend)");
return 0;
}
ERL_NIF_INIT(mob_nif, nif_funcs, nif_load, NULL, NULL, NULL)