refactor(contacts): simplify the sync engine's gates and context

- The worker no longer pre-checks specialDatesEnabled: the engine already
  gates on it, and the worker now consumes the previously-unreachable
  Disabled result instead (without recording a run).
- ReminderContext drops the timed defaults it could never use — managed
  events are always all-day — saving two DataStore reads per sync.
- createCalendar no longer persists the calendar id itself;
  reconcileCalendars is the single place ids are recorded (it always
  followed up with the same write).
- The title-template fallback uses the shared resolveTitleTemplate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 22:40:44 +02:00
parent 67a07a9346
commit 86d4e11584
2 changed files with 28 additions and 25 deletions

View File

@@ -83,9 +83,6 @@ class SpecialDatesSyncWorker(
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val deps = EntryPointAccessors.fromApplication(applicationContext, Deps::class.java) val deps = EntryPointAccessors.fromApplication(applicationContext, Deps::class.java)
val prefs = deps.settingsPrefs() val prefs = deps.settingsPrefs()
// Respect the toggle even for already-queued work; never revive a
// disabled feature.
if (!prefs.specialDatesEnabled.first()) return Result.success()
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
// A foreground resume can fire often — skip if we synced recently. // A foreground resume can fire often — skip if we synced recently.
@@ -95,13 +92,19 @@ class SpecialDatesSyncWorker(
} }
return try { return try {
val result = deps.syncEngine().sync() when (val result = deps.syncEngine().sync()) {
val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) { // The engine re-checks the toggle, so already-queued work never
SpecialDatesStalledReason.PermissionRevoked // revives a disabled feature; don't record a run either.
} else { SpecialDatesSyncResult.Disabled -> return Result.success()
null else -> {
val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) {
SpecialDatesStalledReason.PermissionRevoked
} else {
null
}
prefs.recordSpecialDatesRun(now, stalled)
}
} }
prefs.recordSpecialDatesRun(now, stalled)
if (foreground) prefs.setSpecialDatesLastForegroundSync(now) if (foreground) prefs.setSpecialDatesLastForegroundSync(now)
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -6,7 +6,6 @@ import de.jeanlucmakiola.calendula.data.calendar.ManagedEventRow
import de.jeanlucmakiola.calendula.data.calendar.toWriteTimes import de.jeanlucmakiola.calendula.data.calendar.toWriteTimes
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.data.prefs.resolveDefaultReminder
import de.jeanlucmakiola.calendula.domain.Availability import de.jeanlucmakiola.calendula.domain.Availability
import de.jeanlucmakiola.calendula.domain.EventForm import de.jeanlucmakiola.calendula.domain.EventForm
import de.jeanlucmakiola.calendula.domain.RecurrenceFreq import de.jeanlucmakiola.calendula.domain.RecurrenceFreq
@@ -39,6 +38,16 @@ interface SpecialDatesCalendarSpec {
fun defaultTitleTemplate(type: SpecialDateType): String fun defaultTitleTemplate(type: SpecialDateType): String
} }
/**
* The stored title template for [type], falling back to the localized default
* when blank/absent — the one resolution rule shared by the sync (what gets
* written) and the settings editor (what gets shown).
*/
fun SpecialDatesCalendarSpec.resolveTitleTemplate(
type: SpecialDateType,
stored: Map<SpecialDateType, String>,
): String = stored[type]?.takeIf { it.isNotBlank() } ?: defaultTitleTemplate(type)
/** /**
* Mirrors contact birthdays/anniversaries/custom dates into local calendars, * Mirrors contact birthdays/anniversaries/custom dates into local calendars,
* one per type. Every reconcile is an idempotent diff keyed on the deterministic * one per type. Every reconcile is an idempotent diff keyed on the deterministic
@@ -72,8 +81,7 @@ class SpecialDatesSyncEngine @Inject constructor(
val showYear = prefs.specialDatesShowYear.first() val showYear = prefs.specialDatesShowYear.first()
calendarByType.forEach { (type, calendarId) -> calendarByType.forEach { (type, calendarId) ->
val template = templates[type]?.takeIf { it.isNotBlank() } val template = spec.resolveTitleTemplate(type, templates)
?: spec.defaultTitleTemplate(type)
syncType( syncType(
calendarId = calendarId, calendarId = calendarId,
type = type, type = type,
@@ -143,8 +151,8 @@ class SpecialDatesSyncEngine @Inject constructor(
} }
private suspend fun createCalendar(type: SpecialDateType): Long { private suspend fun createCalendar(type: SpecialDateType): Long {
// reconcileCalendars persists the id — the single place ids are recorded.
val id = calendars.createManagedCalendar(spec.displayName(type), spec.color(type), type) val id = calendars.createManagedCalendar(spec.displayName(type), spec.color(type), type)
prefs.setSpecialDatesCalendarId(type, id)
// Seed a useful all-day reminder default (on the day + a week before), so // Seed a useful all-day reminder default (on the day + a week before), so
// birthdays get lead time out of the box. Per-calendar, user-adjustable — // birthdays get lead time out of the box. Per-calendar, user-adjustable —
// only set when the user hasn't already configured this calendar. // only set when the user hasn't already configured this calendar.
@@ -212,29 +220,21 @@ class SpecialDatesSyncEngine @Inject constructor(
) )
} }
// Managed events are always all-day, so only the all-day defaults apply.
private suspend fun readReminderContext(): ReminderContext = ReminderContext( private suspend fun readReminderContext(): ReminderContext = ReminderContext(
timedGlobal = prefs.defaultReminderMinutes.first(),
allDayGlobal = prefs.defaultAllDayReminderMinutes.first(), allDayGlobal = prefs.defaultAllDayReminderMinutes.first(),
timedOverrides = prefs.perCalendarReminderOverride.first(),
allDayOverrides = prefs.perCalendarAllDayReminderOverride.first(), allDayOverrides = prefs.perCalendarAllDayReminderOverride.first(),
allDayTimeMinutes = prefs.allDayReminderTimeMinutes.first(), allDayTimeMinutes = prefs.allDayReminderTimeMinutes.first(),
) )
private data class ReminderContext( private data class ReminderContext(
val timedGlobal: List<Int>,
val allDayGlobal: List<Int>, val allDayGlobal: List<Int>,
val timedOverrides: Map<Long, List<Int>>,
val allDayOverrides: Map<Long, List<Int>>, val allDayOverrides: Map<Long, List<Int>>,
val allDayTimeMinutes: Int, val allDayTimeMinutes: Int,
) { ) {
fun resolveAllDay(calendarId: Long): List<Int> = resolveDefaultReminder( /** Same semantics as `resolveDefaultReminder`: present-empty = explicit none. */
timedGlobal = timedGlobal, fun resolveAllDay(calendarId: Long): List<Int> =
allDayGlobal = allDayGlobal, allDayOverrides[calendarId] ?: allDayGlobal
timedOverrides = timedOverrides,
allDayOverrides = allDayOverrides,
calendarId = calendarId,
isAllDay = true,
)
} }
private data class BuiltManagedEvent(val desired: ManagedEventDesired, val form: EventForm) private data class BuiltManagedEvent(val desired: ManagedEventDesired, val form: EventForm)