From cb63aa12204989f358455d3642c08c02f379dbaa Mon Sep 17 00:00:00 2001 From: inventory69 Date: Mon, 26 Jan 2026 22:41:00 +0100 Subject: [PATCH] fix(sync): Implement central canSync() gate for WiFi-only check - Add WebDavSyncService.canSync() as single source of truth - Add SyncGateResult data class for structured response - Update MainViewModel.triggerManualSync() to use canSync() - Update MainViewModel.triggerAutoSync() to use canSync() - FIXES onResume bug - Update NoteEditorViewModel.triggerOnSaveSync() to use canSync() - Update SettingsViewModel.syncNow() to use canSync() - Update SyncWorker to use canSync() instead of direct prefs check All 9 sync paths now respect WiFi-only setting through one central gate. --- .../dettmer/simplenotes/sync/SyncWorker.kt | 25 +++++------- .../simplenotes/sync/WebDavSyncService.kt | 38 ++++++++++++++++++ .../ui/editor/NoteEditorViewModel.kt | 26 ++++++------- .../simplenotes/ui/main/MainViewModel.kt | 39 +++++++++---------- .../ui/settings/SettingsViewModel.kt | 14 ++++++- 5 files changed, 91 insertions(+), 51 deletions(-) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt index f30611d..692db55 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt @@ -90,25 +90,20 @@ class SyncWorker( } if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 2.5: Checking WiFi-only setting") + Logger.d(TAG, "📍 Step 2.5: Checking sync gate (canSync)") } - // 🆕 v1.7.0: WiFi-Only Check (zentral für alle Sync-Arten) - val prefs = applicationContext.getSharedPreferences( - Constants.PREFS_NAME, - Context.MODE_PRIVATE - ) - val wifiOnlySync = prefs.getBoolean( - Constants.KEY_WIFI_ONLY_SYNC, - Constants.DEFAULT_WIFI_ONLY_SYNC - ) - - if (wifiOnlySync && !syncService.isOnWiFi()) { - Logger.d(TAG, "⏭️ WiFi-only mode enabled, but not on WiFi - skipping sync") - Logger.d(TAG, " User can still manually sync when on WiFi") + // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config) + val gateResult = syncService.canSync() + if (!gateResult.canSync) { + if (gateResult.isBlockedByWifiOnly) { + Logger.d(TAG, "⏭️ WiFi-only mode enabled, but not on WiFi - skipping sync") + } else { + Logger.d(TAG, "⏭️ Sync blocked by gate: ${gateResult.blockReason ?: "offline/no server"}") + } if (BuildConfig.DEBUG) { - Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (WiFi-only skip)") + Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (gate blocked)") Logger.d(TAG, "═══════════════════════════════════════") } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt index 3f3778e..5d8dbdb 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt @@ -582,6 +582,44 @@ class WebDavSyncService(private val context: Context) { } } + /** + * 🆕 v1.7.0: Zentrale Sync-Gate Prüfung + * Prüft ALLE Voraussetzungen bevor ein Sync gestartet wird. + * Diese Funktion sollte VOR jedem syncNotes() Aufruf verwendet werden. + * + * @return SyncGateResult mit canSync flag und optionalem Blockierungsgrund + */ + fun canSync(): SyncGateResult { + // 1. Offline Mode Check + if (prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)) { + return SyncGateResult(canSync = false, blockReason = null) // Silent skip + } + + // 2. Server configured? + val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) + if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") { + return SyncGateResult(canSync = false, blockReason = null) // Silent skip + } + + // 3. WiFi-Only Check + val wifiOnlySync = prefs.getBoolean(Constants.KEY_WIFI_ONLY_SYNC, Constants.DEFAULT_WIFI_ONLY_SYNC) + if (wifiOnlySync && !isOnWiFi()) { + return SyncGateResult(canSync = false, blockReason = "wifi_only") + } + + return SyncGateResult(canSync = true, blockReason = null) + } + + /** + * 🆕 v1.7.0: Result-Klasse für canSync() + */ + data class SyncGateResult( + val canSync: Boolean, + val blockReason: String? = null + ) { + val isBlockedByWifiOnly: Boolean get() = blockReason == "wifi_only" + } + suspend fun testConnection(): SyncResult = withContext(Dispatchers.IO) { return@withContext try { val sardine = getOrCreateSardine() ?: return@withContext SyncResult( diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt index 40da751..3a67d67 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt @@ -355,6 +355,7 @@ class NoteEditorViewModel( /** * Triggers sync after saving a note (if enabled and server configured) * v1.6.0: New configurable sync trigger + * v1.7.0: Uses central canSync() gate for WiFi-only check * * Separate throttling (5 seconds) to prevent spam when saving multiple times */ @@ -365,24 +366,19 @@ class NoteEditorViewModel( return } - // Check 2: Server configured? - val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) - if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") { - Logger.d(TAG, "⏭️ Offline mode - skipping onSave sync") + // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (inkl. WiFi-Only, Offline Mode, Server Config) + val syncService = WebDavSyncService(getApplication()) + val gateResult = syncService.canSync() + if (!gateResult.canSync) { + if (gateResult.isBlockedByWifiOnly) { + Logger.d(TAG, "⏭️ onSave sync blocked: WiFi-only mode, not on WiFi") + } else { + Logger.d(TAG, "⏭️ onSave sync blocked: ${gateResult.blockReason ?: "offline/no server"}") + } return } - // 🆕 v1.7.0: WiFi-Only Check - val wifiOnlySync = prefs.getBoolean(Constants.KEY_WIFI_ONLY_SYNC, Constants.DEFAULT_WIFI_ONLY_SYNC) - if (wifiOnlySync) { - val syncService = WebDavSyncService(getApplication()) - if (!syncService.isOnWiFi()) { - Logger.d(TAG, "⏭️ onSave sync blocked: WiFi-only mode, not on WiFi") - return - } - } - - // Check 3: Throttling (5 seconds) to prevent spam + // Check 2: Throttling (5 seconds) to prevent spam val lastOnSaveSyncTime = prefs.getLong(Constants.PREF_LAST_ON_SAVE_SYNC_TIME, 0) val now = System.currentTimeMillis() val timeSinceLastSync = now - lastOnSaveSyncTime diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt index 44524cd..024ba3e 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt @@ -500,23 +500,20 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { /** * Trigger manual sync (from toolbar button or pull-to-refresh) + * v1.7.0: Uses central canSync() gate for WiFi-only check */ fun triggerManualSync(source: String = "manual") { - // 🌟 v1.6.0: Block sync in offline mode - if (prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)) { - Logger.d(TAG, "⏭️ $source Sync blocked: Offline mode enabled") - return - } - - // 🆕 v1.7.0: WiFi-Only Check (sofort, kein Netzwerk-Wait) - val wifiOnlySync = prefs.getBoolean(Constants.KEY_WIFI_ONLY_SYNC, Constants.DEFAULT_WIFI_ONLY_SYNC) - if (wifiOnlySync) { - val syncService = WebDavSyncService(getApplication()) - if (!syncService.isOnWiFi()) { + // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (inkl. WiFi-Only, Offline Mode, Server Config) + val syncService = WebDavSyncService(getApplication()) + val gateResult = syncService.canSync() + if (!gateResult.canSync) { + if (gateResult.isBlockedByWifiOnly) { Logger.d(TAG, "⏭️ $source Sync blocked: WiFi-only mode, not on WiFi") SyncStateManager.markError(getString(R.string.sync_wifi_only_hint)) - return + } else { + Logger.d(TAG, "⏭️ $source Sync blocked: ${gateResult.blockReason ?: "offline/no server"}") } + return } // 🆕 v1.7.0: Feedback wenn Sync bereits läuft @@ -536,8 +533,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { viewModelScope.launch { try { - val syncService = WebDavSyncService(getApplication()) - // Check for unsynced changes if (!syncService.hasUnsyncedChanges()) { Logger.d(TAG, "⏭️ $source Sync: No unsynced changes") @@ -584,6 +579,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { * Only runs if server is configured and interval has passed * v1.5.0: Silent-Sync - kein Banner während des Syncs, Fehler werden trotzdem angezeigt * v1.6.0: Configurable trigger - checks KEY_SYNC_TRIGGER_ON_RESUME + * v1.7.0: Uses central canSync() gate for WiFi-only check */ fun triggerAutoSync(source: String = "auto") { // 🌟 v1.6.0: Check if onResume trigger is enabled @@ -597,10 +593,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } - // Check if server is configured - val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) - if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") { - Logger.d(TAG, "⏭️ Offline mode - skipping onResume sync") + // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (inkl. WiFi-Only, Offline Mode, Server Config) + val syncService = WebDavSyncService(getApplication()) + val gateResult = syncService.canSync() + if (!gateResult.canSync) { + if (gateResult.isBlockedByWifiOnly) { + Logger.d(TAG, "⏭️ Auto-sync ($source) blocked: WiFi-only mode, not on WiFi") + } else { + Logger.d(TAG, "⏭️ Auto-sync ($source) blocked: ${gateResult.blockReason ?: "offline/no server"}") + } return } @@ -617,8 +618,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { viewModelScope.launch { try { - val syncService = WebDavSyncService(getApplication()) - // Check for unsynced changes if (!syncService.hasUnsyncedChanges()) { Logger.d(TAG, "⏭️ Auto-sync ($source): No unsynced changes - skipping") diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt index dc5eed8..fbed556 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt @@ -429,9 +429,21 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application viewModelScope.launch { _isSyncing.value = true try { - emitToast(getString(R.string.toast_syncing)) val syncService = WebDavSyncService(getApplication()) + // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung + val gateResult = syncService.canSync() + if (!gateResult.canSync) { + if (gateResult.isBlockedByWifiOnly) { + emitToast(getString(R.string.sync_wifi_only_hint)) + } else { + emitToast(getString(R.string.toast_sync_failed, "Offline mode")) + } + return@launch + } + + emitToast(getString(R.string.toast_syncing)) + if (!syncService.hasUnsyncedChanges()) { emitToast(getString(R.string.toast_already_synced)) return@launch