Skip to content

Commit 9feab90

Browse files
authored
feat(core): add API to call Android plugin (#6239)
1 parent 62f1526 commit 9feab90

File tree

13 files changed

+306
-117
lines changed

13 files changed

+306
-117
lines changed

.changes/refactor-macros.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-macros": patch
3+
"tauri": patch
4+
---
5+
6+
Refactored the implementation of the `mobile_entry_point` macro.

.changes/run-android-plugin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Added `App::run_android_plugin` and `AppHandle::run_android_plugin`.

core/tauri-build/src/mobile.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,9 @@ impl PluginBuilder {
3939

4040
println!("cargo:rerun-if-env-changed=TAURI_PLUGIN_OUTPUT_PATH");
4141
println!("cargo:rerun-if-env-changed=TAURI_GRADLE_SETTINGS_PATH");
42-
println!(
43-
"cargo:rerun-if-changed={}{}{}",
44-
out_dir, MAIN_SEPARATOR, pkg_name
45-
);
46-
println!("cargo:rerun-if-changed={}", gradle_settings_path);
47-
println!("cargo:rerun-if-changed={}", app_build_gradle_path);
42+
println!("cargo:rerun-if-changed={out_dir}{MAIN_SEPARATOR}{pkg_name}",);
43+
println!("cargo:rerun-if-changed={gradle_settings_path}");
44+
println!("cargo:rerun-if-changed={app_build_gradle_path}");
4845

4946
let out_dir = PathBuf::from(out_dir);
5047
let target = out_dir.join(&pkg_name);
@@ -73,7 +70,7 @@ impl PluginBuilder {
7370
}
7471

7572
if let Some(out_dir) = out_dir {
76-
rename(&out_dir, &build_path)?;
73+
rename(out_dir, &build_path)?;
7774
}
7875

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

core/tauri-macros/src/mobile.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ pub fn entry_point(_attributes: TokenStream, item: TokenStream) -> TokenStream {
6262
::tauri::log_stdout();
6363
#[cfg(target_os = "android")]
6464
{
65-
use ::tauri::paste;
66-
::tauri::wry_android_binding!(#domain, #app_name, _start_app, ::tauri::wry);
65+
::tauri::android_binding!(#domain, #app_name, _start_app, ::tauri::wry);
6766
}
6867
stop_unwind(#function_name);
6968
}

core/tauri/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ version = "0.44"
109109
features = [ "Win32_Foundation" ]
110110

111111
[target."cfg(target_os = \"android\")".dependencies]
112-
paste = "1.0"
113112
log = "0.4"
114113
jni = "0.20"
115114

core/tauri/src/app.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,88 @@ macro_rules! shared_app_impl {
860860
}
861861
Ok(())
862862
}
863+
864+
/// Executes the given Android plugin method.
865+
#[cfg(target_os = "android")]
866+
pub fn run_android_plugin<T: serde::de::DeserializeOwned, E: serde::de::DeserializeOwned>(
867+
&self,
868+
plugin: impl Into<String>,
869+
method: impl Into<String>,
870+
payload: impl serde::Serialize
871+
) -> Result<Result<T, E>, jni::errors::Error> {
872+
use jni::{
873+
errors::Error as JniError,
874+
objects::JObject,
875+
JNIEnv,
876+
};
877+
878+
fn run<R: Runtime>(
879+
id: i32,
880+
plugin: String,
881+
method: String,
882+
payload: serde_json::Value,
883+
runtime_handle: &R::Handle,
884+
env: JNIEnv<'_>,
885+
activity: JObject<'_>,
886+
) -> Result<(), JniError> {
887+
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
888+
let plugin_manager = env
889+
.call_method(
890+
activity,
891+
"getPluginManager",
892+
"()Lapp/tauri/plugin/PluginManager;",
893+
&[],
894+
)?
895+
.l()?;
896+
897+
env.call_method(
898+
plugin_manager,
899+
"runPluginMethod",
900+
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
901+
&[
902+
id.into(),
903+
env.new_string(plugin)?.into(),
904+
env.new_string(&method)?.into(),
905+
data.into(),
906+
],
907+
)?;
908+
909+
Ok(())
910+
}
911+
912+
let handle = match self.runtime() {
913+
RuntimeOrDispatch::Runtime(r) => r.handle(),
914+
RuntimeOrDispatch::RuntimeHandle(h) => h,
915+
_ => unreachable!(),
916+
};
917+
918+
let id: i32 = rand::random();
919+
let plugin = plugin.into();
920+
let method = method.into();
921+
let payload = serde_json::to_value(payload).unwrap();
922+
let handle_ = handle.clone();
923+
924+
let (tx, rx) = std::sync::mpsc::channel();
925+
let tx_ = tx.clone();
926+
PENDING_PLUGIN_CALLS
927+
.get_or_init(Default::default)
928+
.lock()
929+
.unwrap().insert(id, Box::new(move |arg| {
930+
tx.send(Ok(arg)).unwrap();
931+
}));
932+
933+
handle.run_on_android_context(move |env, activity, _webview| {
934+
if let Err(e) = run::<R>(id, plugin, method, payload, &handle_, env, activity) {
935+
tx_.send(Err(e)).unwrap();
936+
}
937+
});
938+
939+
rx.recv().unwrap().map(|response| {
940+
response
941+
.map(|r| serde_json::from_value(r).unwrap())
942+
.map_err(|e| serde_json::from_value(e).unwrap())
943+
})
944+
}
863945
}
864946
};
865947
}
@@ -1868,6 +1950,57 @@ impl Default for Builder<crate::Wry> {
18681950
}
18691951
}
18701952

