package com.timesafari.dailynotification import android.app.AlarmManager import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch /** * AlarmManager implementation for user notifications * Implements TTL-at-fire logic and notification delivery * * @author Matthew Raymer * @version 1.1.0 */ class NotifyReceiver : BroadcastReceiver() { companion object { private const val TAG = "DNP-NOTIFY" private const val CHANNEL_ID = "daily_notifications" private const val NOTIFICATION_ID = 1001 private const val REQUEST_CODE = 2001 fun scheduleExactNotification( context: Context, triggerAtMillis: Long, config: UserNotificationConfig ) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, NotifyReceiver::class.java).apply { putExtra("title", config.title) putExtra("body", config.body) putExtra("sound", config.sound ?: true) putExtra("vibration", config.vibration ?: true) putExtra("priority", config.priority ?: "normal") } val pendingIntent = PendingIntent.getBroadcast( context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent ) } else { alarmManager.setExact( AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent ) } Log.i(TAG, "Exact notification scheduled for: $triggerAtMillis") } catch (e: SecurityException) { Log.w(TAG, "Cannot schedule exact alarm, falling back to inexact", e) alarmManager.set( AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent ) } } fun cancelNotification(context: Context) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, NotifyReceiver::class.java) val pendingIntent = PendingIntent.getBroadcast( context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.cancel(pendingIntent) Log.i(TAG, "Notification alarm cancelled") } } override fun onReceive(context: Context, intent: Intent?) { Log.i(TAG, "Notification receiver triggered") CoroutineScope(Dispatchers.IO).launch { try { val db = DailyNotificationDatabase.getDatabase(context) val latestCache = db.contentCacheDao().getLatest() if (latestCache == null) { Log.w(TAG, "No cached content available for notification") recordHistory(db, "notify", "no_content") return@launch } // TTL-at-fire check val now = System.currentTimeMillis() val ttlExpiry = latestCache.fetchedAt + (latestCache.ttlSeconds * 1000L) if (now > ttlExpiry) { Log.i(TAG, "Content TTL expired, skipping notification") recordHistory(db, "notify", "skipped_ttl") return@launch } // Show notification val title = intent?.getStringExtra("title") ?: "Daily Notification" val body = intent?.getStringExtra("body") ?: String(latestCache.payload) val sound = intent?.getBooleanExtra("sound", true) ?: true val vibration = intent?.getBooleanExtra("vibration", true) ?: true val priority = intent?.getStringExtra("priority") ?: "normal" showNotification(context, title, body, sound, vibration, priority) recordHistory(db, "notify", "success") // Fire callbacks fireCallbacks(context, db, "onNotifyDelivered", latestCache) } catch (e: Exception) { Log.e(TAG, "Error in notification receiver", e) try { val db = DailyNotificationDatabase.getDatabase(context) recordHistory(db, "notify", "failure", e.message) } catch (dbError: Exception) { Log.e(TAG, "Failed to record notification failure", dbError) } } } } private fun showNotification( context: Context, title: String, body: String, sound: Boolean, vibration: Boolean, priority: String ) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Create notification channel for Android 8.0+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "Daily Notifications", when (priority) { "high" -> NotificationManager.IMPORTANCE_HIGH "low" -> NotificationManager.IMPORTANCE_LOW else -> NotificationManager.IMPORTANCE_DEFAULT } ).apply { enableVibration(vibration) if (!sound) { setSound(null, null) } } notificationManager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(title) .setContentText(body) .setSmallIcon(android.R.drawable.ic_dialog_info) .setPriority( when (priority) { "high" -> NotificationCompat.PRIORITY_HIGH "low" -> NotificationCompat.PRIORITY_LOW else -> NotificationCompat.PRIORITY_DEFAULT } ) .setAutoCancel(true) .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null) .build() notificationManager.notify(NOTIFICATION_ID, notification) Log.i(TAG, "Notification displayed: $title") } private suspend fun recordHistory( db: DailyNotificationDatabase, kind: String, outcome: String, diagJson: String? = null ) { try { db.historyDao().insert( History( refId = "notify_${System.currentTimeMillis()}", kind = kind, occurredAt = System.currentTimeMillis(), outcome = outcome, diagJson = diagJson ) ) } catch (e: Exception) { Log.e(TAG, "Failed to record history", e) } } private suspend fun fireCallbacks( context: Context, db: DailyNotificationDatabase, eventType: String, contentCache: ContentCache ) { try { val callbacks = db.callbackDao().getEnabled() callbacks.forEach { callback -> try { when (callback.kind) { "http" -> fireHttpCallback(callback, eventType, contentCache) "local" -> fireLocalCallback(context, callback, eventType, contentCache) else -> Log.w(TAG, "Unknown callback kind: ${callback.kind}") } } catch (e: Exception) { Log.e(TAG, "Failed to fire callback ${callback.id}", e) recordHistory(db, "callback", "failure", "{\"callback_id\": \"${callback.id}\", \"error\": \"${e.message}\"}") } } } catch (e: Exception) { Log.e(TAG, "Failed to fire callbacks", e) } } private suspend fun fireHttpCallback( callback: Callback, eventType: String, contentCache: ContentCache ) { // HTTP callback implementation would go here Log.i(TAG, "HTTP callback fired: ${callback.id} for event: $eventType") } private suspend fun fireLocalCallback( context: Context, callback: Callback, eventType: String, contentCache: ContentCache ) { // Local callback implementation would go here Log.i(TAG, "Local callback fired: ${callback.id} for event: $eventType") } }