Android 11+ denies writes to /storage/emulated/0 directly. SyncEngine
now catches this early and returns FAILED with an actionable message
instead of silently logging PARTIAL. LocalBrowserDialog disables the
Select button at the storage root with an inline warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Restore mipmap-anydpi-v26 adaptive icon XMLs so Android 8+ shows
the icon at full size instead of scaling it to 66% safe zone
- Foreground at 108dp sizes (108-432px), background #050E05
- Status colors now semantic (not tied to app red/orange theme):
SUCCESS=green, SYNCING=blue, FAILED=red, PARTIAL=orange, CONFLICT=amber
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upload rollback fix: after uploading a file, do not store the remote
mtime/etag from the upload response PROPFIND. Nextcloud and other
WebDAV servers can change a file's Last-Modified or ETag after upload
(thumbnail generation, checksums, folder aggregation). Storing stale
metadata caused the next sync to see remoteChanged=true and download
the file back, reverting the upload. Leaving remoteAfterTransfer=null
forces the SKIP reconciliation pass to fill in remote state from the
directory listing, which is the same source all future syncs use.
Icon: update foreground to thick ribbons with 3D highlight stripes
(blue/green/red/orange, width 12 + highlight 5); update background
to dark space theme with star dots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. catchupScan used raw dir.walk() with no filters, causing hidden/excluded
files to appear as "new" every startup and trigger a catchup sync.
Fixed by using LocalAccessor.walkFiles(pair) which applies the same
filters and uses the same mtime source (SAF cursor) as SyncEngine.
2. catchupScan compared localModifiedAt.toEpochMilli() vs File.lastModified()
(millisecond precision) while SyncEngine uses second precision. Every file
appeared "modified" after a successful sync. Fixed by using epochSecond.
3. syncDecide() treated !localExists && remoteExists && known==null as
"user deleted local copy → delete remote" even on files that were never
synced. Fixed to treat unknown remote files as new (download them), which
is safe because a genuinely-deleted file will always have a known state
record from the previous sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Start FileWatchService from SyncFlowApp.onCreate() for any existing
enabled ON_CHANGE pairs — previously the watcher only started on
device boot or explicit pair toggle, so existing pairs after an
app update never got watched
- About screen now shows "Version X.Y.Z (build N)" updating
automatically from BuildConfig on every release
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sync icon now rotates (CSS-style spin) in StatusPill, StatusBanner,
and card sync button whenever status is SYNCING
- Launcher icon redesigned: indigo→violet→cyan gradient background,
upload arrow fades white→sky-blue, download arrow fades white→violet,
soft glow ring behind arrows
- Fix ON_CHANGE not triggering: FileWatchService.start() now called
from AddPairViewModel.save() so pairs created with ON_CHANGE
immediately begin watching without needing a toggle or reboot
- Fix FileWatch notification hidden: IMPORTANCE_MIN → IMPORTANCE_LOW
so the "Watching N folders" notification shows in the shade
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add FileWatchService for real-time ON_CHANGE sync (FileObserver for
direct paths, ContentObserver for SAF content:// URIs), 5s debounce
- Fix remote browser stuck spinner: cancel in-flight jobs on navigation,
reset entries immediately, add Retry button on error
- Fix browser reuse bug: LaunchedEffect key now includes initialPath
- Fix WebDavProvider: rethrow XML parse errors (no more silent Empty
folder) and URL-decode file names from href
- Notifications now use BigTextStyle showing per-file-type counts
(Uploaded/Downloaded/Deleted) matching Autosync notification style
- Wire FileWatchService into BootReceiver and HomeViewModel toggle
- Register FileWatchService in AndroidManifest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sync change detection (3rd attempt — now correct):
- After UPLOAD: save null remote metadata (server mtime unknown until
next listing); decide() treats null remoteModifiedAt as "not changed"
- After DOWNLOAD: read actual local mtime via accessor.lastModifiedMs()
so the stored value matches what walkFiles() sees on next scan
- SKIP reconciliation: if known state has null timestamps and both sides
exist, fill them in — stabilises state within 2 syncs after first transfer
- Extract syncDecide() as internal top-level function for testability
Unit tests (14 cases covering all key scenarios):
- First sync decisions (upload/download/conflict)
- Second sync after upload with null remote metadata → SKIP
- Second sync after download with recorded local mtime → SKIP
- Epoch-millis precision: same ms = SKIP, +1ms = change detected
- Regression: epoch-second stored value would have differed → now correct
- Delete behaviour (MIRROR vs KEEP)
- Direction filters (UPLOAD_ONLY, DOWNLOAD_ONLY)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sync change detection:
- DbConverters was using epochSecond but comparisons used epochMilli —
every file appeared modified on every scan, causing full re-sync each time
- DB migration 2→3 clears sync_file_states (all stored timestamps wrong)
- First sync after upgrade re-learns state; subsequent syncs skip unchanged files
Biometric:
- Move prompt trigger from LaunchedEffect to onResume() — guarantees
the activity is in RESUMED state when authenticate() is called
- Add bestAuthenticators(): tries BIOMETRIC_STRONG|DEVICE_CREDENTIAL first,
falls back to BIOMETRIC_WEAK|DEVICE_CREDENTIAL for side-sensor phones
- canAuthenticate() now accepts either strong or weak+credential
- onAuthenticationError always shows Unlock button (no infinite retry loop)
- isLocked/showRetry are Activity-level state, no need for Compose remember
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Biometric:
- Handle onAuthenticationError with auto-retry (except user cancel)
- Show lock screen with proper UI and an Unlock button as fallback
- Add subtitle clarifying fingerprint/PIN options
Sync engine:
- Fix data race: async coroutines now return FileOutcome instead of
mutating shared vars/list concurrently (was causing file states to
not be saved, so every sync re-transferred all files)
- Fix remoteChanged: use || instead of && so either etag or
modifiedAt change is enough to detect a remote modification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Edit icon to PairDetailScreen top bar
- Wire onEdit callback through NavGraph to AddPairScreen with pairId
- Manual "Sync now" (home card + detail screen) now ignores wifiOnly
and chargingOnly constraints so it runs immediately on tap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>