- 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>
Three notification channels:
- sync_progress (LOW): foreground notification while syncing, shows pair name
- sync_complete (LOW): result after success — "↑X ↓X" or "Up to date"
- sync_alerts (DEFAULT): error notification with message on failure
Notifications respect per-pair notifyOnComplete / notifyOnError settings.
All notifications tap-through to MainActivity. Foreground info now names the
pair being synced instead of the generic "Syncing…" text.
Bump to 1.0.14 (code 15).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
App icon: deep indigo-to-violet gradient background with white sync arrows;
replaced flat #2196F3 with layered adaptive icon.
Theme: disabled dynamic color; rich indigo/teal/amber Material3 palette;
edge-to-edge with transparent status bar; tighter typography letterSpacing.
HomeScreen: colored left accent bar per status; URL-decoded SAF paths;
relative timestamps (Just now / N min ago / N hr ago); indigo status pills;
FilledTonalButton empty state.
PairDetailScreen: hero StatusBanner with large icon and relative time;
InfoCard as bordered grid with icon backgrounds; colored dot event timeline;
URL-decoded local path display.
SettingsScreen: section headers with primary left bar; AccountCard with
primaryContainer icon backgrounds; Security/About in bordered cards.
Bump version to 1.0.13 (code 14).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a file was uploaded before state-tracking worked (getFileMetadata was
broken), its SyncFileStateEntity was never saved. On next sync the engine
saw !local + remote + known=null and downloaded it back instead of deleting
it remotely, creating an infinite re-download loop.
Fix: syncDecide() now accepts hasPriorSyncState (derived from whether the
pair has any known states at all). On initial sync (no prior state) unknown
remote files are downloaded as before. Once the pair has been synced, unknown
remote-only files are treated as mirror-eligible deletions — same as if known
state existed — so locally-deleted files propagate to the remote correctly.
Verified live: 3 remote-only orphan files deleted from Nextcloud on sync.
Bump version to 1.0.12 (code 13).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LocalAccessor.Saf.delete() now uses docIdCache (same as openInputStream)
and catches IllegalStateException from DocumentsContract.deleteDocument
instead of propagating it through awaitAll() and crashing the whole sync
- WebDavProvider.getFileMetadata() passes dropFirst=false to parsePropfind
since Depth:0 returns exactly 1 result (the file); drop(1) was discarding it
- SyncEngine.performSync() calls ensureRemoteDirs() before each upload so
MKCOL is issued for any missing parent directories (405=exists is success)
- Bump version to 1.0.11 (code 12)
Verified against live Nextcloud: baseline ↑0 ↓0 ✗0, upload detection ↑1 ↓0 ✗0,
download detection ↑0 ↓1 ✗0.
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>
Without calling takePersistableUriPermission, the content:// URI
permission granted by ACTION_OPEN_DOCUMENT_TREE is revoked on
app reinstall, causing Permission Denial errors during sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SyncEngine now handles content:// URIs via ContentResolver/DocumentsContract
alongside regular file paths; fixes ENOENT on all SAF-backed sync pairs
- ForegroundInfo now passes FOREGROUND_SERVICE_TYPE_DATA_SYNC on API 29+
- Declare foregroundServiceType=dataSync on WorkManager service in manifest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single source of truth: bump VERSION_NAME/VERSION_CODE in version.properties
to release a new version. BuildConfig.VERSION_NAME exposed to the app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Supports WebDAV, SFTP, SFTPGo, Nextcloud, ownCloud, Google Drive,
Dropbox, and OneDrive. Credentials encrypted with Android Keystore.
Biometric app-lock, conflict resolution, and auto-sync via WorkManager.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>