Add RingRenderer (circular gauge mirror of BarRenderer), a widget_layout_rings
layout, a Bars/Rings preference + in-app toggle, and ring rendering branch in
ClaudeUsageWidget. Full (4x2) widget honors the chosen style; compact size stays
bars. Phase 1 of porting hamed-elfayome/Claude-Usage-Tracker features.
recordHistory() only ever stored fiveHourUtilization/weeklyUtilization, but
fetchUsage() returned early with message-count data — before calling the
/usage utilization endpoint — whenever the org JSON carried a message limit.
So utilization was never populated and the history chart stayed stuck on
'Collecting history…'. The prior chart fix only corrected the throttle.
- UsageRepository: always attempt /usage (preferred signal that drives the
weekly bar + history); fall back to org message-count data only when
utilization is unavailable.
- UsageData.sessionReadingPct: utilization preferred, message-count % fallback,
-1f when no reading — so message-only accounts also build history.
- PreferencesManager.recordHistory: record the session line from
sessionReadingPct instead of utilization-only.
- UsageDataTest: cover sessionReadingPct.
History chart: recordHistory() threw away the previous point whenever a
new reading landed within the 2-min de-dup window, but the foreground loop
refreshes every 30s — so history could never grow past one point while the
app was open and the chart stayed stuck on 'Collecting history…'. Now it
throttles by SKIPPING a too-soon reading instead of replacing the last one,
so points accumulate during normal use.
Security:
- Remove hardcoded release keystore passwords from build.gradle.kts; read
from env vars / gitignored keystore.properties; CI injects from Gitea
secrets (KEYSTORE_PASSWORD/KEY_PASSWORD). Signing identity unchanged.
- Make the cookie-never-plaintext invariant explicit on the read path.
- Drop custom ACTION_REFRESH from the exported widget intent-filter so other
apps can't trigger refreshes; internal explicit PendingIntent still works.
- Gate an unguarded Log.w behind BuildConfig.DEBUG.
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>
Three reliability bugs made data inconsistent:
1. Empty-overwrite: a failed or partial fetch returned an empty
UsageData that the worker/app saved unconditionally, wiping the last
good reading and blanking the widget. Added UsageData.mergedWith()
so a fetch that returns nothing usable keeps the previous snapshot,
and a partial fetch falls back per-metric. Never blank again.
2. No in-app auto-refresh: onResume only refreshed when the cache was
>5 min old and there was no live timer. Replaced with a foreground
lifecycle loop that refreshes on open and every 30s while visible,
always painting cached data first. Manual button keeps the spinner;
the loop is silent. App refresh now also pushes the widget update.
3. Spurious logout: a single transient 401/403 (e.g. a Cloudflare
challenge) called clearSession() immediately, logging the user out
and showing "Not signed in". Now clears only after 3 consecutive
auth failures; the counter resets on any successful read.
Battery-friendly: no foreground service. Background widget refresh
stays on the existing alarm + 15-min WorkManager, but with the merge
fix the widget always shows the last data it pulled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Android 16 bug: EncryptedSharedPreferences threw on ANY exception (Keystore
busy during screen-lock/BG wakeup) and the code deleted the encrypted prefs
file on any failure, permanently erasing session cookies. Now only
KeyPermanentlyInvalidatedException (biometric/PIN change) triggers delete;
transient failures preserve the file for the next session.
Also prevents saving cookies to plain-text fallback prefs if encrypted prefs
are unavailable.
WorkManager periodic (15 min, requires network) added alongside AlarmManager
as a Doze-mode backup for Android 16, where inexact alarms can be batched up
to 75 min.
UI: sync icon 24→32dp (large widget), 20→28dp (small); reset-time font
9→11sp (large), 8→10sp (small).
Security:
- All Log.d response-body and URL-bearing logs gated behind BuildConfig.DEBUG
- Cookie header value stripped of CRLF to prevent HTTP header injection
- LoginActivity coroutine migrated from bare CoroutineScope to lifecycleScope
- Widget removed from keyguard (lock-screen) category — usage data is sensitive
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- onResume() no longer triggers a refresh every time — only fetches when
data is >5 min stale, so returning to app shows cached data instantly
without a loading spinner
- Fix CancellationException being swallowed by catch(Exception) in
refreshUsage(), which caused updates to run on a destroyed activity
- EncryptedSharedPreferences key invalidation (caused by enabling/changing
biometrics or screen lock) now deletes the stale encrypted file and
recreates it cleanly, rather than silently using empty fallback prefs
- Wrap all securePrefs read/write ops in try-catch so a mid-session
Keystore failure degrades gracefully instead of crashing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove KEY_COOKIES_BACKUP plaintext fallback from PreferencesManager
- getCookies() now fails closed (force re-login) if EncryptedSharedPreferences unavailable
- Set android:allowBackup="false" to prevent adb backup extraction of session data
- Add missing gradle-wrapper.jar to repo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>