- SyncEngine: accepts onProgress callback — emits uploaded/downloaded/
deleted/bytes counts atomically as each file completes
- SyncWorker: streams progress to WorkManager data so the UI can poll
it live; reports per-run counters in the completion notification;
adds pause/resume support
- HomeViewModel/PairDetailViewModel: subscribe to live WorkManager
progress and surface it via SyncProgress state
- SyncPairEntity/SyncPairDao/SyncDatabase: persist last-run counters
(uploaded, downloaded, deleted, bytesTransferred) in the DB with a
Room migration (v3→v4)
- AppModule: provides WorkManager as an injectable singleton
- .gitignore: add .kotlin/ to exclude compiler session files
Security: no new issues — all logging via Timber (debug-only), DB
queries use Room parameterized API, file sharing via FileProvider.
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>
Root cause: manual sync triggered from the UI had no cooldown set in
FileWatchService, so file writes during any manual sync fired FileObserver
→ debounce → another sync → loop.
Fix: startSyncMonitor() subscribes to getWorkInfosByTagFlow("sync_$pairId")
and watches ALL sync work for each pair — manual, catchup, onchange — via
the tag that SyncWorker.buildOneTimeRequest() always adds.
- When any sync is RUNNING or ENQUEUED: cooldown extended to now+120s
- When sync transitions from running to finished: 60s settle cooldown
- Monitor job stored in syncMonitorJobs map and cancelled in clearWatchers()
This means no matter what triggers a sync, FileObserver events from the
resulting file writes are always suppressed until the folder settles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three additional fixes found via live device logs:
1. Startup race window: FileObserver fires immediately after
startWatching() before catchupScan coroutine runs, starting a 5s
debounce with cooldown=0. Fixed by setting a 15s startup cooldown
in watchPath() BEFORE calling watchDirRecursive.
2. Stale debounce bypass: debounce job started with cooldown=0 fires
5s later even after catchupScan has already set cooldown and started
a catchup sync. Fixed by re-checking cooldown after the 5s delay
and aborting if already active.
3. Debounce not cancelled by catchupScan: if a debounce was queued
before catchupScan ran, catchupScan would enqueue a catchup sync
AND the old debounce would fire 5s later enqueuing a second sync.
Fixed by cancelling pending debounce in catchupScan before enqueue.
Icon: four thick arcs (blue/red/green/orange) in a 4-way pinwheel
with over/under ordering. White sync-arrow circle at center.
Pure black background.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes found via live logcat on device:
1. concurrent refresh() race: onStartCommand received twice causes two
refresh() coroutines to run in parallel, doubling FileObserver and
catchupScan registrations. Fixed with Mutex.withLock on refresh().
2. catchupScan no cooldown: catchup syncs write files but never set
syncCooldownUntil, so every written file immediately re-triggers
onChangeDetected. Fixed by setting cooldown before enqueue and
watching work completion same as onChangeDetected does.
3. CancellationException caught silently: exception handler
catch(_: Exception) was catching CancellationException and resetting
cooldown to 0L, re-opening the loop. Fixed by rethrowing
CancellationException and setting 60s cooldown on other errors.
Icon: interlocked rings (blue/red/green/orange) with sync arrow at
center, pure black background — matches reference image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SyncEngine: self-healing stale folder state detection (isRetry) wipes
orphaned SyncFileStateEntity records when localPath changes without a
pair re-save — prevents repeated DELETE_REMOTE on 32 old files
- SyncEngine: second-precision mtime comparison (/ 1000 / .epochSecond)
eliminates phantom localChanged=true from FAT32/WebDAV precision mismatch
- FileWatchService: syncCooldownUntil map suppresses FileObserver events
for 120s after sync starts and 60s after it finishes, breaking the
download→FileObserver→sync→download feedback loop
- Icon: three bold teardrop shapes (teal/red/amber) rotated 0/120/240°
on dark charcoal background with white cloud at intersection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Icon: three identical parallel arcing arrows (same bezier curve, same blue-to-teal
gradient #64C8FF→#32EDBB, same arrowhead geometry) — visually cohesive and clearly
visible against the near-black background.
FileWatchService: FileObserver is now recursive — watchDirRecursive() creates an
observer for each subdirectory at startup, and adds new watchers when CREATE events
produce new directories. Fixes files added to subdirectories not being detected.
FilesViewModel: openFile/shareFile now fall back to download-then-open when the file
is absent locally. AccountRepository + ProviderFactory injected; downloads to
context.cacheDir/syncflow_open/ with isDownloading state. Path traversal guard added
(reject relativePath containing ".."). file_paths.xml gains cache-path entry.
WebDavProvider: path-traversal guard in parsePropfind — skip any server-returned
filename containing "..", "/" or "\". Replace android.util.Log with Timber so debug
logs are stripped from release builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FilesScreen: per-file context menu (Open, Share, Rename, Delete), rename dialog,
delete confirmation, FileProvider-based open/share intents, Snackbar error feedback
- FilesViewModel: FileAction sealed class + SharedFlow; openFile, shareFile,
deleteFile, renameFile with DB cleanup; resolveFile handles SAF primary: URIs
- FileWatchService: stopWithTask=false keeps watcher alive after app swipe-away;
catchupScan on startup detects changes missed while service was not running;
SyncFileStateDao injected; FileObserver used for real-path SAF URIs
- BootReceiver: handles MY_PACKAGE_REPLACED to restart service after app update
- file_paths.xml: added external-path so FileProvider can serve /storage/emulated/0 files
- ic_launcher_foreground: three curved stroke-based arrows (quadratic bezier, round caps)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Request POST_NOTIFICATIONS permission at runtime in MainActivity (primary fix
for notifications never appearing on Android 13+ phones including Android 16)
- Register all 4 notification channels eagerly in SyncFlowApp.onCreate() instead
of lazily inside workers
- Add FOREGROUND_SERVICE_SHORT_SERVICE permission + shortService foreground type
for Android 16 foreground service compatibility
- Add global activity Log tab (new tab 2 in main nav) showing all sync events
across all pairs, grouped by date with pair name, event icon, and file detail
- Fix FileWatchService ON_CHANGE detection: ContentObserver on SAF tree URIs only
fires for SAF-API writes, not raw filesystem writes. Now resolves primary:/*
tree URIs to /storage/emulated/0/* and uses FileObserver for reliable detection
- Bump version to 1.0.21 (build 22)
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>
- 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>
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>