Fix widget onUpdate crash, skip provider echoes, per-widget redraws (#420, #229)

This commit is contained in:
2026-10-21 17:36:30 +02:00
parent 225901ceee
commit 35f5dc1666
10 changed files with 172 additions and 68 deletions
@@ -44,24 +44,24 @@ class CalendulaApp : Application() {
chooseIssueUrl = getString(R.string.report_issue_choose_url),
),
)
// Before anything that writes, so the widgets see every change (#420).
widgetEntryPoint().widgetRefreshCoordinator().start()
reconcileAutoBackup()
reconcileSpecialDates()
reconcileCalendarVisibility()
startReminderDelivery()
startWidgets()
reconcileWidgetRollover()
}
/**
* Re-arm the widgets' midnight rollover from whatever is actually placed
* (#228), and start redrawing them on the app's own changes (#420). The
* re-arm is idempotent and covers what no broadcast reaches — an alarm
* (#228). Idempotent, and it covers what no broadcast reaches — an alarm
* dropped by a force-stop is armed again the next time the app is opened.
* Off the main thread: a handful of binder calls on every process start.
*/
private fun startWidgets() {
private fun reconcileWidgetRollover() {
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
syncWidgetRollover(this@CalendulaApp)
widgetEntryPoint().widgetRefreshCoordinator().start()
}
}
@@ -4,15 +4,25 @@ import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import androidx.glance.GlanceId
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
import de.jeanlucmakiola.calendula.data.prefs.SettingsKeys
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
import de.jeanlucmakiola.calendula.domain.EventInstance
import de.jeanlucmakiola.calendula.ui.agenda.AgendaDay
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
import de.jeanlucmakiola.calendula.ui.agenda.agendaRange
import de.jeanlucmakiola.calendula.ui.agenda.dayCount
import de.jeanlucmakiola.calendula.ui.agenda.groupAgendaDays
import de.jeanlucmakiola.floret.prefs.get
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
@@ -24,6 +34,7 @@ import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Clock
import kotlin.time.Instant
@@ -33,7 +44,34 @@ import kotlin.time.Instant
* the whole window, and on slow devices a wide one held the widget on "loading"
* (#363).
*/
private const val MONTH_WIDGET_PAD_MONTHS = 3
internal const val MONTH_WIDGET_PAD_MONTHS = 3
/** Every setting the widgets draw with: what the loads read, and what [WidgetRefreshCoordinator] watches. */
internal data class WidgetSettings(
val soften: Boolean,
val weekStart: WeekStartPref,
val timeFormat: TimeFormatPref,
val pastDisplay: PastEventDisplay,
val agendaRange: AgendaRange,
val widgetSize: WidgetSize,
val showToday: Boolean,
) {
/** The part the month widget draws with; the rest is agenda-only. */
val month: Pair<Boolean, WeekStartPref> get() = soften to weekStart
}
internal val SettingsPrefs.widgetSettings: Flow<WidgetSettings>
get() = data.map {
WidgetSettings(
soften = it[SettingsKeys.softenCalendarColors],
weekStart = it[SettingsKeys.weekStart],
timeFormat = it[SettingsKeys.timeFormat],
pastDisplay = it[SettingsKeys.pastEventDisplay],
agendaRange = it[SettingsKeys.agendaWidgetRange],
widgetSize = it[SettingsKeys.widgetSize],
showToday = it[SettingsKeys.agendaShowToday],
)
}.distinctUntilChanged()
internal fun systemZone(): TimeZone = TimeZone.currentSystemDefault()
@@ -78,6 +116,8 @@ sealed interface MonthWidgetSource {
val weekStart: DayOfWeek,
val soften: Boolean,
val instances: List<EventInstance>,
/** Month indices (`year * 12 + monthOrdinal`) whose grids [instances] covers. */
val months: IntRange,
) : MonthWidgetSource
}
@@ -91,22 +131,20 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData {
val zone = systemZone()
val anchor = today(zone)
val ep = widgetEntryPoint()
val prefs = ep.settingsPrefs()
val range = prefs.agendaWidgetRange.first()
val weekStart = prefs.weekStart.first().resolveFirstDay(Locale.getDefault())
val dayCount = range.dayCount(anchor, weekStart)
val settings = ep.settingsPrefs().widgetSettings.first()
val weekStart = settings.weekStart.resolveFirstDay(Locale.getDefault())
val dayCount = settings.agendaRange.dayCount(anchor, weekStart)
val window = agendaRange(anchor, dayCount - 1, zone)
val windowEnd = anchor.plus(dayCount - 1, DateTimeUnit.DAY)
val instances = ep.calendarRepository().instances(window).first()
return AgendaWidgetData.Ready(
today = anchor,
days = groupAgendaDays(anchor, windowEnd, instances, zone),
is24Hour = prefs.timeFormat.first()
.is24Hour(android.text.format.DateFormat.is24HourFormat(this)),
soften = prefs.softenCalendarColors.first(),
pastDisplay = prefs.pastEventDisplay.first(),
showToday = prefs.agendaShowToday.first(),
widgetSize = prefs.widgetSize.first(),
is24Hour = settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(this)),
soften = settings.soften,
pastDisplay = settings.pastDisplay,
showToday = settings.showToday,
widgetSize = settings.widgetSize,
now = Clock.System.now(),
)
}
@@ -122,13 +160,37 @@ private fun firstDayOfMonthIndex(index: Int): LocalDate = LocalDate(index / 12,
*/
internal fun monthWindowCenter(index: Int): Int = Math.floorDiv(index, 3) * 3 + 1
/**
* The last window each month widget loaded, under its refresh stamp, so a new
* session for the same widget (a tap after the last one timed out) draws
* without a query. Every redraw moves the stamp, so an entry is never served
* stale.
*/
private object MonthSourceCache {
private class Entry(val center: Int, val stamp: Long, val source: MonthWidgetSource.Ready)
private val entries = ConcurrentHashMap<GlanceId, Entry>()
fun get(id: GlanceId, center: Int, stamp: Long): MonthWidgetSource.Ready? =
entries[id]?.takeIf { it.center == center && it.stamp == stamp && it.source.today == today(systemZone()) }
?.source
fun put(id: GlanceId, center: Int, stamp: Long, source: MonthWidgetSource) {
if (source is MonthWidgetSource.Ready) entries[id] = Entry(center, stamp, source)
}
}
/** [loadMonthWidgetSource] for widget [id], served from [MonthSourceCache] while its [stamp] holds. */
internal suspend fun Context.loadMonthWidgetSource(id: GlanceId, centerIndex: Int, stamp: Long): MonthWidgetSource =
MonthSourceCache.get(id, centerIndex, stamp)
?: loadMonthWidgetSource(centerIndex).also { MonthSourceCache.put(id, centerIndex, stamp, it) }
/** Read backing the month widget's grid for the window around [centerIndex]. */
internal suspend fun Context.loadMonthWidgetSource(centerIndex: Int): MonthWidgetSource {
private suspend fun Context.loadMonthWidgetSource(centerIndex: Int): MonthWidgetSource {
if (!hasCalendarPermission()) return MonthWidgetSource.NeedsPermission
val zone = systemZone()
val ep = widgetEntryPoint()
val prefs = ep.settingsPrefs()
val weekStart = prefs.weekStart.first().resolveFirstDay(Locale.getDefault())
val settings = ep.settingsPrefs().widgetSettings.first()
val months = (centerIndex - MONTH_WIDGET_PAD_MONTHS)..(centerIndex + MONTH_WIDGET_PAD_MONTHS)
// Padded by the grid's spill into the neighbouring months.
val from = firstDayOfMonthIndex(months.first)
@@ -140,8 +202,9 @@ internal suspend fun Context.loadMonthWidgetSource(centerIndex: Int): MonthWidge
.toInstant(zone)
return MonthWidgetSource.Ready(
today = today(zone),
weekStart = weekStart,
soften = prefs.softenCalendarColors.first(),
weekStart = settings.weekStart.resolveFirstDay(Locale.getDefault()),
soften = settings.soften,
instances = ep.calendarRepository().instances(from..to).first(),
months = months,
)
}
@@ -3,6 +3,7 @@ package de.jeanlucmakiola.calendula.widget
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
import de.jeanlucmakiola.calendula.data.di.ApplicationScope
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
@@ -15,12 +16,15 @@ import de.jeanlucmakiola.floret.glance.WidgetRollover
import de.jeanlucmakiola.floret.glance.updatePlaced
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject
import javax.inject.Singleton
@@ -52,15 +56,32 @@ internal fun syncWidgetRollover(context: Context) =
* refresh stamp moves, and both widgets key their data load on it (#420).
*/
internal suspend fun redrawWidgets(context: Context) {
redrawAgendaWidgets(context)
redrawMonthWidgets(context)
}
private suspend fun redrawAgendaWidgets(context: Context) =
AgendaWidget().updatePlaced(context, AgendaWidgetReceiver::class.java, bumpRefreshStamp = true)
private suspend fun redrawMonthWidgets(context: Context) =
MonthWidget().updatePlaced(context, MonthWidgetReceiver::class.java, bumpRefreshStamp = true)
/** When the app last saw a calendar change of its own, for skipping the `PROVIDER_CHANGED` that echoes it. */
private val lastLocalChange = AtomicLong(0L)
private const val PROVIDER_ECHO_WINDOW_MILLIS = 5_000L
internal fun isProviderEcho(): Boolean {
val last = lastLocalChange.get()
return last != 0L && SystemClock.elapsedRealtime() - last < PROVIDER_ECHO_WINDOW_MILLIS
}
/**
* Redraws the widgets on the app's own calendar writes, visibility changes and
* the settings they draw with, so those don't wait for `PROVIDER_CHANGED`,
* which arrives seconds late or not at all (#420, #229). Changes from outside
* the app are [WidgetUpdateReceiver]'s. Started once from `CalendulaApp`.
* the app are [WidgetUpdateReceiver]'s. A setting only the agenda widget draws
* with leaves the month widget alone. Started once from `CalendulaApp`.
*/
@Singleton
class WidgetRefreshCoordinator @Inject constructor(
@@ -69,19 +90,25 @@ class WidgetRefreshCoordinator @Inject constructor(
private val prefs: SettingsPrefs,
@ApplicationScope private val scope: CoroutineScope,
) {
private val started = AtomicBoolean(false)
/** Subscribes before returning, so a change made right after is never taken for the starting value. */
fun start() {
val refresher = WidgetRefresher(scope) { redrawWidgets(context) }
val settings = listOf(
prefs.softenCalendarColors,
prefs.weekStart,
prefs.timeFormat,
prefs.pastEventDisplay,
prefs.agendaWidgetRange,
prefs.widgetSize,
prefs.agendaShowToday,
).map { flow -> flow.distinctUntilChanged().drop(1).map {} }
scope.launch(Dispatchers.IO) {
merge(repository.localChanges, *settings.toTypedArray()).collect { refresher.request() }
if (!started.compareAndSet(false, true)) return
val agenda = WidgetRefresher(scope) { redrawAgendaWidgets(context) }
val month = WidgetRefresher(scope) { redrawMonthWidgets(context) }
watch(repository.localChanges) {
lastLocalChange.set(SystemClock.elapsedRealtime())
agenda.request()
month.request()
}
watch(prefs.widgetSettings.drop(1)) { agenda.request() }
watch(prefs.widgetSettings.map { it.month }.distinctUntilChanged().drop(1)) { month.request() }
}
private fun watch(changes: Flow<*>, onChange: () -> Unit) {
scope.launch(Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
changes.collect { onChange() }
}
}
}
@@ -12,7 +12,8 @@ import kotlin.time.Duration.Companion.minutes
* the app; the app's own writes go through [WidgetRefreshCoordinator]. A data
* change also re-arms, at most every 15 minutes: the inexact alarm can be
* dropped in a restricted standby bucket, and a calendar change is the one
* signal that keeps arriving.
* signal that keeps arriving. A `PROVIDER_CHANGED` right after one of the
* app's own changes only echoes it, so it doesn't redraw a second time.
*/
class WidgetUpdateReceiver : FloretWidgetUpdateReceiver() {
override val rolloverAction = ACTION_WIDGET_ROLLOVER
@@ -22,5 +23,7 @@ class WidgetUpdateReceiver : FloretWidgetUpdateReceiver() {
override fun rolloverPendingIntent(context: Context): PendingIntent = widgetRolloverPendingIntent(context)
override fun skipRedraw(action: String) = action == Intent.ACTION_PROVIDER_CHANGED && isProviderEcho()
override suspend fun redraw(context: Context) = redrawWidgets(context)
}
@@ -3,7 +3,6 @@ package de.jeanlucmakiola.calendula.widget.agenda
import de.jeanlucmakiola.calendula.domain.isDeclined
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
import android.content.Context
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@@ -17,6 +16,7 @@ import androidx.glance.action.ActionParameters
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.SizeMode
import androidx.glance.color.ColorProvider
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.action.actionStartActivity
@@ -90,14 +90,12 @@ class AgendaWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val stamp = refreshStamp(context, id)
val widgetData = WidgetData(context.loadAgendaWidgetData(), stamp)
val dark = (context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
provideContent {
// A running session never re-runs this preamble, so every redraw
// moves the stamp and the data reloads here instead (#420).
val data = widgetData.current(currentRefreshStamp()) { context.loadAgendaWidgetData() }
CalendulaGlanceTheme {
AgendaWidgetBody(data = data, dark = dark)
AgendaWidgetBody(data = data)
}
}
}
@@ -128,7 +126,7 @@ private sealed interface AgendaRow {
}
@Composable
private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
private fun AgendaWidgetBody(data: AgendaWidgetData) {
// The stripe is re-resolved against the system font scale so it tracks the
// sp-sized text beside it instead of drifting at large accessibility settings.
val size = (data as? AgendaWidgetData.Ready)?.widgetSize ?: WidgetSize.SMALL
@@ -144,7 +142,7 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
Spacer(GlanceModifier.height(4.dp))
when (data) {
AgendaWidgetData.NeedsPermission -> WidgetMessage(R.string.widget_needs_permission, metrics)
is AgendaWidgetData.Ready -> AgendaRows(data = data, dark = dark, metrics = metrics)
is AgendaWidgetData.Ready -> AgendaRows(data = data, metrics = metrics)
}
}
}
@@ -152,7 +150,6 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
@Composable
private fun AgendaRows(
data: AgendaWidgetData.Ready,
dark: Boolean,
metrics: AgendaMetrics,
) {
val pastDisplay = data.pastDisplay
@@ -200,7 +197,6 @@ private fun AgendaRows(
is AgendaRow.Event -> EventRow(
event = row.event,
day = row.date,
dark = dark,
soften = data.soften,
is24Hour = data.is24Hour,
dimmed = pastDisplay == PastEventDisplay.DIM &&
@@ -332,7 +328,6 @@ private fun PlaceholderRow(date: LocalDate, metrics: AgendaMetrics) {
private fun EventRow(
event: EventInstance,
day: LocalDate,
dark: Boolean,
soften: Boolean,
is24Hour: Boolean,
dimmed: Boolean,
@@ -342,9 +337,10 @@ private fun EventRow(
val title = event.title.ifBlank { context.getString(R.string.event_untitled) }
// Glance has no generic alpha modifier, so dim by fading the colour stripe and
// dropping both text lines to the lower-emphasis on-surface-variant tone.
val stripeColor = eventAccent(event.color, dark, soften).let {
fun stripe(dark: Boolean) = eventAccent(event.color, dark, soften).let {
if (dimmed) it.copy(alpha = EventDimAlpha) else it
}
val stripeColor = ColorProvider(day = stripe(dark = false), night = stripe(dark = true))
val titleColor = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface
Row(
modifier = GlanceModifier
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.widget.agenda
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
@@ -34,12 +35,13 @@ class AgendaWidgetReceiver : GlanceAppWidgetReceiver() {
* Self-heal on the system's own `updatePeriodMillis` wake-up — see
* [de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver.onUpdate].
*/
@SuppressLint("MissingSuperCall")
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
// In place of super, which only recomposes and would take the goAsync handle.
reloadOnUpdate(context, appWidgetIds)
syncWidgetRollover(context)
}
@@ -3,8 +3,12 @@ package de.jeanlucmakiola.calendula.widget.month
import de.jeanlucmakiola.calendula.domain.isDeclined
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
import android.content.Context
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.Preferences
@@ -22,6 +26,7 @@ import androidx.glance.action.actionParametersOf
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.SizeMode
import androidx.glance.color.ColorProvider
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.action.actionStartActivity
@@ -131,19 +136,27 @@ class MonthWidget : GlanceAppWidget() {
val stamp = refreshStamp(context, id)
val initialCenter = monthWindowCenter(shown)
// Shared by every size Exact composes, so each loads nothing of its own.
val widgetData = WidgetData(context.loadMonthWidgetSource(initialCenter), initialCenter, stamp)
val dark = (context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val widgetData = WidgetData(context.loadMonthWidgetSource(id, initialCenter, stamp), initialCenter, stamp)
provideContent {
val index = currentState(MONTH_INDEX_KEY) ?: currentMonthIndex(systemZone())
// Reloads when paging leaves the loaded window, and on every redraw,
// which a running session would otherwise never see (#420).
val center = monthWindowCenter(index)
val source = widgetData.current(center, currentRefreshStamp()) {
context.loadMonthWidgetSource(center)
val currentStamp = currentRefreshStamp()
val source = widgetData.current(center, currentStamp) {
context.loadMonthWidgetSource(id, center, currentStamp)
}
// A jump out of the loaded window keeps the last month it covers
// until the reload lands, rather than drawing an empty grid.
var lastShown by remember { mutableIntStateOf(index) }
val shown = when {
source !is MonthWidgetSource.Ready || index in source.months -> index
lastShown in source.months -> lastShown
else -> index.coerceIn(source.months)
}
SideEffect { lastShown = shown }
CalendulaGlanceTheme {
MonthWidgetBody(source = source, index = index, dark = dark)
MonthWidgetBody(source = source, index = shown)
}
}
}
@@ -194,7 +207,7 @@ class ResetMonthAction : ActionCallback {
}
@Composable
private fun MonthWidgetBody(source: MonthWidgetSource, index: Int, dark: Boolean) {
private fun MonthWidgetBody(source: MonthWidgetSource, index: Int) {
Column(
modifier = GlanceModifier
.fillMaxSize()
@@ -221,7 +234,6 @@ private fun MonthWidgetBody(source: MonthWidgetSource, index: Int, dark: Boolean
week = week,
currentMonth = ym.month,
today = source.today,
dark = dark,
soften = source.soften,
colW = colW,
modifier = GlanceModifier.defaultWeight(),
@@ -331,7 +343,6 @@ private fun WeekRow(
week: MonthWeek,
currentMonth: Month,
today: LocalDate,
dark: Boolean,
soften: Boolean,
colW: Dp,
modifier: GlanceModifier,
@@ -375,7 +386,7 @@ private fun WeekRow(
// its columns (colW * n) so it's connected with no seam and rounded ends.
// Lanes past the last one this week fills emit nothing at all.
repeat(week.usedLanes()) { lane ->
LaneRow(week = week, lane = lane, dark = dark, soften = soften, colW = colW)
LaneRow(week = week, lane = lane, soften = soften, colW = colW)
Spacer(GlanceModifier.height(1.dp))
}
if (hidden.any { it > 0 }) OverflowRow(hidden = hidden, colW = colW)
@@ -489,7 +500,7 @@ private fun MonthWeek.laneCells(lane: Int): List<LaneCell> {
}
@Composable
private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean, colW: Dp) {
private fun LaneRow(week: MonthWeek, lane: Int, soften: Boolean, colW: Dp) {
Row(modifier = GlanceModifier.fillMaxWidth()) {
week.laneCells(lane).forEach { cell ->
if (cell.event == null) {
@@ -497,7 +508,6 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean,
} else {
SpanBar(
event = cell.event,
dark = dark,
soften = soften,
width = colW * cell.cols,
)
@@ -508,9 +518,10 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean,
/** A single connected, rounded event bar [width] wide with its clipped title. */
@Composable
private fun SpanBar(event: EventInstance, dark: Boolean, soften: Boolean, width: Dp) {
private fun SpanBar(event: EventInstance, soften: Boolean, width: Dp) {
val context = LocalContext.current
val fill = eventFill(event.color, dark, soften)
val dayFill = eventFill(event.color, dark = false, soften)
val nightFill = eventFill(event.color, dark = true, soften)
Box(
modifier = GlanceModifier
.width(width)
@@ -533,14 +544,14 @@ private fun SpanBar(event: EventInstance, dark: Boolean, soften: Boolean, width:
modifier = GlanceModifier
.fillMaxSize()
.cornerRadius(4.dp)
.background(fill),
.background(ColorProvider(day = dayFill, night = nightFill)),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
maxLines = 1,
style = TextStyle(
color = ColorProvider(eventInk(fill)),
color = ColorProvider(day = eventInk(dayFill), night = eventInk(nightFill)),
fontSize = 9.sp,
textDecoration = glanceDeclinedDecoration(event.isDeclined),
),
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.widget.month
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
@@ -36,12 +37,13 @@ class MonthWidgetReceiver : GlanceAppWidgetReceiver() {
* freeze) is repaired here rather than on the next app open. Re-arming
* closer to midnight also narrows the inexact delivery window.
*/
@SuppressLint("MissingSuperCall")
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
// In place of super, which only recomposes and would take the goAsync handle.
reloadOnUpdate(context, appWidgetIds)
syncWidgetRollover(context)
}
@@ -9,8 +9,8 @@ class MonthWindowCenterTest {
fun `the loaded window always reaches two months past the shown one`() {
for (index in -40..40) {
val center = monthWindowCenter(index)
assertThat(index - 2).isAtLeast(center - 3)
assertThat(index + 2).isAtMost(center + 3)
assertThat(index - 2).isAtLeast(center - MONTH_WIDGET_PAD_MONTHS)
assertThat(index + 2).isAtMost(center + MONTH_WIDGET_PAD_MONTHS)
}
}