You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1597 lines
66 KiB
1597 lines
66 KiB
package com.timesafari.dailynotification
|
|
|
|
import android.Manifest
|
|
import android.app.Activity
|
|
import android.app.AlarmManager
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.os.Build
|
|
import android.os.PowerManager
|
|
import android.util.Log
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.app.NotificationManagerCompat
|
|
import androidx.core.content.ContextCompat
|
|
import com.getcapacitor.JSObject
|
|
import com.getcapacitor.Plugin
|
|
import com.getcapacitor.PluginCall
|
|
import com.getcapacitor.PluginMethod
|
|
import com.getcapacitor.annotation.CapacitorPlugin
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import org.json.JSONObject
|
|
|
|
/**
|
|
* Main Android implementation of Daily Notification Plugin
|
|
* Bridges Capacitor calls to native Android functionality
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.1.0
|
|
*/
|
|
@CapacitorPlugin(name = "DailyNotification")
|
|
open class DailyNotificationPlugin : Plugin() {
|
|
|
|
companion object {
|
|
private const val TAG = "DNP-PLUGIN"
|
|
|
|
/**
|
|
* Static registry for native content fetcher
|
|
* Thread-safe: Volatile ensures visibility across threads
|
|
*/
|
|
@Volatile
|
|
private var nativeFetcher: NativeNotificationContentFetcher? = null
|
|
|
|
/**
|
|
* Get the registered native fetcher (called from Java code)
|
|
*
|
|
* @return Registered NativeNotificationContentFetcher or null if not registered
|
|
*/
|
|
@JvmStatic
|
|
fun getNativeFetcherStatic(): NativeNotificationContentFetcher? {
|
|
return nativeFetcher
|
|
}
|
|
|
|
/**
|
|
* Register a native content fetcher
|
|
*
|
|
* @param fetcher The native fetcher implementation to register
|
|
*/
|
|
@JvmStatic
|
|
fun registerNativeFetcher(fetcher: NativeNotificationContentFetcher?) {
|
|
nativeFetcher = fetcher
|
|
Log.i(TAG, "Native fetcher ${if (fetcher != null) "registered" else "unregistered"}")
|
|
}
|
|
|
|
/**
|
|
* Set the native content fetcher (alias for registerNativeFetcher)
|
|
*
|
|
* @param fetcher The native fetcher implementation to register
|
|
*/
|
|
@JvmStatic
|
|
fun setNativeFetcher(fetcher: NativeNotificationContentFetcher?) {
|
|
registerNativeFetcher(fetcher)
|
|
}
|
|
}
|
|
|
|
private var db: DailyNotificationDatabase? = null
|
|
|
|
override fun load() {
|
|
super.load()
|
|
try {
|
|
if (context == null) {
|
|
Log.e(TAG, "Context is null, cannot initialize database")
|
|
return
|
|
}
|
|
db = DailyNotificationDatabase.getDatabase(context)
|
|
Log.i(TAG, "Daily Notification Plugin loaded successfully")
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
|
|
// Don't throw - allow plugin to load but database operations will fail gracefully
|
|
}
|
|
}
|
|
|
|
private fun getDatabase(): DailyNotificationDatabase {
|
|
if (db == null) {
|
|
if (context == null) {
|
|
throw IllegalStateException("Plugin not initialized: context is null")
|
|
}
|
|
db = DailyNotificationDatabase.getDatabase(context)
|
|
}
|
|
return db!!
|
|
}
|
|
|
|
@PluginMethod
|
|
fun configure(call: PluginCall) {
|
|
try {
|
|
// Capacitor passes the object directly via call.data
|
|
val options = call.data
|
|
Log.i(TAG, "Configure called with options: $options")
|
|
|
|
// Store configuration in database
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
// Implementation would store config in database
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to configure", e)
|
|
call.reject("Configuration failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Configure error", e)
|
|
call.reject("Configuration error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun checkPermissionStatus(call: PluginCall) {
|
|
try {
|
|
if (context == null) {
|
|
return call.reject("Context not available")
|
|
}
|
|
|
|
Log.i(TAG, "Checking permission status")
|
|
|
|
var notificationsEnabled = false
|
|
var exactAlarmEnabled = false
|
|
var wakeLockEnabled = false
|
|
|
|
// Check POST_NOTIFICATIONS permission
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
notificationsEnabled = context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
|
} else {
|
|
notificationsEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
|
}
|
|
|
|
// Check exact alarm permission
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
|
|
exactAlarmEnabled = alarmManager?.canScheduleExactAlarms() ?: false
|
|
} else {
|
|
exactAlarmEnabled = true // Pre-Android 12, exact alarms are always allowed
|
|
}
|
|
|
|
// Check wake lock permission (usually granted by default)
|
|
val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
|
|
wakeLockEnabled = powerManager != null
|
|
|
|
val allPermissionsGranted = notificationsEnabled && exactAlarmEnabled && wakeLockEnabled
|
|
|
|
val result = JSObject().apply {
|
|
put("notificationsEnabled", notificationsEnabled)
|
|
put("exactAlarmEnabled", exactAlarmEnabled)
|
|
put("wakeLockEnabled", wakeLockEnabled)
|
|
put("allPermissionsGranted", allPermissionsGranted)
|
|
}
|
|
|
|
Log.i(TAG, "Permission status: notifications=$notificationsEnabled, exactAlarm=$exactAlarmEnabled, wakeLock=$wakeLockEnabled, all=$allPermissionsGranted")
|
|
call.resolve(result)
|
|
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to check permission status", e)
|
|
call.reject("Permission check failed: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun requestNotificationPermissions(call: PluginCall) {
|
|
try {
|
|
val activity = activity ?: return call.reject("Activity not available")
|
|
val context = context ?: return call.reject("Context not available")
|
|
|
|
Log.i(TAG, "Requesting notification permissions")
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
// For Android 13+, request POST_NOTIFICATIONS permission
|
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
|
== PackageManager.PERMISSION_GRANTED) {
|
|
// Already granted
|
|
val result = JSObject().apply {
|
|
put("status", "granted")
|
|
put("granted", true)
|
|
put("notifications", "granted")
|
|
}
|
|
call.resolve(result)
|
|
} else {
|
|
// Request permission
|
|
ActivityCompat.requestPermissions(
|
|
activity,
|
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
|
1001 // Request code
|
|
)
|
|
// Note: Permission result will be handled by onRequestPermissionsResult
|
|
// For now, resolve with pending status
|
|
val result = JSObject().apply {
|
|
put("status", "prompt")
|
|
put("granted", false)
|
|
put("notifications", "prompt")
|
|
}
|
|
call.resolve(result)
|
|
}
|
|
} else {
|
|
// For older versions, permissions are granted at install time
|
|
val result = JSObject().apply {
|
|
put("status", "granted")
|
|
put("granted", true)
|
|
put("notifications", "granted")
|
|
}
|
|
call.resolve(result)
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to request notification permissions", e)
|
|
call.reject("Permission request failed: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun configureNativeFetcher(call: PluginCall) {
|
|
try {
|
|
// Capacitor passes the object directly via call.data
|
|
val options = call.data ?: return call.reject("Options are required")
|
|
|
|
// Support both jwtToken and jwtSecret for backward compatibility
|
|
val apiBaseUrl = options.getString("apiBaseUrl") ?: return call.reject("apiBaseUrl is required")
|
|
val activeDid = options.getString("activeDid") ?: return call.reject("activeDid is required")
|
|
val jwtToken = options.getString("jwtToken") ?: options.getString("jwtSecret") ?: return call.reject("jwtToken or jwtSecret is required")
|
|
|
|
val nativeFetcher = getNativeFetcherStatic()
|
|
if (nativeFetcher == null) {
|
|
return call.reject("No native fetcher registered. Host app must register a NativeNotificationContentFetcher.")
|
|
}
|
|
|
|
Log.i(TAG, "Configuring native fetcher: apiBaseUrl=$apiBaseUrl, activeDid=$activeDid")
|
|
|
|
// Call the native fetcher's configure method
|
|
// Note: This assumes the native fetcher has a configure method
|
|
// If the native fetcher interface doesn't have configure, we'll need to handle it differently
|
|
try {
|
|
// Store configuration in database for later use
|
|
val configId = "native_fetcher_config"
|
|
val configValue = JSONObject().apply {
|
|
put("apiBaseUrl", apiBaseUrl)
|
|
put("activeDid", activeDid)
|
|
put("jwtToken", jwtToken)
|
|
}.toString()
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val config = com.timesafari.dailynotification.entities.NotificationConfigEntity(
|
|
configId, null, "native_fetcher", "config", configValue, "json"
|
|
)
|
|
getDatabase().notificationConfigDao().insertConfig(config)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to store native fetcher config", e)
|
|
call.reject("Failed to store configuration: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Native fetcher configuration failed", e)
|
|
call.reject("Native fetcher configuration failed: ${e.message}")
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Configure native fetcher error", e)
|
|
call.reject("Configuration error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getNotificationStatus(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val schedules = getDatabase().scheduleDao().getAll()
|
|
val notifySchedules = schedules.filter { it.kind == "notify" && it.enabled }
|
|
|
|
// Get last notification time from history
|
|
val history = getDatabase().historyDao().getRecent(100) // Get last 100 entries
|
|
val lastNotification = history
|
|
.filter { it.kind == "notify" && it.outcome == "success" }
|
|
.maxByOrNull { it.occurredAt }
|
|
val lastNotificationTime = lastNotification?.occurredAt ?: 0
|
|
|
|
val result = JSObject().apply {
|
|
put("isEnabled", notifySchedules.isNotEmpty())
|
|
put("isScheduled", notifySchedules.isNotEmpty())
|
|
put("lastNotificationTime", lastNotificationTime)
|
|
put("nextNotificationTime", notifySchedules.minOfOrNull { it.nextRunAt ?: Long.MAX_VALUE } ?: 0)
|
|
put("scheduledCount", notifySchedules.size)
|
|
put("pending", notifySchedules.size) // Alias for scheduledCount
|
|
put("settings", JSObject().apply {
|
|
put("enabled", notifySchedules.isNotEmpty())
|
|
put("count", notifySchedules.size)
|
|
})
|
|
}
|
|
|
|
call.resolve(result)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get notification status", e)
|
|
call.reject("Failed to get notification status: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun scheduleDailyReminder(call: PluginCall) {
|
|
// Alias for scheduleDailyNotification for backward compatibility
|
|
// scheduleDailyReminder accepts same parameters as scheduleDailyNotification
|
|
try {
|
|
// Capacitor passes the object directly via call.data
|
|
val options = call.data ?: return call.reject("Options are required")
|
|
|
|
// Extract required fields, with defaults
|
|
val time = options.getString("time") ?: return call.reject("Time is required")
|
|
val title = options.getString("title") ?: "Daily Reminder"
|
|
val body = options.getString("body") ?: ""
|
|
val sound = options.getBoolean("sound") ?: true
|
|
val priority = options.getString("priority") ?: "default"
|
|
|
|
Log.i(TAG, "Scheduling daily reminder: time=$time, title=$title")
|
|
|
|
// Convert HH:mm time to cron expression (daily at specified time)
|
|
val cronExpression = convertTimeToCron(time)
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val config = UserNotificationConfig(
|
|
enabled = true,
|
|
schedule = cronExpression,
|
|
title = title,
|
|
body = body,
|
|
sound = sound,
|
|
vibration = options.getBoolean("vibration") ?: true,
|
|
priority = priority
|
|
)
|
|
|
|
val nextRunTime = calculateNextRunTime(cronExpression)
|
|
|
|
// Schedule AlarmManager notification
|
|
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
|
|
|
// Store schedule in database
|
|
val scheduleId = options.getString("id") ?: "daily_reminder_${System.currentTimeMillis()}"
|
|
val schedule = Schedule(
|
|
id = scheduleId,
|
|
kind = "notify",
|
|
cron = cronExpression,
|
|
clockTime = time,
|
|
enabled = true,
|
|
nextRunAt = nextRunTime
|
|
)
|
|
getDatabase().scheduleDao().upsert(schedule)
|
|
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to schedule daily reminder", e)
|
|
call.reject("Daily reminder scheduling failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Schedule daily reminder error", e)
|
|
call.reject("Daily reminder error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun openExactAlarmSettings(call: PluginCall) {
|
|
try {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
val intent = Intent(android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
activity?.startActivity(intent)
|
|
call.resolve()
|
|
} else {
|
|
call.reject("Exact alarm settings are only available on Android 12+")
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to open exact alarm settings", e)
|
|
call.reject("Failed to open exact alarm settings: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun isChannelEnabled(call: PluginCall) {
|
|
try {
|
|
val channelId = call.getString("channelId") ?: "daily_notification_channel"
|
|
val enabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
|
|
|
// Get notification channel importance if available
|
|
var importance = 0
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager?
|
|
val channel = notificationManager?.getNotificationChannel(channelId)
|
|
importance = channel?.importance ?: android.app.NotificationManager.IMPORTANCE_DEFAULT
|
|
}
|
|
|
|
val result = JSObject().apply {
|
|
put("enabled", enabled)
|
|
put("channelId", channelId)
|
|
put("importance", importance)
|
|
}
|
|
call.resolve(result)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to check channel status", e)
|
|
call.reject("Failed to check channel status: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun openChannelSettings(call: PluginCall) {
|
|
try {
|
|
val channelId = call.getString("channelId") ?: "daily_notification_channel"
|
|
val intent = Intent(android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
|
|
putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, context?.packageName)
|
|
putExtra(android.provider.Settings.EXTRA_CHANNEL_ID, channelId)
|
|
}
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
|
|
try {
|
|
activity?.startActivity(intent)
|
|
val result = JSObject().apply {
|
|
put("opened", true)
|
|
put("channelId", channelId)
|
|
}
|
|
call.resolve(result)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to start activity", e)
|
|
val result = JSObject().apply {
|
|
put("opened", false)
|
|
put("channelId", channelId)
|
|
put("error", e.message)
|
|
}
|
|
call.resolve(result)
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to open channel settings", e)
|
|
call.reject("Failed to open channel settings: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun checkStatus(call: PluginCall) {
|
|
// Comprehensive status check
|
|
try {
|
|
if (context == null) {
|
|
return call.reject("Context not available")
|
|
}
|
|
|
|
var postNotificationsGranted = false
|
|
var channelEnabled = false
|
|
var exactAlarmsGranted = false
|
|
var channelImportance = 0
|
|
val channelId = "daily_notification_channel"
|
|
|
|
// Check POST_NOTIFICATIONS permission
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
postNotificationsGranted = context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
|
} else {
|
|
postNotificationsGranted = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
|
}
|
|
|
|
// Check exact alarms permission
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
exactAlarmsGranted = alarmManager.canScheduleExactAlarms()
|
|
} else {
|
|
exactAlarmsGranted = true // Always available on older Android versions
|
|
}
|
|
|
|
// Check channel status
|
|
channelEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager?
|
|
val channel = notificationManager?.getNotificationChannel(channelId)
|
|
channelImportance = channel?.importance ?: android.app.NotificationManager.IMPORTANCE_DEFAULT
|
|
channelEnabled = channel?.importance != android.app.NotificationManager.IMPORTANCE_NONE
|
|
}
|
|
|
|
val canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted
|
|
|
|
val result = JSObject().apply {
|
|
put("canScheduleNow", canScheduleNow)
|
|
put("postNotificationsGranted", postNotificationsGranted)
|
|
put("channelEnabled", channelEnabled)
|
|
put("exactAlarmsGranted", exactAlarmsGranted)
|
|
put("channelImportance", channelImportance)
|
|
put("channelId", channelId)
|
|
}
|
|
|
|
call.resolve(result)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to check status", e)
|
|
call.reject("Failed to check status: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun scheduleContentFetch(call: PluginCall) {
|
|
try {
|
|
val configJson = call.getObject("config")
|
|
val config = parseContentFetchConfig(configJson)
|
|
|
|
Log.i(TAG, "Scheduling content fetch")
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
// Schedule WorkManager fetch
|
|
FetchWorker.scheduleFetch(context, config)
|
|
|
|
// Store schedule in database
|
|
val schedule = Schedule(
|
|
id = "fetch_${System.currentTimeMillis()}",
|
|
kind = "fetch",
|
|
cron = config.schedule,
|
|
enabled = config.enabled,
|
|
nextRunAt = calculateNextRunTime(config.schedule)
|
|
)
|
|
getDatabase().scheduleDao().upsert(schedule)
|
|
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to schedule content fetch", e)
|
|
call.reject("Content fetch scheduling failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Schedule content fetch error", e)
|
|
call.reject("Content fetch error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun scheduleDailyNotification(call: PluginCall) {
|
|
try {
|
|
// Capacitor passes the object directly via call.data
|
|
val options = call.data ?: return call.reject("Options are required")
|
|
|
|
val time = options.getString("time") ?: return call.reject("Time is required")
|
|
val title = options.getString("title") ?: "Daily Notification"
|
|
val body = options.getString("body") ?: ""
|
|
val sound = options.getBoolean("sound") ?: true
|
|
val priority = options.getString("priority") ?: "default"
|
|
val url = options.getString("url") // Optional URL for prefetch
|
|
|
|
Log.i(TAG, "Scheduling daily notification: time=$time, title=$title")
|
|
|
|
// Convert HH:mm time to cron expression (daily at specified time)
|
|
val cronExpression = convertTimeToCron(time)
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val config = UserNotificationConfig(
|
|
enabled = true,
|
|
schedule = cronExpression,
|
|
title = title,
|
|
body = body,
|
|
sound = sound,
|
|
vibration = true,
|
|
priority = priority
|
|
)
|
|
|
|
val nextRunTime = calculateNextRunTime(cronExpression)
|
|
|
|
// Schedule AlarmManager notification
|
|
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
|
|
|
// Schedule prefetch 5 minutes before notification (if URL provided)
|
|
if (url != null) {
|
|
val fetchTime = nextRunTime - (5 * 60 * 1000L) // 5 minutes before
|
|
FetchWorker.scheduleDelayedFetch(
|
|
context,
|
|
fetchTime,
|
|
nextRunTime,
|
|
url
|
|
)
|
|
Log.i(TAG, "Prefetch scheduled: fetchTime=$fetchTime, notificationTime=$nextRunTime")
|
|
}
|
|
|
|
// Store schedule in database
|
|
val schedule = Schedule(
|
|
id = "daily_${System.currentTimeMillis()}",
|
|
kind = "notify",
|
|
cron = cronExpression,
|
|
clockTime = time,
|
|
enabled = true,
|
|
nextRunAt = nextRunTime
|
|
)
|
|
getDatabase().scheduleDao().upsert(schedule)
|
|
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to schedule daily notification", e)
|
|
call.reject("Daily notification scheduling failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Schedule daily notification error", e)
|
|
call.reject("Daily notification error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun scheduleUserNotification(call: PluginCall) {
|
|
try {
|
|
val configJson = call.getObject("config")
|
|
val config = parseUserNotificationConfig(configJson)
|
|
|
|
Log.i(TAG, "Scheduling user notification")
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val nextRunTime = calculateNextRunTime(config.schedule)
|
|
|
|
// Schedule AlarmManager notification
|
|
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
|
|
|
|
// Store schedule in database
|
|
val schedule = Schedule(
|
|
id = "notify_${System.currentTimeMillis()}",
|
|
kind = "notify",
|
|
cron = config.schedule,
|
|
enabled = config.enabled,
|
|
nextRunAt = nextRunTime
|
|
)
|
|
getDatabase().scheduleDao().upsert(schedule)
|
|
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to schedule user notification", e)
|
|
call.reject("User notification scheduling failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Schedule user notification error", e)
|
|
call.reject("User notification error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun scheduleDualNotification(call: PluginCall) {
|
|
try {
|
|
val configJson = call.getObject("config") ?: return call.reject("Config is required")
|
|
val contentFetchObj = configJson.getJSObject("contentFetch") ?: return call.reject("contentFetch config is required")
|
|
val userNotificationObj = configJson.getJSObject("userNotification") ?: return call.reject("userNotification config is required")
|
|
val contentFetchConfig = parseContentFetchConfig(contentFetchObj)
|
|
val userNotificationConfig = parseUserNotificationConfig(userNotificationObj)
|
|
|
|
Log.i(TAG, "Scheduling dual notification")
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
// Schedule both fetch and notification
|
|
FetchWorker.scheduleFetch(context, contentFetchConfig)
|
|
|
|
val nextRunTime = calculateNextRunTime(userNotificationConfig.schedule)
|
|
NotifyReceiver.scheduleExactNotification(context, nextRunTime, userNotificationConfig)
|
|
|
|
// Store both schedules
|
|
val fetchSchedule = Schedule(
|
|
id = "dual_fetch_${System.currentTimeMillis()}",
|
|
kind = "fetch",
|
|
cron = contentFetchConfig.schedule,
|
|
enabled = contentFetchConfig.enabled,
|
|
nextRunAt = calculateNextRunTime(contentFetchConfig.schedule)
|
|
)
|
|
val notifySchedule = Schedule(
|
|
id = "dual_notify_${System.currentTimeMillis()}",
|
|
kind = "notify",
|
|
cron = userNotificationConfig.schedule,
|
|
enabled = userNotificationConfig.enabled,
|
|
nextRunAt = nextRunTime
|
|
)
|
|
|
|
getDatabase().scheduleDao().upsert(fetchSchedule)
|
|
getDatabase().scheduleDao().upsert(notifySchedule)
|
|
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to schedule dual notification", e)
|
|
call.reject("Dual notification scheduling failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Schedule dual notification error", e)
|
|
call.reject("Dual notification error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getDualScheduleStatus(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val enabledSchedules = getDatabase().scheduleDao().getEnabled()
|
|
val latestCache = getDatabase().contentCacheDao().getLatest()
|
|
val recentHistory = getDatabase().historyDao().getSince(System.currentTimeMillis() - (24 * 60 * 60 * 1000L))
|
|
|
|
val status = JSObject().apply {
|
|
put("nextRuns", enabledSchedules.map { it.nextRunAt })
|
|
put("lastOutcomes", recentHistory.map { it.outcome })
|
|
put("cacheAgeMs", latestCache?.let { System.currentTimeMillis() - it.fetchedAt })
|
|
put("staleArmed", latestCache?.let {
|
|
System.currentTimeMillis() > (it.fetchedAt + it.ttlSeconds * 1000L)
|
|
} ?: true)
|
|
put("queueDepth", recentHistory.size)
|
|
}
|
|
|
|
call.resolve(status)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get dual schedule status", e)
|
|
call.reject("Status retrieval failed: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun registerCallback(call: PluginCall) {
|
|
try {
|
|
val name = call.getString("name") ?: return call.reject("Callback name is required")
|
|
val callback = call.getObject("callback") ?: return call.reject("Callback data is required")
|
|
|
|
Log.i(TAG, "Registering callback: $name")
|
|
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val callbackRecord = Callback(
|
|
id = name,
|
|
kind = callback.getString("kind") ?: "local",
|
|
target = callback.getString("target") ?: "",
|
|
headersJson = callback.getString("headers"),
|
|
enabled = true,
|
|
createdAt = System.currentTimeMillis()
|
|
)
|
|
|
|
getDatabase().callbackDao().upsert(callbackRecord)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to register callback", e)
|
|
call.reject("Callback registration failed: ${e.message}")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Register callback error", e)
|
|
call.reject("Callback registration error: ${e.message}")
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getContentCache(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val latestCache = getDatabase().contentCacheDao().getLatest()
|
|
val result = JSObject()
|
|
|
|
if (latestCache != null) {
|
|
result.put("id", latestCache.id)
|
|
result.put("fetchedAt", latestCache.fetchedAt)
|
|
result.put("ttlSeconds", latestCache.ttlSeconds)
|
|
result.put("payload", String(latestCache.payload))
|
|
result.put("meta", latestCache.meta)
|
|
}
|
|
|
|
call.resolve(result)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get content cache", e)
|
|
call.reject("Content cache retrieval failed: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DATABASE ACCESS METHODS
|
|
// ============================================================================
|
|
// These methods provide TypeScript/JavaScript access to the plugin's internal
|
|
// SQLite database. All operations run on background threads for thread safety.
|
|
// ============================================================================
|
|
|
|
// SCHEDULES MANAGEMENT
|
|
|
|
@PluginMethod
|
|
fun getSchedules(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val options = call.getObject("options")
|
|
val kind = options?.getString("kind")
|
|
val enabled = options?.getBoolean("enabled")
|
|
|
|
val schedules = when {
|
|
kind != null && enabled != null ->
|
|
getDatabase().scheduleDao().getByKindAndEnabled(kind, enabled)
|
|
kind != null ->
|
|
getDatabase().scheduleDao().getByKind(kind)
|
|
enabled != null ->
|
|
if (enabled) getDatabase().scheduleDao().getEnabled() else getDatabase().scheduleDao().getAll().filter { !it.enabled }
|
|
else ->
|
|
getDatabase().scheduleDao().getAll()
|
|
}
|
|
|
|
// Return array wrapped in JSObject - Capacitor will serialize correctly
|
|
val schedulesArray = org.json.JSONArray()
|
|
schedules.forEach { schedulesArray.put(scheduleToJson(it)) }
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("schedules", schedulesArray)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get schedules", e)
|
|
call.reject("Failed to get schedules: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getSchedule(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Schedule ID is required")
|
|
|
|
val schedule = getDatabase().scheduleDao().getById(id)
|
|
|
|
if (schedule != null) {
|
|
call.resolve(scheduleToJson(schedule))
|
|
} else {
|
|
call.resolve(JSObject().apply { put("schedule", null) })
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get schedule", e)
|
|
call.reject("Failed to get schedule: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun createSchedule(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val scheduleJson = call.getObject("schedule")
|
|
?: return@launch call.reject("Schedule data is required")
|
|
|
|
val kindStr = scheduleJson.getString("kind") ?: return@launch call.reject("Schedule kind is required")
|
|
val id = scheduleJson.getString("id") ?: "${kindStr}_${System.currentTimeMillis()}"
|
|
val schedule = Schedule(
|
|
id = id,
|
|
kind = kindStr,
|
|
cron = scheduleJson.getString("cron"),
|
|
clockTime = scheduleJson.getString("clockTime"),
|
|
enabled = scheduleJson.getBoolean("enabled") ?: true,
|
|
jitterMs = scheduleJson.getInt("jitterMs") ?: 0,
|
|
backoffPolicy = scheduleJson.getString("backoffPolicy") ?: "exp",
|
|
stateJson = scheduleJson.getString("stateJson")
|
|
)
|
|
|
|
getDatabase().scheduleDao().upsert(schedule)
|
|
call.resolve(scheduleToJson(schedule))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to create schedule", e)
|
|
call.reject("Failed to create schedule: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun updateSchedule(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Schedule ID is required")
|
|
|
|
val updates = call.getObject("updates")
|
|
?: return@launch call.reject("Updates are required")
|
|
|
|
val existing = getDatabase().scheduleDao().getById(id)
|
|
?: return@launch call.reject("Schedule not found: $id")
|
|
|
|
// Update fields
|
|
getDatabase().scheduleDao().update(
|
|
id = id,
|
|
enabled = updates.getBoolean("enabled")?.let { it },
|
|
cron = updates.getString("cron"),
|
|
clockTime = updates.getString("clockTime"),
|
|
jitterMs = updates.getInt("jitterMs")?.let { it },
|
|
backoffPolicy = updates.getString("backoffPolicy"),
|
|
stateJson = updates.getString("stateJson")
|
|
)
|
|
|
|
// Update run times if provided
|
|
val lastRunAt = updates.getLong("lastRunAt")
|
|
val nextRunAt = updates.getLong("nextRunAt")
|
|
if (lastRunAt != null || nextRunAt != null) {
|
|
getDatabase().scheduleDao().updateRunTimes(id, lastRunAt, nextRunAt)
|
|
}
|
|
|
|
val updated = getDatabase().scheduleDao().getById(id)
|
|
call.resolve(scheduleToJson(updated!!))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to update schedule", e)
|
|
call.reject("Failed to update schedule: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun deleteSchedule(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Schedule ID is required")
|
|
|
|
getDatabase().scheduleDao().deleteById(id)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to delete schedule", e)
|
|
call.reject("Failed to delete schedule: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun enableSchedule(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Schedule ID is required")
|
|
|
|
val enabled = call.getBoolean("enabled") ?: true
|
|
|
|
getDatabase().scheduleDao().setEnabled(id, enabled)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to enable/disable schedule", e)
|
|
call.reject("Failed to update schedule: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun calculateNextRunTime(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val schedule = call.getString("schedule")
|
|
?: return@launch call.reject("Schedule expression is required")
|
|
|
|
val nextRun = calculateNextRunTime(schedule)
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("nextRunAt", nextRun)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to calculate next run time", e)
|
|
call.reject("Failed to calculate next run time: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// CONTENT CACHE MANAGEMENT
|
|
|
|
@PluginMethod
|
|
fun getContentCacheById(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val options = call.getObject("options")
|
|
val id = options?.getString("id")
|
|
|
|
val cache = if (id != null) {
|
|
getDatabase().contentCacheDao().getById(id)
|
|
} else {
|
|
getDatabase().contentCacheDao().getLatest()
|
|
}
|
|
|
|
if (cache != null) {
|
|
call.resolve(contentCacheToJson(cache))
|
|
} else {
|
|
call.resolve(JSObject().apply { put("contentCache", null) })
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get content cache", e)
|
|
call.reject("Failed to get content cache: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getLatestContentCache(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val cache = getDatabase().contentCacheDao().getLatest()
|
|
|
|
if (cache != null) {
|
|
call.resolve(contentCacheToJson(cache))
|
|
} else {
|
|
call.resolve(JSObject().apply { put("contentCache", null) })
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get latest content cache", e)
|
|
call.reject("Failed to get latest content cache: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getContentCacheHistory(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val limit = call.getInt("limit") ?: 10
|
|
|
|
val history = getDatabase().contentCacheDao().getHistory(limit)
|
|
|
|
val historyArray = org.json.JSONArray()
|
|
history.forEach { historyArray.put(contentCacheToJson(it)) }
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("history", historyArray)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get content cache history", e)
|
|
call.reject("Failed to get content cache history: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun saveContentCache(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val contentJson = call.getObject("content")
|
|
?: return@launch call.reject("Content data is required")
|
|
|
|
val id = contentJson.getString("id") ?: "cache_${System.currentTimeMillis()}"
|
|
val payload = contentJson.getString("payload")
|
|
?: return@launch call.reject("Payload is required")
|
|
val ttlSeconds = contentJson.getInt("ttlSeconds")
|
|
?: return@launch call.reject("TTL seconds is required")
|
|
|
|
val cache = ContentCache(
|
|
id = id,
|
|
fetchedAt = System.currentTimeMillis(),
|
|
ttlSeconds = ttlSeconds,
|
|
payload = payload.toByteArray(),
|
|
meta = contentJson.getString("meta")
|
|
)
|
|
|
|
getDatabase().contentCacheDao().upsert(cache)
|
|
call.resolve(contentCacheToJson(cache))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to save content cache", e)
|
|
call.reject("Failed to save content cache: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun clearContentCacheEntries(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val options = call.getObject("options")
|
|
val olderThan = options?.getLong("olderThan")
|
|
|
|
if (olderThan != null) {
|
|
getDatabase().contentCacheDao().deleteOlderThan(olderThan)
|
|
} else {
|
|
getDatabase().contentCacheDao().deleteAll()
|
|
}
|
|
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to clear content cache", e)
|
|
call.reject("Failed to clear content cache: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// CALLBACKS MANAGEMENT
|
|
|
|
@PluginMethod
|
|
fun getCallbacks(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val options = call.getObject("options")
|
|
val enabled = options?.getBoolean("enabled")
|
|
|
|
val callbacks = if (enabled != null) {
|
|
getDatabase().callbackDao().getByEnabled(enabled)
|
|
} else {
|
|
getDatabase().callbackDao().getAll()
|
|
}
|
|
|
|
val callbacksArray = org.json.JSONArray()
|
|
callbacks.forEach { callbacksArray.put(callbackToJson(it)) }
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("callbacks", callbacksArray)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get callbacks", e)
|
|
call.reject("Failed to get callbacks: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getCallback(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Callback ID is required")
|
|
|
|
val callback = getDatabase().callbackDao().getById(id)
|
|
|
|
if (callback != null) {
|
|
call.resolve(callbackToJson(callback))
|
|
} else {
|
|
call.resolve(JSObject().apply { put("callback", null) })
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get callback", e)
|
|
call.reject("Failed to get callback: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun registerCallbackConfig(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val callbackJson = call.getObject("callback")
|
|
?: return@launch call.reject("Callback data is required")
|
|
|
|
val id = callbackJson.getString("id")
|
|
?: return@launch call.reject("Callback ID is required")
|
|
val kindStr = callbackJson.getString("kind")
|
|
?: return@launch call.reject("Callback kind is required")
|
|
val targetStr = callbackJson.getString("target")
|
|
?: return@launch call.reject("Callback target is required")
|
|
|
|
val callback = Callback(
|
|
id = id,
|
|
kind = kindStr,
|
|
target = targetStr,
|
|
headersJson = callbackJson.getString("headersJson"),
|
|
enabled = callbackJson.getBoolean("enabled") ?: true,
|
|
createdAt = System.currentTimeMillis()
|
|
)
|
|
|
|
getDatabase().callbackDao().upsert(callback)
|
|
call.resolve(callbackToJson(callback))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to register callback", e)
|
|
call.reject("Failed to register callback: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun updateCallback(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Callback ID is required")
|
|
|
|
val updates = call.getObject("updates")
|
|
?: return@launch call.reject("Updates are required")
|
|
|
|
getDatabase().callbackDao().update(
|
|
id = id,
|
|
kind = updates.getString("kind"),
|
|
target = updates.getString("target"),
|
|
headersJson = updates.getString("headersJson"),
|
|
enabled = updates.getBoolean("enabled")?.let { it }
|
|
)
|
|
|
|
val updated = getDatabase().callbackDao().getById(id)
|
|
if (updated != null) {
|
|
call.resolve(callbackToJson(updated))
|
|
} else {
|
|
call.reject("Callback not found after update")
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to update callback", e)
|
|
call.reject("Failed to update callback: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun deleteCallback(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Callback ID is required")
|
|
|
|
getDatabase().callbackDao().deleteById(id)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to delete callback", e)
|
|
call.reject("Failed to delete callback: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun enableCallback(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val id = call.getString("id")
|
|
?: return@launch call.reject("Callback ID is required")
|
|
|
|
val enabled = call.getBoolean("enabled") ?: true
|
|
|
|
getDatabase().callbackDao().setEnabled(id, enabled)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to enable/disable callback", e)
|
|
call.reject("Failed to update callback: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// HISTORY MANAGEMENT
|
|
|
|
@PluginMethod
|
|
fun getHistory(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val options = call.getObject("options")
|
|
val since = options?.getLong("since")
|
|
val kind = options?.getString("kind")
|
|
val limit = options?.getInt("limit") ?: 50
|
|
|
|
val history = when {
|
|
since != null && kind != null ->
|
|
getDatabase().historyDao().getSinceByKind(since, kind, limit)
|
|
since != null ->
|
|
getDatabase().historyDao().getSince(since).take(limit)
|
|
else ->
|
|
getDatabase().historyDao().getRecent(limit)
|
|
}
|
|
|
|
val historyArray = org.json.JSONArray()
|
|
history.forEach { historyArray.put(historyToJson(it)) }
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("history", historyArray)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get history", e)
|
|
call.reject("Failed to get history: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getHistoryStats(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val allHistory = getDatabase().historyDao().getRecent(Int.MAX_VALUE)
|
|
|
|
val outcomes = mutableMapOf<String, Int>()
|
|
val kinds = mutableMapOf<String, Int>()
|
|
var mostRecent: Long? = null
|
|
var oldest: Long? = null
|
|
|
|
allHistory.forEach { entry ->
|
|
outcomes[entry.outcome] = (outcomes[entry.outcome] ?: 0) + 1
|
|
kinds[entry.kind] = (kinds[entry.kind] ?: 0) + 1
|
|
|
|
if (mostRecent == null || entry.occurredAt > mostRecent!!) {
|
|
mostRecent = entry.occurredAt
|
|
}
|
|
if (oldest == null || entry.occurredAt < oldest!!) {
|
|
oldest = entry.occurredAt
|
|
}
|
|
}
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("totalCount", allHistory.size)
|
|
put("outcomes", JSObject().apply {
|
|
outcomes.forEach { (k, v) -> put(k, v) }
|
|
})
|
|
put("kinds", JSObject().apply {
|
|
kinds.forEach { (k, v) -> put(k, v) }
|
|
})
|
|
put("mostRecent", mostRecent)
|
|
put("oldest", oldest)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get history stats", e)
|
|
call.reject("Failed to get history stats: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// CONFIGURATION MANAGEMENT
|
|
|
|
@PluginMethod
|
|
fun getConfig(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val key = call.getString("key")
|
|
?: return@launch call.reject("Config key is required")
|
|
|
|
val options = call.getObject("options")
|
|
val timesafariDid = options?.getString("timesafariDid")
|
|
|
|
val entity = if (timesafariDid != null) {
|
|
getDatabase().notificationConfigDao().getConfigByKeyAndDid(key, timesafariDid)
|
|
} else {
|
|
getDatabase().notificationConfigDao().getConfigByKey(key)
|
|
}
|
|
|
|
if (entity != null) {
|
|
call.resolve(configToJson(entity))
|
|
} else {
|
|
call.resolve(JSObject().apply { put("config", null) })
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get config", e)
|
|
call.reject("Failed to get config: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun getAllConfigs(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val options = call.getObject("options")
|
|
val timesafariDid = options?.getString("timesafariDid")
|
|
val configType = options?.getString("configType")
|
|
|
|
val configs = when {
|
|
timesafariDid != null && configType != null -> {
|
|
getDatabase().notificationConfigDao().getConfigsByTimeSafariDid(timesafariDid)
|
|
.filter { it.configType == configType }
|
|
}
|
|
timesafariDid != null -> {
|
|
getDatabase().notificationConfigDao().getConfigsByTimeSafariDid(timesafariDid)
|
|
}
|
|
configType != null -> {
|
|
getDatabase().notificationConfigDao().getConfigsByType(configType)
|
|
}
|
|
else -> {
|
|
getDatabase().notificationConfigDao().getAllConfigs()
|
|
}
|
|
}
|
|
|
|
val configsArray = org.json.JSONArray()
|
|
configs.forEach { configsArray.put(configToJson(it)) }
|
|
|
|
call.resolve(JSObject().apply {
|
|
put("configs", configsArray)
|
|
})
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to get all configs", e)
|
|
call.reject("Failed to get configs: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun setConfig(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val configJson = call.getObject("config")
|
|
?: return@launch call.reject("Config data is required")
|
|
|
|
val id = configJson.getString("id") ?: "config_${System.currentTimeMillis()}"
|
|
val timesafariDid = configJson.getString("timesafariDid")
|
|
val configType = configJson.getString("configType")
|
|
?: return@launch call.reject("Config type is required")
|
|
val configKey = configJson.getString("configKey")
|
|
?: return@launch call.reject("Config key is required")
|
|
val configValue = configJson.getString("configValue")
|
|
?: return@launch call.reject("Config value is required")
|
|
val configDataType = configJson.getString("configDataType", "string")
|
|
|
|
val entity = com.timesafari.dailynotification.entities.NotificationConfigEntity(
|
|
id, timesafariDid, configType, configKey, configValue, configDataType
|
|
)
|
|
|
|
// Set optional fields
|
|
configJson.getString("metadata")?.let { entity.metadata = it }
|
|
configJson.getBoolean("isEncrypted", false)?.let {
|
|
entity.isEncrypted = it
|
|
configJson.getString("encryptionKeyId")?.let { entity.encryptionKeyId = it }
|
|
}
|
|
configJson.getLong("ttlSeconds")?.let { entity.ttlSeconds = it }
|
|
configJson.getBoolean("isActive", true)?.let { entity.isActive = it }
|
|
|
|
getDatabase().notificationConfigDao().insertConfig(entity)
|
|
call.resolve(configToJson(entity))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to set config", e)
|
|
call.reject("Failed to set config: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun updateConfig(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val key = call.getString("key")
|
|
?: return@launch call.reject("Config key is required")
|
|
val value = call.getString("value")
|
|
?: return@launch call.reject("Config value is required")
|
|
|
|
val options = call.getObject("options")
|
|
val timesafariDid = options?.getString("timesafariDid")
|
|
|
|
val entity = if (timesafariDid != null) {
|
|
getDatabase().notificationConfigDao().getConfigByKeyAndDid(key, timesafariDid)
|
|
} else {
|
|
getDatabase().notificationConfigDao().getConfigByKey(key)
|
|
}
|
|
|
|
if (entity == null) {
|
|
return@launch call.reject("Config not found")
|
|
}
|
|
|
|
entity.updateValue(value)
|
|
getDatabase().notificationConfigDao().updateConfig(entity)
|
|
call.resolve(configToJson(entity))
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to update config", e)
|
|
call.reject("Failed to update config: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
@PluginMethod
|
|
fun deleteConfig(call: PluginCall) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
val key = call.getString("key")
|
|
?: return@launch call.reject("Config key is required")
|
|
|
|
val options = call.getObject("options")
|
|
val timesafariDid = options?.getString("timesafariDid")
|
|
|
|
val entity = if (timesafariDid != null) {
|
|
getDatabase().notificationConfigDao().getConfigByKeyAndDid(key, timesafariDid)
|
|
} else {
|
|
getDatabase().notificationConfigDao().getConfigByKey(key)
|
|
}
|
|
|
|
if (entity == null) {
|
|
return@launch call.reject("Config not found")
|
|
}
|
|
|
|
getDatabase().notificationConfigDao().deleteConfig(entity.id)
|
|
call.resolve()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to delete config", e)
|
|
call.reject("Failed to delete config: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper methods to convert entities to JSON
|
|
private fun scheduleToJson(schedule: Schedule): JSObject {
|
|
return JSObject().apply {
|
|
put("id", schedule.id)
|
|
put("kind", schedule.kind)
|
|
put("cron", schedule.cron)
|
|
put("clockTime", schedule.clockTime)
|
|
put("enabled", schedule.enabled)
|
|
put("lastRunAt", schedule.lastRunAt)
|
|
put("nextRunAt", schedule.nextRunAt)
|
|
put("jitterMs", schedule.jitterMs)
|
|
put("backoffPolicy", schedule.backoffPolicy)
|
|
put("stateJson", schedule.stateJson)
|
|
}
|
|
}
|
|
|
|
private fun contentCacheToJson(cache: ContentCache): JSObject {
|
|
return JSObject().apply {
|
|
put("id", cache.id)
|
|
put("fetchedAt", cache.fetchedAt)
|
|
put("ttlSeconds", cache.ttlSeconds)
|
|
put("payload", String(cache.payload))
|
|
put("meta", cache.meta)
|
|
}
|
|
}
|
|
|
|
private fun callbackToJson(callback: Callback): JSObject {
|
|
return JSObject().apply {
|
|
put("id", callback.id)
|
|
put("kind", callback.kind)
|
|
put("target", callback.target)
|
|
put("headersJson", callback.headersJson)
|
|
put("enabled", callback.enabled)
|
|
put("createdAt", callback.createdAt)
|
|
}
|
|
}
|
|
|
|
private fun historyToJson(history: History): JSObject {
|
|
return JSObject().apply {
|
|
put("id", history.id)
|
|
put("refId", history.refId)
|
|
put("kind", history.kind)
|
|
put("occurredAt", history.occurredAt)
|
|
put("durationMs", history.durationMs)
|
|
put("outcome", history.outcome)
|
|
put("diagJson", history.diagJson)
|
|
}
|
|
}
|
|
|
|
private fun configToJson(config: com.timesafari.dailynotification.entities.NotificationConfigEntity): JSObject {
|
|
return JSObject().apply {
|
|
put("id", config.id)
|
|
put("timesafariDid", config.timesafariDid)
|
|
put("configType", config.configType)
|
|
put("configKey", config.configKey)
|
|
put("configValue", config.configValue)
|
|
put("configDataType", config.configDataType)
|
|
put("isEncrypted", config.isEncrypted)
|
|
put("encryptionKeyId", config.encryptionKeyId)
|
|
put("createdAt", config.createdAt)
|
|
put("updatedAt", config.updatedAt)
|
|
put("ttlSeconds", config.ttlSeconds)
|
|
put("isActive", config.isActive)
|
|
put("metadata", config.metadata)
|
|
}
|
|
}
|
|
|
|
// Helper methods
|
|
private fun parseContentFetchConfig(configJson: JSObject): ContentFetchConfig {
|
|
val callbacksObj = configJson.getJSObject("callbacks")
|
|
return ContentFetchConfig(
|
|
enabled = configJson.getBoolean("enabled") ?: true,
|
|
schedule = configJson.getString("schedule") ?: "0 9 * * *",
|
|
url = configJson.getString("url"),
|
|
timeout = configJson.getInt("timeout"),
|
|
retryAttempts = configJson.getInt("retryAttempts"),
|
|
retryDelay = configJson.getInt("retryDelay"),
|
|
callbacks = CallbackConfig(
|
|
apiService = callbacksObj?.getString("apiService"),
|
|
database = callbacksObj?.getString("database"),
|
|
reporting = callbacksObj?.getString("reporting")
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun parseUserNotificationConfig(configJson: JSObject): UserNotificationConfig {
|
|
return UserNotificationConfig(
|
|
enabled = configJson.getBoolean("enabled") ?: true,
|
|
schedule = configJson.getString("schedule") ?: "0 9 * * *",
|
|
title = configJson.getString("title"),
|
|
body = configJson.getString("body"),
|
|
sound = configJson.getBoolean("sound"),
|
|
vibration = configJson.getBoolean("vibration"),
|
|
priority = configJson.getString("priority")
|
|
)
|
|
}
|
|
|
|
private fun calculateNextRunTime(schedule: String): Long {
|
|
// Simple implementation - for production, use proper cron parsing
|
|
val now = System.currentTimeMillis()
|
|
return now + (24 * 60 * 60 * 1000L) // Next day
|
|
}
|
|
|
|
/**
|
|
* Convert HH:mm time string to cron expression (daily at specified time)
|
|
* Example: "09:30" -> "30 9 * * *"
|
|
*/
|
|
private fun convertTimeToCron(time: String): String {
|
|
try {
|
|
val parts = time.split(":")
|
|
if (parts.size != 2) {
|
|
throw IllegalArgumentException("Invalid time format: $time. Expected HH:mm")
|
|
}
|
|
val hour = parts[0].toInt()
|
|
val minute = parts[1].toInt()
|
|
|
|
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
throw IllegalArgumentException("Invalid time values: hour=$hour, minute=$minute")
|
|
}
|
|
|
|
// Cron format: minute hour day month day-of-week
|
|
// Daily at specified time: "minute hour * * *"
|
|
return "$minute $hour * * *"
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to convert time to cron: $time", e)
|
|
// Default to 9:00 AM if conversion fails
|
|
return "0 9 * * *"
|
|
}
|
|
}
|
|
}
|
|
|