diff --git a/internal/backends/android-activity/build.rs b/internal/backends/android-activity/build.rs new file mode 100644 index 00000000000..9ca69088c59 --- /dev/null +++ b/internal/backends/android-activity/build.rs @@ -0,0 +1,93 @@ +use std::path::PathBuf; +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +use std::process::Command; +use std::{env, fs}; + +fn main() { + if !env::var("TARGET").unwrap().contains("android") { + return; + } + + let out_dir = env::var("OUT_DIR").unwrap(); + + let slint_path = "dev/slint/android-activity"; + let java_class = "SlintAndroidJavaHelper"; + + let out_class = format!("{out_dir}/java/{slint_path}"); + + let android_home = + PathBuf::from(env_var("ANDROID_HOME").or_else(|_| env_var("ANDROID_SDK_ROOT")).expect( + "Please set the ANDROID_HOME environment variable to the path of the Android SDK", + )); + + let classpath = find_latest_version(android_home.join("platforms"), "android.jar") + .expect("No Android platforms found"); + + // Try to locate javac + let javac_path = match env_var("JAVA_HOME") { + Ok(val) => { + if cfg!(windows) { + format!("{}\\bin\\javac.exe", val) + } else { + format!("{}/bin/javac", val) + } + } + Err(_) => String::from("javac"), + }; + + // Compile the Java file into a .class file + let o = Command::new(&javac_path) + .arg(format!("java/{java_class}.java")) + .arg("-d") + .arg(&out_class) + .arg("-classpath").arg(&classpath) + .arg("--release") + .arg("8") + .output() + .unwrap_or_else(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + panic!("Could not locate the java compiler. Please ensure that the JAVA_HOME environment variable is set correctly.") + } else { + panic!("Could not run {javac_path}: {err}") + } + }); + + if !o.status.success() { + panic!("Java compilation failed: {}", String::from_utf8_lossy(&o.stderr)); + } + + // Convert the .class file into a .dex file + let d8_path = find_latest_version( + android_home.join("build-tools"), + if cfg!(windows) { "d8.exe" } else { "d8" }, + ) + .expect("d8 tool not found"); + let o = Command::new(&d8_path) + .args(&["--classpath", &out_class]) + .arg(format!("{out_class}/{java_class}.class")) + .arg("--output") + .arg(&out_dir) + .output() + .unwrap_or_else(|err| panic!("Error running {d8_path:?}: {err}")); + + if !o.status.success() { + panic!("Dex conversion failed: {}", String::from_utf8_lossy(&o.stderr)); + } + + println!("cargo:rerun-if-changed=java/{java_class}.java"); +} + +fn env_var(var: &str) -> Result { + println!("cargo:rerun-if-env-changed={}", var); + env::var(var) +} + +fn find_latest_version(base: PathBuf, arg: &str) -> Option { + fs::read_dir(base) + .ok()? + .filter_map(|entry| Some(entry.ok()?.path().join(arg))) + .filter(|path| path.exists()) + .max() +} diff --git a/internal/backends/android-activity/java/SlintAndroidJavaHelper.java b/internal/backends/android-activity/java/SlintAndroidJavaHelper.java new file mode 100644 index 00000000000..caff3ff8f19 --- /dev/null +++ b/internal/backends/android-activity/java/SlintAndroidJavaHelper.java @@ -0,0 +1,24 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial + +import android.view.View; +import android.content.Context; +import android.view.inputmethod.InputMethodManager; +import android.app.Activity; + +public class SlintAndroidJavaHelper { + Activity mActivity; + + public SlintAndroidJavaHelper(Activity activity) { + this.mActivity = activity; + } + public void show_keyboard() { + InputMethodManager imm = (InputMethodManager)mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mActivity.getWindow().getDecorView(), 0); + } + public void hide_keyboard() { + InputMethodManager imm = (InputMethodManager)mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mActivity.getWindow().getDecorView().getWindowToken(), 0); + } + +} diff --git a/internal/backends/android-activity/lib.rs b/internal/backends/android-activity/lib.rs index 79aa4ce5bbd..1d950809d6c 100644 --- a/internal/backends/android-activity/lib.rs +++ b/internal/backends/android-activity/lib.rs @@ -44,6 +44,7 @@ impl AndroidPlatform { /// } /// ``` pub fn new(app: AndroidApp) -> Self { + let slint_java_helper = SlintJavaHelper::new(&app).unwrap(); Self { app: app.clone(), window: Rc::::new_cyclic(|w| AndroidWindowAdapter { @@ -52,6 +53,7 @@ impl AndroidPlatform { renderer: i_slint_renderer_skia::SkiaRenderer::default(), event_queue: Default::default(), pending_redraw: Default::default(), + slint_java_helper, }), event_listener: None, } @@ -166,6 +168,7 @@ struct AndroidWindowAdapter { renderer: i_slint_renderer_skia::SkiaRenderer, event_queue: EventQueue, pending_redraw: Cell, + slint_java_helper: SlintJavaHelper, } impl WindowAdapter for AndroidWindowAdapter { @@ -201,7 +204,7 @@ impl i_slint_core::window::WindowAdapterInternal for AndroidWindowAdapter { #[cfg(not(feature = "native-activity"))] self.app.show_soft_input(true); #[cfg(feature = "native-activity")] - show_or_hide_soft_input(&self.app, true).unwrap(); + show_or_hide_soft_input(&self.slint_java_helper, &self.app, true).unwrap(); props } i_slint_core::window::InputMethodRequest::Update(props) => props, @@ -209,7 +212,7 @@ impl i_slint_core::window::WindowAdapterInternal for AndroidWindowAdapter { #[cfg(not(feature = "native-activity"))] self.app.hide_soft_input(true); #[cfg(feature = "native-activity")] - show_or_hide_soft_input(&self.app, false).unwrap(); + show_or_hide_soft_input(&self.slint_java_helper, &self.app, false).unwrap(); return; } _ => return, @@ -724,54 +727,73 @@ fn map_key_code(code: android_activity::input::Keycode) -> Option } } +struct SlintJavaHelper(#[cfg(feature = "native-activity")] jni::objects::GlobalRef); + +impl SlintJavaHelper { + fn new(_app: &AndroidApp) -> Result { + Ok(Self( + #[cfg(feature = "native-activity")] + load_java_helper(_app)?, + )) + } +} + #[cfg(feature = "native-activity")] /// Unfortunately, the way that the android-activity crate uses to show or hide the virtual keyboard doesn't /// work with native-activity. So do it manually with JNI -fn show_or_hide_soft_input(app: &AndroidApp, show: bool) -> Result<(), jni::errors::Error> { - use jni::objects::{JObject, JValue}; +fn show_or_hide_soft_input( + helper: &SlintJavaHelper, + app: &AndroidApp, + show: bool, +) -> Result<(), jni::errors::Error> { + // Safety: as documented in android-activity to obtain a jni::JavaVM + let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }?; + let mut env = vm.attach_current_thread()?; + let helper = helper.0.as_obj(); + if show { + env.call_method(helper, "show_keyboard", "()V", &[])?; + } else { + env.call_method(helper, "hide_keyboard", "()V", &[])?; + }; + Ok(()) +} +#[cfg(feature = "native-activity")] +fn load_java_helper(app: &AndroidApp) -> Result { + use jni::objects::{JObject, JValue}; // Safety: as documented in android-activity to obtain a jni::JavaVM let vm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as *mut _) }?; + let native_activity = unsafe { JObject::from_raw(app.activity_as_ptr() as *mut _) }; + let mut env = vm.attach_current_thread()?; - // https://stackoverflow.com/questions/5864790/how-to-show-the-soft-keyboard-on-native-activity + let dex_data = include_bytes!(concat!(env!("OUT_DIR"), "/classes.dex")); - let native_activity = unsafe { JObject::from_raw(app.activity_as_ptr() as *mut _) }; + // Safety: dex_data is 'static and the InMemoryDexClassLoader will not mutate it it + let dex_buffer = + unsafe { env.new_direct_byte_buffer(dex_data.as_ptr() as *mut _, dex_data.len()).unwrap() }; - let class_context = env.find_class("android/content/Context")?; - let input_method_service = - env.get_static_field(class_context, "INPUT_METHOD_SERVICE", "Ljava/lang/String;")?.l()?; + let dex_loader = env.new_object( + "dalvik/system/InMemoryDexClassLoader", + "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V", + &[JValue::Object(&dex_buffer), JValue::Object(&JObject::null())], + )?; - let input_method_manager = env + let class_name = env.new_string("SlintAndroidJavaHelper").unwrap(); + let helper_class = env .call_method( - &native_activity, - "getSystemService", - "(Ljava/lang/String;)Ljava/lang/Object;", - &[JValue::Object(&input_method_service)], + dex_loader, + "findClass", + "(Ljava/lang/String;)Ljava/lang/Class;", + &[JValue::Object(&class_name)], )? .l()?; - let window = - env.call_method(native_activity, "getWindow", "()Landroid/view/Window;", &[])?.l()?; - let decor_view = env.call_method(window, "getDecorView", "()Landroid/view/View;", &[])?.l()?; - - if show { - env.call_method( - input_method_manager, - "showSoftInput", - "(Landroid/view/View;I)Z", - &[JValue::Object(&decor_view), 0.into()], - )?; - } else { - let binder = - env.call_method(decor_view, "getWindowToken", "()Landroid/os/IBinder;", &[])?.l()?; - env.call_method( - input_method_manager, - "hideSoftInputFromWindow", - "(Landroid/os/IBinder;I)Z", - &[JValue::Object(&binder), 0.into()], - )?; - }; - - Ok(()) + let helper_class: jni::objects::JClass = helper_class.into(); + let helper_instance = env.new_object( + helper_class, + "(Landroid/app/Activity;)V", + &[JValue::Object(&native_activity)], + )?; + Ok(env.new_global_ref(&helper_instance)?) } diff --git a/xtask/src/license_headers_check.rs b/xtask/src/license_headers_check.rs index 4f73bbd7f2f..1b413ef0c4a 100644 --- a/xtask/src/license_headers_check.rs +++ b/xtask/src/license_headers_check.rs @@ -451,6 +451,7 @@ lazy_static! { ("\\.npmignore$", LicenseLocation::NoLicense), ("\\.h$", LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), ("\\.html$", LicenseLocation::NoLicense), + ("\\.java$", LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), ("\\.jpg$", LicenseLocation::NoLicense), ("\\.js$", LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), ("\\.json$", LicenseLocation::NoLicense),