diff --git a/fml/platform/android/jni_util.cc b/fml/platform/android/jni_util.cc index b84a351f1c913..24903dad3b6d3 100644 --- a/fml/platform/android/jni_util.cc +++ b/fml/platform/android/jni_util.cc @@ -165,5 +165,21 @@ std::string GetJavaExceptionInfo(JNIEnv* env, jthrowable java_throwable) { return JavaStringToString(env, exception_string.obj()); } +void ThrowException(JNIEnv* env, const char* class_name, const char* message) { + jclass clazz = env->FindClass(class_name); + FML_DCHECK(clazz); + env->ThrowNew(clazz, message); +} + +ScopedJavaStringChars::ScopedJavaStringChars(JNIEnv* env, jstring str) + : env_(env), str_(str) { + chars_ = env_->GetStringChars(str_, nullptr); + FML_DCHECK(chars_); +} + +ScopedJavaStringChars::~ScopedJavaStringChars() { + env_->ReleaseStringChars(str_, chars_); +} + } // namespace jni } // namespace fml diff --git a/fml/platform/android/jni_util.h b/fml/platform/android/jni_util.h index 9fe32242c503c..cc50f4c742b5d 100644 --- a/fml/platform/android/jni_util.h +++ b/fml/platform/android/jni_util.h @@ -38,6 +38,21 @@ bool ClearException(JNIEnv* env); std::string GetJavaExceptionInfo(JNIEnv* env, jthrowable java_throwable); +void ThrowException(JNIEnv* env, const char* class_name, const char* message); + +class ScopedJavaStringChars { + public: + ScopedJavaStringChars(JNIEnv* env, jstring str); + ~ScopedJavaStringChars(); + + const jchar* chars() { return chars_; } + + private: + JNIEnv* env_; + jstring str_; + const jchar* chars_; +}; + } // namespace jni } // namespace fml diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index e599281f84137..ca1f64b4e1995 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -146,6 +146,15 @@ public static native void nativeOnVsync( @NonNull public static native FlutterCallbackInformation nativeLookupCallbackInformation(long handle); + /** + * Return the index of the first grapheme cluster preceding {@code offset} in {@code text}. + * + *
This is similar to {@link android.text.TextUtils#getOffsetBefore(CharSequence, int, int)}
+ * but it uses the engine's ICU library which is up to date and acts consistently on all versions
+ * of Android.
+ */
+ public static native int nativeGetTextOffsetBefore(String text, int offset);
+
@Nullable private Long nativePlatformViewId;
@Nullable private AccessibilityDelegate accessibilityDelegate;
@Nullable private PlatformMessageHandler platformMessageHandler;
diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
index 8a50cde7b714b..fa921677771fe 100644
--- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
+++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
@@ -13,7 +13,6 @@
import android.text.Layout;
import android.text.Selection;
import android.text.TextPaint;
-import android.text.method.TextKeyListener;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
@@ -21,7 +20,9 @@
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputMethodManager;
+import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
+import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
class InputConnectionAdaptor extends BaseInputConnection {
@@ -111,6 +112,23 @@ public InputConnectionAdaptor(
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
}
+ interface GetOffsetBefore {
+ int getOffsetBefore(String text, int offset);
+ }
+
+ private GetOffsetBefore getOffsetBefore =
+ new GetOffsetBefore() {
+ @Override
+ public int getOffsetBefore(String text, int offset) {
+ return FlutterJNI.nativeGetTextOffsetBefore(text, offset);
+ }
+ };
+
+ @VisibleForTesting
+ void setGetOffsetBefore(GetOffsetBefore value) {
+ getOffsetBefore = value;
+ }
+
// Send the current state of the editable to Flutter.
private void updateEditingState() {
// If the IME is in the middle of a batch edit, then wait until it completes.
@@ -270,18 +288,16 @@ public boolean sendKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
int selEnd = clampIndexToEditable(Selection.getSelectionEnd(mEditable), mEditable);
+ if (selStart == selEnd && selStart > 0) {
+ // Extend selection to the left of the last character.
+ selStart = getOffsetBefore.getOffsetBefore(mEditable.toString(), selStart);
+ }
if (selEnd > selStart) {
// Delete the selection.
Selection.setSelection(mEditable, selStart);
mEditable.delete(selStart, selEnd);
updateEditingState();
return true;
- } else if (selStart > 0) {
- if (TextKeyListener.getInstance().onKeyDown(null, mEditable, event.getKeyCode(), event)) {
- updateEditingState();
- return true;
- }
- return false;
}
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
int selStart = Selection.getSelectionStart(mEditable);
diff --git a/shell/platform/android/platform_view_android_jni.cc b/shell/platform/android/platform_view_android_jni.cc
index 8b70d145dc439..45c3806f21dac 100644
--- a/shell/platform/android/platform_view_android_jni.cc
+++ b/shell/platform/android/platform_view_android_jni.cc
@@ -7,6 +7,7 @@
#include