package me.khodak.claudeusage import android.app.AlarmManager import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.SystemClock import androidx.work.* import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.khodak.claudeusage.data.PreferencesManager import java.util.concurrent.TimeUnit class UsageUpdateWorker( private val context: Context, workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { val prefs = PreferencesManager(context) if (!prefs.isLoggedIn()) return Result.success() prefs.markTodayActive() coroutineScope { val animJob = launch { rotateRefreshIcon() } try { val data = UsageRepository(prefs).fetchUsage() prefs.saveUsageData(data) } catch (_: Exception) {} animJob.cancel() animJob.join() } pushWidgetUpdate() return Result.success() } private suspend fun rotateRefreshIcon() { val manager = AppWidgetManager.getInstance(context) val ids = manager.getAppWidgetIds(ComponentName(context, ClaudeUsageWidget::class.java)) val startMs = System.currentTimeMillis() val msPerRotation = 800L // one full rotation every 0.8 seconds fun angleAt(now: Long) = ((now - startMs) % msPerRotation) * 360f / msPerRotation try { while (true) { ClaudeUsageWidget.currentRotation = angleAt(System.currentTimeMillis()) ids.forEach { id -> ClaudeUsageWidget.updateWidget(context, manager, id) } delay(16) // aim for ~60fps; IPC speed sets the real ceiling } } finally { // Finish the current rotation cleanly — run until at least one full spin withContext(NonCancellable) { val minEndMs = startMs + msPerRotation while (System.currentTimeMillis() < minEndMs) { ClaudeUsageWidget.currentRotation = angleAt(System.currentTimeMillis()) ids.forEach { id -> ClaudeUsageWidget.updateWidget(context, manager, id) } delay(16) } } } } private fun pushWidgetUpdate() { ClaudeUsageWidget.isRefreshing = false ClaudeUsageWidget.currentRotation = 0f val manager = AppWidgetManager.getInstance(context) val ids = manager.getAppWidgetIds(ComponentName(context, ClaudeUsageWidget::class.java)) ids.forEach { id -> ClaudeUsageWidget.updateWidget(context, manager, id) } } companion object { private const val WORK_ONE_SHOT = "claude_oneshot" private const val WORK_PERIODIC = "claude_periodic" private const val ALARM_CODE = 1001 private const val INTERVAL_MS = 5 * 60 * 1000L fun schedulePeriodicRefresh(context: Context) { // 5-min alarm for fast updates when the device is active/awake val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager am.setAndAllowWhileIdle( AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + INTERVAL_MS, alarmIntent(context) ) // WorkManager periodic as a Doze/background backup (Android 16 reliability). // WorkManager uses JobScheduler which survives Doze; AlarmManager may be batched // up to 75 min in Doze mode. KEEP = don't reset the 15-min timer on every alarm. WorkManager.getInstance(context).enqueueUniquePeriodicWork( WORK_PERIODIC, ExistingPeriodicWorkPolicy.KEEP, PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() ) .build() ) } fun cancelPeriodicRefresh(context: Context) { (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager) .cancel(alarmIntent(context)) WorkManager.getInstance(context).cancelUniqueWork(WORK_PERIODIC) } fun triggerImmediateRefresh(context: Context) { WorkManager.getInstance(context).enqueueUniqueWork( WORK_ONE_SHOT, ExistingWorkPolicy.REPLACE, OneTimeWorkRequestBuilder().build() ) } private fun alarmIntent(context: Context) = PendingIntent.getBroadcast( context, ALARM_CODE, Intent(context, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } }