RadioGroup rows only responded to taps on the ~144px radio dot; tapping the
label did nothing. Wrap each row in Modifier.selectable(role=RadioButton) with
RadioButton(onClick=null) so the whole row is one accessible tap target.
Verified on-device: tapping the 'Download only' label now selects it.
The Add-Pair screen defaulted deleteBehavior to MIRROR for every direction,
so an Upload-only backup would delete cloud files when you deleted them on
the phone. Now the default follows the direction:
- Upload-only / Download-only -> KEEP (deleting locally leaves the cloud copy)
- Two-way -> MIRROR
All three options remain selectable; once the user explicitly picks one,
changing direction no longer overrides it, and editing a saved pair keeps
its stored choice. Adds RecommendedDeleteBehaviorTest.
- 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>
New PAUSED status. When a sync is running, the sync button becomes a
pause button (⏸). Tapping it cancels the WorkManager job and sets the
status to PAUSED (purple). The button then becomes a play button (▶) to
resume. Works in both the home screen card and the pair detail screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the synced-files list with a proper file explorer:
- Phone tab: browse all of internal storage with quick-access shortcuts
(Camera, Downloads, Documents, Pictures, Music, Videos), breadcrumb
navigation, search, tap folder to enter, tap file to open/share
- Cloud tab: browse connected cloud accounts, account switcher chips for
multiple accounts, breadcrumb navigation, search, tap file to
download+open
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tap on local folder opens the custom browser again (not system picker).
The custom browser already shows the All files access banner if needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove root folder block from the browser — user can now select
/storage/emulated/0 exactly like Autosync. If MANAGE_EXTERNAL_STORAGE
is not granted a red banner appears with a direct "Grant" button that
opens the Android All files access settings screen. Root guard removed
from SyncEngine; individual file failures (e.g. root-level writes) are
already caught and logged per-file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tapping the local folder field now opens Android's native folder picker
via ACTION_OPEN_DOCUMENT_TREE. The picked content:// URI gets persistent
read/write permission and is stored as-is; the existing Saf backend
handles all sync I/O through it. "Browse manually" link kept for the
raw-path custom browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Android 15+ enforces edge-to-edge on Dialog windows, making standard
Compose WindowInsets APIs return 0 inside dialogs. Fix: use ViewCompat
insets listener inside the Dialog to read actual system bar heights,
with 56dp minimum to guarantee full nav bar clearance. Spacer inside
the button Surface lets the elevated background extend behind the nav
bar. Also make the entire local folder field tappable (not just the
trailing icon) for better UX.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace SAF document picker with custom LocalBrowserDialog (File API,
quick-access shortcuts, breadcrumb nav, search, folder-only listing)
- Rewrite RemoteBrowserDialog as full-screen dialog with breadcrumbs,
search, and new-folder creation; add navigateToBreadcrumb/createFolder
to RemoteBrowserViewModel
- Fix Select button cut off by navigation bar in both browsers: wrap
button in Column(navigationBarsPadding()) so the button sits above
the nav bar rather than behind it
- Tighten icon foreground crop to remove excess black border
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>
Sync loop root-cause fixes (three independent bugs):
1. Folder change clears stale file states (AddPairViewModel): when
localPath or remotePath changes on an existing pair, all
SyncFileStateEntity records are wiped. Previously those stale records
caused every sync to attempt DELETE_REMOTE on the old folder's files
and to treat all new-folder files as changed — causing both the
"deleting 32 files" loop and rewrites on every run.
2. Download stores null localModifiedAt (SyncEngine): SAF document
cursors can return a stale mtime immediately after a write. Storing
null forces the SKIP reconciliation pass on the next sync to read
the actual walkFiles cursor value, breaking the download->changed->
download loop caused by mtime inconsistency.
3. Second-precision mtime comparison (syncDecide): WebDAV RFC-1123 has
1-second precision; FAT32 has 2-second precision. Comparing at
millisecond level caused phantom "changed" detections after syncing
to/from these systems. Now uses epochSecond for both local and remote.
Icon: three bold teal/red/yellow teardrop streaks (Avast palette) flying
into a white cloud centre, on dark charcoal background.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Icon: two thick tube-style arcs with 3D glossy highlights.
Arc 1 (left side): coral #E8665A to orange #E8A040
Arc 2 (right side): steel blue #4A7FD4 to deep purple #7B5EA7
Arrowheads: orange and purple. Background: dark purple-black.
Inspired by the braided knot color palette.
Fix "media not found" when opening photos:
- Intent now sets ClipData alongside FLAG_GRANT_READ_URI_PERMISSION
so the permission correctly propagates through the system chooser
to whichever app the user picks.
- openFile() and downloadToCache() both call MediaScannerConnection
so newly synced/downloaded files appear in gallery MediaStore index
before the viewer launches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix multi-selection: selectedKeys exposed as StateFlow, collected in
FilesScreen so checkboxes and highlights update correctly on every tap.
fileKey() made public so UI can check membership without ViewModel calls.
Icon: white cloud body with two cyan/teal circular sync arcs (AutoSync
style), deep blue-to-teal gradient background.
Security review clean: no hardcoded credentials, cleartext blocked by
network_security_config, allowBackup=false, path traversal guards in
place on both server responses and local resolution.
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>
- Fix SYNC_COMPLETED showing ↑0 ↓0 ✗0 when only deletions occurred: add ✕N
for deleted files to the summary message (↑N ↓N ✕N ✗N format)
- Fix PairDetail Activity section showing raw "SYNC_STARTED" enum names and
"remote" as a plain subtitle: replace dot-based EventRow with the same
polished icon-bubble rows as the global Log tab
- Extract shared SyncEventRow composable + iconAndTint/label helpers to
ui/shared/SyncEventRow.kt; both LogScreen and PairDetailScreen now use it
- Add Files tab (4th tab between Log and Accounts): folder browser showing
all synced files per pair, grouped by subdirectory, with file-type icons,
size, last-synced date, and a summary header (N files, total size)
- Add SyncFileStateDao.observeForPair() reactive Flow query for Files tab
- Completely redesign app icon: near-black radial gradient background with
three bold directional arrows in an S-pattern (coral → silver → teal),
each with gradient fills and tip-glow dots — entirely different from the
typical circular sync-arrow style
- Bump version to 1.0.22 (build 23)
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>
CRITICAL
- SftpProvider: replace PromiscuousVerifier with TofuHostKeyVerifier
(trust-on-first-use; stores SHA-256 fingerprints in EncryptedSharedPreferences;
rejects key changes on subsequent connections)
HIGH
- GoogleDriveProvider: replace raw string interpolation with buildJsonObject
in uploadFile, createDirectory, and moveFile to prevent JSON injection
- DropboxProvider: replace all raw JSON strings and Dropbox-API-Arg headers
with buildJsonObject for the same reason
- OAuthHelper: add cryptographically random state parameter to Dropbox and
OneDrive authorization URLs (stored alongside the PKCE verifier)
- OAuthRedirectActivity: validate returned state against stored value before
exchanging the authorization code (CSRF protection)
MEDIUM
- WebDavProvider: block cross-host redirects in the manual redirect interceptor
so Authorization headers are never forwarded to a different server
- AccountSetupScreen: set FLAG_SECURE on the window while credential fields
are visible to prevent screenshots and screen-recording capture
- libs.versions.toml: security-crypto alpha06 → stable 1.0.0;
biometric-ktx alpha05 → biometric 1.1.0 (stable, non-ktx artifact matches
the BiometricManager/BiometricPrompt API actually used in MainActivity)
- CredentialStore: migrate to security-crypto 1.0.0 API (MasterKeys.getOrCreate
+ positional create() args); add saveHostKey/getHostFingerprint for SFTP TOFU
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>
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>
- 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>
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>