fix(sync): re-enqueue periodic work with UPDATE so upgrades re-apply the schedule #114

Merged
JMR-dev merged 2 commits from fix-workmanager-update-policy into main 2026-07-02 08:17:13 +00:00
JMR-dev commented 2026-07-02 07:28:03 +00:00 (Migrated from github.com)

Problem

SyncScheduler enqueued its three periodic jobs — sync, full-history backfill (#12), and retention prune (#13) — with ExistingPeriodicWorkPolicy.KEEP. Because these are re-enqueued at every app start, KEEP pins an already-installed device to the interval/constraints from whichever app version first scheduled the job. Any later tuning of a cadence or constraint therefore never reaches upgraders (only fresh installs get it).

Fix

Switch the three schedulePeriodic* calls to ExistingPeriodicWorkPolicy.UPDATE (added in WorkManager 2.8; the project is on 2.11.2, so no version bump is needed). UPDATE re-applies the current spec on each re-enqueue while preserving the running period's progress — an unchanged spec is effectively a no-op, so unlike REPLACE (cancel + re-enqueue) it does not reset the schedule or drop in-flight work on every launch. A shared PERIODIC_POLICY constant documents the rationale in one place.

Scope guard: this touches only the periodic existing-work policy. The one-shot kicks (syncNow/pruneNow → REPLACE, backfillNow → KEEP) are a separate concern and keep their deliberate policies. The change is consistent with the recently merged sync/battery work (#88–90 / PR #109); the IDLE-poll fallback in IdleService still re-asserts the periodic fallback, now as a no-op UPDATE (comment updated).

Testability

SyncScheduler now injects Provider<WorkManager> (via a new WorkManagerModule) instead of calling the WorkManager.getInstance() static directly, so the policy is unit-testable with MockK. The Provider keeps resolution lazy (resolved at schedule time, not construction), preserving the previous on-demand init timing so nothing resolves during Hilt's Application field injection.

Tests

New SyncSchedulerTest (JUnit4 + MockK) asserts each periodic job is enqueued with UPDATE, and that the one-shot kicks keep REPLACE/KEEP.

Verification

Fast gate all green: :app:assembleDebug, :app:testDebugUnitTest, :app:lintDebug, :app:ktlintCheck, :app:detekt, plus :app:compileDebugAndroidTestKotlin.

Closes #96

🤖 Generated with Claude Code

## Problem `SyncScheduler` enqueued its three periodic jobs — sync, full-history backfill (#12), and retention prune (#13) — with `ExistingPeriodicWorkPolicy.KEEP`. Because these are re-enqueued at every app start, KEEP pins an already-installed device to the interval/constraints from whichever app version *first* scheduled the job. Any later tuning of a cadence or constraint therefore never reaches upgraders (only fresh installs get it). ## Fix Switch the three `schedulePeriodic*` calls to `ExistingPeriodicWorkPolicy.UPDATE` (added in WorkManager 2.8; the project is on **2.11.2**, so no version bump is needed). `UPDATE` re-applies the current spec on each re-enqueue while **preserving the running period's progress** — an unchanged spec is effectively a no-op, so unlike `REPLACE` (cancel + re-enqueue) it does not reset the schedule or drop in-flight work on every launch. A shared `PERIODIC_POLICY` constant documents the rationale in one place. Scope guard: this touches only the periodic existing-work policy. The one-shot kicks (`syncNow`/`pruneNow` → `REPLACE`, `backfillNow` → `KEEP`) are a separate concern and keep their deliberate policies. The change is consistent with the recently merged sync/battery work (#88–90 / PR #109); the IDLE-poll fallback in `IdleService` still re-asserts the periodic fallback, now as a no-op UPDATE (comment updated). ## Testability `SyncScheduler` now injects `Provider<WorkManager>` (via a new `WorkManagerModule`) instead of calling the `WorkManager.getInstance()` static directly, so the policy is unit-testable with MockK. The `Provider` keeps resolution lazy (resolved at schedule time, not construction), preserving the previous on-demand init timing so nothing resolves during Hilt's Application field injection. ## Tests New `SyncSchedulerTest` (JUnit4 + MockK) asserts each periodic job is enqueued with `UPDATE`, and that the one-shot kicks keep `REPLACE`/`KEEP`. ## Verification Fast gate all green: `:app:assembleDebug`, `:app:testDebugUnitTest`, `:app:lintDebug`, `:app:ktlintCheck`, `:app:detekt`, plus `:app:compileDebugAndroidTestKotlin`. Closes #96 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.