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>
This commit is contained in:
Trey T
2026-04-18 12:54:35 -05:00
parent 58b9371d0d
commit dbff329384
7 changed files with 682 additions and 90 deletions
@@ -0,0 +1,62 @@
package com.tt.honeyDue.widget
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
/**
* Pure-logic schedule for widget refresh cadence. Mirrors the iOS-parity
* split from the P3 parity plan:
*
* - 06:00 (inclusive) .. 23:00 (exclusive) local → refresh every 30 minutes
* - 23:00 (inclusive) .. 06:00 (exclusive) local → refresh every 120 minutes
*
* iOS ([BackgroundTaskManager.swift]) uses a random 12am4am overnight
* BGAppRefreshTask window rather than a fixed cadence, because iOS
* `BGTaskScheduler` is coalesced by the system. Android's WorkManager runs
* user-defined intervals, so this file encodes the ios-parity cadence the
* plan specifies. The split 30/120 preserves the core intent: frequent
* while awake, sparse while the user is asleep.
*/
object WidgetRefreshSchedule {
private const val DAY_START_HOUR_INCLUSIVE = 6 // 06:00 local
private const val DAY_END_HOUR_EXCLUSIVE = 23 // 23:00 local
const val DAY_INTERVAL_MINUTES: Long = 30L
const val NIGHT_INTERVAL_MINUTES: Long = 120L
/**
* Returns the refresh interval (in minutes) for a wall-clock time.
*
* Hour bands:
* - [06:00, 23:00) → [DAY_INTERVAL_MINUTES] (30)
* - [23:00, 06:00) → [NIGHT_INTERVAL_MINUTES] (120)
*/
fun intervalMinutes(at: LocalDateTime): Long {
val hour = at.hour
return if (hour in DAY_START_HOUR_INCLUSIVE until DAY_END_HOUR_EXCLUSIVE) {
DAY_INTERVAL_MINUTES
} else {
NIGHT_INTERVAL_MINUTES
}
}
/**
* Returns `now + intervalMinutes(now)` as a [LocalDateTime].
*
* Arithmetic is performed through [TimeZone.UTC] to avoid ambiguity
* around DST transitions in the local zone — the absolute minute offset
* is what WorkManager's `setInitialDelay` consumes, so the returned
* wall-clock value is for display/testing only.
*/
fun nextRefreshTime(now: LocalDateTime): LocalDateTime {
val interval = intervalMinutes(now)
val instant = now.toInstant(TimeZone.UTC)
val next = instant.plus(interval, DateTimeUnit.MINUTE)
return next.toLocalDateTime(TimeZone.UTC)
}
}