Skip to main content

priv/native/jni/mob_notify_nif.zig

//! mob_notify_nif — Android local + push notifications tier-1 ZIG plugin NIF.
//!
//! Extracted from mob-core's `mob_nif.zig`: nif_notify_schedule /
//! nif_notify_cancel / nif_notify_register_push (mob_nif.zig:2727-2771
//! pre-strip). The Kotlin side is the plugin-owned bridge class
//! `io.mob.notify.MobNotifyBridge` (NotificationManager + AlarmManager +
//! FirebaseMessaging). DELIVERY stays in core/host: the host's
//! NotificationReceiver / MobFirebaseService / MainActivity send through
//! core's mob_deliver_notification / mob_deliver_push_token thunks, keyed on
//! the shared io.mob.plugin.MobNotifyHub.notifyPid (generated by mob_dev).
//!
//! Delivered message shape (this thunk, exact core parity with
//! mob_deliver_push_token, mob_nif.zig:2314-2326):
//!   {:push_token, :android, token_binary}
//!
//! Build path: compiled via `addZigObject` from `-Dplugin_zig_nifs`, reaching
//! mob-core ERTS / JNI bindings through `@import("erts")` / `@import("jni")`.
//! `get_jenv` + `g_jvm` are mob-core exports linked into the same `.so`.
const std = @import("std");
const erts = @import("erts");
const jni = @import("jni");

// mob-core exports (linked into the same .so). NOT duplicated.
extern fn get_jenv(attached: *c_int) ?*jni.JNIEnv;
extern var g_jvm: ?*jni.JavaVM;

// ── Plugin-owned bridge-class method-id cache ────────────────────────────
const NotifyMethods = struct {
    notify_schedule: jni.JMethodID = null,
    notify_cancel: jni.JMethodID = null,
    notify_register_push: jni.JMethodID = null,
};

var g_notify: NotifyMethods = .{};
var g_notify_cls: jni.JClass = null;

// ── nativeRegister thunk — cache the bridge jclass + method ids ───────────
export fn Java_io_mob_notify_MobNotifyBridge_nativeRegister(jenv: *jni.JNIEnv, cls: jni.JClass) callconv(.c) void {
    g_notify_cls = jni.newGlobalRef(jenv, cls);
    if (g_notify_cls == null) return;
    g_notify.notify_schedule = jni.getStaticMethodID(jenv, cls, "notify_schedule", "(JLjava/lang/String;)V");
    g_notify.notify_cancel = jni.getStaticMethodID(jenv, cls, "notify_cancel", "(Ljava/lang/String;)V");
    g_notify.notify_register_push = jni.getStaticMethodID(jenv, cls, "notify_register_push", "(J)V");
}

// ── Thread-attach + pid round-trip helpers (mirror mob-core / camera) ─────
inline fn detachIfAttached(attached: c_int) void {
    if (attached != 0) {
        if (g_jvm) |jvm| jni.detachCurrentThread(jvm);
    }
}

inline fn pidToJlong(pid: erts.ErlNifPid) jni.JLong {
    if (@sizeOf(erts.ERL_NIF_TERM) == @sizeOf(jni.JLong)) {
        return @bitCast(pid.pid);
    }
    return @intCast(pid.pid);
}

inline fn pidFromLong(jpid: jni.JLong) erts.ErlNifPid {
    if (@sizeOf(erts.ERL_NIF_TERM) == @sizeOf(jni.JLong)) {
        return .{ .pid = @bitCast(jpid) };
    }
    const low: u32 = @truncate(@as(u64, @bitCast(jpid)));
    return .{ .pid = low };
}

/// Call `MobNotifyBridge.<method>(pid_long, arg)` — async. Returns :ok.
fn callBridgePidStr(env: ?*erts.ErlNifEnv, method: jni.JMethodID, pid: erts.ErlNifPid, arg: ?[*:0]const u8) erts.ERL_NIF_TERM {
    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    const jarg: jni.JString = if (arg) |a| jni.newStringUTF(jenv, a) else null;
    jenv.*.CallStaticVoidMethod.?(jenv, g_notify_cls, method, pidToJlong(pid), jarg);
    if (jarg != null) jni.deleteLocalRef(jenv, jarg);
    detachIfAttached(attached);
    return erts.ok(env);
}

// ── Inbound delivery thunk ────────────────────────────────────────────────

