Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ log = "0.4"
env_logger = "0.10"

[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21.1"
jni = "0.22.4"
tokio = { version = "^1.43", default-features = false, features = ["rt", "rt-multi-thread", "sync", "time", "macros"] }
veilid-core = { git = "https://gitlab.com/veilid/veilid.git", tag = "v0.5.3" }
blake3 = "1.8.2"
Expand Down
37 changes: 23 additions & 14 deletions build-android.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@
clear

# Auto-detect Android SDK if not provided
if [ -z "$ANDROID_HOME" ] && [ -d "$HOME/Android/Sdk" ]; then
export ANDROID_HOME="$HOME/Android/Sdk"
if [ -z "$ANDROID_HOME" ]; then
if [ -d "$HOME/Android/Sdk" ]; then
export ANDROID_HOME="$HOME/Android/Sdk"
elif [ -d "$HOME/Library/Android/sdk" ]; then
export ANDROID_HOME="$HOME/Library/Android/sdk"
fi
fi

# Auto-detect Android NDK if not provided
if [ -z "$ANDROID_NDK_HOME" ] && [ -d "$HOME/Android/Sdk/ndk" ]; then
NDK_LATEST="$(ls -1 "$HOME/Android/Sdk/ndk" 2>/dev/null | sort -V | tail -n 1)"
if [ -n "$NDK_LATEST" ] && [ -d "$HOME/Android/Sdk/ndk/$NDK_LATEST" ]; then
export ANDROID_NDK_HOME="$HOME/Android/Sdk/ndk/$NDK_LATEST"
if [ -z "$ANDROID_NDK_HOME" ] && [ -n "$ANDROID_HOME" ] && [ -d "$ANDROID_HOME/ndk" ]; then
NDK_LATEST=""
for ndk_dir in "$ANDROID_HOME/ndk"/*/; do
[ -d "$ndk_dir" ] || continue
candidate="$(basename "$ndk_dir")"
if [ -z "$NDK_LATEST" ] || [ "$(printf '%s\n' "$NDK_LATEST" "$candidate" | sort -V | tail -n 1)" = "$candidate" ]; then
NDK_LATEST="$candidate"
fi
done
if [ -n "$NDK_LATEST" ] && [ -d "$ANDROID_HOME/ndk/$NDK_LATEST" ]; then
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_LATEST"
fi
fi

Expand All @@ -35,20 +46,18 @@ mkdir -p $JNI_DIR
#
# cargo update save-dweb-backend

# Add this target if we need to support older devices.
# armv7-linux-androideabi
#
# arm64-v8a: modern 64-bit ARM devices (primary production ABI)
# armv7-linux-androideabi: older 32-bit ARM devices (armeabi-v7a)
# x86_64-linux-android: emulators and x86_64 devices (dev/CI)
rustup target add \
aarch64-linux-android \
armv7-linux-androideabi \
x86_64-linux-android

# Build the android libraries in the jniLibs directory
#
# Add this target if we need to support older devices.
# armeabi-v7a
#
cargo ndk -o $JNI_DIR \
--manifest-path ../Cargo.toml \
-t arm64-v8a \
-t armeabi-v7a \
-t x86_64 \
build --release
build --release
226 changes: 92 additions & 134 deletions src/android_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,187 +6,146 @@ use crate::server;
use crate::server::start;
use crate::{log_debug, log_error, log_info};
use jni::errors::Result as JniResult;
use jni::errors::ThrowRuntimeExAndDefault;
use jni::jni_sig;
use jni::jni_str;
use jni::objects::{JClass, JObject, JString, JValue};
use jni::sys::{jint, jstring};
use jni::{
objects::GlobalRef, objects::JClass, objects::JMethodID, objects::JObject, objects::JString,
objects::JValue, objects::JValueGen, JNIEnv, JavaVM,
};
use lazy_static::lazy_static;
use std::error::Error;
use std::sync::{Arc, Mutex, Once};
use std::thread;
use jni::{Env, EnvUnowned};
use std::time::Duration;
use veilid_core::veilid_core_setup_android;

trait IntoJObject {
fn into_jobject(&self) -> JObject;
}

impl IntoJObject for GlobalRef {
fn into_jobject(&self) -> JObject {
unsafe { JObject::from_raw(self.as_raw()) }
}
}

#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_SnowbirdBridge_initializeRustService(
env: JNIEnv,
class: JClass,
mut env: EnvUnowned,
_class: JClass,
) {
// match jni_globals::setup_android(env, class) {
// Ok(_) => log_debug!(TAG, "Rust service initialized successfully"),
// Err(e) => log_error!(TAG, "Failed to initialize Rust service: {:?}", e),
// }
env.with_env(|_env| -> JniResult<()> {
// match jni_globals::setup_android(env, class) {
// Ok(_) => log_debug!(TAG, "Rust service initialized successfully"),
// Err(e) => log_error!(TAG, "Failed to initialize Rust service: {:?}", e),
// }

log_info!(TAG, "SnowbirdBridge initialized");
log_info!(TAG, "SnowbirdBridge initialized");
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>();
}

#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_SnowbirdBridge_startServer(
mut env: JNIEnv,
mut env: EnvUnowned,
clazz: JClass,
context: JObject,
backend_base_directory: JString,
server_socket_path: JString,
) -> jstring {
let env_ptr = env.get_native_interface();

log_debug!(TAG, "Bridge: starting");

match setup_jni_environments(&mut env, context, clazz) {
Ok(_) => {
// Initialize JNI globals, smoke-test the Java callback, and read Java args while
// EnvUnowned is still available. veilid_core_setup_android consumes env/context.
let (backend_base_directory, server_socket_path, output) = env
.with_env(|env| -> JniResult<(String, String, jstring)> {
jni_globals::init_jni(env, clazz).map_err(|e| {
jni::errors::Error::ParseFailed(format!("Failed to initialize JNI globals: {e}"))
})?;
jni_smoke_test(env)?;

let backend_base_directory = backend_base_directory.try_to_string(env)?;
let server_socket_path = server_socket_path.try_to_string(env)?;
let output = JString::from_str(
env,
format!("Server started on Unix socket: {server_socket_path}"),
)?
.into_raw();

log_debug!(TAG, "JNI stuff successful");
}
Err(e) => {
log_error!(TAG, "Error doing JNI stuff: {:?}", e);
}
}

let backend_base_directory: String = env
.get_string(&backend_base_directory)
.expect("Couldn't get socket path string")
.into();
Ok((backend_base_directory, server_socket_path, output))
})
.resolve::<ThrowRuntimeExAndDefault>();

let server_socket_path: String = env
.get_string(&server_socket_path)
.expect("Couldn't get socket path string")
.into();
// resolve() throws to Java and returns null on failure; do not start Veilid or the server.
if output.is_null() {
return output;
}

let backend_base_directory_clone = backend_base_directory.clone();
let server_socket_path_clone = server_socket_path.clone();
veilid_core_setup_android(env, context);

std::thread::spawn(move || {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
start(&backend_base_directory_clone, &server_socket_path_clone)
start(&backend_base_directory, &server_socket_path)
.await
.unwrap();
});
});

log_debug!(TAG, "Bridge startup complete.");

let output = env
.new_string(format!(
"Server started on Unix socket: {}",
server_socket_path
))
.expect("Couldn't create java string!");

output.into_raw()
output
}

#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_SnowbirdBridge_stopServer(
mut env: JNIEnv,
mut env: EnvUnowned,
_clazz: JClass,
ctx: JObject,
_ctx: JObject,
) -> jstring {
log_debug!(TAG, "Bridge: stopping server");

// Create a runtime to handle async operations
let runtime = tokio::runtime::Runtime::new().unwrap();

// Stop the backend server and clean up Veilid API
let stop_result = runtime.block_on(async {
// First stop the backend
match server::stop().await {
Ok(_) => {
log_info!(TAG, "Backend stopped successfully");

// Get the backend to access Veilid API
if let Ok(backend) = server::get_backend().await {
// Shutdown Veilid API
if let Some(veilid_api) = backend.get_veilid_api().await {
veilid_api.shutdown().await;
log_info!(TAG, "Veilid API shut down successfully");
let stop_ok = env
.with_env(|_env| -> JniResult<bool> {
// Create a runtime to handle async operations
let runtime = tokio::runtime::Runtime::new().unwrap();

// Stop the backend server and clean up Veilid API
runtime.block_on(async {
// First stop the backend
match server::stop().await {
Ok(_) => {
log_info!(TAG, "Backend stopped successfully");

// Get the backend to access Veilid API
if let Ok(backend) = server::get_backend().await {
// Shutdown Veilid API
if let Some(veilid_api) = backend.get_veilid_api().await {
veilid_api.shutdown().await;
log_info!(TAG, "Veilid API shut down successfully");
}
}

// Add a small delay to ensure tasks complete
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(true)
}
Err(e) => {
log_error!(TAG, "Error stopping server: {:?}", e);
Ok(false)
}
}

// Add a small delay to ensure tasks complete
tokio::time::sleep(Duration::from_millis(500)).await;

Ok(())
}
Err(e) => {
log_error!(TAG, "Error stopping server: {:?}", e);
Err(e)
}
}
});
})
})
.resolve::<ThrowRuntimeExAndDefault>();

// Create response string based on result
let response = match stop_result {
Ok(_) => "Server stopped successfully",
Err(_) => "Error stopping server",
let response = if stop_ok {
"Server stopped successfully"
} else {
"Error stopping server"
};

let output = env
.new_string(response)
.expect("Couldn't create java string!");

output.into_raw()
}

fn with_env<F, R>(env: &mut JNIEnv, f: F) -> Result<R, Box<dyn Error>>
where
F: FnOnce(JNIEnv) -> Result<R, Box<dyn Error>>,
{
let env_ptr = env.get_native_interface();
let new_env = unsafe { JNIEnv::from_raw(env_ptr).unwrap() };
f(new_env)
}

fn setup_jni_environments(
env: &mut JNIEnv,
context: JObject,
clazz: JClass,
) -> Result<(), Box<dyn Error>> {
with_env(env, |env| Ok(jni_globals::init_jni(&env, clazz)));

let global_context = env.new_global_ref(context)?;

// Use a new JNIEnv for jni_smoke_test
with_env(env, |env| {
jni_smoke_test(env, global_context.into_jobject())
})?;

// Use another new JNIEnv for veilid_core_setup_android
with_env(env, |env| {
veilid_core_setup_android(env, global_context.into_jobject());
Ok(())
})?;

Ok(())
env.with_env(|env| -> JniResult<jstring> {
let output = JString::from_str(env, response)?;
Ok(output.into_raw())
})
.resolve::<ThrowRuntimeExAndDefault>()
}

fn jni_smoke_test<'local>(
mut env: JNIEnv<'local>,
context: JObject<'local>,
) -> Result<(), Box<dyn std::error::Error>> {
fn jni_smoke_test(env: &mut Env) -> JniResult<()> {
let class_name = "net/opendasharchive/openarchive/services/snowbird/SnowbirdBridge";
let method_name = "updateStatusFromRust";
let method_signature = "(ILjava/lang/String;)V";
Expand All @@ -196,15 +155,14 @@ fn jni_smoke_test<'local>(

// Create a JValue for the String parameter (can be null)
let error_message = env.new_string("Test error message")?;
let error_message_jvalue = JValue::Object(&error_message);

// Call the static method
env.call_static_method(
class_name,
method_name,
method_signature,
&[JValue::Int(status_code), error_message_jvalue],
jni_str!("net/opendasharchive/openarchive/services/snowbird/SnowbirdBridge"),
jni_str!("updateStatusFromRust"),
jni_sig!("(ILjava/lang/String;)V"),
&[JValue::Int(status_code), JValue::Object(&error_message)],
)?;

Ok(())
}
}
Loading
Loading