fix: redraw the next-alarm widget at midnight

The widget worked out "Today" when it was drawn, and nothing redrew it
when the date changed, so a widget drawn on Monday night still named
Tuesday on Tuesday morning. It now shares the analog faces' midnight
redraw, renamed to MidnightRefresh, and redraws on a clock or zone change.
This commit is contained in:
makiolaj committed 2026-10-21 17:35:39 +02:00
1 parent 9af3d2b515
commit 85c12811b0
7 files changed
+55 -23

No files matched your search

+1 -1
View File
@@ -367,7 +367,7 @@
updatePeriodMillis is 0 in every provider-info XML — WidgetSync
is the one refresh trigger for the data widgets, and the analog
widget's date is redrawn at midnight and on a clock or zone
change (widget/AnalogDateRefresh.kt). -->
change (widget/MidnightRefresh.kt). -->
<receiver
android:name=".widget.NextAlarmWidgetReceiver"
android:label="@string/widget_next_alarm_label"
@@ -8,7 +8,8 @@ import de.jeanlucmakiola.clockula.alarm.AlarmEngine
import de.jeanlucmakiola.clockula.stopwatch.StopwatchEngine
import de.jeanlucmakiola.clockula.timer.TimerEngine
import de.jeanlucmakiola.clockula.widget.AnalogClockWidget
import de.jeanlucmakiola.clockula.widget.AnalogDateRefresh
import de.jeanlucmakiola.clockula.widget.MidnightRefresh
import de.jeanlucmakiola.clockula.widget.NextAlarmWidget
import de.jeanlucmakiola.floret.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -92,7 +93,8 @@ class SystemEventReceiver : HiltBroadcastReceiver() {
// The analog faces' date is drawn for the old day or
// zone, and the midnight redraw armed for the old one.
AnalogClockWidget().updateAll(context)
AnalogDateRefresh.schedule(context)
NextAlarmWidget().updateAll(context)
MidnightRefresh.schedule(context)
}
}
} finally {
@@ -264,7 +264,7 @@ private fun RemoteViews.recolor(context: Context, style: AnalogClockStyle, overl
* live in a vector drawable, so this hand is a bitmap — an alpha mask, tinted by
* `setSecondHandTintList` in the launcher so it still follows day/night.
* It is redrawn at midnight and on a clock or zone change
* (`widget/AnalogDateRefresh.kt`).
* (`widget/MidnightRefresh.kt`).
*/
private fun dateHand(context: Context): Bitmap {
val px = 360
@@ -39,10 +39,11 @@ class AnalogClockWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = AnalogClockWidget()
override fun onReceive(context: Context, intent: Intent) {
// Midnight: redraw every analog widget, as the platform's own update
// Midnight: redraw every dated widget, as the platform's own update
// would, which re-arms the next one in `onUpdate`.
if (intent.action == AnalogDateRefresh.ACTION_REFRESH) {
onUpdate(context, AppWidgetManager.getInstance(context), AnalogDateRefresh.placedIds(context))
if (intent.action == MidnightRefresh.ACTION_REFRESH) {
onUpdate(context, AppWidgetManager.getInstance(context), MidnightRefresh.placedIds(context))
MidnightRefresh.updateNextAlarm(context)
return
}
super.onReceive(context, intent)
@@ -50,11 +51,12 @@ class AnalogClockWidgetReceiver : GlanceAppWidgetReceiver() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
AnalogDateRefresh.schedule(context)
MidnightRefresh.schedule(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
AnalogDateRefresh.cancel(context)
// Disarmed only if no next-alarm widget still needs it.
MidnightRefresh.schedule(context)
}
}
@@ -11,23 +11,27 @@ import java.time.Instant
import java.time.ZonedDateTime
/**
* Keeps the analog faces' date current. The dated faces draw today's date
* into a bitmap hand (`ui/widget/AnalogClockWidgetContent.kt`), which only
* changes when the widget is re-rendered — and nothing the app writes
* re-renders it. So the analog widget is redrawn at each local midnight (a
* non-waking, windowed alarm: a dark screen has no date to show, and a
* minute's slack lets the platform batch it) and on a clock or zone change
* Keeps the widgets that show a date current. The dated analog faces draw
* today's date into a bitmap hand (`ui/widget/AnalogClockWidgetContent.kt`),
* and the next-alarm widget says "Today" or names the day, and both only
* change when the widget is re-rendered — which nothing the app writes does
* at midnight. So both are redrawn at each local midnight (a non-waking,
* windowed alarm: a dark screen has no date to show, and a minute's slack
* lets the platform batch it) and on a clock or zone change
* (`SystemEventReceiver`).
*
* The alarm is delivered to the analog receiver, which redraws its own
* widgets and hands the next-alarm widget a standard update broadcast.
*/
object AnalogDateRefresh {
object MidnightRefresh {
const val ACTION_REFRESH = "de.jeanlucmakiola.clockula.widget.action.REFRESH_ANALOG_DATE"
/** Arms the next midnight redraw while any analog widget is placed, and disarms it otherwise. */
/** Arms the next midnight redraw while any dated widget is placed, and disarms it otherwise. */
fun schedule(context: Context) {
val manager = context.getSystemService(AlarmManager::class.java) ?: return
val pendingIntent = pendingIntent(context)
if (placedIds(context).isEmpty()) {
if (placedIds(context).isEmpty() && nextAlarmIds(context).isEmpty()) {
manager.cancel(pendingIntent)
return
}
@@ -38,15 +42,26 @@ object AnalogDateRefresh {
manager.setWindow(AlarmManager.RTC, midnight, WINDOW_MILLIS, pendingIntent)
}
fun cancel(context: Context) {
context.getSystemService(AlarmManager::class.java)?.cancel(pendingIntent(context))
}
/** The placed analog widgets' ids — what an [ACTION_REFRESH] hands to `onUpdate`. */
fun placedIds(context: Context): IntArray =
AppWidgetManager.getInstance(context)
.getAppWidgetIds(ComponentName(context, AnalogClockWidgetReceiver::class.java))
/** Redraws the placed next-alarm widgets the way the platform's own update would. */
fun updateNextAlarm(context: Context) {
val ids = nextAlarmIds(context)
if (ids.isEmpty()) return
context.sendBroadcast(
Intent(context, NextAlarmWidgetReceiver::class.java)
.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids),
)
}
private fun nextAlarmIds(context: Context): IntArray =
AppWidgetManager.getInstance(context)
.getAppWidgetIds(ComponentName(context, NextAlarmWidgetReceiver::class.java))
private fun pendingIntent(context: Context): PendingIntent =
PendingIntent.getBroadcast(
context,
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.clockula.widget
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.appwidget.GlanceAppWidget
@@ -27,4 +28,16 @@ class NextAlarmWidget : GlanceAppWidget() {
class NextAlarmWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = NextAlarmWidget()
// "Today" and the day's name go stale at midnight, so this widget shares
// the analog faces' midnight redraw.
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
MidnightRefresh.schedule(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
MidnightRefresh.schedule(context)
}
}
@@ -33,7 +33,7 @@ import kotlin.time.Duration.Companion.milliseconds
* the stopwatch, not the clocks. The quick-timer widget follows the timers,
* since it shows the countdown of the timer it started
* (`widget/QuickTimerWidget.kt`). The analog clock reads none of them; its
* date is kept current by [AnalogDateRefresh].
* date is kept current by [MidnightRefresh].
*
* Started once, from [de.jeanlucmakiola.clockula.ClockulaApp.onCreate], in the
* app's `@ApplicationScope` — never driven by a `Ticker`, so an idle home