v1.14: usage history chart + threshold notifications
Add an in-app 7-day history chart and opt-in usage alerts, the two features requested from the macOS Claude-Usage-Tracker that map cleanly to an Android widget app. History: - UsageSnapshot model; PreferencesManager records session/weekly utilization on every refresh (7-day retention, <=600 points, collapses readings under 2 min apart to avoid worker+manual double-logging). - HistoryChartView: dependency-free Canvas line chart (session/weekly, 0/50/100% gridlines), breaks the line across >35-min gaps. - New HISTORY card with chart + legend. Notifications: - Notifier posts when session/weekly crosses a user threshold, at most once per limit window (keyed on reset-epoch, re-arms on rollover). - USAGE ALERTS card: enable switch + session/weekly sliders (50-100%, defaults 90/85). POST_NOTIFICATIONS permission + runtime request. - Wired into the existing 5-min background worker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.util.Calendar
|
||||
|
||||
class PreferencesManager(context: Context) {
|
||||
@@ -73,6 +74,61 @@ class PreferencesManager(context: Context) {
|
||||
|
||||
fun isLoggedIn(): Boolean = !getCookies().isNullOrBlank()
|
||||
|
||||
// ── Usage history (for the in-app chart) ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Append a history point if [data] carries a real utilization reading.
|
||||
* De-duplicates rapid double-fires (manual refresh + background worker landing
|
||||
* together) by skipping points within [MIN_HISTORY_GAP_MS] of the last one, and
|
||||
* prunes anything older than [HISTORY_RETENTION_MS] / beyond [MAX_HISTORY_POINTS].
|
||||
*/
|
||||
fun recordHistory(data: UsageData) {
|
||||
if (data.fiveHourUtilization < 0f && data.weeklyUtilization < 0f) return
|
||||
val now = System.currentTimeMillis()
|
||||
val history = getHistory().toMutableList()
|
||||
if (history.isNotEmpty() && now - history.last().epochMs < MIN_HISTORY_GAP_MS) {
|
||||
history.removeAt(history.size - 1) // collapse near-simultaneous readings
|
||||
}
|
||||
history.add(
|
||||
UsageSnapshot(
|
||||
epochMs = now,
|
||||
sessionPct = data.fiveHourUtilization,
|
||||
weeklyPct = data.weeklyUtilization
|
||||
)
|
||||
)
|
||||
val cutoff = now - HISTORY_RETENTION_MS
|
||||
val pruned = history.filter { it.epochMs >= cutoff }
|
||||
.takeLast(MAX_HISTORY_POINTS)
|
||||
prefs.edit().putString(KEY_HISTORY, gson.toJson(pruned)).apply()
|
||||
}
|
||||
|
||||
fun getHistory(): List<UsageSnapshot> {
|
||||
val json = prefs.getString(KEY_HISTORY, null) ?: return emptyList()
|
||||
return try {
|
||||
val type = object : TypeToken<List<UsageSnapshot>>() {}.type
|
||||
gson.fromJson<List<UsageSnapshot>>(json, type) ?: emptyList()
|
||||
} catch (e: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
// ── Notification settings ────────────────────────────────────────────────
|
||||
|
||||
fun isNotifyEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFY_ENABLED, true)
|
||||
fun setNotifyEnabled(v: Boolean) = prefs.edit().putBoolean(KEY_NOTIFY_ENABLED, v).apply()
|
||||
|
||||
fun getSessionThreshold(): Int = prefs.getInt(KEY_NOTIFY_SESSION_PCT, 90)
|
||||
fun setSessionThreshold(pct: Int) = prefs.edit().putInt(KEY_NOTIFY_SESSION_PCT, pct).apply()
|
||||
|
||||
fun getWeeklyThreshold(): Int = prefs.getInt(KEY_NOTIFY_WEEKLY_PCT, 85)
|
||||
fun setWeeklyThreshold(pct: Int) = prefs.edit().putInt(KEY_NOTIFY_WEEKLY_PCT, pct).apply()
|
||||
|
||||
/**
|
||||
* Tracks the reset-epoch a metric was last notified for, so we alert at most once
|
||||
* per limit window. When the window rolls over (reset epoch changes), it re-arms.
|
||||
*/
|
||||
fun getNotifiedResetEpoch(key: String): Long = prefs.getLong("notified_$key", 0L)
|
||||
fun setNotifiedResetEpoch(key: String, epoch: Long) =
|
||||
prefs.edit().putLong("notified_$key", epoch).apply()
|
||||
|
||||
companion object {
|
||||
private const val KEY_COOKIES = "session_cookies"
|
||||
private const val KEY_ORG_ID = "org_id"
|
||||
@@ -80,6 +136,14 @@ class PreferencesManager(context: Context) {
|
||||
private const val KEY_USAGE_DATA = "usage_data"
|
||||
private const val KEY_ACTIVE_WEEK = "active_week"
|
||||
private const val KEY_ACTIVE_MASK = "active_mask"
|
||||
private const val KEY_HISTORY = "usage_history"
|
||||
private const val KEY_NOTIFY_ENABLED = "notify_enabled"
|
||||
private const val KEY_NOTIFY_SESSION_PCT = "notify_session_pct"
|
||||
private const val KEY_NOTIFY_WEEKLY_PCT = "notify_weekly_pct"
|
||||
|
||||
private const val MIN_HISTORY_GAP_MS = 2 * 60 * 1000L // collapse readings <2 min apart
|
||||
private const val HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000L // keep 7 days
|
||||
private const val MAX_HISTORY_POINTS = 600
|
||||
|
||||
fun createSecurePrefs(context: Context, onFallback: (() -> Unit)? = null): android.content.SharedPreferences {
|
||||
return try {
|
||||
|
||||
Reference in New Issue
Block a user