Skip to content

Commit

Permalink
feat(core): add API to call Android plugin (#6239)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored Feb 10, 2023
1 parent 62f1526 commit 9feab90
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 117 deletions.
6 changes: 6 additions & 0 deletions .changes/refactor-macros.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"tauri-macros": patch
"tauri": patch
---

Refactored the implementation of the `mobile_entry_point` macro.
5 changes: 5 additions & 0 deletions .changes/run-android-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Added `App::run_android_plugin` and `AppHandle::run_android_plugin`.
13 changes: 5 additions & 8 deletions core/tauri-build/src/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,9 @@ impl PluginBuilder {

println!("cargo:rerun-if-env-changed=TAURI_PLUGIN_OUTPUT_PATH");
println!("cargo:rerun-if-env-changed=TAURI_GRADLE_SETTINGS_PATH");
println!(
"cargo:rerun-if-changed={}{}{}",
out_dir, MAIN_SEPARATOR, pkg_name
);
println!("cargo:rerun-if-changed={}", gradle_settings_path);
println!("cargo:rerun-if-changed={}", app_build_gradle_path);
println!("cargo:rerun-if-changed={out_dir}{MAIN_SEPARATOR}{pkg_name}",);
println!("cargo:rerun-if-changed={gradle_settings_path}");
println!("cargo:rerun-if-changed={app_build_gradle_path}");

let out_dir = PathBuf::from(out_dir);
let target = out_dir.join(&pkg_name);
Expand Down Expand Up @@ -73,7 +70,7 @@ impl PluginBuilder {
}

if let Some(out_dir) = out_dir {
rename(&out_dir, &build_path)?;
rename(out_dir, &build_path)?;
}

let gradle_settings = fs::read_to_string(&gradle_settings_path)?;
Expand All @@ -84,7 +81,7 @@ project(':{pkg_name}').projectDir = new File('./tauri-plugins/{pkg_name}')"
if !gradle_settings.contains(&include) {
fs::write(
&gradle_settings_path,
&format!("{gradle_settings}\n{include}"),
format!("{gradle_settings}\n{include}"),
)?;
}

Expand Down
3 changes: 1 addition & 2 deletions core/tauri-macros/src/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ pub fn entry_point(_attributes: TokenStream, item: TokenStream) -> TokenStream {
::tauri::log_stdout();
#[cfg(target_os = "android")]
{
use ::tauri::paste;
::tauri::wry_android_binding!(#domain, #app_name, _start_app, ::tauri::wry);
::tauri::android_binding!(#domain, #app_name, _start_app, ::tauri::wry);
}
stop_unwind(#function_name);
}
Expand Down
1 change: 0 additions & 1 deletion core/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ version = "0.44"
features = [ "Win32_Foundation" ]

[target."cfg(target_os = \"android\")".dependencies]
paste = "1.0"
log = "0.4"
jni = "0.20"

Expand Down
133 changes: 133 additions & 0 deletions core/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,88 @@ macro_rules! shared_app_impl {
}
Ok(())
}

/// Executes the given Android plugin method.
#[cfg(target_os = "android")]
pub fn run_android_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
&self,
plugin: impl Into<String>,
method: impl Into<String>,
payload: impl serde::Serialize
) -> Result<Result<T, E>, jni::errors::Error> {
use jni::{
errors::Error as JniError,
objects::JObject,
JNIEnv,
};

fn run<R: Runtime>(
id: i32,
plugin: String,
method: String,
payload: serde_json::Value,
runtime_handle: &R::Handle,
env: JNIEnv<'_>,
activity: JObject<'_>,
) -> Result<(), JniError> {
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
let plugin_manager = env
.call_method(
activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;

env.call_method(
plugin_manager,
"runPluginMethod",
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
&[
id.into(),
env.new_string(plugin)?.into(),
env.new_string(&method)?.into(),
data.into(),
],
)?;

Ok(())
}

let handle = match self.runtime() {
RuntimeOrDispatch::Runtime(r) => r.handle(),
RuntimeOrDispatch::RuntimeHandle(h) => h,
_ => unreachable!(),
};

let id: i32 = rand::random();
let plugin = plugin.into();
let method = method.into();
let payload = serde_json::to_value(payload).unwrap();
let handle_ = handle.clone();

let (tx, rx) = std::sync::mpsc::channel();
let tx_ = tx.clone();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap().insert(id, Box::new(move |arg| {
tx.send(Ok(arg)).unwrap();
}));

handle.run_on_android_context(move |env, activity, _webview| {
if let Err(e) = run::<R>(id, plugin, method, payload, &handle_, env, activity) {
tx_.send(Err(e)).unwrap();
}
});

rx.recv().unwrap().map(|response| {
response
.map(|r| serde_json::from_value(r).unwrap())
.map_err(|e| serde_json::from_value(e).unwrap())
})
}
}
};
}
Expand Down Expand Up @@ -1868,6 +1950,57 @@ impl Default for Builder<crate::Wry> {
}
}

#[cfg(target_os = "android")]
type PendingPluginCallHandler =
Box<dyn FnOnce(std::result::Result<serde_json::Value, serde_json::Value>) + Send + 'static>;