1953+
#[cfg(target_os = "android")]
1954+
type PendingPluginCallHandler =
1955+
Box<dyn FnOnce(std::result::Result<serde_json::Value, serde_json::Value>) + Send + 'static>;
1956+
1957+
#[cfg(target_os = "android")]
1958+
static PENDING_PLUGIN_CALLS: once_cell::sync::OnceCell<
1959+
std::sync::Mutex<HashMap<i32, PendingPluginCallHandler>>,
1960+
> = once_cell::sync::OnceCell::new();
1961+
1962+
#[cfg(target_os = "android")]
1963+
#[doc(hidden)]
1964+
pub fn handle_android_plugin_response(
1965+
env: jni::JNIEnv<'_>,
1966+
id: i32,
1967+
success: jni::objects::JString<'_>,
1968+
error: jni::objects::JString<'_>,
1969+
) {
1970+
let (payload, is_ok): (serde_json::Value, bool) = match (
1971+
env
1972+
.is_same_object(success, jni::objects::JObject::default())
1973+
.unwrap_or_default(),
1974+
env
1975+
.is_same_object(error, jni::objects::JObject::default())
1976+
.unwrap_or_default(),
1977+
) {
1978+
// both null
1979+
(true, true) => (serde_json::Value::Null, true),
1980+
// error null
1981+
(false, true) => (
1982+
serde_json::from_str(env.get_string(success).unwrap().to_str().unwrap()).unwrap(),
1983+
true,
1984+
),
1985+
// success null
1986+
(true, false) => (
1987+
serde_json::from_str(env.get_string(error).unwrap().to_str().unwrap()).unwrap(),
1988+
false,
1989+
),
1990+
// both are set - impossible in the Kotlin code
1991+
(false, false) => unreachable!(),
1992+
};
1993+
1994+
if let Some(handler) = PENDING_PLUGIN_CALLS
1995+
.get_or_init(Default::default)
1996+
.lock()
1997+
.unwrap()
1998+
.remove(&id)
1999+
{
2000+
handler(if is_ok { Ok(payload) } else { Err(payload) });
2001+
}
2002+
}
2003+
18712004
#[cfg(test)]
18722005
mod tests {
18732006
#[test]

core/tauri/src/jni_helpers.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::Runtime;
2+
use jni::{
3+
errors::Error as JniError,
4+
objects::{JObject, JValue},
5+
JNIEnv,
6+
};
7+
use serde_json::Value as JsonValue;
8+
use tauri_runtime::RuntimeHandle;
9+
10+
fn json_to_java<'a, R: Runtime>(
11+
env: JNIEnv<'a>,
12+
activity: JObject<'a>,
13+
runtime_handle: &R::Handle,
14+
json: JsonValue,
15+
) -> Result<(&'static str, JValue<'a>), JniError> {
16+
let (class, v) = match json {
17+
JsonValue::Null => ("Ljava/lang/Object;", JObject::null().into()),
18+
JsonValue::Bool(val) => ("Z", val.into()),
19+
JsonValue::Number(val) => {
20+
if let Some(v) = val.as_i64() {
21+
("J", v.into())
22+
} else if let Some(v) = val.as_f64() {
23+
("D", v.into())
24+
} else {
25+
("Ljava/lang/Object;", JObject::null().into())
26+
}
27+
}
28+
JsonValue::String(val) => (
29+
"Ljava/lang/Object;",
30+
JObject::from(env.new_string(&val)?).into(),
31+
),
32+
JsonValue::Array(val) => {
33+
let js_array_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSArray")?;
34+
let data = env.new_object(js_array_class, "()V", &[])?;
35+
36+
for v in val {
37+
let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, v)?;
38+
env.call_method(
39+
data,
40+
"put",
41+
format!("({signature})Lorg/json/JSONArray;"),
42+
&[val],
43+
)?;
44+
}
45+
46+
("Ljava/lang/Object;", data.into())
47+
}
48+
JsonValue::Object(val) => {
49+
let js_object_class =
50+
runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
51+
let data = env.new_object(js_object_class, "()V", &[])?;
52+
53+
for (key, value) in val {
54+
let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, value)?;
55+
env.call_method(
56+
data,
57+
"put",
58+
format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"),
59+
&[env.new_string(&key)?.into(), val],
60+
)?;
61+
}
62+
63+
("Ljava/lang/Object;", data.into())
64+
}
65+
};
66+
Ok((class, v))
67+
}
68+
69+
pub fn to_jsobject<'a, R: Runtime>(
70+
env: JNIEnv<'a>,
71+
activity: JObject<'a>,
72+
runtime_handle: &R::Handle,
73+
json: JsonValue,
74+
) -> Result<JValue<'a>, JniError> {
75+
if let JsonValue::Object(_) = &json {
76+
json_to_java::<R>(env, activity, runtime_handle, json).map(|(_class, data)| data)
77+
} else {
78+
// currently the Kotlin lib cannot handle nulls or raw values, it must be an object
79+
let js_object_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
80+
let data = env.new_object(js_object_class, "()V", &[])?;
81+
Ok(data.into())
82+
}
83+
}

