diff --git a/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt b/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt
index 788cbfb..0158ba7 100644
--- a/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt
+++ b/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt
@@ -126,7 +126,9 @@ class SyncEngine @Inject constructor(
logEvent(pair.id, SyncEventType.FILE_DOWNLOADED, rel, null, bytes)
FileOutcome(downloaded = 1, bytesTransferred = bytes,
newState = buildState(pair.id, rel,
- LocalFileInfo(rel, remote!!.sizeBytes, localMtime), remoteAfterTransfer = remote))
+ LocalFileInfo(rel, remote!!.sizeBytes, localMtime),
+ remoteAfterTransfer = remote,
+ storeLocalMtime = false))
}
SyncDecision.DELETE_LOCAL -> {
accessor.delete(rel)
@@ -203,10 +205,13 @@ class SyncEngine @Inject constructor(
rel: String,
local: LocalFileInfo?,
remoteAfterTransfer: RemoteFile?,
+ storeLocalMtime: Boolean = true,
) = SyncFileStateEntity(
syncPairId = pairId,
relativePath = rel,
- localModifiedAt = local?.lastModifiedMs?.let { Instant.ofEpochMilli(it) },
+ // When storeLocalMtime=false, leave localModifiedAt null so the SKIP reconciliation
+ // pass on the next sync reads it from the walkFiles cursor (avoids SAF stale-mtime loops).
+ localModifiedAt = if (storeLocalMtime) local?.lastModifiedMs?.let { Instant.ofEpochMilli(it) } else null,
localSizeBytes = local?.sizeBytes ?: 0L,
localHash = null,
remoteModifiedAt = remoteAfterTransfer?.modifiedAt,
@@ -236,12 +241,16 @@ internal fun syncDecide(
// Treat null known timestamps as "not yet recorded" — don't treat as changed.
// The SKIP reconciliation pass will fill them in on the next sync.
+ // Use second-precision for both sides: FAT32 has 2-second mtime resolution, WebDAV
+ // RFC-1123 has 1-second resolution, so millisecond comparison causes phantom "changed"
+ // detections and rewrite loops after a fresh download/upload.
val localChanged = known == null ||
(localExists && known.localModifiedAt != null &&
- local!!.lastModifiedMs != known.localModifiedAt.toEpochMilli())
+ local!!.lastModifiedMs / 1000 != known.localModifiedAt.epochSecond)
val remoteChanged = known == null ||
(remoteExists && known.remoteModifiedAt != null &&
- (remote!!.etag != known.remoteEtag || remote.modifiedAt != known.remoteModifiedAt))
+ (remote!!.etag != known.remoteEtag ||
+ remote.modifiedAt.epochSecond != known.remoteModifiedAt.epochSecond))
return when {
!localExists && !remoteExists -> SyncDecision.SKIP
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 7558316..29f88b3 100644
--- a/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairViewModel.kt
+++ b/app/src/main/kotlin/com/syncflow/ui/addpair/AddPairViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syncflow.data.db.CloudAccountDao
+import com.syncflow.data.db.SyncFileStateDao
import com.syncflow.data.db.SyncPairDao
import com.syncflow.data.db.entities.CloudAccountEntity
import com.syncflow.data.db.entities.SyncPairEntity
@@ -58,6 +59,7 @@ data class AddPairUiState(
@HiltViewModel
class AddPairViewModel @Inject constructor(
private val syncPairDao: SyncPairDao,
+ private val fileStateDao: SyncFileStateDao,
private val accountDao: CloudAccountDao,
@ApplicationContext private val context: Context,
savedState: SavedStateHandle,
@@ -148,7 +150,20 @@ class AddPairViewModel @Inject constructor(
notifyOnComplete = s.notifyOnComplete, notifyOnError = s.notifyOnError,
isEnabled = true, lastSyncAt = null, lastSyncResult = SyncStatus.IDLE, pendingConflicts = 0,
)
- if (editPairId == null) syncPairDao.insert(entity) else syncPairDao.update(entity)
+ if (editPairId == null) {
+ syncPairDao.insert(entity)
+ } else {
+ val existing = syncPairDao.getById(editPairId)
+ syncPairDao.update(entity)
+ // If local or remote folder changed, old file-state records no longer
+ // correspond to any real path — wipe them so the next sync starts fresh
+ // instead of trying to delete/re-upload stale paths.
+ if (existing != null &&
+ (existing.localPath != entity.localPath || existing.remotePath != entity.remotePath)
+ ) {
+ fileStateDao.deleteForPair(editPairId)
+ }
+ }
}
.onSuccess {
if (s.scheduleType == ScheduleType.ON_CHANGE) FileWatchService.start(context)
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 5de05e6..1efe9e4 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -6,14 +6,19 @@
android:viewportWidth="108"
android:viewportHeight="108">
-
-
+
+
+
+
+
+ android:gradientRadius="60"
+ android:centerX="54" android:centerY="50"
+ android:startColor="#3D3A50"
+ android:endColor="#00000000"/>
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 4a63486..e3a3f0b 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,23 +1,12 @@
-
+
+ android:pathData="M 54,28 C 42,28 30,36 32,50 C 22,55 22,70 34,72 L 74,72 C 84,72 88,62 82,55 C 86,43 76,33 66,35 C 62,30 58,28 54,28 Z"
+ android:fillColor="#000000"
+ android:fillAlpha="0.20"
+ android:translateY="2.5"/>
-
+
+
-
-
-
-
+ android:pathData="M 35.5,26.5
+ C 30,21 22,22 22,22
+ C 22,22 27,30 32.5,35.5
+ C 36,38 40,40 43,42
+ C 40,39 36,32 35.5,26.5 Z">
+
+ android:startX="22" android:startY="22"
+ android:endX="43" android:endY="42"
+ android:startColor="#00BFA5"
+ android:endColor="#26D6C0"/>
-
+
-
+ android:pathData="M 72.5,26.5
+ C 78,21 86,22 86,22
+ C 86,22 81,30 75.5,35.5
+ C 72,38 68,40 65,42
+ C 68,39 72,32 72.5,26.5 Z">
+
+ android:startX="86" android:startY="22"
+ android:endX="65" android:endY="42"
+ android:startColor="#E53935"
+ android:endColor="#EF6558"/>
-
+
+ android:pathData="M 48,75
+ C 45,82 47,88 54,88
+ C 61,88 63,82 60,75
+ C 58,71 56,68 54,66
+ C 52,68 50,71 48,75 Z">
+
+
+
+
-
+
+
+
+
+ android:strokeColor="#4000BFA5"/>
-
-
-
-
-
+
+
diff --git a/version.properties b/version.properties
index e6dc9f9..51e12db 100644
--- a/version.properties
+++ b/version.properties
@@ -1,2 +1,2 @@
-VERSION_NAME=1.0.27
-VERSION_CODE=28
+VERSION_NAME=1.0.28
+VERSION_CODE=29