diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt index ccdbd52..7439a27 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt @@ -214,4 +214,35 @@ object SyncStateManager { } } } + + // ═══════════════════════════════════════════════════════════════════════ + // 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Prüft ob seit dem letzten erfolgreichen Sync-Start genügend Zeit vergangen ist. + * Wird von ALLEN Sync-Triggern als erste Prüfung aufgerufen. + * + * @return true wenn ein neuer Sync erlaubt ist + */ + fun canSyncGlobally(prefs: android.content.SharedPreferences): Boolean { + val lastGlobalSync = prefs.getLong(dev.dettmer.simplenotes.utils.Constants.KEY_LAST_GLOBAL_SYNC_TIME, 0) + val now = System.currentTimeMillis() + val elapsed = now - lastGlobalSync + + if (elapsed < dev.dettmer.simplenotes.utils.Constants.MIN_GLOBAL_SYNC_INTERVAL_MS) { + val remainingSec = (dev.dettmer.simplenotes.utils.Constants.MIN_GLOBAL_SYNC_INTERVAL_MS - elapsed) / 1000 + dev.dettmer.simplenotes.utils.Logger.d(TAG, "⏳ Global sync cooldown active - wait ${remainingSec}s") + return false + } + return true + } + + /** + * Markiert den aktuellen Zeitpunkt als letzten Sync-Start (global). + * Aufzurufen wenn ein Sync tatsächlich startet (nach allen Checks). + */ + fun markGlobalSyncStarted(prefs: android.content.SharedPreferences) { + prefs.edit().putLong(dev.dettmer.simplenotes.utils.Constants.KEY_LAST_GLOBAL_SYNC_TIME, System.currentTimeMillis()).apply() + } } 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 3863098..6872f79 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 @@ -104,7 +104,37 @@ class SyncWorker( } if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 2: Checking for unsynced changes (Performance Pre-Check)") + Logger.d(TAG, "📍 Step 2: SyncStateManager coordination & global cooldown (v1.8.1)") + } + + // 🆕 v1.8.1 (IMPL_08): SyncStateManager-Koordination + // Verhindert dass Foreground und Background gleichzeitig syncing-State haben + val prefs = applicationContext.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) + + // Globaler Cooldown-Check (verhindert unnötige Server-Checks) + if (!SyncStateManager.canSyncGlobally(prefs)) { + Logger.d(TAG, "⏭️ SyncWorker: Global sync cooldown active - skipping") + if (BuildConfig.DEBUG) { + Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (cooldown)") + Logger.d(TAG, "═══════════════════════════════════════") + } + return@withContext Result.success() + } + + if (!SyncStateManager.tryStartSync("worker-${tags.firstOrNull() ?: "unknown"}", silent = true)) { + Logger.d(TAG, "⏭️ SyncWorker: Another sync already in progress - skipping") + if (BuildConfig.DEBUG) { + Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (already syncing)") + Logger.d(TAG, "═══════════════════════════════════════") + } + return@withContext Result.success() + } + + // Globalen Cooldown markieren + SyncStateManager.markGlobalSyncStarted(prefs) + + if (BuildConfig.DEBUG) { + Logger.d(TAG, "📍 Step 3: Checking for unsynced changes (Performance Pre-Check)") } // 🔥 v1.1.2: Performance-Optimierung - Skip Sync wenn keine lokalen Änderungen @@ -122,7 +152,7 @@ class SyncWorker( } if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 2.5: Checking sync gate (canSync)") + Logger.d(TAG, "📍 Step 4: Checking sync gate (canSync)") } // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config) @@ -143,7 +173,7 @@ class SyncWorker( } if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 3: Checking server reachability (Pre-Check)") + Logger.d(TAG, "📍 Step 5: Checking server reachability (Pre-Check)") } // ⭐ KRITISCH: Server-Erreichbarkeits-Check VOR Sync @@ -167,7 +197,7 @@ class SyncWorker( } if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 3: Server reachable - proceeding with sync") + Logger.d(TAG, "📍 Step 6: Server reachable - proceeding with sync") Logger.d(TAG, " SyncService: $syncService") } @@ -188,7 +218,7 @@ class SyncWorker( } if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 4: Processing result") + Logger.d(TAG, "📍 Step 7: Processing result") Logger.d( TAG, "📦 Sync result: success=${result.isSuccess}, " + @@ -198,10 +228,13 @@ class SyncWorker( if (result.isSuccess) { if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 5: Success path") + Logger.d(TAG, "📍 Step 8: Success path") } Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes") + // 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren + SyncStateManager.markCompleted() + // Nur Notification zeigen wenn tatsächlich etwas gesynct wurde // UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt) if (result.syncedCount > 0) { @@ -248,9 +281,13 @@ class SyncWorker( Result.success() } else { if (BuildConfig.DEBUG) { - Logger.d(TAG, "📍 Step 5: Failure path") + Logger.d(TAG, "📍 Step 8: Failure path") } Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}") + + // 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren + SyncStateManager.markError(result.errorMessage) + NotificationHelper.showSyncError( applicationContext, result.errorMessage ?: "Unbekannter Fehler" diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt index ea90e82..263252c 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt @@ -27,6 +27,16 @@ class WifiSyncReceiver : BroadcastReceiver() { return } + // 🆕 v1.8.1 (IMPL_08): Globaler Cooldown (verhindert Doppel-Trigger mit NetworkMonitor) + if (!SyncStateManager.canSyncGlobally(prefs)) { + return + } + + // 🆕 v1.8.1 (IMPL_08): Auch KEY_SYNC_TRIGGER_WIFI_CONNECT prüfen (Konsistenz mit NetworkMonitor) + if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_WIFI_CONNECT, Constants.DEFAULT_TRIGGER_WIFI_CONNECT)) { + return + } + // Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0) if (isConnectedToWifi(context)) { scheduleSyncWork(context) 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 89ebde3..360e43a 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 @@ -555,6 +555,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } + // 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (verhindert Auto-Sync direkt danach) + // Manueller Sync prüft NICHT den globalen Cooldown (User will explizit synchronisieren) + val prefs = getApplication().getSharedPreferences( + Constants.PREFS_NAME, + android.content.Context.MODE_PRIVATE + ) + // 🆕 v1.7.0: Feedback wenn Sync bereits läuft // 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant if (!SyncStateManager.tryStartSync(source)) { @@ -571,6 +578,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } + // 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (nach tryStartSync, vor Launch) + SyncStateManager.markGlobalSyncStarted(prefs) + viewModelScope.launch { try { // Check for unsynced changes (Banner zeigt bereits PREPARING) @@ -636,7 +646,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } - // Throttling check + // 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown (alle Trigger teilen sich diesen) + if (!SyncStateManager.canSyncGlobally(prefs)) { + return + } + + // Throttling check (eigener 60s-Cooldown für onResume) if (!canTriggerAutoSync()) { return } @@ -665,6 +680,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { // Update last sync timestamp prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply() + // 🆕 v1.8.1 (IMPL_08): Globalen Sync-Cooldown markieren + SyncStateManager.markGlobalSyncStarted(prefs) + viewModelScope.launch { try { // Check for unsynced changes diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt index bbd94f2..5eacb39 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt @@ -82,4 +82,8 @@ object Constants { // 📋 v1.8.0: Post-Update Changelog const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version" + + // 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown (über alle Trigger hinweg) + const val KEY_LAST_GLOBAL_SYNC_TIME = "last_global_sync_timestamp" + const val MIN_GLOBAL_SYNC_INTERVAL_MS = 30_000L // 30 Sekunden }