From a60c3b39b985b4a459797199c1dd418252d73552 Mon Sep 17 00:00:00 2001 From: wu bobo Date: Fri, 3 Jan 2025 10:18:33 +0000 Subject: [PATCH] Fixed text input on Android, fixed for older Android versions (#7204) * Fixes for older Android versions (6.0 to 9.0) * Fix build script for Android (#4920) * Fix clipboard operations on Android * Fix TextEdit crashing on Android (#7203) * Remove unsafe calls added in 'Fixes for older Android versions' * Update a comment in androidwindowadapter.rs Fixes #7182 --- .../android-activity/androidwindowadapter.rs | 6 ++ internal/backends/android-activity/build.rs | 34 ++++---- .../java/SlintAndroidJavaHelper.java | 50 +++++++++--- .../backends/android-activity/javahelper.rs | 80 +++++++++++++------ internal/renderers/skia/textlayout.rs | 7 ++ 5 files changed, 124 insertions(+), 53 deletions(-) diff --git a/internal/backends/android-activity/androidwindowadapter.rs b/internal/backends/android-activity/androidwindowadapter.rs index 62a85b5ee12..5819250e286 100644 --- a/internal/backends/android-activity/androidwindowadapter.rs +++ b/internal/backends/android-activity/androidwindowadapter.rs @@ -224,6 +224,12 @@ impl AndroidWindowAdapter { None, )?; self.resize(); + + // Fixes a problem for old Android versions: the soft input always prompt out on startup. + #[cfg(feature = "native-activity")] + self.java_helper + .show_or_hide_soft_input(false) + .unwrap_or_else(|e| print_jni_error(&self.app, e)); } } PollEvent::Main( diff --git a/internal/backends/android-activity/build.rs b/internal/backends/android-activity/build.rs index a101209e927..46fc7ec799b 100644 --- a/internal/backends/android-activity/build.rs +++ b/internal/backends/android-activity/build.rs @@ -27,16 +27,14 @@ fn main() { 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"), + // Try to locate javac and java + let javac_java = env_var("JAVA_HOME") + .map(|home| PathBuf::from(home).join("bin")) + .map(|bin| (bin.join("javac"), bin.join("java"))); + let (javac_path, java_path) = if let Ok(ref javac_java) = javac_java { + (javac_java.0.to_str().unwrap(), javac_java.1.to_str().unwrap()) + } else { + ("javac", "java") }; let handle_java_err = |err: std::io::Error| { @@ -87,11 +85,9 @@ fn main() { } // Convert the .class file into a .dex file - let d8_path = find_latest_version( - android_home.join("build-tools"), - if cfg!(windows) { "d8.bat" } else { "d8" }, - ) - .expect("d8 tool not found"); + let d8_path = find_latest_version(android_home.join("build-tools"), "lib") + .map(|path| path.join("d8.jar")) + .expect("d8 tool not found"); // collect all the *.class files let classes = fs::read_dir(&out_class) @@ -101,12 +97,18 @@ fn main() { .map(|entry| entry.path()) .collect::>(); - let o = Command::new(&d8_path) + let o = Command::new(&java_path) + // class path of D8 itself + .arg("-classpath") + .arg(&d8_path) + .arg("com.android.tools.r8.D8") + // class path of D8's input .arg("--classpath") .arg(&out_class) .args(&classes) .arg("--output") .arg(out_dir.as_os_str()) + // workaround for the DexClassLoader in Android 7.x .arg("--min-api") .arg("20") .output() diff --git a/internal/backends/android-activity/java/SlintAndroidJavaHelper.java b/internal/backends/android-activity/java/SlintAndroidJavaHelper.java index bcfe8150638..d47ea98e516 100644 --- a/internal/backends/android-activity/java/SlintAndroidJavaHelper.java +++ b/internal/backends/android-activity/java/SlintAndroidJavaHelper.java @@ -1,6 +1,8 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; import android.view.ActionMode; import android.view.Menu; import android.view.MenuItem; @@ -16,6 +18,7 @@ import android.content.res.TypedArray; import android.graphics.BlendMode; import android.graphics.BlendModeColorFilter; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.Editable; @@ -102,7 +105,11 @@ public void hide() { public void setHandleColor(int color) { Drawable drawable = getDrawable(); if (drawable != null) { - drawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN)); + if (android.os.Build.VERSION.SDK_INT >= 29) { + drawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN)); + } else { + drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } setImageDrawable(drawable); } } @@ -295,8 +302,9 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.setTitle(null); mode.setSubtitle(null); mode.setTitleOptionalHint(true); - - menu.setGroupDividerEnabled(true); + if (android.os.Build.VERSION.SDK_INT >= 28) { + menu.setGroupDividerEnabled(true); + } final TypedArray a = getContext().obtainStyledAttributes(new int[] { android.R.attr.actionModeCutDrawable, @@ -340,6 +348,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public void onDestroyActionMode(ActionMode action) { } + // Introduced in API level 23 @Override public void onGetContentRect(ActionMode mode, View view, Rect outRect) { outRect.set(selectionRect); @@ -467,6 +476,7 @@ public int color_scheme() { public Rect get_view_rect() { Rect rect = new Rect(); mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); + // Note: `View.getRootWindowInsets` requires API level 23 or above WindowInsets insets = mActivity.getWindow().getDecorView().getRootView().getRootWindowInsets(); if (insets != null) { int dx = rect.left - insets.getSystemWindowInsetLeft(); @@ -490,17 +500,35 @@ public void run() { } public String get_clipboard() { - ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard.hasPrimaryClip()) { - ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); - return item.getText().toString(); + FutureTask future = new FutureTask<>(new Callable() { + @Override + public String call() throws Exception { + ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard.hasPrimaryClip()) { + ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); + return item.getText().toString(); + } + return ""; + } + }); + + mActivity.runOnUiThread(future); + try { + return future.get(); // Wait for the result and return it + } catch (Exception e) { + e.printStackTrace(); + return ""; } - return ""; } public void set_clipboard(String text) { - ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(null, text); - clipboard.setPrimaryClip(clip); + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(null, text); + clipboard.setPrimaryClip(clip); + } + }); } } diff --git a/internal/backends/android-activity/javahelper.rs b/internal/backends/android-activity/javahelper.rs index 8d425e8b980..1a861108aeb 100644 --- a/internal/backends/android-activity/javahelper.rs +++ b/internal/backends/android-activity/javahelper.rs @@ -7,7 +7,7 @@ use i_slint_core::graphics::{euclid, Color}; use i_slint_core::items::{ColorScheme, InputType}; use i_slint_core::platform::WindowAdapter; use i_slint_core::SharedString; -use jni::objects::{JClass, JObject, JString, JValue, JValueGen}; +use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::{jboolean, jint}; use jni::JNIEnv; use std::time::Duration; @@ -35,7 +35,7 @@ fn load_java_helper(app: &AndroidApp) -> Result Result Result Result Result Result 0 as jint, }; + env.delete_local_ref(class_it)?; let cur_origin = data.cursor_rect_origin.to_physical(scale_factor); let anchor_origin = data.anchor_point.to_physical(scale_factor); @@ -269,6 +275,7 @@ impl JavaHelper { self.with_jni_env(|env, helper| { let rect = env.call_method(helper, "get_view_rect", "()Landroid/graphics/Rect;", &[])?.l()?; + let rect = env.auto_local(rect); let x = env.get_field(&rect, "left", "I")?.i()?; let y = env.get_field(&rect, "top", "I")?.i()?; let width = env.get_field(&rect, "right", "I")?.i()? - x; @@ -291,10 +298,13 @@ impl JavaHelper { pub fn long_press_timeout(&self) -> Result { self.with_jni_env(|env, _helper| { - let view_configuration = env.find_class("android/view/ViewConfiguration")?; - let view_configuration = JClass::from(view_configuration); let long_press_timeout = env - .call_static_method(view_configuration, "getLongPressTimeout", "()I", &[])? + .call_static_method( + "android/view/ViewConfiguration", + "getLongPressTimeout", + "()I", + &[], + )? .i()?; Ok(Duration::from_millis(long_press_timeout as _)) }) @@ -309,7 +319,7 @@ impl JavaHelper { pub fn set_clipboard(&self, text: &str) -> Result<(), jni::errors::Error> { self.with_jni_env(|env, helper| { - let text = &env.new_string(text)?; + let text = env.auto_local(env.new_string(text)?); env.call_method( helper, "set_clipboard", @@ -322,9 +332,11 @@ impl JavaHelper { pub fn get_clipboard(&self) -> Result { self.with_jni_env(|env, helper| { - let j_string = - env.call_method(helper, "get_clipboard", "()Ljava/lang/String;", &[])?.l()?; - let string = env.get_string(&j_string.into())?.into(); + let j_string = env + .call_method(helper, "get_clipboard", "()Ljava/lang/String;", &[])? + .l() + .map(|l| env.auto_local(l))?; + let string = jni_get_string(j_string.as_ref(), env)?.into(); Ok(string) }) } @@ -340,12 +352,9 @@ extern "system" fn Java_SlintAndroidJavaHelper_updateText( preedit_start: jint, preedit_end: jint, ) { - fn make_shared_string(env: &mut JNIEnv, string: &JString) -> Option { - let java_str = env.get_string(&string).ok()?; - let decoded: std::borrow::Cow = (&java_str).into(); - Some(SharedString::from(decoded.as_ref())) - } - let Some(text) = make_shared_string(&mut env, &text) else { return }; + let Ok(java_str) = jni_get_string(&text, &mut env) else { return }; + let decoded: std::borrow::Cow = (&java_str).into(); + let text = SharedString::from(decoded.as_ref()); let cursor_position = convert_utf16_index_to_utf8(&text, cursor_position as usize); let anchor_position = convert_utf16_index_to_utf8(&text, anchor_position as usize); @@ -515,3 +524,22 @@ extern "system" fn Java_SlintAndroidJavaHelper_popupMenuAction( }) .unwrap() } + +/// Workaround before is merged. +fn jni_get_string<'e, 'a>( + obj: &'a JObject<'a>, + env: &mut JNIEnv<'e>, +) -> Result, jni::errors::Error> { + use jni::errors::{Error::*, JniError}; + + let string_class = env.find_class("java/lang/String")?; + let obj_class = env.get_object_class(obj)?; + let obj_class = env.auto_local(obj_class); + if !env.is_assignable_from(string_class, obj_class)? { + return Err(JniCall(JniError::InvalidArguments)); + } + + let j_string: &jni::objects::JString<'_> = obj.into(); + // SAFETY: We check that the passed in Object is actually a java.lang.String + unsafe { env.get_string_unchecked(j_string) } +} diff --git a/internal/renderers/skia/textlayout.rs b/internal/renderers/skia/textlayout.rs index ce107286f58..131ad132d37 100644 --- a/internal/renderers/skia/textlayout.rs +++ b/internal/renderers/skia/textlayout.rs @@ -269,6 +269,13 @@ pub fn cursor_rect( ); } + // This is needed in case of the cursor is moving to the end of the text (#7203). + let cursor_pos = cursor_pos.min(string.len()); + // Not doing this check may cause crashing with non-ASCII text. + if !string.is_char_boundary(cursor_pos) { + return Default::default(); + } + // SkParagraph::getRectsForRange() does not report the text box of a trailing newline // correctly. Use the last line's metrics to get the correct coordinates (#3590). if cursor_pos == string.len()