fix(aw-sync): forward API key on Android JNI client - #666
Conversation
Android stores [auth].api_key in filesDir/config.toml. The JNI get_client() path still used AwClient::new() without that key, so GET /api/0/buckets returned 401 and sync wrote nothing (ActivityWatch/aw-android#247).
Greptile SummaryThe PR forwards the embedded Android server’s API key to the loopback sync client and aligns the sync library’s configuration directory with the app-owned files directory.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant App as Android app
participant JNI as aw-sync JNI
participant Config as filesDir/config.toml
participant Server as Local aw-server
App->>JNI: Start sync
JNI->>JNI: Derive filesDir from XDG_DATA_HOME
JNI->>Config: Read auth.api_key
JNI->>Server: Request with optional API key
Server-->>JNI: Bucket and event data
Reviews (2): Last reviewed commit: "fix(aw-sync): point Android JNI client a..." | Re-trigger Greptile |
| fn get_client(port: i32) -> Result<AwClient, String> { | ||
| let host = "127.0.0.1"; | ||
| AwClient::new(host, port as u16, "aw-sync-android") | ||
| let api_key = match crate::util::get_server_config(false, None) { |
There was a problem hiding this comment.
Sync library keeps stale data path
If the app supplies a files directory different from the hardcoded fallback, libaw_sync.so reads its independent, uninitialized copy of the server data-directory state, so it loads the wrong config.toml and sends no or an incorrect API key, causing authenticated sync requests to return 401.
Knowledge Base Used:
There was a problem hiding this comment.
Fixed in f4b4145.
Agreed: separate cdylibs, independent ANDROID_DATA_DIR. get_client now derives filesDir from XDG_DATA_HOME=$filesDir/data (already set by SyncInterface.kt) so debug/work-profile installs hit the same config.toml as the server. Also added SyncInterface.setDataDir JNI.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #666 +/- ##
==========================================
+ Coverage 70.81% 78.62% +7.80%
==========================================
Files 51 66 +15
Lines 2916 5464 +2548
==========================================
+ Hits 2065 4296 +2231
- Misses 851 1168 +317 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@TimeToBuildBob review |
libaw_sync.so keeps its own ANDROID_DATA_DIR static, so RustInterface.setDataDir on libaw_server.so never updates it. The hardcoded default only matches the release user-0 filesDir; debug (.debug suffix) and work-profile installs still 401. Derive filesDir from XDG_DATA_HOME ($filesDir/data, already set by SyncInterface.kt) and add SyncInterface.setDataDir JNI.
|
Greptile P1 is real: Fixed in f4b4145. Current Kotlin already sets Release path (the ActivityWatch/aw-android#247 report) was already correct. Kotlin |
|
@greptileai review |
Windows CI failed on test_push_does_not_reexport_synced_buckets with SQLITE_BUSY / database is locked. Two parallel tests in this binary both call round_trip() and can land on the same PID+name+nanosecond path when the clock tick is coarser than 1 ns (common on Windows). Same uniqueness shape as ActivityWatch#655: per-process atomic counter for within-run uniqueness, timestamp for PID-reuse across runs. Test-only, no production change.
|
Windows CI ( Pushed 0500a9a — test-only uniqueness (atomic counter + timestamp), same shape as #655. Local |
|
Review landed; you merged this. Follow-up: ActivityWatch/aw-android#249 bumps |
ActivityWatch#247: sync appeared to run but wrote nothing because GET /api/0/buckets 401'd. The JNI client now forwards [auth].api_key (ActivityWatch/aw-server-rust#666). Bump the submodule to 2ded7d7 (includes #666) and call SyncInterface.setDataDir so libaw_sync.so reads filesDir/config.toml on debug and work-profile installs, not only the release user-0 path. Git-Session-Id: e6b1
Master's ActivityWatch#666 reads the embedded server's config from filesDir for the API key. After rebasing onto that, get_server_config_path must not switch Android onto desktop XDG + appname_for (those helpers are cfg-gated off Android). Desktop still uses the isolated activitywatch-<profile> root.
* feat(profile): named instance profiles via --profile flag Replace the two-valued `testing: bool` with a named profile string so that more than two parallel ActivityWatch instances can coexist on one machine. - `dirs.rs`: `db_path(profile)` → `sqlite.db` / `sqlite-<profile>.db`; add `validate_profile()` (lowercase alnum + `-_`, max 32 chars, starts with alnum); tests for suffix rule and validation - `config.rs`: replace `static mut TESTING: bool` with `OnceLock<String> PROFILE`; `set_profile()` is idempotent for same value, panics on conflict; `get_profile()` / `is_testing()` derived from it; config file is `config.toml` / `config-<profile>.toml` - `logging.rs`: `setup_logger(module, profile, verbose)` — logfile suffix is `<module>-<profile>_<ts>.log` for non-default profiles - `main.rs`: add `--profile NAME`; `--testing` remains as alias for `--profile testing`; debug builds still default to "testing"; profile is validated before use - `android/mod.rs`: update two call-sites to pass `"default"` Backwards compatibility: - `--testing` still works (alias for `--profile testing`) - `default` profile maps to existing unsuffixed paths (sqlite.db, config.toml) — no migration required - `testing` profile maps to existing -testing suffix paths Part of ActivityWatch/activitywatch#1399. * fix(aw-sync): update setup_logger call to pass profile string setup_logger signature changed to accept profile: &str instead of testing: bool. Convert opts.testing bool to "testing"/"default" profile string at the call site. Also run cargo fmt to fix long assert! lines in dirs.rs tests. * fix(config): eliminate TOCTOU race in set_profile via atomic OnceLock::set result Previously the function checked PROFILE.get() then PROFILE.set() in two separate steps. Two threads with different profile values could both see get()==None before either set, causing the loser's set() error to be discarded and the loser to silently proceed under the wrong profile. Fix: use the atomic OnceLock::set return value directly. If Ok(()), we won the race. If Err(_), the lock was already set by a concurrent caller; check the existing value and panic only if it differs. * fix(config): set_testing delegates to set_profile to propagate conflict panics Greptile P1: bare PROFILE.set() in set_testing silently discarded conflicts even when the existing profile differed, allowing a losing caller to proceed under the wrong instance. Delegating to set_profile() reuses its idempotent same-value check and conflict panic, matching the documented semantics. * fix(tests): mark in-process server as testing * feat(profile): isolate dir roots per profile and report profile in server info --profile only suffixed the DB filename, so a non-default instance still shared config.toml, the cache dir and the log dir with prod — the one thing profile isolation is for. Move the profile into the platformdirs appname instead ("activitywatch-<profile>"), which isolates config/data/cache/logs and everything nested under them in one place, with no per-module path changes. default and testing deliberately keep the bare "activitywatch" root: their legacy per-file suffixes (sqlite-testing.db, config-testing.toml, port 5666) already separate them, and moving their root would orphan existing installs. Also add Info.profile so clients (webui badge) can tell concurrent instances apart; it deserializes with a "default" fallback so a new client still parses an older server's /api/0/info. * feat(profile): fall back to AW_PROFILE env when --profile is absent aw-qt exports AW_PROFILE for the modules it spawns (ActivityWatch/aw-qt#128), so a profile set on the launcher reaches aw-server-rust without every module growing its own flag. --profile still wins when given. * feat(profile): isolate aw-sync config dirs via shared appname aw-sync hard-coded activitywatch/aw-sync, so a research instance would read prod's sync config. Use aw_server::dirs::appname() and the same config-{profile}.toml filename rule as the server. aw-sync now resolves --profile / AW_PROFILE / --testing and calls set_profile so appname() is the named profile, not always default. * feat(profile): testing-root fallback with legacy artifacts Adopt the activitywatch#1399 rule so rust matches aw-core#152: 1. activitywatch-testing/ exists → use it 2. else legacy testing files in activitywatch/ → stay on the shared root (sqlite-testing.db, config-testing.toml) 3. else fresh setup → create and use activitywatch-testing/ Isolated profile roots use bare sqlite.db / config.toml / log names. set_profile now runs before setup_logger so named profiles log into their own cache dir. * fix(aw-sync): keep Android filesDir config path after profile rebase Master's #666 reads the embedded server's config from filesDir for the API key. After rebasing onto that, get_server_config_path must not switch Android onto desktop XDG + appname_for (those helpers are cfg-gated off Android). Desktop still uses the isolated activitywatch-<profile> root.
…#249) #247: sync appeared to run but wrote nothing because GET /api/0/buckets 401'd. The JNI client now forwards [auth].api_key (ActivityWatch/aw-server-rust#666). Bump the submodule to 2ded7d7 (includes #666) and call SyncInterface.setDataDir so libaw_sync.so reads filesDir/config.toml on debug and work-profile installs, not only the release user-0 path. Git-Session-Id: e6b1
Four fixes to the Android sync path, all read from source and not yet run on a device. 1. get_client() built a plain AwClient with no API key, so every JNI sync call got 401 on GET /api/0/buckets, reported success, and moved nothing. This is upstream of the previously documented blockers: push never obtains bucket data, so no .db can appear whatever the directory layout is. A regression introduced here -- the multi-device rewrite of android.rs kept the helpers (util::get_server_config, dirs::files_dir_from_xdg_data_home) and dropped the call sites. Upstream hit the same bug (aw-android#247, fixed in ActivityWatch#666 / ActivityWatch#249); ported by hand rather than cherry-picked, because ActivityWatch#249's Kotlin needs a setDataDir JNI symbol this fork does not export -- that would be an UnsatisfiedLinkError at load. 2. push_with_hostname_and_device_id joined a <device_id>_staging level before sync_run, which then added its own <device_id>, landing the db three levels deep while get_remotes() looks two levels down. get_remotes() returned empty and pull was a silent no-op reporting success. The _staging level caused the mismatch rather than solving anything; dropped. 3. pull() selected the largest db by file size and discarded the rest. Now iterates every db. Note this was only ever on the legacy path -- Android's multi-device path (pull_all_from_all_hostnames) already iterated all of them. 4. New SyncInterface.getDeviceId(port) JNI export returning the server's info.device_id. aw-server already mints a persisted Uuid::new_v4(), and setup_local_remote names the device directory from it -- so Kotlin now reads that identity instead of minting a competing second one. Also removes an unused sync_datastores import found by cargo check. Verified: cargo check -p aw-sync --lib (host) passes. android.rs is #[cfg(target_os = "android")] and is NOT covered by the host check -- CI is its first compile check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four fixes to the Android sync path, all read from source and not yet run on a device. 1. get_client() built a plain AwClient with no API key, so every JNI sync call got 401 on GET /api/0/buckets, reported success, and moved nothing. This is upstream of the previously documented blockers: push never obtains bucket data, so no .db can appear whatever the directory layout is. A regression introduced here -- the multi-device rewrite of android.rs kept the helpers (util::get_server_config, dirs::files_dir_from_xdg_data_home) and dropped the call sites. Upstream hit the same bug (aw-android#247, fixed in ActivityWatch#666 / ActivityWatch#249); ported by hand rather than cherry-picked, because ActivityWatch#249's Kotlin needs a setDataDir JNI symbol this fork does not export -- that would be an UnsatisfiedLinkError at load. 2. push_with_hostname_and_device_id joined a <device_id>_staging level before sync_run, which then added its own <device_id>, landing the db three levels deep while get_remotes() looks two levels down. get_remotes() returned empty and pull was a silent no-op reporting success. The _staging level caused the mismatch rather than solving anything; dropped. 3. pull() selected the largest db by file size and discarded the rest. Now iterates every db. Note this was only ever on the legacy path -- Android's multi-device path (pull_all_from_all_hostnames) already iterated all of them. 4. New SyncInterface.getDeviceId(port) JNI export returning the server's info.device_id. aw-server already mints a persisted Uuid::new_v4(), and setup_local_remote names the device directory from it -- so Kotlin now reads that identity instead of minting a competing second one. Also removes an unused sync_datastores import found by cargo check. Verified: cargo check -p aw-sync --lib (host) passes. android.rs is #[cfg(target_os = "android")] and is NOT covered by the host check -- CI is its first compile check.
Problem
v0.14.0b2 user report (ActivityWatch/aw-android#247): sync appears to run but the chosen folder never changes. Diagnosis:
Desktop
aw-syncalready forwards[auth].api_keyfromconfig.toml(#640). The Android JNI path inaw-sync/src/android.rsstill usedAwClient::new()with no key. Android does write that key (ConfigManager / Auth Settings) tofilesDir/config.toml, the same file the embedded server reads.Fix
get_server_config/get_server_config_pathon Android (they werecfg(not(target_os = "android"))).get_client()reads[auth].api_keyfrom that config and constructs the client withAwClient::new_with_api_key().127.0.0.1, so the local key is never attached to a remote.Testing
cargo test -p aw-sync --lib -- util::tests— 5 passed, including a new test that commented/emptyapi_keyis treated as absent (matches Android's default commented config.toml).Submodule bump in aw-android is a follow-up after this merges.
Not a complete close of aw-android#247: remaining items (sync UX feedback, drawer discoverability, category-import save persistence) are tracked on that issue.