Skip to content

feat(sync): show last completion status - #251

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/sync-status-feedback
Sep 10, 2026
Merged

feat(sync): show last completion status#251
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/sync-status-feedback

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Problem

#247 reports that sync runs silently: users cannot tell whether it succeeded, failed, or when it last completed. A failed SAF mirror was also logged as non-fatal, so the app could report native sync success while the user-selected directory stayed unchanged.

Fix

  • Persist the timestamp and outcome of each full sync attempt.
  • Show Last sync: never, Last sync succeeded at …, or Last sync failed at … in Sync Settings.
  • Treat a failed configured-directory mirror as a failed worker sync instead of silently reporting success.
  • Keep the status compact and avoid persisting native error details, which can include filesystem paths.

This addresses the feedback/status portion of #247. The 401 root cause, document picker, and unknown Activity route are already merged via #249, #248, and #250. Category-import persistence remains separate.

Testing

JAVA_HOME=/opt/android-studio/jbr ANDROID_HOME=/home/bob/Android/Sdk \
  ./gradlew :mobile:testDebugUnitTest --no-daemon

Passed (full JVM unit-test suite).

Git-Session-Id: fb82cf8b-dc14-5fab-8861-32589542190d
@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR persists the outcome and completion time of full sync attempts and displays that status in Sync Settings.

  • Full-sync callers now wait for configured SAF mirroring and treat inaccessible, interrupted, or incomplete mirrors as failures.
  • Sync status changes are persisted without native error details and broadcast to refresh the visible settings screen.
  • Unit tests cover formatting for never-run, successful, and failed states.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported mirror-failure, stale-status, and partial-mirror cancellation paths are addressed at the current head.

Important Files Changed

Filename Overview
mobile/src/main/java/net/activitywatch/android/AWPreferences.kt Persists compact sync completion metadata and broadcasts status changes after updating preferences.
mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Records full-sync outcomes only after native synchronization and configured SAF mirroring complete.
mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt Formats the saved status and refreshes it through lifecycle reconciliation and a package-scoped receiver.
mobile/src/main/res/layout/activity_sync_settings.xml Adds the compact last-sync status row to Sync Settings.
mobile/src/test/java/net/activitywatch/android/SyncSettingsActivityTest.kt Covers status text for no prior attempt and successful or failed completion.

Sequence Diagram

sequenceDiagram
    participant Trigger as Scheduler / Worker
    participant Sync as SyncInterface
    participant SAF as Configured SAF directory
    participant Prefs as AWPreferences
    participant UI as Sync Settings

    Trigger->>Sync: Start full sync
    Sync->>Sync: Run native synchronization
    alt Native sync succeeds
        Sync->>SAF: Mirror sync files
        SAF-->>Sync: Complete or throw
    end
    Sync->>Prefs: Persist completion time and outcome
    Prefs-->>UI: Send package-scoped status broadcast
    UI->>Prefs: Read latest status
    UI->>UI: Refresh status row
Loading

Reviews (3): Last reviewed commit: "fix(sync): fail incomplete SAF mirrors" | Re-trigger Greptile

Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt Outdated
Git-Session-Id: fb82cf8b-dc14-5fab-8861-32589542190d
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt
Git-Session-Id: df671383-72f8-5921-b499-2fc7fec7dabe
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

TimeToBuildBob commented Sep 1, 2026

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. The last Greptile P1 thread (cancellation during SAF mirroring) was already fixed in 67343c2 and is now resolved.

Judemasic added a commit to Judemasic/aw-android that referenced this pull request Sep 3, 2026
…laim corrected

1.5's final check passes on hardware. The tablet's server lists
aw-watcher-android-synced-from-jude_s_s25_ultra (hostname jude_s_s25_ultra,
6,797 events) beside its own 485 — a bucket it could not have produced — and
the Activity query against that host returns 15,503.1s. Phase 1 closes.

The same measurement quantifies 1.8's caveat across sync: flood alone yields
366,699.9s, the ["app","title"] merge yields 15,503.1s. 4.2%. The other 96% is
pre-fix phone history, already on the tablet, invisible only because of
aw-webui#959 — an upstream fix reveals it with no re-sync.

Corrects a wrong claim this document carried: 1.8 stated the regression shipped
in v0.14.0b2 with "nine days of exposure". It does not. Verified by ancestry
rather than by comparing dates — `git merge-base --is-ancestor bf0fc84 749585f`
is false, and 749585f is what v0.14.0b2 pins via aw-server-rust e8e6e90.
Released users are unaffected; only master builds carry it. The corrected chain
is now a table with the command to reproduce it.

