Current section
Files
Jump to
Current section
Files
priv/native/android/MobNotifyBridge.kt
// mob_notify plugin — Android bridge (local notifications via AlarmManager +
// push registration via FirebaseMessaging).
//
// Extracted from mob-core's MobBridge notify_schedule / notify_cancel /
// notify_register_push (MobBridge.kt.eex ~1379-1444 pre-strip). DELIVERY
// stays in core/host: the host-package NotificationReceiver displays the
// scheduled notification + the tap routes through MainActivity.onNewIntent;
// MobFirebaseService handles incoming pushes + token refreshes. The shared
// state lives in io.mob.plugin.MobNotifyHub (GENERATED by mob_dev next to
// MobActivityAware) — the stable seam both sides can reference, since this
// bridge can't name host-package classes and the host can't name plugin
// packages without breaking plugin-less builds.
//
// HOST-CLASS NAME ASSUMPTION: the alarm intent targets
// "<applicationId>.NotificationReceiver" via setClassName — true for every
// mob_new-generated app (the Kotlin package IS the applicationId). A host
// with a divergent package must keep its receiver at that name.
package io.mob.notify
import android.app.Activity
import android.app.AlarmManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import io.mob.plugin.MobNotifyHub
import java.lang.ref.WeakReference
object MobNotifyBridge : io.mob.plugin.MobActivityAware {
private var activityRef: WeakReference<Activity>? = null
@JvmStatic external fun nativeRegister()
// {:push_token, :android, token} — register_push's immediate token only;
// refreshed tokens flow through the host MobFirebaseService → core thunk.
@JvmStatic external fun nativeDeliverNotifyPushToken(pid: Long, token: String)
@JvmStatic fun register() = nativeRegister()
override fun setActivity(activity: Activity) {
activityRef = WeakReference(activity)
}
// Logic copied from core MobBridge.notify_schedule: parse the opts JSON,
// ensure the channel, arm an exact AlarmManager broadcast at trigger_at
// targeting the HOST's NotificationReceiver (which displays + handles tap).
// pid is accepted for zig-call parity; scheduling has no direct reply.
@JvmStatic
fun notify_schedule(pid: Long, optsJson: String) {
val activity = activityRef?.get() ?: return
try {
val opts = org.json.JSONObject(optsJson)
val id = opts.getString("id")
val title = opts.getString("title")
val body = opts.getString("body")
val triggerAt = opts.getLong("trigger_at") * 1000L // to ms
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= 26) {
nm.createNotificationChannel(
NotificationChannel(MobNotifyHub.CHANNEL_ID, "Notifications", NotificationManager.IMPORTANCE_DEFAULT))
}
val intent = Intent().apply {
setClassName(activity, activity.packageName + ".NotificationReceiver")
putExtra("title", title)
putExtra("body", body)
putExtra("id", id)
putExtra("data", opts.optJSONObject("data")?.toString() ?: "{}")
}
val pi = PendingIntent.getBroadcast(activity, id.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val am = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= 23) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi)
} else {
am.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pi)
}
} catch (e: Exception) {
android.util.Log.e("MobNotify", "notify_schedule failed: ${e.message}")
}
}
@JvmStatic
fun notify_cancel(id: String) {
val activity = activityRef?.get() ?: return
val intent = Intent().apply {
setClassName(activity, activity.packageName + ".NotificationReceiver")
}
val pi = PendingIntent.getBroadcast(activity, id.hashCode(), intent,
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE)
pi?.let {
val am = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
am.cancel(it)
}
}
// Registers the delivery target (the hub pid the host delivery paths key
// on) and resolves the FCM token: a refresh that arrived while no screen
// was registered is drained first; otherwise fetch fresh.
@JvmStatic
fun notify_register_push(pid: Long) {
MobNotifyHub.notifyPid = pid
MobNotifyHub.pendingToken?.let { token ->
MobNotifyHub.pendingToken = null
nativeDeliverNotifyPushToken(pid, token)
return
}
com.google.firebase.messaging.FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (task.isSuccessful) nativeDeliverNotifyPushToken(pid, task.result)
}
}
}