Custom unicode handling for Android backspace via JNI to ICU (#17960)
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
old mode 100644
new mode 100755
index f79af12..7da6048
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -752,6 +752,7 @@
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCodec.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 1ed6886..8f5a6e3 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -201,6 +201,7 @@
"io/flutter/plugin/common/StandardMessageCodec.java",
"io/flutter/plugin/common/StandardMethodCodec.java",
"io/flutter/plugin/common/StringCodec.java",
+ "io/flutter/plugin/editing/FlutterTextUtils.java",
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
"io/flutter/plugin/editing/TextInputPlugin.java",
"io/flutter/plugin/platform/AccessibilityEventsDelegate.java",
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
index e599281..894ee37 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
@@ -146,6 +146,20 @@
@NonNull
public static native FlutterCallbackInformation nativeLookupCallbackInformation(long handle);
+ // ----- Start FlutterTextUtils Methods ----
+
+ public native boolean nativeFlutterTextUtilsIsEmoji(int codePoint);
+
+ public native boolean nativeFlutterTextUtilsIsEmojiModifier(int codePoint);
+
+ public native boolean nativeFlutterTextUtilsIsEmojiModifierBase(int codePoint);
+
+ public native boolean nativeFlutterTextUtilsIsVariationSelector(int codePoint);
+
+ public native boolean nativeFlutterTextUtilsIsRegionalIndicator(int codePoint);
+
+ // ----- End Engine FlutterTextUtils Methods ----
+
@Nullable private Long nativePlatformViewId;
@Nullable private AccessibilityDelegate accessibilityDelegate;
@Nullable private PlatformMessageHandler platformMessageHandler;
diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java
new file mode 100644
index 0000000..19eb1f0
--- /dev/null
+++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java
@@ -0,0 +1,189 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugin.editing;
+
+import io.flutter.embedding.engine.FlutterJNI;
+
+class FlutterTextUtils {
+ public static final int LINE_FEED = 0x0A;
+ public static final int CARRIAGE_RETURN = 0x0D;
+ public static final int COMBINING_ENCLOSING_KEYCAP = 0x20E3;
+ public static final int CANCEL_TAG = 0xE007F;
+ public static final int ZERO_WIDTH_JOINER = 0x200D;
+ private final FlutterJNI flutterJNI;
+
+ public FlutterTextUtils(FlutterJNI flutterJNI) {
+ this.flutterJNI = flutterJNI;
+ }
+
+ public boolean isEmoji(int codePoint) {
+ return flutterJNI.nativeFlutterTextUtilsIsEmoji(codePoint);
+ }
+
+ public boolean isEmojiModifier(int codePoint) {
+ return flutterJNI.nativeFlutterTextUtilsIsEmojiModifier(codePoint);
+ }
+
+ public boolean isEmojiModifierBase(int codePoint) {
+ return flutterJNI.nativeFlutterTextUtilsIsEmojiModifierBase(codePoint);
+ }
+
+ public boolean isVariationSelector(int codePoint) {
+ return flutterJNI.nativeFlutterTextUtilsIsVariationSelector(codePoint);
+ }
+
+ public boolean isRegionalIndicatorSymbol(int codePoint) {
+ return flutterJNI.nativeFlutterTextUtilsIsRegionalIndicator(codePoint);
+ }
+
+ public boolean isTagSpecChar(int codePoint) {
+ return 0xE0020 <= codePoint && codePoint <= 0xE007E;
+ }
+
+ public boolean isKeycapBase(int codePoint) {
+ return ('0' <= codePoint && codePoint <= '9') || codePoint == '#' || codePoint == '*';
+ }
+
+ /**
+ * Start offset for backspace key or moving left from the current offset. Same methods are also
+ * included in Android APIs but they don't work as expected in API Levels lower than 24. Reference
+ * for the logic in this code is the Android source code.
+ *
+ * @see <a target="_new"
+ * href="https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111">https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111</a>
+ */
+ public int getOffsetBefore(CharSequence text, int offset) {
+ if (offset <= 1) {
+ return 0;
+ }
+
+ int codePoint = Character.codePointBefore(text, offset);
+ int deleteCharCount = Character.charCount(codePoint);
+ int lastOffset = offset - deleteCharCount;
+
+ if (lastOffset == 0) {
+ return 0;
+ }
+
+ // Line Feed
+ if (codePoint == LINE_FEED) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ if (codePoint == CARRIAGE_RETURN) {
+ ++deleteCharCount;
+ }
+ return offset - deleteCharCount;
+ }
+
+ // Flags
+ if (isRegionalIndicatorSymbol(codePoint)) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ int regionalIndicatorSymbolCount = 1;
+ while (lastOffset > 0 && isRegionalIndicatorSymbol(codePoint)) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ regionalIndicatorSymbolCount++;
+ }
+ if (regionalIndicatorSymbolCount % 2 == 0) {
+ deleteCharCount += 2;
+ }
+ return offset - deleteCharCount;
+ }
+
+ // Keycaps
+ if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ if (lastOffset > 0 && isVariationSelector(codePoint)) {
+ int tmpCodePoint = Character.codePointBefore(text, lastOffset);
+ if (isKeycapBase(tmpCodePoint)) {
+ deleteCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
+ }
+ } else if (isKeycapBase(codePoint)) {
+ deleteCharCount += Character.charCount(codePoint);
+ }
+ return offset - deleteCharCount;
+ }
+
+ /**
+ * Following if statements for Emoji tag sequence and Variation selector are skipping these
+ * modifiers for going through the last statement that is for handling emojis. They return the
+ * offset if they don't find proper base characters
+ */
+ // Emoji Tag Sequence
+ if (codePoint == CANCEL_TAG) { // tag_end
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ while (lastOffset > 0 && isTagSpecChar(codePoint)) { // tag_spec
+ deleteCharCount += Character.charCount(codePoint);
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ }
+ if (!isEmoji(codePoint)) { // tag_base not found. Just delete the end.
+ return offset - 2;
+ }
+ deleteCharCount += Character.charCount(codePoint);
+ }
+
+ if (isVariationSelector(codePoint)) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ if (!isEmoji(codePoint)) {
+ return offset - deleteCharCount;
+ }
+ deleteCharCount += Character.charCount(codePoint);
+
+ lastOffset -= deleteCharCount;
+ }
+
+ if (isEmoji(codePoint)) {
+ boolean isZwj = false;
+ int lastSeenVariantSelectorCharCount = 0;
+ do {
+ if (isZwj) {
+ deleteCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1;
+ isZwj = false;
+ }
+ lastSeenVariantSelectorCharCount = 0;
+ if (isEmojiModifier(codePoint)) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ if (lastOffset > 0 && isVariationSelector(codePoint)) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ if (!isEmoji(codePoint)) {
+ return offset - deleteCharCount;
+ }
+ lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
+ lastOffset -= Character.charCount(codePoint);
+ }
+ if (isEmojiModifierBase(codePoint)) {
+ deleteCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
+ }
+ break;
+ }
+
+ if (lastOffset > 0) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ if (codePoint == ZERO_WIDTH_JOINER) {
+ isZwj = true;
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastOffset -= Character.charCount(codePoint);
+ if (lastOffset > 0 && isVariationSelector(codePoint)) {
+ codePoint = Character.codePointBefore(text, lastOffset);
+ lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
+ lastOffset -= Character.charCount(codePoint);
+ }
+ }
+ }
+
+ if (lastOffset == 0) {
+ break;
+ }
+ } while (isZwj && isEmoji(codePoint));
+ }
+
+ return offset - deleteCharCount;
+ }
+}
diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
index a990ff3..b3eea58 100644
--- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
+++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
@@ -16,7 +16,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;
@@ -27,6 +26,7 @@
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import io.flutter.Log;
+import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
class InputConnectionAdaptor extends BaseInputConnection {
@@ -38,6 +38,7 @@
private int mBatchCount;
private InputMethodManager mImm;
private final Layout mLayout;
+ private FlutterTextUtils flutterTextUtils;
// Used to determine if Samsung-specific hacks should be applied.
private final boolean isSamsung;
@@ -96,7 +97,8 @@
int client,
TextInputChannel textInputChannel,
Editable editable,
- EditorInfo editorInfo) {
+ EditorInfo editorInfo,
+ FlutterJNI flutterJNI) {
super(view, true);
mFlutterView = view;
mClient = client;
@@ -104,6 +106,7 @@
mEditable = editable;
mEditorInfo = editorInfo;
mBatchCount = 0;
+ this.flutterTextUtils = new FlutterTextUtils(flutterJNI);
// We create a dummy Layout with max width so that the selection
// shifting acts as if all text were in one line.
mLayout =
@@ -120,6 +123,15 @@
isSamsung = isSamsung();
}
+ public InputConnectionAdaptor(
+ View view,
+ int client,
+ TextInputChannel textInputChannel,
+ Editable editable,
+ EditorInfo editorInfo) {
+ this(view, client, textInputChannel, editable, editorInfo, new FlutterJNI());
+ }
+
// 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.
@@ -315,19 +327,18 @@
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 left of the last character
+ selStart = flutterTextUtils.getOffsetBefore(mEditable, 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;
}
+ return false;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
int selStart = Selection.getSelectionStart(mEditable);
int selEnd = Selection.getSelectionEnd(mEditable);
diff --git a/shell/platform/android/platform_view_android_jni.cc b/shell/platform/android/platform_view_android_jni.cc
index 8b70d14..4f6f925 100644
--- a/shell/platform/android/platform_view_android_jni.cc
+++ b/shell/platform/android/platform_view_android_jni.cc
@@ -7,6 +7,7 @@
#include <android/native_window_jni.h>
#include <utility>
+#include "unicode/uchar.h"
#include "flutter/assets/directory_asset_bundle.h"
#include "flutter/common/settings.h"
@@ -484,6 +485,35 @@
);
}
+static jboolean FlutterTextUtilsIsEmoji(JNIEnv* env,
+ jobject obj,
+ jint codePoint) {
+ return u_hasBinaryProperty(codePoint, UProperty::UCHAR_EMOJI);
+}
+
+static jboolean FlutterTextUtilsIsEmojiModifier(JNIEnv* env,
+ jobject obj,
+ jint codePoint) {
+ return u_hasBinaryProperty(codePoint, UProperty::UCHAR_EMOJI_MODIFIER);
+}
+
+static jboolean FlutterTextUtilsIsEmojiModifierBase(JNIEnv* env,
+ jobject obj,
+ jint codePoint) {
+ return u_hasBinaryProperty(codePoint, UProperty::UCHAR_EMOJI_MODIFIER_BASE);
+}
+
+static jboolean FlutterTextUtilsIsVariationSelector(JNIEnv* env,
+ jobject obj,
+ jint codePoint) {
+ return u_hasBinaryProperty(codePoint, UProperty::UCHAR_VARIATION_SELECTOR);
+}
+
+static jboolean FlutterTextUtilsIsRegionalIndicator(JNIEnv* env,
+ jobject obj,
+ jint codePoint) {
+ return u_hasBinaryProperty(codePoint, UProperty::UCHAR_REGIONAL_INDICATOR);
+}
bool RegisterApi(JNIEnv* env) {
static const JNINativeMethod flutter_jni_methods[] = {
// Start of methods from FlutterJNI
@@ -599,6 +629,36 @@
.signature = "(J)Lio/flutter/view/FlutterCallbackInformation;",
.fnPtr = reinterpret_cast<void*>(&LookupCallbackInformation),
},
+
+ // Start of methods for FlutterTextUtils
+ {
+ .name = "nativeFlutterTextUtilsIsEmoji",
+ .signature = "(I)Z",
+ .fnPtr = reinterpret_cast<void*>(&FlutterTextUtilsIsEmoji),
+ },
+ {
+ .name = "nativeFlutterTextUtilsIsEmojiModifier",
+ .signature = "(I)Z",
+ .fnPtr = reinterpret_cast<void*>(&FlutterTextUtilsIsEmojiModifier),
+ },
+ {
+ .name = "nativeFlutterTextUtilsIsEmojiModifierBase",
+ .signature = "(I)Z",
+ .fnPtr =
+ reinterpret_cast<void*>(&FlutterTextUtilsIsEmojiModifierBase),
+ },
+ {
+ .name = "nativeFlutterTextUtilsIsVariationSelector",
+ .signature = "(I)Z",
+ .fnPtr =
+ reinterpret_cast<void*>(&FlutterTextUtilsIsVariationSelector),
+ },
+ {
+ .name = "nativeFlutterTextUtilsIsRegionalIndicator",
+ .signature = "(I)Z",
+ .fnPtr =
+ reinterpret_cast<void*>(&FlutterTextUtilsIsRegionalIndicator),
+ },
};
if (env->RegisterNatives(g_flutter_jni_class->obj(), flutter_jni_methods,
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
index bbca246..37ae918 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
@@ -3,16 +3,19 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.ClipboardManager;
import android.content.res.AssetManager;
import android.text.Editable;
+import android.text.Emoji;
import android.text.InputType;
import android.text.Selection;
import android.text.SpannableStringBuilder;
@@ -316,7 +319,7 @@
@Test
public void testSendKeyEvent_delKeyDeletesBackward() {
int selStart = 29;
- Editable editable = sampleRtlEditable(selStart, selStart);
+ Editable editable = sampleEditable(selStart, selStart, SAMPLE_RTL_TEXT);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
@@ -334,9 +337,171 @@
assertEquals(Selection.getSelectionStart(editable), 10);
}
+ @Test
+ public void testSendKeyEvent_delKeyDeletesBackwardComplexEmojis() {
+ int selStart = 75;
+ Editable editable = sampleEditable(selStart, selStart, SAMPLE_EMOJI_TEXT);
+ InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
+
+ KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
+ boolean didConsume;
+
+ // Normal Character
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 74);
+
+ // Non-Spacing Mark
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 73);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 72);
+
+ // Keycap
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 69);
+
+ // Keycap with invalid base
+ adaptor.setSelection(68, 68);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 66);
+ adaptor.setSelection(67, 67);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 66);
+
+ // Zero Width Joiner
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 55);
+
+ // Zero Width Joiner with invalid base
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 53);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 52);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 51);
+
+ // ----- Start Emoji Tag Sequence with invalid base testing ----
+ // Delete base tag
+ adaptor.setSelection(39, 39);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 37);
+
+ // Delete the sequence
+ adaptor.setSelection(49, 49);
+ for (int i = 0; i < 6; i++) {
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ }
+ assertEquals(Selection.getSelectionStart(editable), 37);
+ // ----- End Emoji Tag Sequence with invalid base testing ----
+
+ // Emoji Tag Sequence
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 23);
+
+ // Variation Selector with invalid base
+ adaptor.setSelection(22, 22);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 21);
+ adaptor.setSelection(22, 22);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 21);
+
+ // Variation Selector
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 19);
+
+ // Emoji Modifier
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 16);
+
+ // Emoji Modifier with invalid base
+ adaptor.setSelection(14, 14);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 13);
+ adaptor.setSelection(14, 14);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 13);
+
+ // Line Feed
+ adaptor.setSelection(12, 12);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 11);
+
+ // Carriage Return
+ adaptor.setSelection(12, 12);
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 11);
+
+ // Carriage Return and Line Feed
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 9);
+
+ // Regional Indicator Symbol odd
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 7);
+
+ // Regional Indicator Symbol even
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 3);
+
+ // Simple Emoji
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 1);
+
+ // First CodePoint
+ didConsume = adaptor.sendKeyEvent(downKeyDown);
+ assertTrue(didConsume);
+ assertEquals(Selection.getSelectionStart(editable), 0);
+ }
+
private static final String SAMPLE_TEXT =
"Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit.";
+ private static final String SAMPLE_EMOJI_TEXT =
+ "a" // First CodePoint
+ + "😂" // Simple Emoji
+ + "🇮🇷" // Regional Indicator Symbol even
+ + "🇷" // Regional Indicator Symbol odd
+ + "\r\n" // Carriage Return and Line Feed
+ + "\r\n"
+ + "✋🏿" // Emoji Modifier
+ + "✋🏿"
+ + "⚠️" // Variant Selector
+ + "⚠️"
+ + "🏴" // Emoji Tag Sequence
+ + "🏴"
+ + "a👨" // Zero Width Joiner
+ + "👨👩👧👦"
+ + "5️⃣" // Keycap
+ + "5️⃣"
+ + "عَ" // Non-Spacing Mark
+ + "a"; // Normal Character
+
private static final String SAMPLE_RTL_TEXT = "متن ساختگی" + "\nبرای تستfor test😊";
private static Editable sampleEditable(int selStart, int selEnd) {
@@ -345,8 +510,8 @@
return sample;
}
- private static Editable sampleRtlEditable(int selStart, int selEnd) {
- SpannableStringBuilder sample = new SpannableStringBuilder(SAMPLE_RTL_TEXT);
+ private static Editable sampleEditable(int selStart, int selEnd, String text) {
+ SpannableStringBuilder sample = new SpannableStringBuilder(text);
Selection.setSelection(sample, selStart, selEnd);
return sample;
}
@@ -355,7 +520,24 @@
View testView = new View(RuntimeEnvironment.application);
int client = 0;
TextInputChannel textInputChannel = mock(TextInputChannel.class);
- return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null);
+ FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
+ when(mockFlutterJNI.nativeFlutterTextUtilsIsEmoji(anyInt()))
+ .thenAnswer((invocation) -> Emoji.isEmoji((int) invocation.getArguments()[0]));
+ when(mockFlutterJNI.nativeFlutterTextUtilsIsEmojiModifier(anyInt()))
+ .thenAnswer((invocation) -> Emoji.isEmojiModifier((int) invocation.getArguments()[0]));
+ when(mockFlutterJNI.nativeFlutterTextUtilsIsEmojiModifierBase(anyInt()))
+ .thenAnswer((invocation) -> Emoji.isEmojiModifierBase((int) invocation.getArguments()[0]));
+ when(mockFlutterJNI.nativeFlutterTextUtilsIsVariationSelector(anyInt()))
+ .thenAnswer(
+ (invocation) -> {
+ int codePoint = (int) invocation.getArguments()[0];
+ return 0xFE0E <= codePoint && codePoint <= 0xFE0F;
+ });
+ when(mockFlutterJNI.nativeFlutterTextUtilsIsRegionalIndicator(anyInt()))
+ .thenAnswer(
+ (invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0]));
+ return new InputConnectionAdaptor(
+ testView, client, textInputChannel, editable, null, mockFlutterJNI);
}
private class TestTextInputChannel extends TextInputChannel {
diff --git a/tools/android_lint/project.xml b/tools/android_lint/project.xml
index e817435..f557fb5 100644
--- a/tools/android_lint/project.xml
+++ b/tools/android_lint/project.xml
@@ -65,6 +65,7 @@
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCall.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/EventChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java" />
+ <src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterNativeView.java" />