Also records two things found while inspecting the devices:
- Syncthing versioning puts archived peer directories in .stversions/, which has
  the exact shape the importer scans for. isSafeEntryName()'s dot-prefix guard
  rejects it, and peers=1 in the logs proves it.
- Two stale app-private databases, one a true leftover and one regenerated by
  sync_run's pull side-effect. The shared folder itself is clean.

Notes upstream PR ActivityWatch#251, which touches the same four files as 1.7 and fixes the
success-reported-on-failure bug this fork still has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Judemasic added a commit to Judemasic/aw-android that referenced this pull request Sep 3, 2026
Upstream PR ActivityWatch#251 fixes this class of bug for the export path: "a failed SAF
mirror was also logged as non-fatal, so the app could report native sync success
while the user-selected directory stayed unchanged." This fork had it in both
directions, and the import side is worse -- a failed import is exactly the
"sync works but the other device never appears" symptom.

The root cause was a data structure, not a catch block. Both passes tallied into
`counts = intArrayOf(0, 0) // [copied, skipped]`, and `skipped` counted two
unrelated things: a file already current, and a file that failed to copy.
importFile's own contract admitted it -- "true if bytes were copied; false if
the file was already current, or on any failure." A boolean cannot carry three
states, so a pass where every file failed logged `copied=0 skipped=1`, identical
to a healthy pass with nothing to do.

- FileOutcome { COPIED, SKIPPED, FAILED } replaces the Boolean.
- TransferResult (copied/skipped/failed/peers + first error reason) replaces
  IntArray(2), separating deliberate skips from breakage and keeping a reason
  worth showing the user.
- runTransfer() converts an unexpected throw into a failed result. It still
  catches -- one bad pass must not abort a cycle that can finish -- but the
  failure now travels back with the result instead of dying in logcat.
- The SAF export moved before the callback. An export that runs after the caller
  has been told "success" can never correct that answer. This removes the only
  reason syncBothAndMirrorAsync existed, so the two entry points collapse into
  one and SyncWorker calls it directly.

SAF failures are collected rather than thrown: a failed import still leaves the
native sync worth running, since peer databases from an earlier cycle are on
disk and readable. Native pull/push failures still end the cycle.

Sync Settings now shows "Sync failed: export failed: sync folder unreachable
(permission revoked, or folder deleted)" where it used to show "Sync complete",
and logcat gains a failed=N column with unsuccessful syncs logged at warning.

Deliberately unchanged: a failed native pull still skips the export. Noted in
roadmap 1.9 so it reads as a decision rather than an oversight.

Compiles; scripts/check-local.sh passes. Not yet run -- a local assembleDebug
produces an APK with zero .so files, so this needs a CI build to test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Judemasic added a commit to Judemasic/aw-android that referenced this pull request Sep 3, 2026
1.10 — the Timeline truncates every peer's name at the first underscore.
Reported by the owner as "I don't know which is the S25U and which the tab".
Not a naming preference: aw-webui's shortenBucketLabel drops everything from the
first "_" onward, because on desktop that separates the host
(aw-watcher-window_erb-m2.localdomain -> window). Android's synced buckets are
aw-watcher-android-synced-from-<hostname>, and the hostname itself carries the
underscores, so the cut lands inside the peer's name. The branch meant to catch
this, /^([^_]+)_.*-synced-from-(.+)$/, requires an underscore before
"-synced-from-" and so cannot fire for Android at all.

Verified by simulating the function against this device's real bucket list:
aw-watcher-android-synced-from-jude_s_s25_ultra renders as
"android-synced-from-jude". Every device belonging to the same person collapses
to the same label.

Documents the no-code workaround with its real cost: the hostname comes from
Android's DEVICE_NAME with non-[a-z0-9_-] runs replaced by "_", so a device name
without spaces produces an underscore-free hostname that survives the shortener
(hyphens are preserved). But the hostname is also the sync directory name, so a
rename strands the old directory in the shared folder as a permanent phantom
peer — the 1.4a failure mode again.

Also records a decision that would otherwise be re-litigated: keep the 1.8 title
fix rather than withdrawing it once aw-webui#959 is fixed. #959 is an open issue
with nobody assigned; the two repair different layers; and a real field cannot be
re-broken by a later query edit, which is exactly how the regression happened.
Names the one condition that would change the answer.

Flags upstream PR ActivityWatch#251 as overlapping 1.7 and 1.9 in all four of the same files,
with the advice to rebase onto it rather than propose a parallel design.

Updates docs/README.md, whose "sync works one-way, never run" summary predated
Phase 1 closing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ErikBjare
ErikBjare merged commit 5b2449a into ActivityWatch:master Sep 10, 2026
8 checks passed
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.

2 participants