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