feat(android)!: implement Phase 2 Android core with WorkManager + AlarmManager + SQLite

- Add complete SQLite schema with Room database (content_cache, schedules, callbacks, history)
- Implement WorkManager FetchWorker with exponential backoff and network constraints
- Add AlarmManager NotifyReceiver with TTL-at-fire logic and notification delivery
- Create BootReceiver for automatic rescheduling after device reboot
- Update AndroidManifest.xml with necessary permissions and receivers
- Add Room, WorkManager, and Kotlin coroutines dependencies to build.gradle

feat(callback-registry)!: implement callback registry with circuit breaker

- Add CallbackRegistryImpl with HTTP, local, and queue callback support
- Implement circuit breaker pattern with exponential backoff retry logic
- Add CallbackEvent interface with structured event types
- Support for exactly-once delivery semantics with retry queue
- Include callback status monitoring and health checks

feat(observability)!: add comprehensive observability and health monitoring

- Implement ObservabilityManager with structured logging and event codes
- Add performance metrics tracking (fetch, notify, callback times)
- Create health status API with circuit breaker monitoring
- Include log compaction and metrics reset functionality
- Support for DNP-* event codes throughout the system

feat(web)!: enhance web implementation with new functionality

- Integrate callback registry and observability into web platform
- Add mock implementations for dual scheduling methods
- Implement performance tracking and structured logging
- Support for local callback registration and management
- Enhanced error handling and event logging

BREAKING CHANGE: New Android dependencies require Room, WorkManager, and Kotlin coroutines
This commit is contained in:
Matthew Raymer
2025-09-22 09:02:04 +00:00
parent 3994b2eb17
commit 0bb5a8d218
12 changed files with 2048 additions and 32 deletions

View File

@@ -0,0 +1,294 @@
package com.timesafari.dailynotification
import android.content.Context
import android.util.Log
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")
class DailyNotificationPlugin : Plugin() {
companion object {
private const val TAG = "DNP-PLUGIN"
}
private lateinit var db: DailyNotificationDatabase
override fun load() {
super.load()
db = DailyNotificationDatabase.getDatabase(context)
Log.i(TAG, "Daily Notification Plugin loaded")
}
@PluginMethod
fun configure(call: PluginCall) {
try {
val options = call.getObject("options")
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 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)
)
db.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 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
)
db.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")
val contentFetchConfig = parseContentFetchConfig(configJson.getObject("contentFetch"))
val userNotificationConfig = parseUserNotificationConfig(configJson.getObject("userNotification"))
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
)
db.scheduleDao().upsert(fetchSchedule)
db.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 = db.scheduleDao().getEnabled()
val latestCache = db.contentCacheDao().getLatest()
val recentHistory = db.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")
val callback = call.getObject("callback")
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()
)
db.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 = db.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}")
}
}
}
// Helper methods
private fun parseContentFetchConfig(configJson: JSObject): ContentFetchConfig {
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 = configJson.getObject("callbacks")?.getString("apiService"),
database = configJson.getObject("callbacks")?.getString("database"),
reporting = configJson.getObject("callbacks")?.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
}
}