// {:push_token, :android, token_binary} — EXACT replica of core's
// mob_deliver_push_token (mob_nif.zig:2314-2326). Only register_push's
// immediate token lands here; REFRESHED tokens still flow through the host's
// MobFirebaseService → core thunk (delivery stays in core).
export fn Java_io_mob_notify_MobNotifyBridge_nativeDeliverNotifyPushToken(
    jenv: *jni.JNIEnv,
    cls: jni.JClass,
    pid_long: jni.JLong,
    token: jni.JString,
) callconv(.c) void {
    _ = cls;
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);

    const token_c = jenv.*.GetStringUTFChars.?(jenv, token, null) orelse return;
    defer jenv.*.ReleaseStringUTFChars.?(jenv, token, token_c);

    const tl = std.mem.len(token_c);
    var tb: erts.ErlNifBinary = undefined;
    if (erts.enif_alloc_binary(tl, &tb) == 0) return;
    @memcpy(tb.data[0..tl], token_c[0..tl]);

    const msg = erts.makeTuple(env, .{
        erts.atom(env, "push_token"),
        erts.atom(env, "android"),
        erts.enif_make_binary(env, &tb),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── NIFs ──────────────────────────────────────────────────────────────────

// PARITY: core's nif_notify_schedule (mob_nif.zig:2727-2738) passed the JSON
// opts binary straight through to the bridge with the caller pid.
fn nif_notify_schedule(env: ?*erts.ErlNifEnv, argc: c_int, argv: [*]const erts.ERL_NIF_TERM) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    var bin: erts.ErlNifBinary = undefined;
    if (erts.enif_inspect_binary(env, argv[0], &bin) == 0 and
        erts.enif_inspect_iolist_as_binary(env, argv[0], &bin) == 0)
    {
        return erts.badarg(env);
    }
    var buf: [4096]u8 = @splat(0);
    if (bin.size + 1 > buf.len) return erts.badarg(env);
    @memcpy(buf[0..bin.size], bin.data[0..bin.size]);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    return callBridgePidStr(env, g_notify.notify_schedule, pid, jni.asCStr(&buf));
}

// PARITY: core's nif_notify_cancel (mob_nif.zig:2741-2755) — id string only,
// no pid (cancel has no reply).
fn nif_notify_cancel(env: ?*erts.ErlNifEnv, argc: c_int, argv: [*]const erts.ERL_NIF_TERM) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    var bin: erts.ErlNifBinary = undefined;
    if (erts.enif_inspect_binary(env, argv[0], &bin) == 0 and
        erts.enif_inspect_iolist_as_binary(env, argv[0], &bin) == 0)
    {
        return erts.badarg(env);
    }
    var buf: [256]u8 = @splat(0);
    if (bin.size + 1 > buf.len) return erts.badarg(env);
    @memcpy(buf[0..bin.size], bin.data[0..bin.size]);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    const js: jni.JString = jni.newStringUTF(jenv, jni.asCStr(&buf));
    jenv.*.CallStaticVoidMethod.?(jenv, g_notify_cls, g_notify.notify_cancel, js);
    if (js != null) jni.deleteLocalRef(jenv, js);
    detachIfAttached(attached);
    return erts.ok(env);
}

// PARITY: core's nif_notify_register_push (mob_nif.zig:2761-2770) — pid only
// (core passed a null string arg; the plugin bridge signature drops it).
fn nif_notify_register_push(env: ?*erts.ErlNifEnv, argc: c_int, argv: [*]const erts.ERL_NIF_TERM) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    _ = argv;
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    jenv.*.CallStaticVoidMethod.?(jenv, g_notify_cls, g_notify.notify_register_push, pidToJlong(pid));
    detachIfAttached(attached);
    return erts.ok(env);
}

// ── NIF table + init entry point ─────────────────────────────────────────
fn nifLoad(env: ?*erts.ErlNifEnv, priv: *?*anyopaque, info: erts.ERL_NIF_TERM) callconv(.c) c_int {
    _ = env;
    _ = priv;
    _ = info;
    return 0;
}

const nif_funcs = [_]erts.ErlNifFunc{
    .{ .name = "notify_schedule", .arity = 1, .fptr = nif_notify_schedule, .flags = 0 },
    .{ .name = "notify_cancel", .arity = 1, .fptr = nif_notify_cancel, .flags = 0 },
    .{ .name = "notify_register_push", .arity = 0, .fptr = nif_notify_register_push, .flags = 0 },
};

var nif_entry: erts.ErlNifEntry = .{
    .major = erts.ERL_NIF_MAJOR_VERSION,
    .minor = erts.ERL_NIF_MINOR_VERSION,
    .name = "mob_notify_nif",
    .num_of_funcs = nif_funcs.len,
    .funcs = &nif_funcs,
    .load = nifLoad,
    .reload = null,
    .upgrade = null,
    .unload = null,
    .vm_variant = erts.ERL_NIF_VM_VARIANT,
    .options = 1,
    .sizeof_ErlNifResourceTypeInit = erts.SIZEOF_ErlNifResourceTypeInit,
    .min_erts = erts.ERL_NIF_MIN_ERTS_VERSION,
};

pub export fn mob_notify_nif_nif_init() callconv(.c) *erts.ErlNifEntry {
    return &nif_entry;
}