#[cfg(target_os = "android")]
static PENDING_PLUGIN_CALLS: once_cell::sync::OnceCell<
std::sync::Mutex<HashMap<i32, PendingPluginCallHandler>>,
> = once_cell::sync::OnceCell::new();

#[cfg(target_os = "android")]
#[doc(hidden)]
pub fn handle_android_plugin_response(
env: jni::JNIEnv<'_>,
id: i32,
success: jni::objects::JString<'_>,
error: jni::objects::JString<'_>,
) {
let (payload, is_ok): (serde_json::Value, bool) = match (
env
.is_same_object(success, jni::objects::JObject::default())
.unwrap_or_default(),
env
.is_same_object(error, jni::objects::JObject::default())
.unwrap_or_default(),
) {
// both null
(true, true) => (serde_json::Value::Null, true),
// error null
(false, true) => (
serde_json::from_str(env.get_string(success).unwrap().to_str().unwrap()).unwrap(),
true,
),
// success null
(true, false) => (
serde_json::from_str(env.get_string(error).unwrap().to_str().unwrap()).unwrap(),
false,
),
// both are set - impossible in the Kotlin code
(false, false) => unreachable!(),
};

if let Some(handler) = PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.remove(&id)
{
handler(if is_ok { Ok(payload) } else { Err(payload) });
}
}

#[cfg(test)]
mod tests {
#[test]
Expand Down
83 changes: 83 additions & 0 deletions core/tauri/src/jni_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::Runtime;
use jni::{
errors::Error as JniError,
objects::{JObject, JValue},
JNIEnv,
};
use serde_json::Value as JsonValue;
use tauri_runtime::RuntimeHandle;

fn json_to_java<'a, R: Runtime>(
env: JNIEnv<'a>,
activity: JObject<'a>,
runtime_handle: &R::Handle,
json: JsonValue,
) -> Result<(&'static str, JValue<'a>), JniError> {
let (class, v) = match json {
JsonValue::Null => ("Ljava/lang/Object;", JObject::null().into()),
JsonValue::Bool(val) => ("Z", val.into()),
JsonValue::Number(val) => {
if let Some(v) = val.as_i64() {
("J", v.into())
} else if let Some(v) = val.as_f64() {
("D", v.into())
} else {
("Ljava/lang/Object;", JObject::null().into())
}
}
JsonValue::String(val) => (
"Ljava/lang/Object;",
JObject::from(env.new_string(&val)?).into(),
),
JsonValue::Array(val) => {
let js_array_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSArray")?;
let data = env.new_object(js_array_class, "()V", &[])?;

for v in val {
let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, v)?;
env.call_method(
data,
"put",
format!("({signature})Lorg/json/JSONArray;"),
&[val],
)?;
}

("Ljava/lang/Object;", data.into())
}
JsonValue::Object(val) => {
let js_object_class =
runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
let data = env.new_object(js_object_class, "()V", &[])?;

for (key, value) in val {
let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, value)?;
env.call_method(
data,
"put",
format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"),
&[env.new_string(&key)?.into(), val],
)?;
}

("Ljava/lang/Object;", data.into())
}
};
Ok((class, v))
}

pub fn to_jsobject<'a, R: Runtime>(
env: JNIEnv<'a>,
activity: JObject<'a>,
runtime_handle: &R::Handle,
json: JsonValue,
) -> Result<JValue<'a>, JniError> {
if let JsonValue::Object(_) = &json {
json_to_java::<R>(env, activity, runtime_handle, json).map(|(_class, data)| data)
} else {
// currently the Kotlin lib cannot handle nulls or raw values, it must be an object
let js_object_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
let data = env.new_object(js_object_class, "()V", &[])?;
Ok(data.into())
}
}
30 changes: 28 additions & 2 deletions core/tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ mod pattern;
pub mod plugin;
pub mod window;
use tauri_runtime as runtime;
#[cfg(target_os = "android")]
mod jni_helpers;
/// The allowlist scopes.
pub mod scope;
mod state;
Expand All @@ -204,11 +206,35 @@ pub type Wry = tauri_runtime_wry::Wry<EventLoopMessage>;

#[cfg(all(feature = "wry", target_os = "android"))]
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "wry", target_os = "android"))))]
pub use tauri_runtime_wry::wry::android_binding as wry_android_binding;
#[doc(hidden)]
#[macro_export]
macro_rules! android_binding {
($domain:ident, $package:ident, $main: ident, $wry: path) => {
::tauri::wry::android_binding!($domain, $package, $main, $wry);
::tauri::wry::application::android_fn!(
app_tauri,
plugin,
PluginManager,
handlePluginResponse,
[i32, JString, JString],
);

#[allow(non_snake_case)]
pub unsafe fn handlePluginResponse(
env: JNIEnv,
_: JClass,
id: i32,
success: JString,
error: JString,
) {
::tauri::handle_android_plugin_response(env, id, success, error);
}
};
}

#[cfg(all(feature = "wry", target_os = "android"))]
#[doc(hidden)]
pub use paste;
pub use app::handle_android_plugin_response;
#[cfg(all(feature = "wry", target_os = "android"))]
#[doc(hidden)]
pub use tauri_runtime_wry::wry;
Expand Down
Loading

0 comments on commit 9feab90

Please sign in to comment.