Files
honeyDueKMP/composeApp/src/androidMain/kotlin/com/tt/honeyDue/widget/WidgetUpdateManager.kt
T
Trey T dbff329384 P3 Stream L: widget refresh scheduler (WorkManager, iOS cadence)
WidgetRefreshSchedule: 30-min day / 120-min overnight (6am–11pm split).
WidgetRefreshWorker: CoroutineWorker fetches via APILayer -> repo -> widget.update.
WidgetUpdateManager: chained one-time enqueue pattern (WorkManager PeriodicWork
can't vary cadence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:54:35 -05:00

84 lines
3.3 KiB
Kotlin

package com.tt.honeyDue.widget
import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import java.util.concurrent.TimeUnit
/**
* Scheduler for the widget-refresh background work. Thin wrapper over
* [WorkManager] that enqueues a [WidgetRefreshWorker] with the cadence
* defined by [WidgetRefreshSchedule].
*
* We use a chained one-time-work pattern rather than `PeriodicWorkRequest`
* because:
* - `PeriodicWorkRequest` has a 15-minute floor which is fine, but more
* importantly can't *vary* its cadence between runs.
* - The iOS-parity spec needs 30-min during the day and 120-min overnight
* — so each run computes the next interval based on the local clock
* and enqueues the next one-time request.
*
* On [schedulePeriodic], the worker is enqueued with an initial delay of
* `intervalMinutes(now)`. On successful completion [WidgetRefreshWorker]
* calls [schedulePeriodic] again to chain the next wake.
*/
object WidgetUpdateManager {
/** Unique name for the periodic (chained) refresh queue. */
const val UNIQUE_WORK_NAME: String = "widget_refresh_periodic"
/** Unique name for user- / app-triggered forced refreshes. */
const val FORCE_REFRESH_WORK_NAME: String = "widget_refresh_force"
/**
* Schedule the next periodic refresh. Delay = [WidgetRefreshSchedule.intervalMinutes]
* evaluated against the current local-zone clock. Existing work under
* [UNIQUE_WORK_NAME] is replaced — the new interval always wins.
*/
fun schedulePeriodic(context: Context) {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
val delayMinutes = WidgetRefreshSchedule.intervalMinutes(now)
val request = OneTimeWorkRequestBuilder<WidgetRefreshWorker>()
.setInitialDelay(delayMinutes, TimeUnit.MINUTES)
.addTag(TAG)
.build()
WorkManager.getInstance(context.applicationContext)
.enqueueUniqueWork(UNIQUE_WORK_NAME, ExistingWorkPolicy.REPLACE, request)
}
/**
* Force an immediate refresh. Runs as an expedited worker so the OS
* treats it as a foreground-ish job (best-effort — may be denied
* quota, in which case it falls back to a regular one-time enqueue).
*/
fun forceRefresh(context: Context) {
val request = OneTimeWorkRequestBuilder<WidgetRefreshWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.addTag(TAG)
.build()
WorkManager.getInstance(context.applicationContext)
.enqueueUniqueWork(FORCE_REFRESH_WORK_NAME, ExistingWorkPolicy.REPLACE, request)
}
/**
* Cancel any pending/chained periodic refresh. Does not affect
* in-flight forced refreshes — call [cancel] from a logout flow to
* stop the scheduler wholesale, or clear both queues explicitly.
*/
fun cancel(context: Context) {
val wm = WorkManager.getInstance(context.applicationContext)
wm.cancelUniqueWork(UNIQUE_WORK_NAME)
wm.cancelUniqueWork(FORCE_REFRESH_WORK_NAME)
}
private const val TAG = "widget_refresh"
}