package me.khodak.claudeusage 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 androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import me.khodak.claudeusage.data.PreferencesManager import me.khodak.claudeusage.data.UsageData /** * Posts a notification when session or weekly utilization crosses the user's threshold. * Each metric fires at most once per limit window: we remember the reset-epoch we alerted * for, and only re-arm when that window rolls over (epoch changes) — so the user isn't * pinged every 5 minutes while sitting above the line. */ object Notifier { private const val CHANNEL_ID = "usage_alerts" private const val SESSION_NOTIF_ID = 2001 private const val WEEKLY_NOTIF_ID = 2002 fun checkAndNotify(context: Context, prefs: PreferencesManager, data: UsageData) { if (!prefs.isNotifyEnabled()) return val mgr = NotificationManagerCompat.from(context) if (!mgr.areNotificationsEnabled()) return // OS-level or runtime permission off ensureChannel(context) val session = data.fiveHourUtilization if (session >= 0f) { maybeFire( context, mgr, prefs, key = "session", util = session.toInt(), threshold = prefs.getSessionThreshold(), resetEpoch = data.effectiveResetEpoch, notifId = SESSION_NOTIF_ID, title = "Session usage at ${session.toInt()}%", body = "Your current 5-hour window is nearly used up." ) } val weekly = data.weeklyUtilization if (weekly >= 0f) { maybeFire( context, mgr, prefs, key = "weekly", util = weekly.toInt(), threshold = prefs.getWeeklyThreshold(), resetEpoch = data.weeklyResetAtEpoch, notifId = WEEKLY_NOTIF_ID, title = "Weekly usage at ${weekly.toInt()}%", body = "You're approaching your weekly Claude limit." ) } } private fun maybeFire( context: Context, mgr: NotificationManagerCompat, prefs: PreferencesManager, key: String, util: Int, threshold: Int, resetEpoch: Long, notifId: Int, title: String, body: String ) { if (util < threshold) return // Already alerted for this exact window? Skip. (resetEpoch<=0 means "unknown window" — // fall back to a coarse marker so we still alert once instead of never.) val windowMarker = if (resetEpoch > 0) resetEpoch else 1L if (prefs.getNotifiedResetEpoch(key) == windowMarker) return val notif = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_claude_burst) .setContentTitle(title) .setContentText(body) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) .setContentIntent(openAppIntent(context)) .build() try { mgr.notify(notifId, notif) prefs.setNotifiedResetEpoch(key, windowMarker) } catch (_: SecurityException) { // Notifications revoked between the check and post — nothing to do. } } private fun ensureChannel(context: Context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (mgr.getNotificationChannel(CHANNEL_ID) != null) return mgr.createNotificationChannel( NotificationChannel( CHANNEL_ID, "Usage alerts", NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "Alerts when you approach your Claude usage limits" } ) } private fun openAppIntent(context: Context): PendingIntent { val intent = Intent(context, MainActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) return PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } }