Review follow-up on the midnight alarm. The month widget could still show the wrong month after the rollover, and by way of the exact workaround #228's reporter described. ShiftMonthAction stored an absolute index on every tap, so paging forward and back — which looks like a no-op and was how people forced a redraw — silently pinned the widget to the month that was current at the time. It kept redrawing correctly and kept drawing August into September, today's circle nowhere on the grid. Landing back on the current month now clears the key instead of writing it, so the widget goes back to following the date. Paging somewhere else and staying there is left alone; that one is a choice, and the today button undoes it. The alarm also had no way back once something dropped it without telling us — a force-stop, a battery-restricted transition, an OEM freeze — short of the user opening the app. onUpdate is the one wake-up the system still owns through updatePeriodMillis, so both receivers re-arm from it. That doubles as a window narrower: setAndAllowWhileIdle's delivery slack scales with how far out the alarm was set, so re-arming every half hour keeps midnight's window at minutes rather than hours. Boot and package-replace now re-arm and stop there. The host sends APPWIDGET_UPDATE after both, so the redraw was a second pair of wide provider reads and RemoteViews serialisations in a cold process, exactly when the device is busiest. Smaller things: the unreachable lookahead fallback armed 5 seconds out, which would have been a wake-loop rather than the late redraw the comment claimed — an hour now. The action guard's comment was copied from ReminderScheduleReceiver along with its claim that the broadcasts are protected; PROVIDER_CHANGED is not, and the real reason the guard is worth having is that the receiver must stay exported. Application start does its sync off the main thread now. Tests moved onto frozen historical transitions — Berlin 2024 both ways, Havana 2018 — since the 2026 dates they used depend on DST rules that can still change under a tzdata bump. The Berlin "fall back" case never repeated midnight, so it now says what it actually checks (a 25-hour day must not overshoot) and a real repeated-midnight zone, pre-2019 Sao Paulo, pins the known limitation instead of implying it is handled. Dropped the two assertions that restated the code, and added the one that was missing: that the receiver's action guard admits the action the alarm is sent with, which is the single point where the whole thing would fail silently.
89 lines
4.1 KiB
Kotlin
89 lines
4.1 KiB
Kotlin
package de.jeanlucmakiola.calendula.widget
|
|
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import androidx.glance.appwidget.updateAll
|
|
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
|
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.SupervisorJob
|
|
import kotlinx.coroutines.launch
|
|
|
|
/**
|
|
* Redraws both home-screen widgets when their data goes stale, and keeps the
|
|
* midnight rollover alarm armed. Triggered by:
|
|
* - `PROVIDER_CHANGED` from the calendar provider — fires on any data change,
|
|
* so it covers both the app's own writes and external sync.
|
|
* - [ACTION_ROLLOVER], the app's own alarm from [WidgetRolloverScheduler] —
|
|
* the day boundary, so "today" highlighting and the agenda's past-event
|
|
* dimming move on (#228).
|
|
* - `TIME_SET` / `TIMEZONE_CHANGED` — a clock or zone change moves the day
|
|
* boundary relative to the armed alarm, so both redraw *and* re-arm.
|
|
* - `BOOT_COMPLETED` / `MY_PACKAGE_REPLACED` — both wipe pending alarms. The
|
|
* package-replaced one is also what arms existing installs that upgrade into
|
|
* the fix without re-adding their widget.
|
|
*
|
|
* `DATE_CHANGED` is still in the manifest filter as a free extra, but nothing
|
|
* depends on it: it is not an exempted implicit broadcast, so a manifest-declared
|
|
* receiver has not actually been given it since Android 8. The widgets also carry
|
|
* an `updatePeriodMillis` backstop in their provider XML, and the month widget's
|
|
* refresh button forces an immediate redraw.
|
|
*
|
|
* Exported for the system broadcasts; an extra redraw triggered by another app
|
|
* is harmless.
|
|
*/
|
|
class WidgetUpdateReceiver : BroadcastReceiver() {
|
|
override fun onReceive(context: Context, intent: Intent) {
|
|
// The receiver has to stay exported for the system broadcasts, so an
|
|
// explicit intent can reach it with anything in it. Nothing here reads
|
|
// the intent's data and nothing crosses a trust boundary, but narrowing
|
|
// to the actions we actually asked for keeps a stray broadcast from
|
|
// costing two wide provider reads.
|
|
if (intent.action !in HANDLED_ACTIONS) return
|
|
val appContext = context.applicationContext
|
|
// Re-arm first: whatever happens to the redraw, the next day boundary is
|
|
// covered. Boot and package-replace dropped the alarm outright; a
|
|
// rollover just consumed it; a clock change invalidated it.
|
|
WidgetRolloverScheduler.sync(appContext)
|
|
// Boot and package-replace only cost us the alarm. The host sends
|
|
// APPWIDGET_UPDATE after both anyway, so redrawing here would just repeat
|
|
// two wide provider reads and two RemoteViews serialisations in a cold
|
|
// process, at the moment the device is most contended.
|
|
if (intent.action in REARM_ONLY_ACTIONS) return
|
|
val pending = goAsync()
|
|
// Calendar data may have changed (sync / our own write) — drop the cached
|
|
// month window so the widgets reload fresh. Month paging does NOT call
|
|
// this, so arrow taps stay instant.
|
|
invalidateMonthWidgetCache()
|
|
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
|
try {
|
|
AgendaWidget().updateAll(appContext)
|
|
MonthWidget().updateAll(appContext)
|
|
} finally {
|
|
pending.finish()
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
/** The app's own midnight wake-up; see [WidgetRolloverScheduler]. */
|
|
const val ACTION_ROLLOVER = "de.jeanlucmakiola.calendula.widget.ROLLOVER"
|
|
|
|
/** Both wipe pending alarms, and the host redraws the widgets itself after them. */
|
|
private val REARM_ONLY_ACTIONS = setOf(
|
|
Intent.ACTION_BOOT_COMPLETED,
|
|
Intent.ACTION_MY_PACKAGE_REPLACED,
|
|
)
|
|
|
|
internal val HANDLED_ACTIONS = REARM_ONLY_ACTIONS + setOf(
|
|
ACTION_ROLLOVER,
|
|
Intent.ACTION_PROVIDER_CHANGED,
|
|
Intent.ACTION_DATE_CHANGED,
|
|
Intent.ACTION_TIME_CHANGED,
|
|
Intent.ACTION_TIMEZONE_CHANGED,
|
|
)
|
|
}
|
|
}
|