Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions aw-sync/src/android.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_void};
use std::panic;
use std::path::Path;
use std::sync::Once;

use aw_client_rust::blocking::AwClient;
Expand Down Expand Up @@ -110,10 +111,72 @@ fn rust_string_to_jstring(env: &JNIEnv, s: String) -> jstring {
output.into_raw()
}

/// Helper function to get AwClient from port
/// Point this library's `ANDROID_DATA_DIR` at the app filesDir.
///
/// `libaw_sync.so` and `libaw_server.so` are separate cdylibs, so
/// `RustInterface.setDataDir` does not update the copy compiled into this
/// `.so`. SyncInterface.kt already sets `XDG_DATA_HOME=$filesDir/data`, which
/// is the path that works for debug (`applicationIdSuffix ".debug"`) and
/// work-profile installs — the hardcoded default is only the release user-0
/// path.
fn apply_android_data_dir_from_env() {
let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") else {
return;
};
let Some(files_dir) = crate::dirs::files_dir_from_xdg_data_home(Path::new(&xdg_data)) else {
warn!(
"XDG_DATA_HOME={} is not $filesDir/data; leaving android data dir unchanged",
xdg_data
);
return;
};
let path = files_dir.to_string_lossy();
info!("android data dir from XDG_DATA_HOME: {}", path);
aw_server::dirs::set_android_data_dir(&path);
}

/// Mirror of `RustInterface.setDataDir`. Prefer this explicit path; the XDG
/// fallback in `get_client` covers current Kotlin that does not call it.
#[no_mangle]
pub extern "C" fn Java_net_activitywatch_android_SyncInterface_setDataDir(
mut env: JNIEnv,
_class: JClass,
java_dir: JString,
) {
init_android_logging();
match env.get_string(&java_dir) {
Ok(s) => {
let path: String = s.into();
info!("Setting android data dir as {}", path);
aw_server::dirs::set_android_data_dir(&path);
}
Err(e) => {
error!("setDataDir: failed to read path: {}", e);
}
}
}

/// Helper function to get AwClient from port.
///
/// Android enables API-key auth whenever `config.toml` has `[auth].api_key`.
/// The desktop CLI path (`main.rs`) already forwards that key; this JNI path
/// used `AwClient::new()` and 401'd on `GET /api/0/buckets` (aw-android#247).
fn get_client(port: i32) -> Result<AwClient, String> {
apply_android_data_dir_from_env();
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.

Ok(cfg) => {
if cfg.api_key.is_some() {
info!("using API key from config.toml for local client");
}
cfg.api_key
}
Err(e) => {
warn!("failed to read server config for API key: {}", e);
None
}
};
AwClient::new_with_api_key(host, port as u16, "aw-sync-android", api_key)
.map_err(|e| format!("Failed to create client: {}", e))
}

Expand Down
52 changes: 51 additions & 1 deletion aw-sync/src/dirs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use dirs::home_dir;
use std::error::Error;
use std::fs;
#[cfg(any(target_os = "android", test))]
use std::path::Path;
use std::path::PathBuf;

// TODO: This could be refactored to share logic with aw-server/src/dirs.rs
Expand All @@ -16,7 +18,8 @@ pub fn get_config_dir() -> Result<PathBuf, Box<dyn Error>> {
Ok(dir)
}

