v1.16: simplify usage alerts to fixed 90% and 100% (less aggressive)
Build APK / build (push) Successful in 1m50s
Build APK / build (push) Successful in 1m50s
Replace the configurable threshold sliders with two fixed alert levels — 90% and 100% — per metric. Anti-spam now uses hysteresis instead of the API reset-epoch (which could drift and re-fire): each level fires once when crossed and re-arms only after usage drops back below it. Alerts are posted only by the background worker, never the in-app refresh loop, so you're not pinged while looking at the app. UI drops the sliders for a one-line description; settings keep just the on/off switch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,16 +12,18 @@ 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.
|
||||
* Fires exactly two alerts per metric — one at 90% and one at 100% — and no more.
|
||||
*
|
||||
* Uses hysteresis, not the API's reset timestamp: each level fires once when usage first
|
||||
* crosses it, and only re-arms after usage drops back below that level (i.e. a new window /
|
||||
* usage reset). That keeps it quiet even if the reset time drifts between fetches. Only the
|
||||
* background worker calls this — never the in-app refresh loop — so you're not pinged while
|
||||
* you're already looking at the app.
|
||||
*/
|
||||
object Notifier {
|
||||
|
||||
private const val CHANNEL_ID = "usage_alerts"
|
||||
private const val SESSION_NOTIF_ID = 2001
|
||||
private const val WEEKLY_NOTIF_ID = 2002
|
||||
private val LEVELS = intArrayOf(90, 100)
|
||||
|
||||
fun checkAndNotify(context: Context, prefs: PreferencesManager, data: UsageData) {
|
||||
if (!prefs.isNotifyEnabled()) return
|
||||
@@ -29,53 +31,50 @@ object Notifier {
|
||||
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."
|
||||
)
|
||||
}
|
||||
evaluate(context, mgr, prefs, "session", data.fiveHourUtilization, "5-hour session")
|
||||
evaluate(context, mgr, prefs, "weekly", data.weeklyUtilization, "weekly")
|
||||
}
|
||||
|
||||
private fun maybeFire(
|
||||
private fun evaluate(
|
||||
context: Context,
|
||||
mgr: NotificationManagerCompat,
|
||||
prefs: PreferencesManager,
|
||||
key: String,
|
||||
util: Int,
|
||||
threshold: Int,
|
||||
resetEpoch: Long,
|
||||
notifId: Int,
|
||||
title: String,
|
||||
body: String
|
||||
metric: String,
|
||||
utilization: Float,
|
||||
label: 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
|
||||
if (utilization < 0f) return
|
||||
val util = utilization.toInt()
|
||||
for ((i, level) in LEVELS.withIndex()) {
|
||||
val key = "${metric}_$level"
|
||||
if (util >= level) {
|
||||
if (!prefs.wasNotified(key)) {
|
||||
fire(context, mgr, notifId(metric, i), level, label, util)
|
||||
prefs.setNotified(key, true)
|
||||
}
|
||||
} else if (prefs.wasNotified(key)) {
|
||||
prefs.setNotified(key, false) // dropped below the line → re-arm for next window
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fire(
|
||||
context: Context,
|
||||
mgr: NotificationManagerCompat,
|
||||
notifId: Int,
|
||||
level: Int,
|
||||
label: String,
|
||||
util: Int
|
||||
) {
|
||||
val title: String
|
||||
val body: String
|
||||
if (level >= 100) {
|
||||
title = "${label.replaceFirstChar { it.uppercase() }} limit reached"
|
||||
body = "You've hit 100% of your $label limit."
|
||||
} else {
|
||||
title = "${label.replaceFirstChar { it.uppercase() }} at $util%"
|
||||
body = "You're at $level% of your $label limit."
|
||||
}
|
||||
val notif = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_claude_burst)
|
||||
.setContentTitle(title)
|
||||
@@ -84,15 +83,17 @@ object Notifier {
|
||||
.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.
|
||||
}
|
||||
}
|
||||
|
||||
// Stable id per (metric, level) so re-posts replace rather than stack.
|
||||
private fun notifId(metric: String, levelIndex: Int) =
|
||||
2000 + (if (metric == "weekly") 10 else 0) + levelIndex
|
||||
|
||||
private fun ensureChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
@@ -100,7 +101,7 @@ object Notifier {
|
||||
mgr.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID, "Usage alerts", NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply { description = "Alerts when you approach your Claude usage limits" }
|
||||
).apply { description = "Alerts at 90% and 100% of your Claude usage limits" }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user