core/tauri/src/lib.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ mod pattern;
188188
pub mod plugin;
189189
pub mod window;
190190
use tauri_runtime as runtime;
191+
#[cfg(target_os = "android")]
192+
mod jni_helpers;
191193
/// The allowlist scopes.
192194
pub mod scope;
193195
mod state;
@@ -204,11 +206,35 @@ pub type Wry = tauri_runtime_wry::Wry<EventLoopMessage>;
204206

205207
#[cfg(all(feature = "wry", target_os = "android"))]
206208
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "wry", target_os = "android"))))]
207-
pub use tauri_runtime_wry::wry::android_binding as wry_android_binding;
209+
#[doc(hidden)]
210+
#[macro_export]
211+
macro_rules! android_binding {
212+
($domain:ident, $package:ident, $main: ident, $wry: path) => {
213+
::tauri::wry::android_binding!($domain, $package, $main, $wry);
214+
::tauri::wry::application::android_fn!(
215+
app_tauri,
216+
plugin,
217+
PluginManager,
218+
handlePluginResponse,
219+
[i32, JString, JString],
220+
);
221+
222+
#[allow(non_snake_case)]
223+
pub unsafe fn handlePluginResponse(
224+
env: JNIEnv,
225+
_: JClass,
226+
id: i32,
227+
success: JString,
228+
error: JString,
229+
) {
230+
::tauri::handle_android_plugin_response(env, id, success, error);
231+
}
232+
};
233+
}
208234

209235
#[cfg(all(feature = "wry", target_os = "android"))]
210236
#[doc(hidden)]
211-
pub use paste;
237+
pub use app::handle_android_plugin_response;
212238
#[cfg(all(feature = "wry", target_os = "android"))]
213239
#[doc(hidden)]
214240
pub use tauri_runtime_wry::wry;

0 commit comments

Comments
 (0)