diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt b/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt index ac17606..64e321a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt @@ -48,19 +48,20 @@ class CalendulaApp : Application() { reconcileSpecialDates() reconcileCalendarVisibility() startReminderDelivery() - reconcileWidgetRollover() - widgetEntryPoint().widgetRefreshCoordinator().start() + startWidgets() } /** * Re-arm the widgets' midnight rollover from whatever is actually placed - * (#228). Idempotent, and it covers what no broadcast reaches — an alarm + * (#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 * 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 reconcileWidgetRollover() { + private fun startWidgets() { CoroutineScope(SupervisorJob() + Dispatchers.Default).launch { syncWidgetRollover(this@CalendulaApp) + widgetEntryPoint().widgetRefreshCoordinator().start() } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt index 8d946ec..3838327 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt @@ -19,8 +19,8 @@ interface CalendarRepository { /** Re-query every open [calendars] / [instances] flow. */ fun refresh() - /** Emits whenever [instances] would re-query: a provider change, an own write, or a visibility change. */ - val changes: Flow + /** Emits on the app's own writes and visibility changes; not on provider notifications or [refresh]. */ + val localChanges: Flow suspend fun eventDetail(eventId: Long): EventDetail diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt index 2ffa916..ec6017d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt @@ -78,17 +78,24 @@ class CalendarRepositoryImpl @Inject constructor( ticks.tryEmit(Unit) } + private val writes = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + /** Run a provider write on [io], then re-query whether or not it threw. */ private suspend fun write(block: suspend () -> T): T = withContext(io) { try { block() } finally { refresh() + writes.tryEmit(Unit) } } - override val changes: Flow = merge( - ticks, + override val localChanges: Flow = merge( + writes, prefs.pendingDisabledCalendarIds.distinctUntilChanged().drop(1).map {}, prefs.hiddenCalendarIds.distinctUntilChanged().drop(1).map {}, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index c98e092..1d4b93a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -46,7 +46,6 @@ import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN import de.jeanlucmakiola.calendula.widget.WidgetSize -import de.jeanlucmakiola.calendula.widget.redrawWidgets import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -58,8 +57,6 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import javax.inject.Inject @@ -172,17 +169,6 @@ class SettingsViewModel @Inject constructor( private val _fontImportFailed = MutableSharedFlow(extraBufferCapacity = 1) val fontImportFailed: SharedFlow = _fontImportFailed - // Serialises widget redraws, so a quick run of setting changes lands in order. - private val widgetRefreshMutex = Mutex() - - /** Saves a setting the widgets draw with, then reloads them. */ - private fun saveAndRedrawWidgets(block: suspend SettingsPrefs.() -> Unit) { - viewModelScope.launch { - prefs.block() - widgetRefreshMutex.withLock { redrawWidgets(appContext) } - } - } - /** Contact special-dates sub-page (issue #15). */ val specialDatesState: StateFlow = prefs.data.map { stored -> val templates = stored.specialDatesTitleTemplates @@ -265,7 +251,7 @@ class SettingsViewModel @Inject constructor( fun setDynamicColor(enabled: Boolean) = save { setDynamicColor(enabled) } - fun setSoftenColors(enabled: Boolean) = saveAndRedrawWidgets { setSoftenCalendarColors(enabled) } + fun setSoftenColors(enabled: Boolean) = save { setSoftenCalendarColors(enabled) } fun setFont(role: FontRole, token: String) = save { when (role) { @@ -293,20 +279,20 @@ class SettingsViewModel @Inject constructor( } } - fun setWeekStart(pref: WeekStartPref) = saveAndRedrawWidgets { setWeekStart(pref) } + fun setWeekStart(pref: WeekStartPref) = save { setWeekStart(pref) } fun setAgendaScreenRange(range: AgendaRange) = save { setAgendaScreenRange(range) } - fun setAgendaWidgetRange(range: AgendaRange) = saveAndRedrawWidgets { setAgendaWidgetRange(range) } + fun setAgendaWidgetRange(range: AgendaRange) = save { setAgendaWidgetRange(range) } fun setAgendaShowRangeBar(enabled: Boolean) = save { setAgendaShowRangeBar(enabled) } /** The size the agenda widget draws its text at (#51); the month widget sizes itself (#103). */ - fun setWidgetSize(size: WidgetSize) = saveAndRedrawWidgets { setWidgetSize(size) } + fun setWidgetSize(size: WidgetSize) = save { setWidgetSize(size) } - fun setAgendaShowToday(enabled: Boolean) = saveAndRedrawWidgets { setAgendaShowToday(enabled) } + fun setAgendaShowToday(enabled: Boolean) = save { setAgendaShowToday(enabled) } - fun setTimeFormat(pref: TimeFormatPref) = saveAndRedrawWidgets { setTimeFormat(pref) } + fun setTimeFormat(pref: TimeFormatPref) = save { setTimeFormat(pref) } fun setMultiDayCount(count: Int) = save { setMultiDayCount(count) } @@ -335,7 +321,7 @@ class SettingsViewModel @Inject constructor( } } - fun setPastEventDisplay(mode: PastEventDisplay) = saveAndRedrawWidgets { setPastEventDisplay(mode) } + fun setPastEventDisplay(mode: PastEventDisplay) = save { setPastEventDisplay(mode) } fun setDimCompletedEvents(enabled: Boolean) = save { setDimCompletedEvents(enabled) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt index 259ffba..6dfe5fd 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt @@ -9,7 +9,6 @@ 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 @@ -51,15 +50,12 @@ sealed interface AgendaWidgetData { data object NeedsPermission : AgendaWidgetData data class Ready( val today: LocalDate, - /** Days with events across [range], unfiltered; hiding finished ones happens in the composition. */ + /** Days with events across the configured range, unfiltered; hiding finished ones happens in the composition. */ val days: List, /** Resolved clock convention for event time labels (the time-format pref). */ val is24Hour: Boolean, /** Whether event colours are softened to pastels, or shown raw (#36). */ val soften: Boolean, - /** First day of the week, for the calendar-aligned "this week" range. */ - val weekStart: DayOfWeek, - val range: AgendaRange, val pastDisplay: PastEventDisplay, /** "Always show today" (#35): anchor today at the top even with no events left. */ val showToday: Boolean, @@ -73,7 +69,7 @@ sealed interface AgendaWidgetData { * Source data for the month widget: a window of instances plus the week-start * preference and today. The widget computes each displayed month's grid from * this in-memory list (via `layoutMonthWeeks`) as the user pages, so paging - * inside [months] is pure recomposition. + * inside the loaded window is pure recomposition. */ sealed interface MonthWidgetSource { data object NeedsPermission : MonthWidgetSource @@ -82,8 +78,6 @@ sealed interface MonthWidgetSource { val weekStart: DayOfWeek, val soften: Boolean, val instances: List, - /** Month indices (`year * 12 + monthOrdinal`) whose grids [instances] covers. */ - val months: IntRange, ) : MonthWidgetSource } @@ -110,8 +104,6 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData { is24Hour = prefs.timeFormat.first() .is24Hour(android.text.format.DateFormat.is24HourFormat(this)), soften = prefs.softenCalendarColors.first(), - weekStart = weekStart, - range = range, pastDisplay = prefs.pastEventDisplay.first(), showToday = prefs.agendaShowToday.first(), widgetSize = prefs.widgetSize.first(), @@ -151,6 +143,5 @@ internal suspend fun Context.loadMonthWidgetSource(centerIndex: Int): MonthWidge weekStart = weekStart, soften = prefs.softenCalendarColors.first(), instances = ep.calendarRepository().instances(from..to).first(), - months = months, ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetRedraw.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetRedraw.kt index 360f0f4..a62c72a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetRedraw.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetRedraw.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.di.ApplicationScope +import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetReceiver import de.jeanlucmakiola.calendula.widget.month.MonthWidget @@ -15,6 +16,10 @@ import de.jeanlucmakiola.floret.glance.updatePlaced import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +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 javax.inject.Inject import javax.inject.Singleton @@ -52,26 +57,31 @@ internal suspend fun redrawWidgets(context: Context) { } /** - * Redraws the widgets on every calendar change the app itself sees (its own - * writes above all), so they don't wait for `PROVIDER_CHANGED`, which arrives - * seconds late or not at all (#420, #229). Started once from `CalendulaApp`. + * 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`. */ @Singleton class WidgetRefreshCoordinator @Inject constructor( @ApplicationContext private val context: Context, private val repository: CalendarRepository, + private val prefs: SettingsPrefs, @ApplicationScope private val scope: CoroutineScope, ) { - private var started = false - fun start() { - if (started) return - started = true - val refresher = WidgetRefresher(scope) { - if (WidgetRollover.hasPlacedWidgets(context, WIDGET_RECEIVER_CLASSES)) redrawWidgets(context) - } + 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) { - repository.changes.collect { refresher.request() } + merge(repository.localChanges, *settings.toTypedArray()).collect { refresher.request() } } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index cb98ecb..7fc4ace 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -5,7 +5,6 @@ import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration import android.content.Context import android.content.res.Configuration import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.glance.ColorFilter @@ -59,9 +58,10 @@ import de.jeanlucmakiola.calendula.widget.AgendaWidgetData import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme import de.jeanlucmakiola.calendula.widget.WidgetSize import de.jeanlucmakiola.calendula.widget.loadAgendaWidgetData -import de.jeanlucmakiola.calendula.widget.redrawWidgets +import de.jeanlucmakiola.floret.glance.WidgetData import de.jeanlucmakiola.floret.glance.currentRefreshStamp -import de.jeanlucmakiola.floret.glance.rememberWidgetData +import de.jeanlucmakiola.floret.glance.refreshStamp +import de.jeanlucmakiola.floret.glance.updateWithRefresh import de.jeanlucmakiola.calendula.widget.systemZone import de.jeanlucmakiola.calendula.widget.today import kotlinx.datetime.DateTimeUnit @@ -88,13 +88,14 @@ class AgendaWidget : GlanceAppWidget() { override val sizeMode = SizeMode.Single override suspend fun provideGlance(context: Context, id: GlanceId) { - val initial = context.loadAgendaWidgetData() + 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 by rememberWidgetData(initial, currentRefreshStamp()) { context.loadAgendaWidgetData() } + val data = widgetData.current(currentRefreshStamp()) { context.loadAgendaWidgetData() } CalendulaGlanceTheme { AgendaWidgetBody(data = data, dark = dark) } @@ -102,10 +103,10 @@ class AgendaWidget : GlanceAppWidget() { } } -/** Reloads and redraws the widgets (header refresh button). */ +/** Reloads and redraws the tapped widget (header refresh button). */ class RefreshAgendaAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - redrawWidgets(context.applicationContext) + AgendaWidget().updateWithRefresh(context.applicationContext, glanceId) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidgetReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidgetReceiver.kt index 294cbd8..5d24f62 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidgetReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidgetReceiver.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver import de.jeanlucmakiola.calendula.widget.syncWidgetRollover +import de.jeanlucmakiola.floret.glance.reloadOnUpdate /** * Host-facing receiver for the agenda widget. Declared in the manifest with the @@ -39,6 +40,7 @@ class AgendaWidgetReceiver : GlanceAppWidgetReceiver() { appWidgetIds: IntArray, ) { super.onUpdate(context, appWidgetManager, appWidgetIds) + reloadOnUpdate(context, appWidgetIds) syncWidgetRollover(context) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index d5d2b88..8834e48 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -5,7 +5,6 @@ import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration import android.content.Context import android.content.res.Configuration import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.datastore.preferences.core.Preferences @@ -63,8 +62,10 @@ import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme import de.jeanlucmakiola.calendula.widget.MonthWidgetSource import de.jeanlucmakiola.calendula.widget.loadMonthWidgetSource import de.jeanlucmakiola.calendula.widget.monthWindowCenter +import de.jeanlucmakiola.floret.glance.WidgetData import de.jeanlucmakiola.floret.glance.currentRefreshStamp -import de.jeanlucmakiola.floret.glance.rememberWidgetData +import de.jeanlucmakiola.floret.glance.refreshStamp +import de.jeanlucmakiola.floret.glance.updateWithRefresh import de.jeanlucmakiola.calendula.widget.monthIndexOf import de.jeanlucmakiola.calendula.widget.systemZone import de.jeanlucmakiola.calendula.widget.today @@ -127,7 +128,10 @@ class MonthWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { val shown = getAppWidgetState(context, id)[MONTH_INDEX_KEY] ?: currentMonthIndex(systemZone()) - val initial = context.loadMonthWidgetSource(monthWindowCenter(shown)) + 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 provideContent { @@ -135,7 +139,7 @@ class MonthWidget : GlanceAppWidget() { // 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 by rememberWidgetData(initial, center, currentRefreshStamp()) { + val source = widgetData.current(center, currentRefreshStamp()) { context.loadMonthWidgetSource(center) } CalendulaGlanceTheme { @@ -181,11 +185,11 @@ class ShiftMonthAction : ActionCallback { } } -/** Jump the displayed month back to the current month. See [ShiftMonthAction]. */ +/** Jump the displayed month back to the current month, reloading it. See [ShiftMonthAction]. */ class ResetMonthAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { updateAppWidgetState(context, glanceId) { prefs -> prefs.remove(MONTH_INDEX_KEY) } - MonthWidget().update(context.applicationContext, glanceId) + MonthWidget().updateWithRefresh(context.applicationContext, glanceId) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidgetReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidgetReceiver.kt index 82830fa..b1c42c1 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidgetReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidgetReceiver.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver import de.jeanlucmakiola.calendula.widget.syncWidgetRollover +import de.jeanlucmakiola.floret.glance.reloadOnUpdate /** * Host-facing receiver for the month widget. Declared in the manifest with the @@ -41,6 +42,7 @@ class MonthWidgetReceiver : GlanceAppWidgetReceiver() { appWidgetIds: IntArray, ) { super.onUpdate(context, appWidgetManager, appWidgetIds) + reloadOnUpdate(context, appWidgetIds) syncWidgetRollover(context) } } diff --git a/floret-kit b/floret-kit index acc983f..818dfa0 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit acc983f2ac4ca73769e28bc0d71da56293f42b2f +Subproject commit 818dfa0398cb0c8339c1680cc8da7ef9df6b8f0d