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 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 ALARM_CODE = 1001 private const val INTERVAL_MS = 5 * 60 * 1000L fun schedulePeriodicRefresh(context: Context) { val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager am.setAndAllowWhileIdle( AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + INTERVAL_MS, alarmIntent(context) ) } fun cancelPeriodicRefresh(context: Context) = (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager) .cancel(alarmIntent(context)) 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 ) } }