This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule floret-kit updated: 818dfa0398...1ae8dd2b35
Reference in New Issue
Block a user