From dc478516cdfb68596018c4a8f36f1aa2733c824f Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 27 Jun 2026 22:35:58 +0200 Subject: [PATCH] fix(backup): stop orphaned backup work when disabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turning automatic backup off only cancelled the periodic work, so a run-now that kept failing (e.g. its folder was deleted) retried forever and spammed failure notifications. - Cancel the immediate "run now" work too when backup is disabled. - Worker no-ops (no retry) when the toggle is off, so already-queued work can't revive itself. - Reconcile scheduled work against saved settings on every app launch — re-arms after a reinstall and clears orphaned work once backup is off. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../jeanlucmakiola/calendula/CalendulaApp.kt | 27 +++++++++++++++++++ .../calendula/data/backup/AutoBackup.kt | 6 +++++ 2 files changed, 33 insertions(+) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt b/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt index 2ec6c24..ac300d1 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt @@ -1,8 +1,16 @@ package de.jeanlucmakiola.calendula import android.app.Application +import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.HiltAndroidApp +import de.jeanlucmakiola.calendula.data.backup.BackupScheduler +import de.jeanlucmakiola.calendula.data.backup.BackupWorker import de.jeanlucmakiola.calendula.data.crash.CrashReporter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch /** * Application entry point. Registered as android:name=".CalendulaApp" @@ -16,5 +24,24 @@ class CalendulaApp : Application() { // Install first thing so startup crashes are captured too (privacy- // respecting, on-device; the user submits the report by hand). CrashReporter.install(this) + reconcileAutoBackup() + } + + /** + * Bring the scheduled auto-backup work back in line with the saved settings + * on every launch — re-arms it after a reinstall and, crucially, cancels any + * orphaned work once backup has been turned off. + */ + private fun reconcileAutoBackup() { + val deps = EntryPointAccessors.fromApplication(this, BackupWorker.Deps::class.java) + CoroutineScope(SupervisorJob() + Dispatchers.Default).launch { + val prefs = deps.settingsPrefs() + BackupScheduler.apply( + context = this@CalendulaApp, + enabled = prefs.autoBackupEnabled.first(), + intervalMinutes = prefs.autoBackupIntervalMinutes.first(), + hasFolder = prefs.autoBackupFolderUri.first() != null, + ) + } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt index c08fa72..64f5091 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/backup/AutoBackup.kt @@ -55,6 +55,9 @@ object BackupScheduler { val workManager = WorkManager.getInstance(context) if (!enabled || !hasFolder) { workManager.cancelUniqueWork(WORK_NAME) + // Also cancel any pending immediate run — otherwise a failing run-now + // keeps retrying after the user has turned backup off. + workManager.cancelUniqueWork(WORK_NAME_NOW) return } // WorkManager's own floor is 15 min; the UI floors the user choice at 30. @@ -106,6 +109,9 @@ class BackupWorker( 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: if backup was turned + // off, no-op (and don't retry) so a lingering run can't revive itself. + if (!prefs.autoBackupEnabled.first()) return Result.success() val folder = prefs.autoBackupFolderUri.first() ?: return Result.failure() // nothing to write to — leave scheduling to settings val now = System.currentTimeMillis()