111 lines
4.0 KiB
Kotlin
111 lines
4.0 KiB
Kotlin
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<UsageUpdateWorker>().build()
|
|
)
|
|
}
|
|
|
|
private fun alarmIntent(context: Context) = PendingIntent.getBroadcast(
|
|
context, ALARM_CODE,
|
|
Intent(context, AlarmReceiver::class.java),
|
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
)
|
|
}
|
|
}
|