Direction-aware default for deletion behaviour (don't wipe backups)
Build & Release APK / build (push) Successful in 12m54s
Build & Release APK / build (push) Successful in 12m54s
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.
This commit is contained in:
@@ -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}" },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user