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.
This commit is contained in:
inventory69
2026-01-26 22:41:00 +01:00
parent 0df8282eb4
commit cb63aa1220
5 changed files with 91 additions and 51 deletions

View File

@@ -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, "═══════════════════════════════════════")
}

View File

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

View File

@@ -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

View File

@@ -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")

View File

@@ -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