Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

DeMonkeyCoder
Copy link
Contributor

@DeMonkeyCoder DeMonkeyCoder commented Apr 26, 2020

The android backspace key is currently handled by TextKeyListener from native Android API in my older pull request #17393.
I figured out that this function has problems on lower Android API levels and I also think flutter needs a method to handle this functionality by itself.
So started scraping emoji characters from http://www.unicode.org/reports/tr51/#emoji_data by a python script and handled emojis based on the Unicode's documentation.
The utility I wrote is supporting all characters in Emoji 13.0

I also handled some states that were not handled in the android source code.

@auto-assign auto-assign bot requested a review from GaryQian April 26, 2020 12:27
@DeMonkeyCoder DeMonkeyCoder changed the title Handle android backspace by handling last unicode characters Handle android backspace by handling the last Unicode characters Apr 26, 2020
@DeMonkeyCoder DeMonkeyCoder force-pushed the android-backspace branch 5 times, most recently from ac6e8e8 to 5718c22 Compare April 26, 2020 14:10
@GaryQian
Copy link
Contributor

Can you file an issue to fully explain the problem? This would help with both understanding this and for future archeology to figure out what was going on.

We may want to consider using the Emoji handling that is already included in the ICU library that the C++ engine imports. This would ensure that we are using a standardized solution to this problem and that we don't have repeated logic in Java and C++. We should explore the tradeoffs of a direct custom java solution like yours vs a JNI into C++ to use ICU solution.

@DeMonkeyCoder
Copy link
Contributor Author

DeMonkeyCoder commented Apr 28, 2020

I started an emulator with Android 5.1 and wasn't able to delete characters with TextKeyListener.

I tried using icu for java but requires Android API level 21.
http://site.icu-project.org/download/66#TOC-ICU4J-Download

I saw the android source code is still handling some new Emojis alongside icu library before updating to the latest version.
https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/Emoji.java#75

How can I use C++ engine modules inside this java code?
Also, How can I compare the performance between the two solutions? (using JNI - my Java code)
@GaryQian

@justinmc
Copy link
Contributor

I am able to delete complex emojis like 👨‍👩‍👦 using the existing implementation with TextKeyListener.getInstance().onKeyDown. I'm using an Android Pixel 3 emulator with API 28.
Can you post specific steps to reproduce? Maybe open an issue in the framework repository like Gary said, or find an existing issue that covers it.

I'll defer to Gary on the C++ and performance questions.

