Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed text input on Android, fixed for older Android versions #7204

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/backends/android-activity/androidwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@ impl AndroidWindowAdapter {
None,
)?;
self.resize();

// FIXME: I don't know why this is the case, but without this, 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")));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this also work on windows without the .exe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have tested it, it works on Windows 10 indeed.

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 <[email protected]>
// 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);
}
});
}
}
72 changes: 46 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,12 @@ 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))?;
// Safety: `get_clipboard` returns a non-null Java string object.
let string = unsafe { jni_get_string(j_string.as_ref(), env) }?.into();
Ok(string)
})
}
Expand All @@ -340,12 +353,10 @@ 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 };
// Safety: `SlintEditable.toString()` returns a non-null Java string object.
let Ok(java_str) = (unsafe { 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 +526,12 @@ extern "system" fn Java_SlintAndroidJavaHelper_popupMenuAction(
})
.unwrap()
}

/// workaround before <https://github.com/jni-rs/jni-rs/pull/557> is merged.
unsafe fn jni_get_string<'e, 'a>(
obj: &'a JObject<'a>,
env: &mut JNIEnv<'e>,
) -> Result<jni::strings::JavaStr<'e, 'a, 'a>, jni::errors::Error> {
let j_string: &jni::objects::JString<'_> = obj.into();
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
Loading