This commit is contained in:
1 parent
77e103c66c
commit
225901ceee
11 files changed
+69
-65
No files matched your search
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Unit>
|
||||
/** Emits on the app's own writes and visibility changes; not on provider notifications or [refresh]. */
|
||||
val localChanges: Flow<Unit>
|
||||
|
||||
suspend fun eventDetail(eventId: Long): EventDetail
|
||||
|
||||
|
||||
+9
-2
@@ -78,17 +78,24 @@ class CalendarRepositoryImpl @Inject constructor(
|
||||
ticks.tryEmit(Unit)
|
||||
}
|
||||
|
||||
private val writes = MutableSharedFlow<Unit>(
|
||||
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 <T> write(block: suspend () -> T): T = withContext(io) {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
refresh()
|
||||
writes.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override val changes: Flow<Unit> = merge(
|
||||
ticks,
|
||||
override val localChanges: Flow<Unit> = merge(
|
||||
writes,
|
||||
prefs.pendingDisabledCalendarIds.distinctUntilChanged().drop(1).map {},
|
||||
prefs.hiddenCalendarIds.distinctUntilChanged().drop(1).map {},
|
||||
)
|
||||
|
||||
@@ -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<Unit>(extraBufferCapacity = 1)
|
||||
val fontImportFailed: SharedFlow<Unit> = _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<SpecialDatesUiState> = 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) }
|
||||
|
||||
|
||||
@@ -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<AgendaDay>,
|
||||
/** 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<EventInstance>,
|
||||
/** 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,
|
||||
)
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<Preferences>(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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule floret-kit updated: acc983f2ac...818dfa0398.
Reference in new issue
Block a user