Skip to content

feat(sync): add SyncSettingsActivity — enable/disable toggle + SAF directory picker - #204

Open
TimeToBuildBob wants to merge 6 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/sync-saf-settings
Open

feat(sync): add SyncSettingsActivity — enable/disable toggle + SAF directory picker#204
TimeToBuildBob wants to merge 6 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/sync-saf-settings

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #203 (filed in ActivityWatch/activitywatch#1357) and the sync-disabled-by-default PR (#184). Adds the user-facing sync settings screen.

What this adds:

  • SyncSettingsActivity: enable/disable toggle + ACTION_OPEN_DOCUMENT_TREE directory picker
  • AWPreferences: getSyncDirUri/setSyncDirUri to persist the user-chosen SAF URI with persistable permissions across reboots
  • "Sync Settings" menu item in MainActivity's options menu
  • activity_sync_settings.xml layout

How it works:

  1. User opens the overflow menu → "Sync Settings"
  2. Toggle enables/disables sync (backed by existing AWPreferences.isSyncEnabled())
  3. "Choose Directory" launches ACTION_OPEN_DOCUMENT_TREE — the user picks any folder their sync tool (Syncthing, Dropbox, etc.) can see
  4. The chosen URI is stored with contentResolver.takePersistableUriPermission() so it survives reboots
  5. The screen shows the selected directory name on return

Phase 1 scope: This PR wires up the UI and preference plumbing. The native aw-sync library continues writing to the internal scoped directory via AW_SYNC_DIR. Phase 2 will bridge the internal sync dir to the SAF-granted location (file-copy or DocumentFile-based I/O rewrite) once the permissions infrastructure is confirmed working on real devices.

APIs used: All API 26+ (minSdkVersion 26 confirmed), including DocumentsContract.EXTRA_INITIAL_URI for suggesting a sensible starting location.

Test plan

  • Build and install debug APK
  • Open overflow menu → verify "Sync Settings" appears
  • Tap "Sync Settings" → verify screen opens with toggle and "Choose Directory" button
  • Toggle enable/disable → confirm preference persists across app restarts
  • Tap "Choose Directory" → verify system folder picker opens
  • Select a folder → verify display name appears in status text
  • Reinstall the app → verify SAF URI permission survives reboot (granted via takePersistableUriPermission)

Relates to ActivityWatch/activitywatch#1357 (@ErikBjare approved: "approved, go")

…tory picker

- Add SAF URI storage to AWPreferences (getSyncDirUri/setSyncDirUri) so the
  chosen directory persists across reboots with granted content:// permissions
- Add SyncSettingsActivity: enable/disable toggle backed by
  AWPreferences.setSyncEnabled(), plus ACTION_OPEN_DOCUMENT_TREE directory
  picker that takes persistable URI permission and displays the chosen directory
  name via DocumentsContract
- Add activity_sync_settings.xml layout following the AuthSettingsActivity pattern
- Register SyncSettingsActivity in AndroidManifest.xml
- Add "Sync Settings" item to the options menu, wired in MainActivity

Phase 1: UI infrastructure only. The native aw-sync library still writes to the
internal scoped directory via AW_SYNC_DIR. Phase 2 will add file-copy bridging
from the internal dir to the SAF-granted location (or a DocumentFile-based I/O
rewrite) once the permissions and preference plumbing is confirmed working.

Closes ActivityWatch#203
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a native sync-settings screen and supporting preference/service wiring.

  • Adds a toggle that persists sync state and immediately updates the running scheduler.
  • Adds a SAF directory picker with persisted-grant acquisition, safe replacement, and selected-directory status.
  • Registers the activity and exposes it through MainActivity’s overflow menu.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Implements the toggle and SAF picker, including validated grant modes, non-destructive failure handling, and safe same-URI reselection.
mobile/src/main/java/net/activitywatch/android/BackgroundService.kt Handles live sync-setting changes while preserving complete initialization when the action recreates the service.
mobile/src/main/java/net/activitywatch/android/AWPreferences.kt Adds SharedPreferences accessors for the selected SAF tree URI.
mobile/src/main/java/net/activitywatch/android/MainActivity.kt Opens the new sync-settings activity from the overflow-menu action.
mobile/src/main/res/layout/activity_sync_settings.xml Defines the settings screen’s sync toggle, directory status, picker button, and explanatory text.

Sequence Diagram

sequenceDiagram
    actor User
    participant Settings as SyncSettingsActivity
    participant Prefs as AWPreferences
    participant Service as BackgroundService
    participant Scheduler as SyncScheduler
    participant SAF as Android SAF

    User->>Settings: Toggle sync
    Settings->>Prefs: Persist enabled state
    Settings->>Service: ACTION_SYNC_ENABLED_CHANGED
    Service->>Scheduler: Start or stop

    User->>Settings: Choose directory
    Settings->>SAF: ACTION_OPEN_DOCUMENT_TREE
    SAF-->>Settings: Tree URI and granted modes
    Settings->>SAF: Persist new URI permission
    Settings->>SAF: Release different old URI permission
    Settings->>Prefs: Store selected URI
Loading

Reviews (5): Last reviewed commit: "fix(sync): preserve grant when reselecti..." | Re-trigger Greptile

Comment thread mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Outdated
P1: Toggle leaves scheduler state stale
- Add ACTION_SYNC_ENABLED_CHANGED to BackgroundService; handle in onStartCommand
  to start/stop the running SyncScheduler immediately, without a service restart.
- SyncSettingsActivity sends this Intent when the toggle changes.

P1: Persisting ungranted URI modes crashes
- Only pass flag bits the provider actually granted (from result.data?.flags) to
  takePersistableUriPermission(); wrap in try-catch for providers that still decline.

P2: Replaced directories retain stale grants
- Release the old URI's persistable permission before granting the new one, so
  stale grants don't accumulate against Android's bounded allowance.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread mobile/src/main/java/net/activitywatch/android/BackgroundService.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Outdated
…nit guard

Two P1 findings from re-review:

1. BackgroundService: ACTION_SYNC_ENABLED_CHANGED skips full initialization
   when Android kills and recreates the service while SyncSettingsActivity is
   open. Guard with `isFullyStarted` flag (set at end of normal onStartCommand)
   so the toggle only short-circuits on an already-running service instance.

2. SyncSettingsActivity: old URI grant released before new one confirmed, and
   URI saved as success even when takePersistableUriPermission fails. Fix:
   take the new grant first; abort early with error toast on failure; only
   then release the old grant and commit the new URI.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Outdated
… flags

When ACTION_OPEN_DOCUMENT_TREE returns a URI with no READ or WRITE flags in
persistable mode, the previous commit's zero-flag branch skipped the permission
take but still fell through to release the old grant and save the new URI as
success. After the transient access expires, the directory is inaccessible.

Fix: explicit early-return + error toast when persistableFlags == 0, leaving
the old grant and URI intact. Also simplify the flow so takePersistableUriPermission
is now unconditional (persistableFlags is always nonzero at that point), with its
SecurityException still aborting before the old grant is released.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant