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 {
val deps = EntryPointAccessors.fromApplication(applicationContext, Deps::class.java)
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()
// A foreground resume can fire often — skip if we synced recently.
@@ -95,13 +92,19 @@ class SpecialDatesSyncWorker(
}
return try {
val result = deps.syncEngine().sync()
val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) {
SpecialDatesStalledReason.PermissionRevoked
} else {
null
when (val result = deps.syncEngine().sync()) {
// The engine re-checks the toggle, so already-queued work never
// revives a disabled feature; don't record a run either.
SpecialDatesSyncResult.Disabled -> return Result.success()
else -> {
val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) {
SpecialDatesStalledReason.PermissionRevoked
} else {
null
}
prefs.recordSpecialDatesRun(now, stalled)
}
}
prefs.recordSpecialDatesRun(now, stalled)
if (foreground) prefs.setSpecialDatesLastForegroundSync(now)
Result.success()
} 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.prefs.CalendarReminderOverride
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.EventForm
import de.jeanlucmakiola.calendula.domain.RecurrenceFreq
@@ -39,6 +38,16 @@ interface SpecialDatesCalendarSpec {
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,
* 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()
calendarByType.forEach { (type, calendarId) ->
val template = templates[type]?.takeIf { it.isNotBlank() }
?: spec.defaultTitleTemplate(type)
val template = spec.resolveTitleTemplate(type, templates)
syncType(
calendarId = calendarId,
type = type,
@@ -143,8 +151,8 @@ class SpecialDatesSyncEngine @Inject constructor(
}
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)
prefs.setSpecialDatesCalendarId(type, id)
// 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 —
// 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(
timedGlobal = prefs.defaultReminderMinutes.first(),
allDayGlobal = prefs.defaultAllDayReminderMinutes.first(),
timedOverrides = prefs.perCalendarReminderOverride.first(),
allDayOverrides = prefs.perCalendarAllDayReminderOverride.first(),
allDayTimeMinutes = prefs.allDayReminderTimeMinutes.first(),
)
private data class ReminderContext(
val timedGlobal: List<Int>,
val allDayGlobal: List<Int>,
val timedOverrides: Map<Long, List<Int>>,
val allDayOverrides: Map<Long, List<Int>>,
val allDayTimeMinutes: Int,
) {
fun resolveAllDay(calendarId: Long): List<Int> = resolveDefaultReminder(
timedGlobal = timedGlobal,
allDayGlobal = allDayGlobal,
timedOverrides = timedOverrides,
allDayOverrides = allDayOverrides,
calendarId = calendarId,
isAllDay = true,
)
/** Same semantics as `resolveDefaultReminder`: present-empty = explicit none. */
fun resolveAllDay(calendarId: Long): List<Int> =
allDayOverrides[calendarId] ?: allDayGlobal
}
private data class BuiltManagedEvent(val desired: ManagedEventDesired, val form: EventForm)