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:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<android.app.Application>().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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user