Skip to content

fix(aw-sync): forward API key on Android JNI client - #666

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/android-sync-api-key
Aug 31, 2026
Merged

fix(aw-sync): forward API key on Android JNI client#666
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/android-sync-api-key

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Problem

v0.14.0b2 user report (ActivityWatch/aw-android#247): sync appears to run but the chosen folder never changes. Diagnosis:

sync_datastores → ds_from.get_buckets() → GET /api/0/buckets/ → 401 Unauthorized

Desktop aw-sync already forwards [auth].api_key from config.toml (#640). The Android JNI path in aw-sync/src/android.rs still used AwClient::new() with no key. Android does write that key (ConfigManager / Auth Settings) to filesDir/config.toml, the same file the embedded server reads.

Fix

  • Enable get_server_config / get_server_config_path on Android (they were cfg(not(target_os = "android"))).
  • get_client() reads [auth].api_key from that config and constructs the client with AwClient::new_with_api_key().
  • Host is hardcoded 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/empty api_key is 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.

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-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown

Greptile Summary

The 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.

  • Enables Android server-configuration lookup and optional API-key parsing.
  • Derives the Android files directory from XDG_DATA_HOME and adds an explicit JNI directory setter.
  • Adds coverage for path derivation and empty or commented API keys.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
aw-sync/src/android.rs Adds Android data-directory initialization and forwards the local configuration’s API key to the loopback client.
aw-sync/src/dirs.rs Enables Android server-config path resolution and safely derives filesDir from the expected XDG data path.
aw-sync/src/util.rs Enables shared server-config parsing on Android and tests that empty or commented API keys remain absent.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (2): Last reviewed commit: "fix(aw-sync): point Android JNI client a..." | Re-trigger Greptile

Comment thread aw-sync/src/android.rs
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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.62%. Comparing base (656f3c9) to head (0500a9a).
⚠️ Report is 96 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ErikBjare

Copy link
Copy Markdown
Member

@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.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Greptile P1 is real: libaw_sync.so has its own ANDROID_DATA_DIR static, and RustInterface.setDataDir only updates libaw_server.so. The hardcoded default is the release user-0 filesDir — debug (.debug suffix) and work-profile installs still 401.

Fixed in f4b4145. Current Kotlin already sets XDG_DATA_HOME=$filesDir/data; we derive filesDir from that so this .so works without an app change. Also added SyncInterface.setDataDir JNI for an explicit call later.

Release path (the ActivityWatch/aw-android#247 report) was already correct. Kotlin setDataDir call is a follow-up after merge, not a blocker.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@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.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Windows CI (test_push_does_not_reexport_synced_buckets) failed with SQLITE_BUSY / database is locked. First run on this PR was green; this is the known parallel tmp_db path collision (same flake as #655), not the Android API-key change. Can't rerun jobs here (no admin).

Pushed 0500a9a — test-only uniqueness (atomic counter + timestamp), same shape as #655. Local cargo test -p aw-sync --test sync_roundtrip 3/3.

@ErikBjare
ErikBjare merged commit 6d4fdf7 into ActivityWatch:master Aug 31, 2026
7 checks passed
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Review landed; you merged this.

Follow-up: ActivityWatch/aw-android#249 bumps aw-server-rust to 2ded7d7 and calls SyncInterface.setDataDir, so the 401 fix is in the app. Debug/work-profile installs need that explicit JNI path; the XDG fallback remains.

TimeToBuildBob added a commit to TimeToBuildBob/aw-android that referenced this pull request Aug 31, 2026
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
TimeToBuildBob added a commit to TimeToBuildBob/aw-server-rust that referenced this pull request Aug 31, 2026
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.
ErikBjare pushed a commit that referenced this pull request Aug 31, 2026
* 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.
ErikBjare pushed a commit to ActivityWatch/aw-android that referenced this pull request Aug 31, 2026
…#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
Judemasic added a commit to Judemasic/aw-server-rust that referenced this pull request Sep 2, 2026
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>
Judemasic added a commit to Judemasic/aw-server-rust that referenced this pull request Sep 2, 2026
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.
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