feat(sync): IMPL_08 - Add global sync rate-limiting & battery protection

Changes:
- Constants.kt: Add KEY_LAST_GLOBAL_SYNC_TIME and MIN_GLOBAL_SYNC_INTERVAL_MS (30s)
- SyncStateManager.kt: Add canSyncGlobally() and markGlobalSyncStarted() methods
- MainViewModel.triggerAutoSync(): Check global cooldown before individual throttle
- MainViewModel.triggerAutoSync(): Mark global sync start before viewModelScope.launch
- MainViewModel.triggerManualSync(): Mark global sync start after tryStartSync()
- SyncWorker.doWork(): Add SyncStateManager.tryStartSync() coordination with silent=true
- SyncWorker.doWork(): Check global cooldown before expensive server checks
- SyncWorker.doWork(): Call markCompleted()/markError() to update SyncStateManager
- WifiSyncReceiver: Add global cooldown check and KEY_SYNC_TRIGGER_WIFI_CONNECT validation
This commit is contained in:
inventory69
2026-02-11 09:13:06 +01:00
parent fe6935a6b7
commit ffe0e46e3d
5 changed files with 108 additions and 8 deletions

View File

@@ -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()
}
} }

View File

@@ -104,7 +104,37 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { 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 // 🔥 v1.1.2: Performance-Optimierung - Skip Sync wenn keine lokalen Änderungen
@@ -122,7 +152,7 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { 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) // 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config)
@@ -143,7 +173,7 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { 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 // ⭐ KRITISCH: Server-Erreichbarkeits-Check VOR Sync
@@ -167,7 +197,7 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { 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") Logger.d(TAG, " SyncService: $syncService")
} }
@@ -188,7 +218,7 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 4: Processing result") Logger.d(TAG, "📍 Step 7: Processing result")
Logger.d( Logger.d(
TAG, TAG,
"📦 Sync result: success=${result.isSuccess}, " + "📦 Sync result: success=${result.isSuccess}, " +
@@ -198,10 +228,13 @@ class SyncWorker(
if (result.isSuccess) { if (result.isSuccess) {
if (BuildConfig.DEBUG) { 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") 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 // Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
// UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt) // UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt)
if (result.syncedCount > 0) { if (result.syncedCount > 0) {
@@ -248,9 +281,13 @@ class SyncWorker(
Result.success() Result.success()
} else { } else {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 5: Failure path") Logger.d(TAG, "📍 Step 8: Failure path")
} }
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}") Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
// 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren
SyncStateManager.markError(result.errorMessage)
NotificationHelper.showSyncError( NotificationHelper.showSyncError(
applicationContext, applicationContext,
result.errorMessage ?: "Unbekannter Fehler" result.errorMessage ?: "Unbekannter Fehler"

View File

@@ -27,6 +27,16 @@ class WifiSyncReceiver : BroadcastReceiver() {
return 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) // Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0)
if (isConnectedToWifi(context)) { if (isConnectedToWifi(context)) {
scheduleSyncWork(context) scheduleSyncWork(context)

View File

@@ -555,6 +555,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return 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<android.app.Application>().getSharedPreferences(
Constants.PREFS_NAME,
android.content.Context.MODE_PRIVATE
)
// 🆕 v1.7.0: Feedback wenn Sync bereits läuft // 🆕 v1.7.0: Feedback wenn Sync bereits läuft
// 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant // 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant
if (!SyncStateManager.tryStartSync(source)) { if (!SyncStateManager.tryStartSync(source)) {
@@ -571,6 +578,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return return
} }
// 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (nach tryStartSync, vor Launch)
SyncStateManager.markGlobalSyncStarted(prefs)
viewModelScope.launch { viewModelScope.launch {
try { try {
// Check for unsynced changes (Banner zeigt bereits PREPARING) // Check for unsynced changes (Banner zeigt bereits PREPARING)
@@ -636,7 +646,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return 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()) { if (!canTriggerAutoSync()) {
return return
} }
@@ -665,6 +680,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Update last sync timestamp // Update last sync timestamp
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply() 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 { viewModelScope.launch {
try { try {
// Check for unsynced changes // Check for unsynced changes

View File

@@ -82,4 +82,8 @@ object Constants {
// 📋 v1.8.0: Post-Update Changelog // 📋 v1.8.0: Post-Update Changelog
const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version" 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
} }