From ad08f0cc7a153ff9098ae1e3314016a93f9b469b Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 16 Oct 2025 19:56:12 +0300 Subject: [PATCH 01/10] feat(aw-sync): add Android JNI support and library build Phase 1: Library Preparation - Made CLI dependencies (clap, ctrlc) optional in Cargo.toml - Added jni dependency for Android target - Added android module to lib.rs with conditional compilation - Created android.rs with JNI bindings for sync operations: * syncPullAll - pull from all hosts * syncPull - pull from specific host * syncPush - push local data * syncBoth - full bidirectional sync * getSyncDir - get current sync directory path - Made main.rs conditional on 'cli' feature for Android compatibility Phase 2: Android Build Integration - Updated compile-android.sh to build aw-sync alongside aw-server - aw-sync built with --no-default-features to exclude CLI dependencies - Libraries generated for all Android architectures (arm64, x86_64, x86, arm) JNI functions return JSON responses with success/error status. All sync operations use the existing aw-sync Rust API. Sync directory configurable via AW_SYNC_DIR environment variable. Next: Android app integration (phases 4-5) in aw-android repo. --- aw-sync/Cargo.toml | 13 ++- aw-sync/src/android.rs | 181 +++++++++++++++++++++++++++++++++++++++++ aw-sync/src/lib.rs | 3 + aw-sync/src/main.rs | 2 + compile-android.sh | 7 ++ 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 aw-sync/src/android.rs diff --git a/aw-sync/Cargo.toml b/aw-sync/Cargo.toml index ca61d3cf..4a0e0b61 100644 --- a/aw-sync/Cargo.toml +++ b/aw-sync/Cargo.toml @@ -19,11 +19,13 @@ chrono = { version = "0.4", features = ["serde"] } serde = "1.0" serde_json = "1.0" reqwest = { version = "0.11", features = ["json", "blocking"] } -clap = { version = "4.1", features = ["derive"] } appdirs = "0.2.0" dirs = "6.0.0" gethostname = "0.4.3" -ctrlc = "3.4.5" + +# CLI-only dependencies (optional) +clap = { version = "4.1", features = ["derive"], optional = true } +ctrlc = { version = "3.4.5", optional = true } aw-server = { path = "../aw-server" } aw-models = { path = "../aw-models" } @@ -32,3 +34,10 @@ aw-client-rust = { path = "../aw-client-rust" } [target.'cfg(target_os="linux")'.dependencies] openssl = { version = "0.10.64", features = ["vendored"] } # https://github.com/ActivityWatch/aw-server-rust/issues/478 + +[target.'cfg(target_os="android")'.dependencies] +jni = { version = "0.21", default-features = false } + +[features] +default = ["cli"] +cli = ["clap", "ctrlc"] diff --git a/aw-sync/src/android.rs b/aw-sync/src/android.rs new file mode 100644 index 00000000..38a17556 --- /dev/null +++ b/aw-sync/src/android.rs @@ -0,0 +1,181 @@ +use jni::JNIEnv; +use jni::objects::{JClass, JString}; +use jni::sys::jstring; +use std::ffi::{CString, CStr}; +use aw_client_rust::blocking::AwClient; +use serde_json::json; + +use crate::{pull, pull_all, push}; +use crate::util::get_server_port; + +/// Helper function to convert Rust string to Java string +fn rust_string_to_jstring(env: &JNIEnv, s: String) -> jstring { + let output = env.new_string(s) + .expect("Couldn't create java string!"); + output.into_raw() +} + +/// Helper function to get AwClient from port +fn get_client(port: i32) -> Result { + let host = "127.0.0.1"; + AwClient::new(host, port as u16, "aw-sync-android") + .map_err(|e| format!("Failed to create client: {}", e)) +} + +/// Pull sync data from all hosts in the sync directory +#[no_mangle] +pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( + env: JNIEnv, + _class: JClass, + port: i32, +) -> jstring { + let result = (|| { + let client = get_client(port)?; + pull_all(&client) + .map_err(|e| format!("Sync pull failed: {}", e))?; + Ok(json!({ + "success": true, + "message": "Successfully pulled from all hosts" + }).to_string()) + })(); + + match result { + Ok(msg) => rust_string_to_jstring(&env, msg), + Err(e) => { + error!("syncPullAll error: {}", e); + let error_json = json!({ + "success": false, + "error": e + }).to_string(); + rust_string_to_jstring(&env, error_json) + } + } +} + +/// Pull sync data from a specific host +#[no_mangle] +pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( + env: JNIEnv, + _class: JClass, + port: i32, + hostname: JString, +) -> jstring { + let result = (|| { + let client = get_client(port)?; + let hostname_str: String = env.get_string(hostname) + .map_err(|e| format!("Failed to get hostname string: {}", e))? + .into(); + + pull(&hostname_str, &client) + .map_err(|e| format!("Sync pull failed: {}", e))?; + + Ok(json!({ + "success": true, + "message": format!("Successfully pulled from host: {}", hostname_str) + }).to_string()) + })(); + + match result { + Ok(msg) => rust_string_to_jstring(&env, msg), + Err(e) => { + error!("syncPull error: {}", e); + let error_json = json!({ + "success": false, + "error": e + }).to_string(); + rust_string_to_jstring(&env, error_json) + } + } +} + +/// Push local sync data to the sync directory +#[no_mangle] +pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( + env: JNIEnv, + _class: JClass, + port: i32, +) -> jstring { + let result = (|| { + let client = get_client(port)?; + push(&client) + .map_err(|e| format!("Sync push failed: {}", e))?; + Ok(json!({ + "success": true, + "message": "Successfully pushed local data" + }).to_string()) + })(); + + match result { + Ok(msg) => rust_string_to_jstring(&env, msg), + Err(e) => { + error!("syncPush error: {}", e); + let error_json = json!({ + "success": false, + "error": e + }).to_string(); + rust_string_to_jstring(&env, error_json) + } + } +} + +/// Perform full sync (pull from all hosts, then push local data) +#[no_mangle] +pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncBoth( + env: JNIEnv, + _class: JClass, + port: i32, +) -> jstring { + let result = (|| { + let client = get_client(port)?; + + pull_all(&client) + .map_err(|e| format!("Pull phase failed: {}", e))?; + + push(&client) + .map_err(|e| format!("Push phase failed: {}", e))?; + + Ok(json!({ + "success": true, + "message": "Successfully completed full sync" + }).to_string()) + })(); + + match result { + Ok(msg) => rust_string_to_jstring(&env, msg), + Err(e) => { + error!("syncBoth error: {}", e); + let error_json = json!({ + "success": false, + "error": e + }).to_string(); + rust_string_to_jstring(&env, error_json) + } + } +} + +/// Get the sync directory path +#[no_mangle] +pub extern "C" fn Java_net_activitywatch_android_SyncInterface_getSyncDir( + env: JNIEnv, + _class: JClass, +) -> jstring { + let result = crate::dirs::get_sync_dir(); + + match result { + Ok(path) => { + let path_str = path.to_string_lossy().to_string(); + let response = json!({ + "success": true, + "path": path_str + }).to_string(); + rust_string_to_jstring(&env, response) + } + Err(e) => { + let error_json = json!({ + "success": false, + "error": format!("Failed to get sync dir: {}", e) + }).to_string(); + rust_string_to_jstring(&env, error_json) + } + } +} diff --git a/aw-sync/src/lib.rs b/aw-sync/src/lib.rs index 8ba7bbd4..2e402923 100644 --- a/aw-sync/src/lib.rs +++ b/aw-sync/src/lib.rs @@ -19,3 +19,6 @@ pub use accessmethod::AccessMethod; mod dirs; mod util; + +#[cfg(target_os = "android")] +pub mod android; diff --git a/aw-sync/src/main.rs b/aw-sync/src/main.rs index 2ba69999..e453d718 100644 --- a/aw-sync/src/main.rs +++ b/aw-sync/src/main.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "cli")] + // What needs to be done: // - [x] Setup local sync bucket // - [x] Import local buckets and sync events from aw-server (either through API or through creating a read-only Datastore) diff --git a/compile-android.sh b/compile-android.sh index 62a229d8..097461c5 100755 --- a/compile-android.sh +++ b/compile-android.sh @@ -90,5 +90,12 @@ for archtargetstr in \ # People suggest to use this, but ime it needs all the same workarounds anyway :shrug: #cargo ndk build -p aw-server --target $target --lib $($RELEASE && echo '--release') + + # Build aw-server + echo "Building aw-server for $arch..." cargo build -p aw-server --target $target --lib $($RELEASE && echo '--release') + + # Build aw-sync (without cli feature for Android) + echo "Building aw-sync for $arch..." + cargo build -p aw-sync --target $target --lib --no-default-features $($RELEASE && echo '--release') done From 3286012330d5070ad939032287ff7ceafa9b73e5 Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 16 Oct 2025 19:57:38 +0300 Subject: [PATCH 02/10] fix(aw-sync): make clap import and ValueEnum derive conditional on cli feature The SyncMode enum now only derives ValueEnum when the cli feature is enabled. This allows the library to compile without clap on Android. --- aw-sync/src/sync.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aw-sync/src/sync.rs b/aw-sync/src/sync.rs index ee7bba17..bb0bec48 100644 --- a/aw-sync/src/sync.rs +++ b/aw-sync/src/sync.rs @@ -18,11 +18,14 @@ use chrono::{DateTime, Utc}; use aw_datastore::{Datastore, DatastoreError}; use aw_models::{Bucket, Event}; + +#[cfg(feature = "cli")] use clap::ValueEnum; use crate::accessmethod::AccessMethod; -#[derive(PartialEq, Eq, Copy, Clone, ValueEnum)] +#[derive(PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "cli", derive(ValueEnum))] pub enum SyncMode { Push, Pull, From a98e4c1a13dca867e0d5a5cc28abc0d3a3b6eaa5 Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 16 Oct 2025 20:06:31 +0300 Subject: [PATCH 03/10] fix(aw-sync): fix type annotations and JNI reference in android.rs - Remove unused imports (CString, CStr, get_server_port) - Add explicit Result type annotations for closures - Fix hostname parameter to use reference (&hostname) - Explicitly type error messages as &str for JSON serialization - All compilation errors resolved --- aw-sync/src/android.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/aw-sync/src/android.rs b/aw-sync/src/android.rs index 38a17556..e6feed97 100644 --- a/aw-sync/src/android.rs +++ b/aw-sync/src/android.rs @@ -1,12 +1,10 @@ use jni::JNIEnv; use jni::objects::{JClass, JString}; use jni::sys::jstring; -use std::ffi::{CString, CStr}; use aw_client_rust::blocking::AwClient; use serde_json::json; use crate::{pull, pull_all, push}; -use crate::util::get_server_port; /// Helper function to convert Rust string to Java string fn rust_string_to_jstring(env: &JNIEnv, s: String) -> jstring { @@ -29,7 +27,7 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( _class: JClass, port: i32, ) -> jstring { - let result = (|| { + let result: Result = (|| { let client = get_client(port)?; pull_all(&client) .map_err(|e| format!("Sync pull failed: {}", e))?; @@ -43,9 +41,10 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( Ok(msg) => rust_string_to_jstring(&env, msg), Err(e) => { error!("syncPullAll error: {}", e); + let error_msg: &str = &e; let error_json = json!({ "success": false, - "error": e + "error": error_msg }).to_string(); rust_string_to_jstring(&env, error_json) } @@ -60,9 +59,9 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( port: i32, hostname: JString, ) -> jstring { - let result = (|| { + let result: Result = (|| { let client = get_client(port)?; - let hostname_str: String = env.get_string(hostname) + let hostname_str: String = env.get_string(&hostname) .map_err(|e| format!("Failed to get hostname string: {}", e))? .into(); @@ -79,9 +78,10 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( Ok(msg) => rust_string_to_jstring(&env, msg), Err(e) => { error!("syncPull error: {}", e); + let error_msg: &str = &e; let error_json = json!({ "success": false, - "error": e + "error": error_msg }).to_string(); rust_string_to_jstring(&env, error_json) } @@ -95,7 +95,7 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( _class: JClass, port: i32, ) -> jstring { - let result = (|| { + let result: Result = (|| { let client = get_client(port)?; push(&client) .map_err(|e| format!("Sync push failed: {}", e))?; @@ -109,9 +109,10 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( Ok(msg) => rust_string_to_jstring(&env, msg), Err(e) => { error!("syncPush error: {}", e); + let error_msg: &str = &e; let error_json = json!({ "success": false, - "error": e + "error": error_msg }).to_string(); rust_string_to_jstring(&env, error_json) } @@ -125,7 +126,7 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncBoth( _class: JClass, port: i32, ) -> jstring { - let result = (|| { + let result: Result = (|| { let client = get_client(port)?; pull_all(&client) @@ -144,9 +145,10 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncBoth( Ok(msg) => rust_string_to_jstring(&env, msg), Err(e) => { error!("syncBoth error: {}", e); + let error_msg: &str = &e; let error_json = json!({ "success": false, - "error": e + "error": error_msg }).to_string(); rust_string_to_jstring(&env, error_json) } From bfe3c4b95ad4b74eced663f0777a36a3cfa57b63 Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 16 Oct 2025 20:14:35 +0300 Subject: [PATCH 04/10] fix(aw-sync): make env mutable and conditionally compile config functions for Android - Made env parameter mutable in syncPull JNI function - Made get_config_dir() conditional to not compile on Android - Made get_server_config_path() conditional to not compile on Android - These functions use appdirs which doesn't support Android - Android uses environment variables (AW_SYNC_DIR) instead --- aw-sync/src/android.rs | 2 +- aw-sync/src/dirs.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/aw-sync/src/android.rs b/aw-sync/src/android.rs index e6feed97..35d60fcf 100644 --- a/aw-sync/src/android.rs +++ b/aw-sync/src/android.rs @@ -54,7 +54,7 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( /// Pull sync data from a specific host #[no_mangle] pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( - env: JNIEnv, + mut env: JNIEnv, _class: JClass, port: i32, hostname: JString, diff --git a/aw-sync/src/dirs.rs b/aw-sync/src/dirs.rs index 9c7ecf68..e6da8a03 100644 --- a/aw-sync/src/dirs.rs +++ b/aw-sync/src/dirs.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; // TODO: This could be refactored to share logic with aw-server/src/dirs.rs // TODO: add proper config support +#[cfg(not(target_os = "android"))] #[allow(dead_code)] pub fn get_config_dir() -> Result> { let mut dir = appdirs::user_config_dir(Some("activitywatch"), None, false) @@ -14,6 +15,7 @@ pub fn get_config_dir() -> Result> { Ok(dir) } +#[cfg(not(target_os = "android"))] pub fn get_server_config_path(testing: bool) -> Result { let dir = aw_server::dirs::get_config_dir()?; Ok(dir.join(if testing { From e979220685469518e5840379085d3053c4a5eb8a Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 16 Oct 2025 20:15:46 +0300 Subject: [PATCH 05/10] fix(aw-sync): make get_server_port conditional for Android - Made get_server_port function conditional to not compile on Android - On Android, port is passed as parameter to JNI functions - This avoids dependency on config file reading which is not available on Android --- aw-sync/src/util.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/aw-sync/src/util.rs b/aw-sync/src/util.rs index 05ad88d3..5681d9ef 100644 --- a/aw-sync/src/util.rs +++ b/aw-sync/src/util.rs @@ -6,6 +6,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; /// Returns the port of the local aw-server instance +#[cfg(not(target_os = "android"))] pub fn get_server_port(testing: bool) -> Result> { // TODO: get aw-server config more reliably let aw_server_conf = crate::dirs::get_server_config_path(testing) From 63b5844f8a9c8410dd16ae0aa05a16765f4f3c13 Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 16 Oct 2025 20:17:13 +0300 Subject: [PATCH 06/10] fix(aw-sync): add cdylib crate-type for JNI shared library generation - Added crate-type = ["cdylib", "rlib"] to [lib] section - cdylib is required to generate .so files for Android JNI - rlib allows the library to be used by other Rust code --- aw-sync/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/aw-sync/Cargo.toml b/aw-sync/Cargo.toml index 4a0e0b61..cea766ce 100644 --- a/aw-sync/Cargo.toml +++ b/aw-sync/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [lib] name = "aw_sync" path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] [[bin]] name = "aw-sync" From 9b32e31928c369facb16154672969e589b6ea36b Mon Sep 17 00:00:00 2001 From: Brayo Date: Sat, 18 Oct 2025 18:00:03 +0300 Subject: [PATCH 07/10] feat(aw-sync): pass hostname from Android to JNI for proper sync directory structure - Updated JNI function signatures to accept hostname parameter - Parse hostname from JString in syncPullAll, syncPush, syncBoth - Set AW_HOSTNAME environment variable before sync operations - This fixes sync directory structure to use actual device name instead of 'localhost' Hostname is passed from Android Settings.Global.DEVICE_NAME or Build.MODEL --- aw-sync/src/android.rs | 52 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/aw-sync/src/android.rs b/aw-sync/src/android.rs index 35d60fcf..ff783af1 100644 --- a/aw-sync/src/android.rs +++ b/aw-sync/src/android.rs @@ -23,10 +23,25 @@ fn get_client(port: i32) -> Result { /// Pull sync data from all hosts in the sync directory #[no_mangle] pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( - env: JNIEnv, + mut env: JNIEnv, _class: JClass, port: i32, + hostname: JString, ) -> jstring { + let hostname_str: String = match env.get_string(&hostname) { + Ok(s) => s.into(), + Err(e) => { + let error_msg = format!("Failed to get hostname: {}", e); + error!("syncPullAll: {}", error_msg); + return rust_string_to_jstring(&env, json!({ + "success": false, + "error": error_msg + }).to_string()); + } + }; + // Set hostname for sync operations + std::env::set_var("AW_HOSTNAME", &hostname_str); + let result: Result = (|| { let client = get_client(port)?; pull_all(&client) @@ -91,10 +106,26 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( /// Push local sync data to the sync directory #[no_mangle] pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( - env: JNIEnv, + mut env: JNIEnv, _class: JClass, port: i32, + hostname: JString, ) -> jstring { + let hostname_str: String = match env.get_string(&hostname) { + Ok(s) => s.into(), + Err(e) => { + let error_msg = format!("Failed to get hostname: {}", e); + error!("syncPush: {}", error_msg); + return rust_string_to_jstring(&env, json!({ + "success": false, + "error": error_msg + }).to_string()); + } + }; + + // Set hostname for sync operations + std::env::set_var("AW_HOSTNAME", &hostname_str); + let result: Result = (|| { let client = get_client(port)?; push(&client) @@ -122,10 +153,25 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( /// Perform full sync (pull from all hosts, then push local data) #[no_mangle] pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncBoth( - env: JNIEnv, + mut env: JNIEnv, _class: JClass, port: i32, + hostname: JString, ) -> jstring { + let hostname_str: String = match env.get_string(&hostname) { + Ok(s) => s.into(), + Err(e) => { + let error_msg = format!("Failed to get hostname: {}", e); + error!("syncBoth: {}", error_msg); + return rust_string_to_jstring(&env, json!({ + "success": false, + "error": error_msg + }).to_string()); + } + }; + // Set hostname for sync operations + std::env::set_var("AW_HOSTNAME", &hostname_str); + let result: Result = (|| { let client = get_client(port)?; From 9c1bd01b9eda329a74ac9d0d9fb47dffe415000e Mon Sep 17 00:00:00 2001 From: Brayo Date: Mon, 22 Dec 2025 21:46:11 +0300 Subject: [PATCH 08/10] fix(aw-sync): use passed hostname directly instead of client.hostname - Created push_with_hostname() function that accepts hostname parameter - Updated JNI functions to use push_with_hostname() with passed hostname - This ensures sync directory uses actual device name from Android - Fixes issue where 'localhost' was used despite passing device name The hostname is now passed directly from Android through JNI to the sync functions, bypassing the client's hostname entirely. --- Cargo.lock | 89 ++++++++++++++++++++++++++- aw-sync/src/android.rs | 116 ++++++++++++++++++++---------------- aw-sync/src/lib.rs | 2 +- aw-sync/src/main.rs | 2 +- aw-sync/src/sync_wrapper.rs | 6 +- 5 files changed, 157 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69c77733..aa582f0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ dependencies = [ "fern", "gethostname", "jemallocator", - "jni", + "jni 0.20.0", "lazy_static", "libc", "log", @@ -309,6 +309,7 @@ dependencies = [ "ctrlc", "dirs", "gethostname", + "jni 0.21.1", "log", "openssl", "reqwest", @@ -813,7 +814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1420,6 +1421,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -2356,7 +2373,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3344,6 +3361,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3371,6 +3397,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3402,6 +3443,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3414,6 +3461,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3426,6 +3479,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3444,6 +3503,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3456,6 +3521,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3468,6 +3539,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3480,6 +3557,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/aw-sync/src/android.rs b/aw-sync/src/android.rs index ff783af1..a64c81d0 100644 --- a/aw-sync/src/android.rs +++ b/aw-sync/src/android.rs @@ -1,15 +1,14 @@ -use jni::JNIEnv; +use aw_client_rust::blocking::AwClient; use jni::objects::{JClass, JString}; use jni::sys::jstring; -use aw_client_rust::blocking::AwClient; +use jni::JNIEnv; use serde_json::json; -use crate::{pull, pull_all, push}; +use crate::{pull, pull_all, push_with_hostname}; /// Helper function to convert Rust string to Java string fn rust_string_to_jstring(env: &JNIEnv, s: String) -> jstring { - let output = env.new_string(s) - .expect("Couldn't create java string!"); + let output = env.new_string(s).expect("Couldn't create java string!"); output.into_raw() } @@ -33,23 +32,25 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( Err(e) => { let error_msg = format!("Failed to get hostname: {}", e); error!("syncPullAll: {}", error_msg); - return rust_string_to_jstring(&env, json!({ - "success": false, - "error": error_msg - }).to_string()); + return rust_string_to_jstring( + &env, + json!({ + "success": false, + "error": error_msg + }) + .to_string(), + ); } }; - // Set hostname for sync operations - std::env::set_var("AW_HOSTNAME", &hostname_str); - + let result: Result = (|| { let client = get_client(port)?; - pull_all(&client) - .map_err(|e| format!("Sync pull failed: {}", e))?; + pull_all(&client).map_err(|e| format!("Sync pull failed: {}", e))?; Ok(json!({ "success": true, "message": "Successfully pulled from all hosts" - }).to_string()) + }) + .to_string()) })(); match result { @@ -60,7 +61,8 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPullAll( let error_json = json!({ "success": false, "error": error_msg - }).to_string(); + }) + .to_string(); rust_string_to_jstring(&env, error_json) } } @@ -76,17 +78,18 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( ) -> jstring { let result: Result = (|| { let client = get_client(port)?; - let hostname_str: String = env.get_string(&hostname) + let hostname_str: String = env + .get_string(&hostname) .map_err(|e| format!("Failed to get hostname string: {}", e))? .into(); - - pull(&hostname_str, &client) - .map_err(|e| format!("Sync pull failed: {}", e))?; - + + pull(&hostname_str, &client).map_err(|e| format!("Sync pull failed: {}", e))?; + Ok(json!({ "success": true, "message": format!("Successfully pulled from host: {}", hostname_str) - }).to_string()) + }) + .to_string()) })(); match result { @@ -97,7 +100,8 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPull( let error_json = json!({ "success": false, "error": error_msg - }).to_string(); + }) + .to_string(); rust_string_to_jstring(&env, error_json) } } @@ -116,24 +120,26 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( Err(e) => { let error_msg = format!("Failed to get hostname: {}", e); error!("syncPush: {}", error_msg); - return rust_string_to_jstring(&env, json!({ - "success": false, - "error": error_msg - }).to_string()); + return rust_string_to_jstring( + &env, + json!({ + "success": false, + "error": error_msg + }) + .to_string(), + ); } }; - - // Set hostname for sync operations - std::env::set_var("AW_HOSTNAME", &hostname_str); - + let result: Result = (|| { let client = get_client(port)?; - push(&client) + push_with_hostname(&client, &hostname_str) .map_err(|e| format!("Sync push failed: {}", e))?; Ok(json!({ "success": true, "message": "Successfully pushed local data" - }).to_string()) + }) + .to_string()) })(); match result { @@ -144,7 +150,8 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncPush( let error_json = json!({ "success": false, "error": error_msg - }).to_string(); + }) + .to_string(); rust_string_to_jstring(&env, error_json) } } @@ -163,28 +170,30 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncBoth( Err(e) => { let error_msg = format!("Failed to get hostname: {}", e); error!("syncBoth: {}", error_msg); - return rust_string_to_jstring(&env, json!({ - "success": false, - "error": error_msg - }).to_string()); + return rust_string_to_jstring( + &env, + json!({ + "success": false, + "error": error_msg + }) + .to_string(), + ); } }; - // Set hostname for sync operations - std::env::set_var("AW_HOSTNAME", &hostname_str); - + let result: Result = (|| { let client = get_client(port)?; - - pull_all(&client) - .map_err(|e| format!("Pull phase failed: {}", e))?; - - push(&client) + + pull_all(&client).map_err(|e| format!("Pull phase failed: {}", e))?; + + push_with_hostname(&client, &hostname_str) .map_err(|e| format!("Push phase failed: {}", e))?; - + Ok(json!({ "success": true, "message": "Successfully completed full sync" - }).to_string()) + }) + .to_string()) })(); match result { @@ -195,7 +204,8 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_syncBoth( let error_json = json!({ "success": false, "error": error_msg - }).to_string(); + }) + .to_string(); rust_string_to_jstring(&env, error_json) } } @@ -208,21 +218,23 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_getSyncDir( _class: JClass, ) -> jstring { let result = crate::dirs::get_sync_dir(); - + match result { Ok(path) => { let path_str = path.to_string_lossy().to_string(); let response = json!({ "success": true, "path": path_str - }).to_string(); + }) + .to_string(); rust_string_to_jstring(&env, response) } Err(e) => { let error_json = json!({ "success": false, "error": format!("Failed to get sync dir: {}", e) - }).to_string(); + }) + .to_string(); rust_string_to_jstring(&env, error_json) } } diff --git a/aw-sync/src/lib.rs b/aw-sync/src/lib.rs index 2e402923..31bae33c 100644 --- a/aw-sync/src/lib.rs +++ b/aw-sync/src/lib.rs @@ -11,8 +11,8 @@ pub use sync::sync_run; pub use sync::SyncSpec; mod sync_wrapper; -pub use sync_wrapper::push; pub use sync_wrapper::{pull, pull_all}; +pub use sync_wrapper::{push, push_with_hostname}; mod accessmethod; pub use accessmethod::AccessMethod; diff --git a/aw-sync/src/main.rs b/aw-sync/src/main.rs index e453d718..d06516f5 100644 --- a/aw-sync/src/main.rs +++ b/aw-sync/src/main.rs @@ -157,7 +157,7 @@ fn main() -> Result<(), Box> { let port = opts .port - .map(|a| Ok(a)) + .map(Ok) .unwrap_or_else(|| util::get_server_port(opts.testing))?; let client = AwClient::new(&opts.host, port, "aw-sync")?; diff --git a/aw-sync/src/sync_wrapper.rs b/aw-sync/src/sync_wrapper.rs index 2f6d3328..47067a58 100644 --- a/aw-sync/src/sync_wrapper.rs +++ b/aw-sync/src/sync_wrapper.rs @@ -57,9 +57,13 @@ pub fn pull(host: &str, client: &AwClient) -> Result<(), Box> { } pub fn push(client: &AwClient) -> Result<(), Box> { + push_with_hostname(client, &client.hostname) +} + +pub fn push_with_hostname(client: &AwClient, hostname: &str) -> Result<(), Box> { let sync_dir = crate::dirs::get_sync_dir() .map_err(|_| "Could not get sync dir")? - .join(&client.hostname); + .join(hostname); let sync_spec = SyncSpec { path: sync_dir, From babea6c3564c11b2c7aff5e8671293e3717a8816 Mon Sep 17 00:00:00 2001 From: Brayo Date: Wed, 24 Dec 2025 11:26:52 +0300 Subject: [PATCH 09/10] feat: set max-page size to 16KB for shared objects --- compile-android.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compile-android.sh b/compile-android.sh index 097461c5..152dee8f 100755 --- a/compile-android.sh +++ b/compile-android.sh @@ -73,6 +73,8 @@ for archtargetstr in \ # Needed for runtime error: https://github.com/termux/termux-packages/issues/8029 # java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "__extenddftf2" export RUSTFLAGS+=" -C link-arg=$($NDK_ARCH_DIR/${target}-clang -print-libgcc-file-name)" + # Align to 16KB for Android 15 compatibility + export RUSTFLAGS+=" -C link-arg=-z -C link-arg=max-page-size=16384" echo RUSTFLAGS=$RUSTFLAGS # fix armv7 -> arm From 5f9e8da930b5a0e6dae9a0c3b889306f42fe878e Mon Sep 17 00:00:00 2001 From: Brayo Date: Mon, 9 Feb 2026 18:38:27 +0300 Subject: [PATCH 10/10] feat: add an JNI endpoint for android querying --- Cargo.lock | 1 + aw-server/Cargo.toml | 1 + aw-server/src/android/mod.rs | 93 +++++++++++++++++++++++++++++++++++- aw-webui | 2 +- 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa582f0a..8ea71c35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,6 +270,7 @@ version = "0.13.1" dependencies = [ "android_logger", "appdirs", + "aw-client-rust", "aw-datastore", "aw-models", "aw-query", diff --git a/aw-server/Cargo.toml b/aw-server/Cargo.toml index 34f36f07..a22631be 100644 --- a/aw-server/Cargo.toml +++ b/aw-server/Cargo.toml @@ -46,3 +46,4 @@ jni = { version = "0.20", default-features = false } libc = "0.2" android_logger = "0.13" openssl-sys = { version = "0.9.82", features = ["vendored"]} +aw-client-rust = { path = "../aw-client-rust" } diff --git a/aw-server/src/android/mod.rs b/aw-server/src/android/mod.rs index df50ce1a..3ee2c0c3 100644 --- a/aw-server/src/android/mod.rs +++ b/aw-server/src/android/mod.rs @@ -41,8 +41,12 @@ pub mod android { use crate::config::AWConfig; use crate::endpoints; use crate::endpoints::ServerState; + use aw_client_rust::classes::default_classes; + use aw_client_rust::queries::{ + build_android_canonical_events, AndroidQueryParams, QueryParamsBase, + }; use aw_datastore::Datastore; - use aw_models::{Bucket, Event}; + use aw_models::{Bucket, Event, TimeInterval}; static mut DATASTORE: Option = None; @@ -243,4 +247,91 @@ pub mod android { ), } } + + #[no_mangle] + pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_query( + env: JNIEnv, + _: JClass, + java_query: JString, + java_timeperiods: JString, + ) -> jstring { + let query_code = jstring_to_string(&env, java_query); + let timeperiods_str = jstring_to_string(&env, java_timeperiods); + let timeperiods: Vec = match serde_json::from_str(&timeperiods_str) { + Ok(json) => json, + Err(err) => return create_error_object(&env, err.to_string()), + }; + + let datastore = openDatastore(); + let mut results = Vec::new(); + + for interval in &timeperiods { + let result = match aw_query::query(&query_code, interval, &datastore) { + Ok(data) => data, + Err(e) => { + return create_error_object( + &env, + format!("Something went wrong when trying to query: {:?}", e), + ) + } + }; + results.push(result); + } + + string_to_jstring(&env, json!(results).to_string()) + } + + #[no_mangle] + pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_androidQuery( + env: JNIEnv, + _: JClass, + java_timeperiods: JString, + ) -> jstring { + let timeperiods_str = jstring_to_string(&env, java_timeperiods); + + let timeperiods: Vec = match serde_json::from_str(&timeperiods_str) { + Ok(json) => json, + Err(err) => return create_error_object(&env, err.to_string()), + }; + + // Hardcoded bucket ID for testing + let bid_android = "aw-watcher-android-test".to_string(); + + // Build canonical Android query + let params = AndroidQueryParams { + base: QueryParamsBase { + bid_browsers: Vec::new(), + classes: default_classes(), + filter_classes: Vec::new(), + filter_afk: true, + include_audible: true, + }, + bid_android, + }; + let query_code = format!( + r#"{} +duration = sum_durations(events); +cat_events = sort_by_duration(merge_events_by_keys(events, ["$category"])); +RETURN = {{"events": events, "duration": duration, "cat_events": cat_events}};"#, + build_android_canonical_events(¶ms) + ); + + let datastore = openDatastore(); + let mut results = Vec::new(); + + for interval in &timeperiods { + let result = match aw_query::query(&query_code, interval, &datastore) { + Ok(data) => data, + Err(e) => { + return create_error_object( + &env, + format!("Something went wrong when trying to query: {:?}", e), + ) + } + }; + results.push(result); + } + + string_to_jstring(&env, json!(results).to_string()) + } } diff --git a/aw-webui b/aw-webui index 6e3c4d8f..e493d03c 160000 --- a/aw-webui +++ b/aw-webui @@ -1 +1 @@ -Subproject commit 6e3c4d8f7e739f9b632cacaabfaa7315bb54054d +Subproject commit e493d03c0eb9d54a1495b3a417cffe6dc03123f8