feat: notify user when claude.ai session expires (tap to re-auth)
Build APK / build (push) Successful in 1m35s
Build APK / build (push) Successful in 1m35s
Detection already cleared the session after 3x 401/403 but did so silently — the widget would just go stale. Now fires a one-time HIGH-priority notification on its own 'Sign-in alerts' channel that opens LoginActivity for re-paste.
This commit is contained in:
@@ -23,8 +23,42 @@ import me.khodak.claudeusage.data.UsageData
|
|||||||
object Notifier {
|
object Notifier {
|
||||||
|
|
||||||
private const val CHANNEL_ID = "usage_alerts"
|
private const val CHANNEL_ID = "usage_alerts"
|
||||||
|
private const val AUTH_CHANNEL_ID = "session_alerts"
|
||||||
|
private const val AUTH_NOTIF_ID = 3000
|
||||||
private val LEVELS = intArrayOf(90, 100)
|
private val LEVELS = intArrayOf(90, 100)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired once when the saved claude.ai session is cleared after repeated 401/403s — without
|
||||||
|
* this the widget would just silently go stale and the user would have to notice the dead
|
||||||
|
* data themselves. Tapping opens LoginActivity straight to the cookie re-paste flow. Posts on
|
||||||
|
* its own channel so it can be muted independently of usage alerts, and is not gated on the
|
||||||
|
* usage-alert toggle: a dead widget is a functional failure the user needs to know about.
|
||||||
|
*/
|
||||||
|
fun notifySessionExpired(context: Context) {
|
||||||
|
val mgr = NotificationManagerCompat.from(context)
|
||||||
|
if (!mgr.areNotificationsEnabled()) return
|
||||||
|
ensureAuthChannel(context)
|
||||||
|
val intent = Intent(context, LoginActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
val pi = PendingIntent.getActivity(
|
||||||
|
context, 1, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val notif = NotificationCompat.Builder(context, AUTH_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_claude_burst)
|
||||||
|
.setContentTitle("Claude sign-in expired")
|
||||||
|
.setContentText("Tap to sign in again — the widget can't update until you do.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
mgr.notify(AUTH_NOTIF_ID, notif)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
// Notifications revoked between the check and post — nothing to do.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkAndNotify(context: Context, prefs: PreferencesManager, data: UsageData) {
|
fun checkAndNotify(context: Context, prefs: PreferencesManager, data: UsageData) {
|
||||||
if (!prefs.isNotifyEnabled()) return
|
if (!prefs.isNotifyEnabled()) return
|
||||||
val mgr = NotificationManagerCompat.from(context)
|
val mgr = NotificationManagerCompat.from(context)
|
||||||
@@ -94,6 +128,17 @@ object Notifier {
|
|||||||
private fun notifId(metric: String, levelIndex: Int) =
|
private fun notifId(metric: String, levelIndex: Int) =
|
||||||
2000 + (if (metric == "weekly") 10 else 0) + levelIndex
|
2000 + (if (metric == "weekly") 10 else 0) + levelIndex
|
||||||
|
|
||||||
|
private fun ensureAuthChannel(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (mgr.getNotificationChannel(AUTH_CHANNEL_ID) != null) return
|
||||||
|
mgr.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
AUTH_CHANNEL_ID, "Sign-in alerts", NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply { description = "Tells you when your claude.ai session expires and needs re-auth" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureChannel(context: Context) {
|
private fun ensureChannel(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|||||||
@@ -31,10 +31,16 @@ class UsageUpdateWorker(
|
|||||||
val animJob = launch { rotateRefreshIcon() }
|
val animJob = launch { rotateRefreshIcon() }
|
||||||
try {
|
try {
|
||||||
val data = UsageRepository(prefs).fetchUsage()
|
val data = UsageRepository(prefs).fetchUsage()
|
||||||
// Preserve last-good data so a failed/partial fetch never blanks the widget.
|
// We passed the isLoggedIn() guard above, so if the session is gone now the
|
||||||
prefs.saveUsageData(data.mergedWith(prefs.getUsageData()))
|
// cookie just expired this cycle — tell the user once (next cycle returns early).
|
||||||
prefs.recordHistory(data) // history records only fresh readings
|
if (!prefs.isLoggedIn()) {
|
||||||
Notifier.checkAndNotify(context, prefs, data)
|
Notifier.notifySessionExpired(context)
|
||||||
|
} else {
|
||||||
|
// Preserve last-good data so a failed/partial fetch never blanks the widget.
|
||||||
|
prefs.saveUsageData(data.mergedWith(prefs.getUsageData()))
|
||||||
|
prefs.recordHistory(data) // history records only fresh readings
|
||||||
|
Notifier.checkAndNotify(context, prefs, data)
|
||||||
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
animJob.cancel()
|
animJob.cancel()
|
||||||
animJob.join()
|
animJob.join()
|
||||||
|
|||||||
Reference in New Issue
Block a user