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:
@@ -90,25 +90,20 @@ class SyncWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
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)
|
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config)
|
||||||
val prefs = applicationContext.getSharedPreferences(
|
val gateResult = syncService.canSync()
|
||||||
Constants.PREFS_NAME,
|
if (!gateResult.canSync) {
|
||||||
Context.MODE_PRIVATE
|
if (gateResult.isBlockedByWifiOnly) {
|
||||||
)
|
|
||||||
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, "⏭️ WiFi-only mode enabled, but not on WiFi - skipping sync")
|
||||||
Logger.d(TAG, " User can still manually sync when on WiFi")
|
} else {
|
||||||
|
Logger.d(TAG, "⏭️ Sync blocked by gate: ${gateResult.blockReason ?: "offline/no server"}")
|
||||||
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (WiFi-only skip)")
|
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (gate blocked)")
|
||||||
Logger.d(TAG, "═══════════════════════════════════════")
|
Logger.d(TAG, "═══════════════════════════════════════")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
suspend fun testConnection(): SyncResult = withContext(Dispatchers.IO) {
|
||||||
return@withContext try {
|
return@withContext try {
|
||||||
val sardine = getOrCreateSardine() ?: return@withContext SyncResult(
|
val sardine = getOrCreateSardine() ?: return@withContext SyncResult(
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ class NoteEditorViewModel(
|
|||||||
/**
|
/**
|
||||||
* Triggers sync after saving a note (if enabled and server configured)
|
* Triggers sync after saving a note (if enabled and server configured)
|
||||||
* v1.6.0: New configurable sync trigger
|
* 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
|
* Separate throttling (5 seconds) to prevent spam when saving multiple times
|
||||||
*/
|
*/
|
||||||
@@ -365,24 +366,19 @@ class NoteEditorViewModel(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 2: Server configured?
|
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (inkl. WiFi-Only, Offline Mode, Server Config)
|
||||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
|
||||||
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
|
||||||
Logger.d(TAG, "⏭️ Offline mode - skipping onSave sync")
|
|
||||||
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())
|
val syncService = WebDavSyncService(getApplication())
|
||||||
if (!syncService.isOnWiFi()) {
|
val gateResult = syncService.canSync()
|
||||||
|
if (!gateResult.canSync) {
|
||||||
|
if (gateResult.isBlockedByWifiOnly) {
|
||||||
Logger.d(TAG, "⏭️ onSave sync blocked: WiFi-only mode, not on WiFi")
|
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
|
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 lastOnSaveSyncTime = prefs.getLong(Constants.PREF_LAST_ON_SAVE_SYNC_TIME, 0)
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val timeSinceLastSync = now - lastOnSaveSyncTime
|
val timeSinceLastSync = now - lastOnSaveSyncTime
|
||||||
|
|||||||
@@ -500,23 +500,20 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger manual sync (from toolbar button or pull-to-refresh)
|
* 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") {
|
fun triggerManualSync(source: String = "manual") {
|
||||||
// 🌟 v1.6.0: Block sync in offline mode
|
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (inkl. WiFi-Only, Offline Mode, Server Config)
|
||||||
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())
|
val syncService = WebDavSyncService(getApplication())
|
||||||
if (!syncService.isOnWiFi()) {
|
val gateResult = syncService.canSync()
|
||||||
|
if (!gateResult.canSync) {
|
||||||
|
if (gateResult.isBlockedByWifiOnly) {
|
||||||
Logger.d(TAG, "⏭️ $source Sync blocked: WiFi-only mode, not on WiFi")
|
Logger.d(TAG, "⏭️ $source Sync blocked: WiFi-only mode, not on WiFi")
|
||||||
SyncStateManager.markError(getString(R.string.sync_wifi_only_hint))
|
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
|
// 🆕 v1.7.0: Feedback wenn Sync bereits läuft
|
||||||
@@ -536,8 +533,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val syncService = WebDavSyncService(getApplication())
|
|
||||||
|
|
||||||
// Check for unsynced changes
|
// Check for unsynced changes
|
||||||
if (!syncService.hasUnsyncedChanges()) {
|
if (!syncService.hasUnsyncedChanges()) {
|
||||||
Logger.d(TAG, "⏭️ $source Sync: No unsynced changes")
|
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
|
* 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.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.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") {
|
fun triggerAutoSync(source: String = "auto") {
|
||||||
// 🌟 v1.6.0: Check if onResume trigger is enabled
|
// 🌟 v1.6.0: Check if onResume trigger is enabled
|
||||||
@@ -597,10 +593,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if server is configured
|
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (inkl. WiFi-Only, Offline Mode, Server Config)
|
||||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
val syncService = WebDavSyncService(getApplication())
|
||||||
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
val gateResult = syncService.canSync()
|
||||||
Logger.d(TAG, "⏭️ Offline mode - skipping onResume sync")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,8 +618,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val syncService = WebDavSyncService(getApplication())
|
|
||||||
|
|
||||||
// Check for unsynced changes
|
// Check for unsynced changes
|
||||||
if (!syncService.hasUnsyncedChanges()) {
|
if (!syncService.hasUnsyncedChanges()) {
|
||||||
Logger.d(TAG, "⏭️ Auto-sync ($source): No unsynced changes - skipping")
|
Logger.d(TAG, "⏭️ Auto-sync ($source): No unsynced changes - skipping")
|
||||||
|
|||||||
@@ -429,9 +429,21 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isSyncing.value = true
|
_isSyncing.value = true
|
||||||
try {
|
try {
|
||||||
emitToast(getString(R.string.toast_syncing))
|
|
||||||
val syncService = WebDavSyncService(getApplication())
|
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()) {
|
if (!syncService.hasUnsyncedChanges()) {
|
||||||
emitToast(getString(R.string.toast_already_synced))
|
emitToast(getString(R.string.toast_already_synced))
|
||||||
return@launch
|
return@launch
|
||||||
|
|||||||
Reference in New Issue
Block a user