Thanks for opening this by the way! It may unblock my PR for Dpad navigation through emoji characters (#17420) if we can figure that out.

@jason-simmons
Copy link
Member

I can reproduce the issue using an emulator running an API 21 (Lollipop) image. In that environment the TextKeyListener.onKeyDown based implementation does not remove any text when the delete key is pressed.

I think the getOffsetBefore approach is a good improvement, but I'd like to avoid reimplementing a grapheme cluster breaker in the Android embedding.

Could the InputConnectionAdaptor call Android's android.text.TextUtils.getOffsetBefore on older platforms and use the android.icu.text.BreakIterator on platforms that offer it (API 24 and later)?
TextUtils.getOffsetBefore will not handle the latest emoji, but it should provide accuracy equivalent to native text fields on the Android versions where it would be used.

The JNI-based alternative would involve adding a new JNI exported function to the engine that provides an API like getOffsetBefore. The native implementation would use the grapheme breaker in the ICU C++ library that is already linked into the Flutter engine. I'd prefer to reuse that if there is no sufficiently accurate breaker that is already available to Java.

@DeMonkeyCoder
Copy link
Contributor Author

DeMonkeyCoder commented Apr 29, 2020

I don't trust Android APIs for this specific problem as we encountered problems and if we are about to use something to avoid implementing, I prefer using JNI solution.
Working on it.

BreakIterator iterates over the graphemes while we don't always want to delete a complete grapheme.
Consider سً which is a single grapheme in Persian or Arabic locale (with zero width joiner I think) but after a backspace, should be converted to ‌س and shouldn't be completely deleted.
I'm working on bringing Emoji handlers from the native icu module.

@DeMonkeyCoder
Copy link
Contributor Author

So I'm using JNI and icu module from the native engine.
I tested on emulators and code worked fine but in flutter tests I got:

java.lang.UnsatisfiedLinkError: io.flutter.plugin.editing.EmojiUtils.nativeEmojiUtilsIsRegionalIndicator(I)Z
	at io.flutter.plugin.editing.EmojiUtils.nativeEmojiUtilsIsRegionalIndicator(Native Method)

And I don't know why. Can you help me with this, please?

@GaryQian
Copy link
Contributor

You should look at FlutterJNI.java. We currently implement all JNI methods in there, an explanation is given in the docs at the top.

I am not familiar enough with JNI to be able to tell you what is immediately wrong just from looking, but in any case, to stay consistent with the rest of the engine, you should move this into FlutterJNI.java anyways. There should be plenty of example methods already implemented in there to work off of.

@DeMonkeyCoder DeMonkeyCoder force-pushed the android-backspace branch 3 times, most recently from 4078a0b to 349f694 Compare April 29, 2020 14:51
@DeMonkeyCoder
Copy link
Contributor Author

DeMonkeyCoder commented Apr 29, 2020

I tried FlutterJNI.java instance. Still the same error.
But worked normal on emulators.

@GaryQian
Copy link
Contributor

Hmm, which tests are you running exactly to encounter that?

@DeMonkeyCoder
Copy link
Contributor Author

InputConnectionAdaptor last two tests.
It is also failing with the same error in Cirrus CI Linux Android Debug Engine.

@jason-simmons
Copy link
Member

I posted a version of this that finds the character boundary by using the icu::BreakIterator through JNI:
#18041

Can you try it and see if it works for your use case?

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.

}

// Emoji Tag Sequence
if (codePoint == CANCEL_TAG) { // tag_base
Copy link
Contributor

Choose a reason for hiding this comment

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

did you mean // tag_end?

Copy link
Contributor Author

@DeMonkeyCoder DeMonkeyCoder May 1, 2020

Choose a reason for hiding this comment

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

was a mistake. fixed.

codePoint = Character.codePointBefore(text, lastOffset);
lastOffset -= Character.charCount(codePoint);
}
if (lastOffset == 0) { // tag_end not found. Just delete the end.
Copy link
Contributor

Choose a reason for hiding this comment

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

did you mean tag_base?

Copy link
Contributor Author

@DeMonkeyCoder DeMonkeyCoder May 1, 2020

Choose a reason for hiding this comment

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

fixed.

}
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

private static final String SAMPLE_TEXT =
"Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit.";

private static final String SAMPLE_EMOJI_TEXT =
Copy link
Contributor

Choose a reason for hiding this comment

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

You should add some additional edge cases, including handling of invalid sequences. Invalid sequences may be generated programmatically, and we should make sure we can handle/recover from these cases.

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

@GaryQian
Copy link
Contributor

GaryQian commented May 1, 2020

Implementation roughly LGTM, will take another look during final pass.

@DeMonkeyCoder DeMonkeyCoder force-pushed the android-backspace branch 2 times, most recently from 3af0e4e to 23f37cb Compare May 1, 2020 13:53
@DeMonkeyCoder DeMonkeyCoder requested a review from GaryQian May 1, 2020 13:53
@DeMonkeyCoder
Copy link
Contributor Author

I handled invalid sequences and unknown cases in the last changes

Copy link
Contributor

@GaryQian GaryQian left a comment

Choose a reason for hiding this comment

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

Did some manual testing with this, and seems to be working well. LGTM, I'll see if @jason-simmons has any final thoughts.

@GaryQian GaryQian added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label May 6, 2020
@GaryQian GaryQian changed the title Handle android backspace by handling the last Unicode characters Custom unicode handling for Android backspace via JNI to ICU May 6, 2020
@fluttergithubbot fluttergithubbot merged commit 9cdb5a9 into flutter:master May 6, 2020
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request May 6, 2020
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request May 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants