Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
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
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/PluginReg
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
Expand Down
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ android_java_sources = [
"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",
Expand Down
14 changes: 14 additions & 0 deletions shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ public static native void nativeOnVsync(
@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;
Expand Down
189 changes: 189 additions & 0 deletions shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the Unicode spec refers to this as TAG_END, should probably stick to same naming scheme

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

@GaryQian GaryQian May 1, 2020

Choose a reason for hiding this comment

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

Jk, it looks like they do call this CANCEL_TAG in Android, this is fine, but I still think TAG_END may be a more appropriate name. Up to you if you want to change it. Just depends on if you want to follow Android or Unicode's lead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's better to follow the Android guideline because we are using it as the reference.

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;
}

Copy link
Contributor

@GaryQian GaryQian May 1, 2020

Choose a reason for hiding this comment

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

Since you have unrolled the state machine, can you add some notation here indicating that the following few if statements are meant to act like a fall through and do not always return like the if statements above?

The separation will make the code much more parse-able.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

/**
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -38,6 +38,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
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;

Expand Down Expand Up @@ -96,14 +97,16 @@ public InputConnectionAdaptor(
int client,
TextInputChannel textInputChannel,
Editable editable,
EditorInfo editorInfo) {
EditorInfo editorInfo,
FlutterJNI flutterJNI) {
super(view, true);
mFlutterView = view;
mClient = client;
this.textInputChannel = textInputChannel;
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 =
Expand All @@ -120,6 +123,15 @@ public InputConnectionAdaptor(
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.
Expand Down Expand Up @@ -315,19 +327,18 @@ 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 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);
Expand Down
60 changes: 60 additions & 0 deletions shell/platform/android/platform_view_android_jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -484,6 +485,35 @@ static void InvokePlatformMessageEmptyResponseCallback(JNIEnv* env,
);
}

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
Expand Down Expand Up @@ -599,6 +629,36 @@ bool RegisterApi(JNIEnv* env) {
.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,
Expand Down
Loading