Quellcode durchsuchen

feat(core): add API to call Android plugin (#6239)

Lucas Fernandes Nogueira vor 2 Jahren
Ursprung
Commit
9feab904bf

+ 6 - 0
.changes/refactor-macros.md

@@ -0,0 +1,6 @@
+---
+"tauri-macros": patch
+"tauri": patch
+---
+
+Refactored the implementation of the `mobile_entry_point` macro.

+ 5 - 0
.changes/run-android-plugin.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Added `App::run_android_plugin` and `AppHandle::run_android_plugin`.

+ 5 - 8
core/tauri-build/src/mobile.rs

@@ -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);
@@ -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)?;
@@ -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}"),
             )?;
           }
 

+ 1 - 2
core/tauri-macros/src/mobile.rs

@@ -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);
       }

+ 0 - 1
core/tauri/Cargo.toml

@@ -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"
 

+ 133 - 0
core/tauri/src/app.rs

@@ -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())
+        })
+      }
     }
   };
 }
@@ -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]

+ 83 - 0
core/tauri/src/jni_helpers.rs

@@ -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())
+  }
+}

+ 28 - 2
core/tauri/src/lib.rs

@@ -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;
@@ -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;

+ 4 - 81
core/tauri/src/window.rs

@@ -1365,89 +1365,12 @@ impl<R: Runtime> Window<R> {
               let plugin = plugin.to_string();
               self.with_webview(move |webview| {
                 webview.jni_handle().exec(move |env, activity, webview| {
-                  use crate::api::ipc::CallbackFn;
                   use jni::{
                     errors::Error as JniError,
-                    objects::{JObject, JValue},
+                    objects::JObject,
                     JNIEnv,
                   };
-                  use serde_json::Value as JsonValue;
-
-                  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))
-                  }
-
-                  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 {
-                      Ok(JObject::null().into())
-                    }
-                  }
+                  use crate::api::ipc::CallbackFn;
 
                   fn handle_message<R: Runtime>(
                     plugin: &str,
@@ -1459,7 +1382,7 @@ impl<R: Runtime> Window<R> {
                     activity: JObject<'_>,
                     webview: JObject<'_>,
                   ) -> Result<(), JniError> {
-                    let data = to_jsobject::<R>(env, activity, runtime_handle, message.payload)?;
+                    let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, message.payload)?;
                     let plugin_manager = env
                       .call_method(
                         activity,
@@ -1471,7 +1394,7 @@ impl<R: Runtime> Window<R> {
 
                     env.call_method(
                       plugin_manager,
-                      "postMessage",
+                      "postIpcMessage",
                       "(Landroid/webkit/WebView;Ljava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;JJ)V",
                       &[
                         webview.into(),

+ 0 - 7
examples/api/src-tauri/Cargo.lock

@@ -2052,12 +2052,6 @@ dependencies = [
  "windows-sys 0.45.0",
 ]
 
-[[package]]
-name = "paste"
-version = "1.0.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
-
 [[package]]
 name = "pathdiff"
 version = "0.2.1"
@@ -3028,7 +3022,6 @@ dependencies = [
  "open",
  "os_info",
  "os_pipe",
- "paste",
  "percent-encoding",
  "png",
  "rand 0.8.5",

+ 10 - 0
examples/api/src-tauri/src/lib.rs

@@ -94,6 +94,16 @@ impl AppBuilder {
         #[cfg(debug_assertions)]
         window.open_devtools();
 
+        #[cfg(target_os = "android")]
+        {
+          let response = app.run_android_plugin::<serde_json::Value, serde_json::Value>(
+            "sample",
+            "ping",
+            serde_json::Value::default(),
+          );
+          println!("got response: {:?}", response);
+        }
+
         std::thread::spawn(|| {
           let server = match tiny_http::Server::http("localhost:3003") {
             Ok(s) => s,

+ 10 - 10
tooling/cli/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt

@@ -7,7 +7,7 @@ import app.tauri.Logger
 
 class Invoke(
   private val sendResponse: (succcess: PluginResult?, error: PluginResult?) -> Unit,
-  val data: JSObject?) {
+  val data: JSObject) {
 
   fun resolve(data: JSObject?) {
     val result = PluginResult(data)
@@ -68,7 +68,7 @@ class Invoke(
   }
 
   fun getString(name: String, defaultValue: String?): String? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     return if (value is String) {
       value
     } else defaultValue
@@ -79,7 +79,7 @@ class Invoke(
   }
 
   fun getInt(name: String, defaultValue: Int?): Int? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     return if (value is Int) {
       value
     } else defaultValue
@@ -90,7 +90,7 @@ class Invoke(
   }
 
   fun getLong(name: String, defaultValue: Long?): Long? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     return if (value is Long) {
       value
     } else defaultValue
@@ -101,7 +101,7 @@ class Invoke(
   }
 
   fun getFloat(name: String, defaultValue: Float?): Float? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     if (value is Float) {
       return value
     }
@@ -118,7 +118,7 @@ class Invoke(
   }
 
   fun getDouble(name: String, defaultValue: Double?): Double? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     if (value is Double) {
       return value
     }
@@ -135,7 +135,7 @@ class Invoke(
   }
 
   fun getBoolean(name: String, defaultValue: Boolean?): Boolean? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     return if (value is Boolean) {
       value
     } else defaultValue
@@ -146,7 +146,7 @@ class Invoke(
   }
 
   fun getObject(name: String, defaultValue: JSObject?): JSObject? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     return if (value is JSObject) value else defaultValue
   }
 
@@ -155,11 +155,11 @@ class Invoke(
   }
 
   fun getArray(name: String, defaultValue: JSArray?): JSArray? {
-    val value = data!!.opt(name) ?: return defaultValue
+    val value = data.opt(name) ?: return defaultValue
     return if (value is JSArray) value else defaultValue
   }
 
   fun hasOption(name: String): Boolean {
-    return data!!.has(name)
+    return data.has(name)
   }
 }

+ 21 - 6
tooling/cli/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt

@@ -22,12 +22,7 @@ class PluginManager {
     }
   }
 
-  fun postMessage(webView: WebView, pluginId: String, methodName: String, data: JSObject, callback: Long, error: Long) {
-    Logger.verbose(
-      Logger.tags("Plugin"),
-      "Tauri plugin: pluginId: $pluginId, methodName: $methodName, callback: $callback, error: $error"
-    )
-
+  fun postIpcMessage(webView: WebView, pluginId: String, methodName: String, data: JSObject, callback: Long, error: Long) {
     val invoke = Invoke({ successResult, errorResult ->
       val (fn, result) = if (errorResult == null) Pair(callback, successResult) else Pair(
         error,
@@ -35,6 +30,24 @@ class PluginManager {
       )
       webView.evaluateJavascript("window['_$fn']($result)", null)
     }, data)
+
+    dispatchPluginMessage(invoke, pluginId, methodName)
+  }
+
+  fun runPluginMethod(id: Int, pluginId: String, methodName: String, data: JSObject) {
+    val invoke = Invoke({ successResult, errorResult ->
+      handlePluginResponse(id, successResult?.toString(), errorResult?.toString())
+    }, data)
+
+    dispatchPluginMessage(invoke, pluginId, methodName)
+  }
+
+  private fun dispatchPluginMessage(invoke: Invoke, pluginId: String, methodName: String) {
+    Logger.verbose(
+      Logger.tags("Plugin"),
+      "Tauri plugin: pluginId: $pluginId, methodName: $methodName"
+    )
+
     try {
       val plugin = plugins[pluginId]
       if (plugin == null) {
@@ -46,4 +59,6 @@ class PluginManager {
       invoke.reject(e.toString())
     }
   }
+
+  private external fun handlePluginResponse(id: Int, success: String?, error: String?)
 }