Packages
mob
0.3.9
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 "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 photos_pick;
jmethodID files_pick;
jmethodID audio_start_recording;
jmethodID audio_stop_recording;
jmethodID motion_start;
jmethodID motion_stop;
jmethodID scanner_scan;
jmethodID notify_schedule;
jmethodID notify_cancel;
jmethodID notify_register_push;
jmethodID take_launch_notification;
// Cached before nif_load (used during BEAM startup before NIFs are loaded)
jmethodID set_startup_phase;
jmethodID set_startup_error;
} 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}
} 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
// 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"); }
// ── 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;
}
}
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);
}
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_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_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 ─────────────────────────────────────────────────────────
static ErlNifFunc nif_funcs[] = {
{"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},
{"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},
{"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},
};
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; }
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(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(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")
#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; }
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)