package ai.sirrus.reactnativesdk import android.app.ActivityManager import android.app.Activity import android.content.Context import android.content.Intent import android.os.Process import android.util.Log import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import androidx.core.app.NotificationManagerCompat import org.json.JSONArray import org.json.JSONObject object SirrusNotificationEventDispatcher { private const val PREFS_NAME = "sirrus_sdk_notifications" private const val PENDING_EVENT_KEY = "pending_notification_event" private const val PENDING_EVENTS_KEY = "pending_notification_events" private const val LATEST_PUSH_TOKEN_KEY = "latest_push_token" private const val LATEST_PUSH_PROVIDER_KEY = "latest_push_provider" private const val EVENT_NAME = "sirrusNotificationEvent" private const val MAX_PENDING_EVENTS = 25 internal const val EVENT_EXTRA_KEY = "sirrusNotificationEventJson" private const val LOG_TAG = "SirrusSDK" private var reactContext: ReactApplicationContext? = null fun attachReactContext(context: ReactApplicationContext) { reactContext = context } fun emitOrStore( context: Context, eventJson: JSONObject, alwaysStore: Boolean = false ) { val activeContext = reactContext val hasActiveReactInstance = activeContext?.hasActiveReactInstance() == true val appInForeground = isAppInForeground(context.applicationContext) if (hasActiveReactInstance) { activeContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(EVENT_NAME, jsonToWritableMap(eventJson)) } if (alwaysStore || !hasActiveReactInstance || !appInForeground) { storePendingEvent(context.applicationContext, eventJson) } } fun consumeActivityLaunchEvent(context: Context, activity: Activity?): WritableMap? { val eventJson = consumeIntentEvent(activity?.intent) ?: return null cancelNotificationFromEvent(context.applicationContext, eventJson) Log.i(LOG_TAG, "Sirrus notification launch payload: $eventJson") return jsonToWritableMap(eventJson) } fun emitOrStoreFromIntent(context: Context, intent: Intent?) { val eventJson = consumeIntentEvent(intent) ?: return cancelNotificationFromEvent(context.applicationContext, eventJson) Log.i(LOG_TAG, "Sirrus notification launch payload: $eventJson") emitOrStore(context, eventJson, alwaysStore = true) } fun consumeStoredEvent(context: Context): WritableMap? { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val storedEvents = prefs.getString(PENDING_EVENTS_KEY, null) if (!storedEvents.isNullOrBlank()) { val events = JSONArray(storedEvents) if (events.length() > 0) { val firstEvent = events.getJSONObject(0) val remainingEvents = JSONArray() for (index in 1 until events.length()) { remainingEvents.put(events.getJSONObject(index)) } prefs.edit() .putString(PENDING_EVENTS_KEY, remainingEvents.toString()) .apply() return jsonToWritableMap(firstEvent) } } val stored = prefs.getString(PENDING_EVENT_KEY, null) ?: return null prefs.edit().remove(PENDING_EVENT_KEY).apply() return jsonToWritableMap(JSONObject(stored)) } private fun consumeIntentEvent(intent: Intent?): JSONObject? { val serializedEvent = intent?.getStringExtra(EVENT_EXTRA_KEY) ?: return null intent.removeExtra(EVENT_EXTRA_KEY) return try { JSONObject(serializedEvent) } catch (_: Throwable) { null } } private fun cancelNotificationFromEvent(context: Context, eventJson: JSONObject) { if (!eventJson.has("notificationId")) { return } NotificationManagerCompat.from(context).cancel(eventJson.optInt("notificationId")) } fun storeLatestPushToken(context: Context, token: String, provider: String) { context .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .putString(LATEST_PUSH_TOKEN_KEY, token) .putString(LATEST_PUSH_PROVIDER_KEY, provider) .apply() } fun consumeLatestPushToken(context: Context): WritableMap? { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val token = prefs.getString(LATEST_PUSH_TOKEN_KEY, null) ?: return null val provider = prefs.getString(LATEST_PUSH_PROVIDER_KEY, "fcm") ?: "fcm" prefs.edit() .remove(LATEST_PUSH_TOKEN_KEY) .remove(LATEST_PUSH_PROVIDER_KEY) .apply() return Arguments.createMap().apply { putString("token", token) putString("provider", provider) } } private fun jsonToWritableMap(jsonObject: JSONObject): WritableMap { val map = Arguments.createMap() val keys = jsonObject.keys() while (keys.hasNext()) { val key = keys.next() when (val value = jsonObject.get(key)) { JSONObject.NULL -> map.putNull(key) is String -> map.putString(key, value) is Boolean -> map.putBoolean(key, value) is Int -> map.putInt(key, value) is Double -> map.putDouble(key, value) is JSONObject -> map.putMap(key, jsonToWritableMap(value)) is JSONArray -> map.putArray(key, jsonToWritableArray(value)) else -> map.putString(key, value.toString()) } } return map } private fun jsonToWritableArray(jsonArray: JSONArray): WritableArray { val array = Arguments.createArray() for (index in 0 until jsonArray.length()) { when (val value = jsonArray.get(index)) { JSONObject.NULL -> array.pushNull() is String -> array.pushString(value) is Boolean -> array.pushBoolean(value) is Int -> array.pushInt(value) is Double -> array.pushDouble(value) is JSONObject -> array.pushMap(jsonToWritableMap(value)) is JSONArray -> array.pushArray(jsonToWritableArray(value)) else -> array.pushString(value.toString()) } } return array } private fun storePendingEvent(context: Context, eventJson: JSONObject) { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val pendingEvents = JSONArray(prefs.getString(PENDING_EVENTS_KEY, "[]") ?: "[]") pendingEvents.put(eventJson) val trimmedEvents = JSONArray() val startIndex = maxOf(0, pendingEvents.length() - MAX_PENDING_EVENTS) for (index in startIndex until pendingEvents.length()) { trimmedEvents.put(pendingEvents.getJSONObject(index)) } prefs.edit() .putString(PENDING_EVENTS_KEY, trimmedEvents.toString()) .remove(PENDING_EVENT_KEY) .apply() } private fun isAppInForeground(context: Context): Boolean { val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager ?: return false val appProcesses = activityManager.runningAppProcesses ?: return false val currentPid = Process.myPid() return appProcesses.any { process -> process.pid == currentPid && ( process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND || process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE ) } } }