diff --git a/Cargo.lock b/Cargo.lock index 69c77733..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", @@ -279,7 +280,7 @@ dependencies = [ "fern", "gethostname", "jemallocator", - "jni", + "jni 0.20.0", "lazy_static", "libc", "log", @@ -309,6 +310,7 @@ dependencies = [ "ctrlc", "dirs", "gethostname", + "jni 0.21.1", "log", "openssl", "reqwest", @@ -813,7 +815,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 +1422,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 +2374,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3344,6 +3362,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 +3398,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 +3444,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 +3462,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 +3480,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 +3504,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 +3522,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 +3540,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 +3558,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-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-sync/Cargo.toml b/aw-sync/Cargo.toml index ca61d3cf..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" @@ -19,11 +20,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 +35,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..a64c81d0 --- /dev/null +++ b/aw-sync/src/android.rs @@ -0,0 +1,241 @@ +use aw_client_rust::blocking::AwClient; +use jni::objects::{JClass, JString}; +use jni::sys::jstring; +use jni::JNIEnv; +use serde_json::json; + +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!"); + 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( + 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(), + ); + } + }; + + let result: 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_msg: &str = &e; + let error_json = json!({ + "success": false, + "error": error_msg + }) + .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( + mut env: JNIEnv, + _class: JClass, + port: i32, + hostname: JString, +) -> jstring { + let result: 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_msg: &str = &e; + let error_json = json!({ + "success": false, + "error": error_msg + }) + .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( + 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(), + ); + } + }; + + let result: Result = (|| { + let client = get_client(port)?; + 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()) + })(); + + match result { + 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": error_msg + }) + .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( + 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(), + ); + } + }; + + let result: Result = (|| { + let client = get_client(port)?; + + 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()) + })(); + + match result { + 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": error_msg + }) + .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/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 { diff --git a/aw-sync/src/lib.rs b/aw-sync/src/lib.rs index 8ba7bbd4..31bae33c 100644 --- a/aw-sync/src/lib.rs +++ b/aw-sync/src/lib.rs @@ -11,11 +11,14 @@ 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; 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..d06516f5 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) @@ -155,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.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, 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, 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) 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 diff --git a/compile-android.sh b/compile-android.sh index 62a229d8..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 @@ -90,5 +92,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