#[cfg(not(target_os = "android"))]
/// Path to the embedded/local aw-server config. On Android this is
/// `filesDir/config.toml` (same file ConfigManager and the server use).
#[allow(dead_code)]
pub fn get_server_config_path(testing: bool) -> Result<PathBuf, ()> {
let dir = aw_server::dirs::get_config_dir()?;
Expand All @@ -35,3 +38,50 @@ pub fn get_sync_dir() -> Result<PathBuf, Box<dyn Error>> {
let home_dir = home_dir().ok_or("Unable to read home_dir")?;
Ok(home_dir.join("ActivityWatchSync"))
}

/// SyncInterface.kt sets `XDG_DATA_HOME=$filesDir/data` before `loadLibrary`.
/// `libaw_sync.so` is a separate cdylib from `libaw_server.so`, so
/// `RustInterface.setDataDir` does not update this library's `ANDROID_DATA_DIR`.
/// Parent of that env var is the app filesDir (release, `.debug`, work profile).
#[cfg(any(target_os = "android", test))]
pub(crate) fn files_dir_from_xdg_data_home(xdg_data_home: &Path) -> Option<PathBuf> {
if xdg_data_home.file_name() != Some(std::ffi::OsStr::new("data")) {
return None;
}
let parent = xdg_data_home.parent()?;
if parent.as_os_str().is_empty() || parent == Path::new("/") {
return None;
}
Some(parent.to_path_buf())
}

#[cfg(test)]
mod tests {
use super::files_dir_from_xdg_data_home;
use std::path::Path;

#[test]
fn xdg_data_home_parent_is_files_dir() {
let debug = Path::new("/data/user/0/net.activitywatch.android.debug/files/data");
assert_eq!(
files_dir_from_xdg_data_home(debug).as_deref(),
Some(Path::new(
"/data/user/0/net.activitywatch.android.debug/files"
))
);

let work_profile = Path::new("/data/user/10/net.activitywatch.android/files/data");
assert_eq!(
files_dir_from_xdg_data_home(work_profile).as_deref(),
Some(Path::new("/data/user/10/net.activitywatch.android/files"))
);
}

#[test]
fn rejects_non_data_leaf_and_root() {
assert!(files_dir_from_xdg_data_home(Path::new("/data")).is_none());
assert!(files_dir_from_xdg_data_home(Path::new("/")).is_none());
assert!(files_dir_from_xdg_data_home(Path::new("data")).is_none());
assert!(files_dir_from_xdg_data_home(Path::new("/tmp/config")).is_none());
}
}
28 changes: 25 additions & 3 deletions aw-sync/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ use std::io::Read;
use std::net::IpAddr;
use std::path::{Path, PathBuf};

#[cfg(not(target_os = "android"))]
pub struct ServerConfig {
pub port: u16,
pub api_key: Option<String>,
}

#[cfg(not(target_os = "android"))]
impl ServerConfig {
pub fn default_for(testing: bool) -> Self {
Self {
Expand All @@ -23,7 +21,10 @@ impl ServerConfig {
}

/// Returns the settings aw-sync needs from the selected aw-server config.
#[cfg(not(target_os = "android"))]
///
/// Also used on Android: the embedded server writes `config.toml` under
/// `filesDir`, and `get_client()` in `android.rs` must send the same
/// `[auth].api_key` or `/api/0/buckets` returns 401 (aw-android#247).
pub fn get_server_config(
testing: bool,
config_override: Option<&Path>,
Expand Down Expand Up @@ -120,6 +121,27 @@ mod tests {
assert!(testing.api_key.is_none());
}

#[test]
fn commented_or_empty_api_key_is_absent() {
let config_path = std::env::temp_dir().join(format!(
"aw-sync-config-commented-{}-{}.toml",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::write(
&config_path,
"port = 5600\n[auth]\n#api_key = \"secret\"\napi_key = \"\"\n",
)
.unwrap();

let config = get_server_config(false, Some(&config_path)).unwrap();
fs::remove_file(config_path).unwrap();
assert!(config.api_key.is_none());
}

#[test]
fn recognizes_only_loopback_hosts() {
for host in ["127.0.0.1", "127.0.0.2", "::1", "localhost", "LOCALHOST"] {
Expand Down
22 changes: 17 additions & 5 deletions aw-sync/tests/sync_roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,33 @@
///
/// Both copies then render in /timeline, so every event is shown twice.
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};

use aw_datastore::Datastore;
use aw_models::{Bucket, BucketMetadata};
use aw_sync::{sync_datastores, AccessMethod, SyncSpec};

// Tests in this binary run in parallel. Use a monotonic counter to guarantee
// each datastore gets a unique path even when two tests start within the same
// clock tick (Windows/macOS SystemTime resolution can be coarser than 1 ns,
// causing path collisions: "duplicate column name" on macOS, "database is
// locked" on Windows).
static DB_COUNTER: AtomicU64 = AtomicU64::new(0);

fn tmp_db(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
// Counter: within-run uniqueness. Timestamp: cross-run uniqueness when
// the PID is reused and the counter resets to 0. Same as #655.
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!(
"aw-sync-roundtrip-{}-{}-{}.db",
"aw-sync-roundtrip-{}-{}-{}-{}.db",
std::process::id(),
name,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
DB_COUNTER.fetch_add(1, Ordering::Relaxed),
ts,
));
p
}
Expand Down
Loading