Skip to content

Commit

Permalink
Fixed text input on Android, fixed for older Android versions (#7204)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wuwbobo2021 authored Jan 3, 2025
1 parent 1ed40e6 commit a60c3b3
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 53 deletions.
6 changes: 6 additions & 0 deletions internal/backends/android-activity/androidwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
34 changes: 18 additions & 16 deletions internal/backends/android-activity/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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)
Expand All @@ -101,12 +97,18 @@ fn main() {
.map(|entry| entry.path())
.collect::<Vec<_>>();

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()
Expand Down
50 changes: 39 additions & 11 deletions internal/backends/android-activity/java/SlintAndroidJavaHelper.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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<String> future = new FutureTask<>(new Callable<String>() {
@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);
}
});
}
}
80 changes: 54 additions & 26 deletions internal/backends/android-activity/javahelper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,7 +35,7 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
let dex_buffer =
unsafe { env.new_direct_byte_buffer(dex_data.as_ptr() as *mut _, dex_data.len()).unwrap() };

let parent_dex_loader = env
let parent_class_loader = env
.call_method(&native_activity, "getClassLoader", "()Ljava/lang/ClassLoader;", &[])?
.l()?;

Expand All @@ -46,7 +46,7 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
env.new_object(
"dalvik/system/InMemoryDexClassLoader",
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V",
&[JValue::Object(&dex_buffer), JValue::Object(&parent_dex_loader)],
&[JValue::Object(&dex_buffer), JValue::Object(&parent_class_loader)],
)?
} else {
// writes the dex data into the application internal storage
Expand All @@ -59,13 +59,12 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
&native_activity,
"getDir",
"(Ljava/lang/String;I)Ljava/io/File;",
&[JValue::Object(&dex_dir), JValueGen::Int(0)],
&[JValue::Object(&dex_dir), JValue::from(0 as jint)],
)?
.l()?;
let dex_name = env.new_string(env!("CARGO_CRATE_NAME").to_string() + ".dex")?;
let file_class = env.find_class("java/io/File")?;
let dex_path = env.new_object(
file_class,
"java/io/File",
"(Ljava/io/File;Ljava/lang/String;)V",
&[JValue::Object(&dex_dir_path), JValue::Object(&dex_name)],
)?;
Expand All @@ -79,22 +78,28 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
&native_activity,
"getDir",
"(Ljava/lang/String;I)Ljava/io/File;",
&[JValue::Object(&out_dex_dir), JValueGen::Int(0)],
&[JValue::Object(&out_dex_dir), JValue::from(0 as jint)],
)?
.l()?;
let out_dex_dir_path = env
.call_method(&out_dex_dir_path, "getAbsolutePath", "()Ljava/lang/String;", &[])?
.l()?;

// writes the dex data
let output_stream_class = env.find_class("java/io/FileOutputStream")?;
let write_stream =
env.new_object(output_stream_class, "(Ljava/lang/String;)V", &[(&dex_path).into()])?;
let write_stream = env.new_object(
"java/io/FileOutputStream",
"(Ljava/lang/String;)V",
&[(&dex_path).into()],
)?;
env.call_method(
&write_stream,
"write",
"([BII)V",
&[JValue::Object(&dex_byte_array), JValueGen::Int(0), JValueGen::Int(dex_data_len)],
&[
JValue::Object(&dex_byte_array),
JValue::from(0 as jint),
JValue::from(dex_data_len as jint),
],
)?;
env.call_method(&write_stream, "close", "()V", &[])?;

Expand All @@ -106,7 +111,7 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
JValue::Object(&dex_path),
JValue::Object(&out_dex_dir_path),
JValue::Object(&JObject::null()),
JValue::Object(&parent_dex_loader),
JValue::Object(&parent_class_loader),
],
)?
};
Expand Down Expand Up @@ -205,7 +210,7 @@ impl JavaHelper {
}

let to_utf16 = |x| convert_utf8_index_to_utf16(&text, x as usize);
let text = &env.new_string(text.as_str())?;
let text = &env.auto_local(env.new_string(text.as_str())?);

let class_it = env.find_class("android/text/InputType")?;
let input_type = match data.input_type {
Expand All @@ -223,6 +228,7 @@ impl JavaHelper {
}
_ => 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);
Expand Down Expand Up @@ -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;
Expand All @@ -291,10 +298,13 @@ impl JavaHelper {

pub fn long_press_timeout(&self) -> Result<Duration, jni::errors::Error> {
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 _))
})
Expand All @@ -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",
Expand All @@ -322,9 +332,11 @@ impl JavaHelper {

pub fn get_clipboard(&self) -> Result<String, jni::errors::Error> {
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)
})
}
Expand All @@ -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<SharedString> {
let java_str = env.get_string(&string).ok()?;
let decoded: std::borrow::Cow<str> = (&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<str> = (&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);
Expand Down Expand Up @@ -515,3 +524,22 @@ extern "system" fn Java_SlintAndroidJavaHelper_popupMenuAction(
})
.unwrap()
}

/// Workaround before <https://github.com/jni-rs/jni-rs/pull/557> is merged.
fn jni_get_string<'e, 'a>(
obj: &'a JObject<'a>,
env: &mut JNIEnv<'e>,
) -> Result<jni::strings::JavaStr<'e, 'a, 'a>, 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) }
}
7 changes: 7 additions & 0 deletions internal/renderers/skia/textlayout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit a60c3b3

Please sign in to comment.