From 160a3e5478f52424f466d0cc8b4b75d51d715639 Mon Sep 17 00:00:00 2001 From: Friday Date: Fri, 5 Jun 2026 02:39:49 +0000 Subject: [PATCH] Direction-aware default for deletion behaviour (don't wipe backups) The Add-Pair screen defaulted deleteBehavior to MIRROR for every direction, so an Upload-only backup would delete cloud files when you deleted them on the phone. Now the default follows the direction: - Upload-only / Download-only -> KEEP (deleting locally leaves the cloud copy) - Two-way -> MIRROR All three options remain selectable; once the user explicitly picks one, changing direction no longer overrides it, and editing a saved pair keeps its stored choice. Adds RecommendedDeleteBehaviorTest. --- .../com/syncflow/ui/addpair/AddPairScreen.kt | 4 +-- .../syncflow/ui/addpair/AddPairViewModel.kt | 28 ++++++++++++++++++- .../addpair/RecommendedDeleteBehaviorTest.kt | 23 +++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 app/src/test/kotlin/com/syncflow/ui/addpair/RecommendedDeleteBehaviorTest.kt diff --git a/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairScreen.kt b/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairScreen.kt index 0692431..6af5d3a 100644 --- a/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairScreen.kt +++ b/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairScreen.kt @@ -163,7 +163,7 @@ fun AddPairScreen(onDone: () -> Unit, vm: AddPairViewModel = hiltViewModel()) { label = "Direction", options = SyncDirection.entries, selected = s.syncDirection, - onSelect = { vm.update { copy(syncDirection = it) } }, + onSelect = { vm.setDirection(it) }, itemLabel = { "${it.label} — ${it.description}" }, ) Spacer(Modifier.height(8.dp)) @@ -179,7 +179,7 @@ fun AddPairScreen(onDone: () -> Unit, vm: AddPairViewModel = hiltViewModel()) { label = "Deletion behaviour", options = DeleteBehavior.entries, selected = s.deleteBehavior, - onSelect = { vm.update { copy(deleteBehavior = it) } }, + onSelect = { vm.setDeleteBehavior(it) }, itemLabel = { "${it.label} — ${it.description}" }, ) } diff --git a/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairViewModel.kt b/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairViewModel.kt index 29f88b3..e87485f 100644 --- a/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairViewModel.kt +++ b/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairViewModel.kt @@ -28,7 +28,10 @@ data class AddPairUiState( // ── Sync type ──────────────────────────────────────────────────────────── val syncDirection: SyncDirection = SyncDirection.TWO_WAY, val conflictStrategy: ConflictStrategy = ConflictStrategy.KEEP_NEWEST, - val deleteBehavior: DeleteBehavior = DeleteBehavior.MIRROR, + val deleteBehavior: DeleteBehavior = recommendedDeleteBehavior(SyncDirection.TWO_WAY), + // True once the user explicitly picks a deletion behaviour, so changing direction stops + // auto-overriding their choice. + val deleteBehaviorTouched: Boolean = false, val recursive: Boolean = true, // ── Schedule ───────────────────────────────────────────────────────────── val scheduleType: ScheduleType = ScheduleType.INTERVAL, @@ -56,6 +59,16 @@ data class AddPairUiState( val done: Boolean = false, ) +/** + * Safe default deletion behaviour for a given direction. One-way backups must NOT propagate a + * local deletion to the cloud (the whole point of a backup), so they default to KEEP; two-way + * sync defaults to MIRROR. The user can always override — all three options stay selectable. + */ +internal fun recommendedDeleteBehavior(direction: SyncDirection): DeleteBehavior = when (direction) { + SyncDirection.UPLOAD_ONLY, SyncDirection.DOWNLOAD_ONLY -> DeleteBehavior.KEEP + SyncDirection.TWO_WAY -> DeleteBehavior.MIRROR +} + @HiltViewModel class AddPairViewModel @Inject constructor( private val syncPairDao: SyncPairDao, @@ -93,6 +106,7 @@ class AddPairViewModel @Inject constructor( syncDirection = pair.syncDirection, conflictStrategy = pair.conflictStrategy, deleteBehavior = pair.deleteBehavior, + deleteBehaviorTouched = true, // preserve the saved choice when editing recursive = pair.recursive, scheduleType = pair.scheduleType, intervalMinutes = pair.scheduleIntervalMinutes, @@ -119,6 +133,18 @@ class AddPairViewModel @Inject constructor( fun update(transform: AddPairUiState.() -> AddPairUiState) = _state.update(transform) + /** Changing direction re-applies the safe deletion default unless the user already chose one. */ + fun setDirection(direction: SyncDirection) = _state.update { s -> + s.copy( + syncDirection = direction, + deleteBehavior = if (s.deleteBehaviorTouched) s.deleteBehavior else recommendedDeleteBehavior(direction), + ) + } + + fun setDeleteBehavior(behavior: DeleteBehavior) = _state.update { + it.copy(deleteBehavior = behavior, deleteBehaviorTouched = true) + } + fun save() { val s = _state.value val errors = buildList { diff --git a/app/src/test/kotlin/com/syncflow/ui/addpair/RecommendedDeleteBehaviorTest.kt b/app/src/test/kotlin/com/syncflow/ui/addpair/RecommendedDeleteBehaviorTest.kt new file mode 100644 index 0000000..4fefa70 --- /dev/null +++ b/app/src/test/kotlin/com/syncflow/ui/addpair/RecommendedDeleteBehaviorTest.kt @@ -0,0 +1,23 @@ +package com.syncflow.ui.addpair + +import com.syncflow.domain.model.DeleteBehavior +import com.syncflow.domain.model.SyncDirection +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * The Add-Pair screen's default deletion behaviour must never wipe a backup. One-way directions + * default to KEEP so deleting a file on the phone leaves the cloud copy intact; two-way defaults + * to MIRROR. (The user can still override to any of the three options.) + */ +class RecommendedDeleteBehaviorTest { + + @Test fun `upload-only defaults to KEEP so backups are never deleted`() = + assertEquals(DeleteBehavior.KEEP, recommendedDeleteBehavior(SyncDirection.UPLOAD_ONLY)) + + @Test fun `download-only defaults to KEEP`() = + assertEquals(DeleteBehavior.KEEP, recommendedDeleteBehavior(SyncDirection.DOWNLOAD_ONLY)) + + @Test fun `two-way defaults to MIRROR`() = + assertEquals(DeleteBehavior.MIRROR, recommendedDeleteBehavior(SyncDirection.TWO_WAY)) +}