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.
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
SyncSchedulerenqueued its three periodic jobs — sync, full-history backfill (#12), and retention prune (#13) — withExistingPeriodicWorkPolicy.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 toExistingPeriodicWorkPolicy.UPDATE(added in WorkManager 2.8; the project is on 2.11.2, so no version bump is needed).UPDATEre-applies the current spec on each re-enqueue while preserving the running period's progress — an unchanged spec is effectively a no-op, so unlikeREPLACE(cancel + re-enqueue) it does not reset the schedule or drop in-flight work on every launch. A sharedPERIODIC_POLICYconstant 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 inIdleServicestill re-asserts the periodic fallback, now as a no-op UPDATE (comment updated).Testability
SyncSchedulernow injectsProvider<WorkManager>(via a newWorkManagerModule) instead of calling theWorkManager.getInstance()static directly, so the policy is unit-testable with MockK. TheProviderkeeps 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 withUPDATE, and that the one-shot kicks keepREPLACE/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