From bec1116ef3fb44e06fe27a3fc6b8d7cc93cd1fef Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 30 Mar 2020 16:56:51 -0700 Subject: [PATCH 01/14] Fix bug where cursor divides grapheme clusters with dpad --- .../editing/InputConnectionAdaptor.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index b3eea58aed1ff..a1f9d28d303ab 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -343,22 +343,28 @@ public boolean sendKeyEvent(KeyEvent event) { int selStart = Selection.getSelectionStart(mEditable); int selEnd = Selection.getSelectionEnd(mEditable); if (selStart == selEnd && !event.isShiftPressed()) { - int newSel = Math.max(selStart - 1, 0); - setSelection(newSel, newSel); + Selection.moveLeft(mEditable, mLayout); + int newSelStart = Selection.getSelectionStart(mEditable); + setSelection(newSelStart, newSelStart); } else { - int newSelEnd = Math.max(selEnd - 1, 0); - setSelection(selStart, newSelEnd); + Selection.extendLeft(mEditable, mLayout); + int newSelStart = Selection.getSelectionStart(mEditable); + int newSelEnd = Selection.getSelectionEnd(mEditable); + setSelection(newSelStart, newSelEnd); } return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { int selStart = Selection.getSelectionStart(mEditable); int selEnd = Selection.getSelectionEnd(mEditable); if (selStart == selEnd && !event.isShiftPressed()) { - int newSel = Math.min(selStart + 1, mEditable.length()); - setSelection(newSel, newSel); + Selection.moveRight(mEditable, mLayout); + int newSelStart = Selection.getSelectionStart(mEditable); + setSelection(newSelStart, newSelStart); } else { - int newSelEnd = Math.min(selEnd + 1, mEditable.length()); - setSelection(selStart, newSelEnd); + Selection.extendRight(mEditable, mLayout); + int newSelStart = Selection.getSelectionStart(mEditable); + int newSelEnd = Selection.getSelectionEnd(mEditable); + setSelection(newSelStart, newSelEnd); } return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) { From 36dad1dd96ff4a9b481bbe8bbe86076960d18e7d Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 11 Jun 2020 16:49:02 -0700 Subject: [PATCH 02/14] Implement left arrow using new getOffsetBefore --- .../plugin/editing/InputConnectionAdaptor.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index a1f9d28d303ab..650da37ad35f2 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -343,14 +343,11 @@ public boolean sendKeyEvent(KeyEvent event) { int selStart = Selection.getSelectionStart(mEditable); int selEnd = Selection.getSelectionEnd(mEditable); if (selStart == selEnd && !event.isShiftPressed()) { - Selection.moveLeft(mEditable, mLayout); - int newSelStart = Selection.getSelectionStart(mEditable); - setSelection(newSelStart, newSelStart); + int newSel = Math.max(flutterTextUtils.getOffsetBefore(mEditable, selStart), 0); + setSelection(newSel, newSel); } else { - Selection.extendLeft(mEditable, mLayout); - int newSelStart = Selection.getSelectionStart(mEditable); - int newSelEnd = Selection.getSelectionEnd(mEditable); - setSelection(newSelStart, newSelEnd); + int newSelEnd = Math.max(flutterTextUtils.getOffsetBefore(mEditable, selEnd), 0); + setSelection(selStart, newSelEnd); } return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { From aaba40d3e56c90867a66b58d99486c609c2f689a Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 12 Jun 2020 18:01:16 -0700 Subject: [PATCH 03/14] getOffsetAfter adapted from getOffsetBefore --- .../plugin/editing/FlutterTextUtils.java | 130 ++++++++++++++++++ .../editing/InputConnectionAdaptor.java | 11 +- 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index 19eb1f0790a03..ee8e57bf7b065 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -4,7 +4,9 @@ package io.flutter.plugin.editing; +import android.graphics.Paint; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.Log; class FlutterTextUtils { public static final int LINE_FEED = 0x0A; @@ -141,12 +143,14 @@ public int getOffsetBefore(CharSequence text, int offset) { boolean isZwj = false; int lastSeenVariantSelectorCharCount = 0; do { + Log.e("justin", "do " + lastOffset); if (isZwj) { deleteCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; isZwj = false; } lastSeenVariantSelectorCharCount = 0; if (isEmojiModifier(codePoint)) { + Log.e("justin", "Before: isEmojiModifier"); codePoint = Character.codePointBefore(text, lastOffset); lastOffset -= Character.charCount(codePoint); if (lastOffset > 0 && isVariationSelector(codePoint)) { @@ -164,13 +168,16 @@ public int getOffsetBefore(CharSequence text, int offset) { } if (lastOffset > 0) { + Log.e("justin", "Before: lastOffset > 0 (" + lastOffset + ")"); codePoint = Character.codePointBefore(text, lastOffset); lastOffset -= Character.charCount(codePoint); if (codePoint == ZERO_WIDTH_JOINER) { + Log.e("justin", "is ZWJ"); isZwj = true; codePoint = Character.codePointBefore(text, lastOffset); lastOffset -= Character.charCount(codePoint); if (lastOffset > 0 && isVariationSelector(codePoint)) { + Log.e("justin", "is ZWJ and lastOffset > 0 and isVariationSelector"); codePoint = Character.codePointBefore(text, lastOffset); lastSeenVariantSelectorCharCount = Character.charCount(codePoint); lastOffset -= Character.charCount(codePoint); @@ -186,4 +193,127 @@ public int getOffsetBefore(CharSequence text, int offset) { return offset - deleteCharCount; } + + /** + * Gets the offset of the next character following the given offset, with + * consideration for multi-byte characters. + */ + public int getOffsetAfter(CharSequence text, int offset, Paint paint) { + Log.e("justin", "getOffsetAfter " + text + ", " + offset); + final int len = text.length(); + + if (offset >= len - 1) { + Log.e("justin", "over length " + len); + return len; + } + + int codePoint = Character.codePointAt(text, offset); + int nextCharCount = Character.charCount(codePoint); + int nextOffset = offset + nextCharCount; + + if (nextOffset == 0) { + Log.e("justin", "nextOffset == zero"); + return 0; + } + + // Line Feed + if (codePoint == LINE_FEED) { + Log.e("justin", "line feed"); + codePoint = Character.codePointAt(text, nextOffset); + if (codePoint == CARRIAGE_RETURN) { + ++nextCharCount; + } + return offset + nextCharCount; + } + + // Flags + if (isRegionalIndicatorSymbol(codePoint)) { + Log.e("justin", "flag"); + codePoint = Character.codePointAt(text, nextOffset); + nextOffset += Character.charCount(codePoint); + int regionalIndicatorSymbolCount = 1; + while (nextOffset < len && isRegionalIndicatorSymbol(codePoint)) { + codePoint = Character.codePointAt(text, nextOffset); + nextOffset += Character.charCount(codePoint); + regionalIndicatorSymbolCount++; + } + if (regionalIndicatorSymbolCount % 2 == 0) { + nextCharCount += 2; + } + return offset + nextCharCount; + } + + // Keycaps + if (codePoint == COMBINING_ENCLOSING_KEYCAP) { + Log.e("justin", "keycap"); + codePoint = Character.codePointBefore(text, nextOffset); + nextOffset += Character.charCount(codePoint); + if (nextOffset < len && isVariationSelector(codePoint)) { + int tmpCodePoint = Character.codePointAt(text, nextOffset); + if (isKeycapBase(tmpCodePoint)) { + nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint); + } + } else if (isKeycapBase(codePoint)) { + nextCharCount += Character.charCount(codePoint); + } + return offset + nextCharCount; + } + + if (isEmoji(codePoint)) { + Log.e("justin", "emoji at " + nextOffset); + boolean isZwj = false; + int lastSeenVariantSelectorCharCount = 0; + do { + if (isZwj) { + nextCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; + isZwj = false; + } + lastSeenVariantSelectorCharCount = 0; + if (isEmojiModifier(codePoint)) { + Log.e("justin", "isEmojiModifier"); + codePoint = Character.codePointAt(text, nextOffset); + nextOffset += Character.charCount(codePoint); + if (nextOffset < len && isVariationSelector(codePoint)) { + codePoint = Character.codePointAt(text, nextOffset); + if (!isEmoji(codePoint)) { + Log.e("justin", "emoji not emoji so return " + offset + " + " + nextCharCount); + return offset + nextCharCount; + } + lastSeenVariantSelectorCharCount = Character.charCount(codePoint); + nextOffset += Character.charCount(codePoint); + } + if (isEmojiModifierBase(codePoint)) { + nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); + } + Log.e("justin", "break b/c isEmojiModifier"); + break; + } + + if (nextOffset < len) { + codePoint = Character.codePointAt(text, nextOffset); + nextOffset += Character.charCount(codePoint); + Log.e("justin", "emoji nextOffset < len so move forward. is this a zwj? " + (codePoint == ZERO_WIDTH_JOINER) + " at " + nextOffset + " after moved forward by " + Character.charCount(codePoint)); + if (codePoint == ZERO_WIDTH_JOINER) { + isZwj = true; + codePoint = Character.codePointAt(text, nextOffset); + nextOffset += Character.charCount(codePoint); + if (nextOffset < len && isVariationSelector(codePoint)) { + codePoint = Character.codePointAt(text, nextOffset); + lastSeenVariantSelectorCharCount = Character.charCount(codePoint); + nextOffset += Character.charCount(codePoint); + } + } + } + + if (nextOffset >= len) { + Log.e("justin", "emoji nextOffset >= len " + nextOffset + " >= " + len); + break; + } + Log.e("justin", "emoji while " + isZwj + " and " + isEmoji(codePoint)); + } while (isZwj && isEmoji(codePoint)); + } + + Log.e("justin", "done " + (offset + nextCharCount)); + return offset + nextCharCount; + } } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 650da37ad35f2..baff1d705b4e3 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -354,14 +354,11 @@ public boolean sendKeyEvent(KeyEvent event) { int selStart = Selection.getSelectionStart(mEditable); int selEnd = Selection.getSelectionEnd(mEditable); if (selStart == selEnd && !event.isShiftPressed()) { - Selection.moveRight(mEditable, mLayout); - int newSelStart = Selection.getSelectionStart(mEditable); - setSelection(newSelStart, newSelStart); + int newSel = Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart, new TextPaint()), mEditable.length()); + setSelection(newSel, newSel); } else { - Selection.extendRight(mEditable, mLayout); - int newSelStart = Selection.getSelectionStart(mEditable); - int newSelEnd = Selection.getSelectionEnd(mEditable); - setSelection(newSelStart, newSelEnd); + int newSelEnd = Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart, new TextPaint()), mEditable.length()); + setSelection(selStart, newSelEnd); } return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) { From f863ebfd7ce88ef8644fa4268afddebcb7758fec Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 23 Jun 2020 11:46:11 -0700 Subject: [PATCH 04/14] Test for left arrow, still trying to run, still need right arrow --- .../editing/InputConnectionAdaptorTest.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) 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 bdf2d6b48c5d6..3f3f893120252 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -154,6 +154,149 @@ public void testSendKeyEvent_leftKeyMovesCaretLeft() { assertEquals(selStart - 1, Selection.getSelectionEnd(editable)); } + @Test + public void testSendKeyEvent_leftKeyMovesCaretLeftComplexEmoji() { + SAMPLE_EMOJI_TEXT + int selStart = 75; + Editable editable = sampleEditable(selStart, selStart, SAMPLE_EMOJI_TEXT); + InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); + + KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); + 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); + } + @Test public void testSendKeyEvent_leftKeyExtendsSelectionLeft() { int selStart = 5; From 3c31ba4b7c9ba80e59eececc7cda26b6bc365eb5 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 23 Jun 2020 15:43:08 -0700 Subject: [PATCH 05/14] Formatting and removing logs --- .../plugin/editing/FlutterTextUtils.java | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index ee8e57bf7b065..99e62cc697e61 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -5,8 +5,8 @@ package io.flutter.plugin.editing; import android.graphics.Paint; -import io.flutter.embedding.engine.FlutterJNI; import io.flutter.Log; +import io.flutter.embedding.engine.FlutterJNI; class FlutterTextUtils { public static final int LINE_FEED = 0x0A; @@ -143,14 +143,12 @@ public int getOffsetBefore(CharSequence text, int offset) { boolean isZwj = false; int lastSeenVariantSelectorCharCount = 0; do { - Log.e("justin", "do " + lastOffset); if (isZwj) { deleteCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; isZwj = false; } lastSeenVariantSelectorCharCount = 0; if (isEmojiModifier(codePoint)) { - Log.e("justin", "Before: isEmojiModifier"); codePoint = Character.codePointBefore(text, lastOffset); lastOffset -= Character.charCount(codePoint); if (lastOffset > 0 && isVariationSelector(codePoint)) { @@ -168,16 +166,13 @@ public int getOffsetBefore(CharSequence text, int offset) { } if (lastOffset > 0) { - Log.e("justin", "Before: lastOffset > 0 (" + lastOffset + ")"); codePoint = Character.codePointBefore(text, lastOffset); lastOffset -= Character.charCount(codePoint); if (codePoint == ZERO_WIDTH_JOINER) { - Log.e("justin", "is ZWJ"); isZwj = true; codePoint = Character.codePointBefore(text, lastOffset); lastOffset -= Character.charCount(codePoint); if (lastOffset > 0 && isVariationSelector(codePoint)) { - Log.e("justin", "is ZWJ and lastOffset > 0 and isVariationSelector"); codePoint = Character.codePointBefore(text, lastOffset); lastSeenVariantSelectorCharCount = Character.charCount(codePoint); lastOffset -= Character.charCount(codePoint); @@ -195,15 +190,13 @@ public int getOffsetBefore(CharSequence text, int offset) { } /** - * Gets the offset of the next character following the given offset, with - * consideration for multi-byte characters. + * Gets the offset of the next character following the given offset, with consideration for + * multi-byte characters. */ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { - Log.e("justin", "getOffsetAfter " + text + ", " + offset); final int len = text.length(); if (offset >= len - 1) { - Log.e("justin", "over length " + len); return len; } @@ -212,13 +205,11 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { int nextOffset = offset + nextCharCount; if (nextOffset == 0) { - Log.e("justin", "nextOffset == zero"); return 0; } // Line Feed if (codePoint == LINE_FEED) { - Log.e("justin", "line feed"); codePoint = Character.codePointAt(text, nextOffset); if (codePoint == CARRIAGE_RETURN) { ++nextCharCount; @@ -228,7 +219,6 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { // Flags if (isRegionalIndicatorSymbol(codePoint)) { - Log.e("justin", "flag"); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); int regionalIndicatorSymbolCount = 1; @@ -245,7 +235,6 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { // Keycaps if (codePoint == COMBINING_ENCLOSING_KEYCAP) { - Log.e("justin", "keycap"); codePoint = Character.codePointBefore(text, nextOffset); nextOffset += Character.charCount(codePoint); if (nextOffset < len && isVariationSelector(codePoint)) { @@ -260,7 +249,6 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } if (isEmoji(codePoint)) { - Log.e("justin", "emoji at " + nextOffset); boolean isZwj = false; int lastSeenVariantSelectorCharCount = 0; do { @@ -270,13 +258,11 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } lastSeenVariantSelectorCharCount = 0; if (isEmojiModifier(codePoint)) { - Log.e("justin", "isEmojiModifier"); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); if (nextOffset < len && isVariationSelector(codePoint)) { codePoint = Character.codePointAt(text, nextOffset); if (!isEmoji(codePoint)) { - Log.e("justin", "emoji not emoji so return " + offset + " + " + nextCharCount); return offset + nextCharCount; } lastSeenVariantSelectorCharCount = Character.charCount(codePoint); @@ -285,14 +271,12 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { if (isEmojiModifierBase(codePoint)) { nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); } - Log.e("justin", "break b/c isEmojiModifier"); break; } if (nextOffset < len) { codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); - Log.e("justin", "emoji nextOffset < len so move forward. is this a zwj? " + (codePoint == ZERO_WIDTH_JOINER) + " at " + nextOffset + " after moved forward by " + Character.charCount(codePoint)); if (codePoint == ZERO_WIDTH_JOINER) { isZwj = true; codePoint = Character.codePointAt(text, nextOffset); @@ -306,14 +290,11 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } if (nextOffset >= len) { - Log.e("justin", "emoji nextOffset >= len " + nextOffset + " >= " + len); break; } - Log.e("justin", "emoji while " + isZwj + " and " + isEmoji(codePoint)); } while (isZwj && isEmoji(codePoint)); } - Log.e("justin", "done " + (offset + nextCharCount)); return offset + nextCharCount; } } From ad96ee115ca6e4589a9d8ab8e8695562651821aa Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 23 Jun 2020 15:43:47 -0700 Subject: [PATCH 06/14] Fix start/end swapped bug --- .../flutter/plugin/editing/InputConnectionAdaptor.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index baff1d705b4e3..473803b4b324a 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -354,10 +354,16 @@ public boolean sendKeyEvent(KeyEvent event) { int selStart = Selection.getSelectionStart(mEditable); int selEnd = Selection.getSelectionEnd(mEditable); if (selStart == selEnd && !event.isShiftPressed()) { - int newSel = Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart, new TextPaint()), mEditable.length()); + int newSel = + Math.min( + flutterTextUtils.getOffsetAfter(mEditable, selStart, new TextPaint()), + mEditable.length()); setSelection(newSel, newSel); } else { - int newSelEnd = Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart, new TextPaint()), mEditable.length()); + int newSelEnd = + Math.min( + flutterTextUtils.getOffsetAfter(mEditable, selEnd, new TextPaint()), + mEditable.length()); setSelection(selStart, newSelEnd); } return true; From 8111d57c20c3554dbb229b6c08536e372185519a Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 23 Jun 2020 17:53:14 -0700 Subject: [PATCH 07/14] WIP right arrow test --- .../editing/InputConnectionAdaptorTest.java | 146 +++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) 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 3f3f893120252..4cfe728971b2b 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -37,6 +37,7 @@ @Config(manifest = Config.NONE, shadows = ShadowClipboardManager.class) @RunWith(RobolectricTestRunner.class) public class InputConnectionAdaptorTest { + /* @Test public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { View testView = new View(RuntimeEnvironment.application); @@ -156,7 +157,6 @@ public void testSendKeyEvent_leftKeyMovesCaretLeft() { @Test public void testSendKeyEvent_leftKeyMovesCaretLeftComplexEmoji() { - SAMPLE_EMOJI_TEXT int selStart = 75; Editable editable = sampleEditable(selStart, selStart, SAMPLE_EMOJI_TEXT); InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); @@ -341,7 +341,150 @@ public void testSendKeyEvent_rightKeyMovesCaretRight() { assertEquals(selStart + 1, Selection.getSelectionStart(editable)); assertEquals(selStart + 1, Selection.getSelectionEnd(editable)); } + */ + @Test + public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { + int selStart = 0; + Editable editable = sampleEditable(selStart, selStart, SAMPLE_EMOJI_TEXT); + InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); + + KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); + boolean didConsume; + + // First CodePoint + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 1); + + // Simple Emoji + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 3); + + // Regional Indicator Symbol odd + // TODO(justinmc): This seems to be wrong. The character is commented below + // as even, but the code treats it as odd. + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 5); + + // Regional Indicator Symbol even + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 9); + + // Carriage Return + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 10); + + // Line Feed and Carriage Return + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 12); + + // Line Feed + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 13); + + + // TODO(justinmc): 403 is failing, keep checking failures from here. + + + // Emoji Modifier with invalid base + //adaptor.setSelection(14, 14); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 14); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 15); + + // Emoji Modifier + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 16); + + // Variation Selector + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 19); + + // Variation Selector with invalid base + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 20); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 21); + + // Emoji Tag Sequence + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 23); + + // ----- Start Emoji Tag Sequence with invalid base testing ---- + // Pass base tag + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 37); + + // Pass the sequence + for (int i = 0; i < 6; i++) { + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + } + assertEquals(Selection.getSelectionStart(editable), 49); + // ----- End Emoji Tag Sequence with invalid base testing ---- + + // Zero Width Joiner with invalid base + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 51); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 52); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 53); + + // Zero Width Joiner + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 55); + + // Keycap with invalid base + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 56); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 58); + + // Keycap + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 59); + + // Non-Spacing Mark + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 60); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 61); + + // Normal Character + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 62); + + // TODO(justinmc): I'm definiteely missing a bunch, should end like 74. + } + + /* @Test public void testSendKeyEvent_rightKeyExtendsSelectionRight() { int selStart = 5; @@ -621,6 +764,7 @@ public void testSendKeyEvent_delKeyDeletesBackwardComplexEmojis() { assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 0); } + */ private static final String SAMPLE_TEXT = "Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit."; From faf1aefe40f7a321ec9ac712aa79a0262ef4635e Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 1 Jul 2020 10:23:44 -0700 Subject: [PATCH 08/14] WIP Trying to get getOffsetAfter working with right arrow test, probably needs major refactor --- .../io/flutter/plugin/editing/FlutterTextUtils.java | 7 +++++++ .../plugin/editing/InputConnectionAdaptorTest.java | 13 +++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index 99e62cc697e61..303a040a595b0 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -203,6 +203,7 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { int codePoint = Character.codePointAt(text, offset); int nextCharCount = Character.charCount(codePoint); int nextOffset = offset + nextCharCount; + Log.e("justin", "nextCharCount at " + offset + " is " + nextCharCount + " so nextOffset is " + nextOffset); if (nextOffset == 0) { return 0; @@ -249,9 +250,11 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } if (isEmoji(codePoint)) { + Log.e("justin", "Is emoji " + offset + ", " + nextOffset); boolean isZwj = false; int lastSeenVariantSelectorCharCount = 0; do { + Log.e("justin", "do it " + nextOffset); if (isZwj) { nextCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; isZwj = false; @@ -260,6 +263,7 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { if (isEmojiModifier(codePoint)) { codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); + Log.e("justin", "Is emoji modifier " + offset + ", " + nextOffset); if (nextOffset < len && isVariationSelector(codePoint)) { codePoint = Character.codePointAt(text, nextOffset); if (!isEmoji(codePoint)) { @@ -275,8 +279,10 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } if (nextOffset < len) { + Log.e("justin", "about to move forward from nextOffset " + nextOffset); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); + Log.e("justin", "moved to next, is it an emoji? " + isEmoji(codePoint) + " for " + nextOffset); if (codePoint == ZERO_WIDTH_JOINER) { isZwj = true; codePoint = Character.codePointAt(text, nextOffset); @@ -295,6 +301,7 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } while (isZwj && isEmoji(codePoint)); } + Log.e("justin", "return " + offset + " + " + nextCharCount + " = " + (offset + nextCharCount)); return offset + nextCharCount; } } 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 4cfe728971b2b..22b1fd365637a 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.flutter.Log; import android.content.ClipboardManager; import android.content.res.AssetManager; import android.text.Editable; @@ -352,6 +353,7 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); boolean didConsume; + /* // First CodePoint didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); @@ -388,29 +390,32 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 13); + */ // TODO(justinmc): 403 is failing, keep checking failures from here. + Log.e("justin", "This is what's left: |" + SAMPLE_EMOJI_TEXT.substring(13, 13 + 3 + 3) + "| which has length " + SAMPLE_EMOJI_TEXT.substring(13, 13 + 3 + 3).length()); // Emoji Modifier with invalid base //adaptor.setSelection(14, 14); + adaptor.setSelection(13, 13); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 14); + assertEquals(Selection.getSelectionStart(editable), 17); // actually 14 when no setselection didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 15); + assertEquals(Selection.getSelectionStart(editable), 16); // Emoji Modifier didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 16); + assertEquals(Selection.getSelectionStart(editable), 19); // Variation Selector didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 19); + assertEquals(Selection.getSelectionStart(editable), 21); // Variation Selector with invalid base didConsume = adaptor.sendKeyEvent(downKeyDown); From fbc8d8c3a8c285a23bf7b5ffb543d2b96f92099c Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 1 Jul 2020 14:40:50 -0700 Subject: [PATCH 09/14] WIP Seems to be working, needs cleanup --- .../plugin/editing/FlutterTextUtils.java | 68 ++++++++++++----- .../editing/InputConnectionAdaptorTest.java | 74 ++++++++----------- 2 files changed, 81 insertions(+), 61 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index 303a040a595b0..95257ee746641 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -5,7 +5,6 @@ package io.flutter.plugin.editing; import android.graphics.Paint; -import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; class FlutterTextUtils { @@ -220,32 +219,45 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { // Flags if (isRegionalIndicatorSymbol(codePoint)) { + Log.e("justin", "flag length " + Character.charCount(codePoint)); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); - int regionalIndicatorSymbolCount = 1; + int regionalIndicatorSymbolCount = 0; while (nextOffset < len && isRegionalIndicatorSymbol(codePoint)) { + Log.e("justin", "while " + nextOffset + " and next addition " + Character.charCount(codePoint)); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); regionalIndicatorSymbolCount++; } - if (regionalIndicatorSymbolCount % 2 == 0) { + if (regionalIndicatorSymbolCount > 0 && regionalIndicatorSymbolCount % 2 == 0) { + Log.e("justin", "even for " + regionalIndicatorSymbolCount); nextCharCount += 2; } + Log.e("justin", "return flag " + offset + " + " + nextCharCount); return offset + nextCharCount; } // Keycaps + if (isKeycapBase(codePoint)) { + Log.e("justin", "isKeycapBase"); + nextCharCount += Character.charCount(codePoint); + } if (codePoint == COMBINING_ENCLOSING_KEYCAP) { + Log.e("justin", "keycap!"); codePoint = Character.codePointBefore(text, nextOffset); nextOffset += Character.charCount(codePoint); if (nextOffset < len && isVariationSelector(codePoint)) { + Log.e("justin", "keycap variation selector"); int tmpCodePoint = Character.codePointAt(text, nextOffset); if (isKeycapBase(tmpCodePoint)) { + Log.e("justin", "keycap variation selector base"); nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint); } } else if (isKeycapBase(codePoint)) { + Log.e("justin", "keycap base"); nextCharCount += Character.charCount(codePoint); } + Log.e("justin", "keycap return " + offset + " + " + nextCharCount); return offset + nextCharCount; } @@ -261,29 +273,47 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } lastSeenVariantSelectorCharCount = 0; if (isEmojiModifier(codePoint)) { - codePoint = Character.codePointAt(text, nextOffset); - nextOffset += Character.charCount(codePoint); - Log.e("justin", "Is emoji modifier " + offset + ", " + nextOffset); - if (nextOffset < len && isVariationSelector(codePoint)) { - codePoint = Character.codePointAt(text, nextOffset); - if (!isEmoji(codePoint)) { - return offset + nextCharCount; - } - lastSeenVariantSelectorCharCount = Character.charCount(codePoint); - nextOffset += Character.charCount(codePoint); - } - if (isEmojiModifierBase(codePoint)) { - nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); - } + Log.e("justin", "first isEmojiModifier"); break; } + if (isVariationSelector(codePoint)) { + Log.e("justin", "isVariationSelector"); + } if (nextOffset < len) { - Log.e("justin", "about to move forward from nextOffset " + nextOffset); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); - Log.e("justin", "moved to next, is it an emoji? " + isEmoji(codePoint) + " for " + nextOffset); + if (codePoint == COMBINING_ENCLOSING_KEYCAP) { + Log.e("justin", "keycap inside of emoji block"); + codePoint = Character.codePointBefore(text, nextOffset); + nextOffset += Character.charCount(codePoint); + Log.e("justin", "next is " + nextOffset); + if (nextOffset < len && isVariationSelector(codePoint)) { + Log.e("justin", "keycap variation selector"); + int tmpCodePoint = Character.codePointAt(text, nextOffset); + if (isKeycapBase(tmpCodePoint)) { + Log.e("justin", "keycap variation selector base"); + nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint); + } + } else if (isKeycapBase(codePoint)) { + Log.e("justin", "keycap base"); + nextCharCount += Character.charCount(codePoint); + } + Log.e("justin", "keycap return " + offset + " + " + nextCharCount); + return offset + nextCharCount; + } + if (isEmojiModifier(codePoint)) { + Log.e("justin", "second isEmojiModifier"); + nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); + break; + } + if (isVariationSelector(codePoint)) { + Log.e("justin", "second isVariationSelector"); + nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); + break; + } if (codePoint == ZERO_WIDTH_JOINER) { + Log.e("justin", "is zwj"); isZwj = true; codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); 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 22b1fd365637a..fabb55768bab1 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -353,7 +353,6 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); boolean didConsume; - /* // First CodePoint didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); @@ -364,14 +363,12 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 3); - // Regional Indicator Symbol odd - // TODO(justinmc): This seems to be wrong. The character is commented below - // as even, but the code treats it as odd. + // Regional Indicator Symbol even didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 5); + assertEquals(Selection.getSelectionStart(editable), 7); - // Regional Indicator Symbol even + // Regional Indicator Symbol odd didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 9); @@ -390,24 +387,20 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 13); - */ - - // TODO(justinmc): 403 is failing, keep checking failures from here. - - - Log.e("justin", "This is what's left: |" + SAMPLE_EMOJI_TEXT.substring(13, 13 + 3 + 3) + "| which has length " + SAMPLE_EMOJI_TEXT.substring(13, 13 + 3 + 3).length()); - // Emoji Modifier with invalid base - //adaptor.setSelection(14, 14); - adaptor.setSelection(13, 13); + // Modified Emoji didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 17); // actually 14 when no setselection + assertEquals(Selection.getSelectionStart(editable), 16); + + // Emoji Modifier + adaptor.setSelection(14, 14); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 16); - // Emoji Modifier + // Emoji Modifier with invalid base + adaptor.setSelection(18, 18); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 19); @@ -418,75 +411,72 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { assertEquals(Selection.getSelectionStart(editable), 21); // Variation Selector with invalid base - didConsume = adaptor.sendKeyEvent(downKeyDown); - assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 20); - didConsume = adaptor.sendKeyEvent(downKeyDown); - assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 21); - - // Emoji Tag Sequence + adaptor.setSelection(22, 22); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 23); - // ----- Start Emoji Tag Sequence with invalid base testing ---- - // Pass base tag - didConsume = adaptor.sendKeyEvent(downKeyDown); - assertTrue(didConsume); + // Emoji Tag Sequence + for (int i = 0; i < 7; i++) { + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 25 + 2 * i); + } assertEquals(Selection.getSelectionStart(editable), 37); + // ----- Start Emoji Tag Sequence with invalid base testing ---- // Pass the sequence + adaptor.setSelection(39, 39); for (int i = 0; i < 6; i++) { didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 41 + 2 * i); } - assertEquals(Selection.getSelectionStart(editable), 49); + assertEquals(Selection.getSelectionStart(editable), 51); // ----- End Emoji Tag Sequence with invalid base testing ---- // Zero Width Joiner with invalid base didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 51); - didConsume = adaptor.sendKeyEvent(downKeyDown); - assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 52); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 53); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 55); // Zero Width Joiner didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 55); + assertEquals(Selection.getSelectionStart(editable), 66); // Keycap with invalid base + adaptor.setSelection(67, 67); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 56); + assertEquals(Selection.getSelectionStart(editable), 68); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 58); + assertEquals(Selection.getSelectionStart(editable), 69); // Keycap didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 59); + assertEquals(Selection.getSelectionStart(editable), 72); // Non-Spacing Mark didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 60); + assertEquals(Selection.getSelectionStart(editable), 73); didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 61); + assertEquals(Selection.getSelectionStart(editable), 74); // Normal Character didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); - assertEquals(Selection.getSelectionStart(editable), 62); - - // TODO(justinmc): I'm definiteely missing a bunch, should end like 74. + assertEquals(Selection.getSelectionStart(editable), 75); } /* From fc74a5cf13dde2599a1ed649ef038a3556996be1 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 1 Jul 2020 14:44:53 -0700 Subject: [PATCH 10/14] Cleaned up and working --- .../plugin/editing/FlutterTextUtils.java | 30 +------------------ .../editing/InputConnectionAdaptor.java | 4 +-- .../editing/InputConnectionAdaptorTest.java | 4 --- 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index 95257ee746641..845e8eb78ab01 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -4,7 +4,6 @@ package io.flutter.plugin.editing; -import android.graphics.Paint; import io.flutter.embedding.engine.FlutterJNI; class FlutterTextUtils { @@ -192,7 +191,7 @@ public int getOffsetBefore(CharSequence text, int offset) { * Gets the offset of the next character following the given offset, with consideration for * multi-byte characters. */ - public int getOffsetAfter(CharSequence text, int offset, Paint paint) { + public int getOffsetAfter(CharSequence text, int offset) { final int len = text.length(); if (offset >= len - 1) { @@ -202,7 +201,6 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { int codePoint = Character.codePointAt(text, offset); int nextCharCount = Character.charCount(codePoint); int nextOffset = offset + nextCharCount; - Log.e("justin", "nextCharCount at " + offset + " is " + nextCharCount + " so nextOffset is " + nextOffset); if (nextOffset == 0) { return 0; @@ -219,101 +217,76 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { // Flags if (isRegionalIndicatorSymbol(codePoint)) { - Log.e("justin", "flag length " + Character.charCount(codePoint)); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); int regionalIndicatorSymbolCount = 0; while (nextOffset < len && isRegionalIndicatorSymbol(codePoint)) { - Log.e("justin", "while " + nextOffset + " and next addition " + Character.charCount(codePoint)); codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); regionalIndicatorSymbolCount++; } if (regionalIndicatorSymbolCount > 0 && regionalIndicatorSymbolCount % 2 == 0) { - Log.e("justin", "even for " + regionalIndicatorSymbolCount); nextCharCount += 2; } - Log.e("justin", "return flag " + offset + " + " + nextCharCount); return offset + nextCharCount; } // Keycaps if (isKeycapBase(codePoint)) { - Log.e("justin", "isKeycapBase"); nextCharCount += Character.charCount(codePoint); } if (codePoint == COMBINING_ENCLOSING_KEYCAP) { - Log.e("justin", "keycap!"); codePoint = Character.codePointBefore(text, nextOffset); nextOffset += Character.charCount(codePoint); if (nextOffset < len && isVariationSelector(codePoint)) { - Log.e("justin", "keycap variation selector"); int tmpCodePoint = Character.codePointAt(text, nextOffset); if (isKeycapBase(tmpCodePoint)) { - Log.e("justin", "keycap variation selector base"); nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint); } } else if (isKeycapBase(codePoint)) { - Log.e("justin", "keycap base"); nextCharCount += Character.charCount(codePoint); } - Log.e("justin", "keycap return " + offset + " + " + nextCharCount); return offset + nextCharCount; } if (isEmoji(codePoint)) { - Log.e("justin", "Is emoji " + offset + ", " + nextOffset); boolean isZwj = false; int lastSeenVariantSelectorCharCount = 0; do { - Log.e("justin", "do it " + nextOffset); if (isZwj) { nextCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1; isZwj = false; } lastSeenVariantSelectorCharCount = 0; if (isEmojiModifier(codePoint)) { - Log.e("justin", "first isEmojiModifier"); break; } - if (isVariationSelector(codePoint)) { - Log.e("justin", "isVariationSelector"); - } if (nextOffset < len) { codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); if (codePoint == COMBINING_ENCLOSING_KEYCAP) { - Log.e("justin", "keycap inside of emoji block"); codePoint = Character.codePointBefore(text, nextOffset); nextOffset += Character.charCount(codePoint); - Log.e("justin", "next is " + nextOffset); if (nextOffset < len && isVariationSelector(codePoint)) { - Log.e("justin", "keycap variation selector"); int tmpCodePoint = Character.codePointAt(text, nextOffset); if (isKeycapBase(tmpCodePoint)) { - Log.e("justin", "keycap variation selector base"); nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint); } } else if (isKeycapBase(codePoint)) { - Log.e("justin", "keycap base"); nextCharCount += Character.charCount(codePoint); } - Log.e("justin", "keycap return " + offset + " + " + nextCharCount); return offset + nextCharCount; } if (isEmojiModifier(codePoint)) { - Log.e("justin", "second isEmojiModifier"); nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); break; } if (isVariationSelector(codePoint)) { - Log.e("justin", "second isVariationSelector"); nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint); break; } if (codePoint == ZERO_WIDTH_JOINER) { - Log.e("justin", "is zwj"); isZwj = true; codePoint = Character.codePointAt(text, nextOffset); nextOffset += Character.charCount(codePoint); @@ -331,7 +304,6 @@ public int getOffsetAfter(CharSequence text, int offset, Paint paint) { } while (isZwj && isEmoji(codePoint)); } - Log.e("justin", "return " + offset + " + " + nextCharCount + " = " + (offset + nextCharCount)); return offset + nextCharCount; } } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 473803b4b324a..d5df6584dd8a8 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -356,13 +356,13 @@ public boolean sendKeyEvent(KeyEvent event) { if (selStart == selEnd && !event.isShiftPressed()) { int newSel = Math.min( - flutterTextUtils.getOffsetAfter(mEditable, selStart, new TextPaint()), + flutterTextUtils.getOffsetAfter(mEditable, selStart), mEditable.length()); setSelection(newSel, newSel); } else { int newSelEnd = Math.min( - flutterTextUtils.getOffsetAfter(mEditable, selEnd, new TextPaint()), + flutterTextUtils.getOffsetAfter(mEditable, selEnd), mEditable.length()); setSelection(selStart, newSelEnd); } 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 fabb55768bab1..92c6d3751529a 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -38,7 +38,6 @@ @Config(manifest = Config.NONE, shadows = ShadowClipboardManager.class) @RunWith(RobolectricTestRunner.class) public class InputConnectionAdaptorTest { - /* @Test public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { View testView = new View(RuntimeEnvironment.application); @@ -342,7 +341,6 @@ public void testSendKeyEvent_rightKeyMovesCaretRight() { assertEquals(selStart + 1, Selection.getSelectionStart(editable)); assertEquals(selStart + 1, Selection.getSelectionEnd(editable)); } - */ @Test public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { @@ -479,7 +477,6 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { assertEquals(Selection.getSelectionStart(editable), 75); } - /* @Test public void testSendKeyEvent_rightKeyExtendsSelectionRight() { int selStart = 5; @@ -759,7 +756,6 @@ public void testSendKeyEvent_delKeyDeletesBackwardComplexEmojis() { assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 0); } - */ private static final String SAMPLE_TEXT = "Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit."; From 152d8970df1f94050ebce55498cbcdd2df763fc5 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 1 Jul 2020 14:59:00 -0700 Subject: [PATCH 11/14] Analyzer --- .../io/flutter/plugin/editing/InputConnectionAdaptor.java | 8 ++------ .../plugin/editing/InputConnectionAdaptorTest.java | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index d5df6584dd8a8..29b3fb859e1ef 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -355,15 +355,11 @@ public boolean sendKeyEvent(KeyEvent event) { int selEnd = Selection.getSelectionEnd(mEditable); if (selStart == selEnd && !event.isShiftPressed()) { int newSel = - Math.min( - flutterTextUtils.getOffsetAfter(mEditable, selStart), - mEditable.length()); + Math.min(flutterTextUtils.getOffsetAfter(mEditable, selStart), mEditable.length()); setSelection(newSel, newSel); } else { int newSelEnd = - Math.min( - flutterTextUtils.getOffsetAfter(mEditable, selEnd), - mEditable.length()); + Math.min(flutterTextUtils.getOffsetAfter(mEditable, selEnd), mEditable.length()); setSelection(selStart, newSelEnd); } return true; 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 92c6d3751529a..a8bd5c9375287 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -12,7 +12,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.flutter.Log; import android.content.ClipboardManager; import android.content.res.AssetManager; import android.text.Editable; From c1902096bfd7ce507084a90c88355bf406df8ebf Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 6 Jul 2020 13:16:23 -0700 Subject: [PATCH 12/14] Fix region indicator code and add a specific test for it --- .../plugin/editing/FlutterTextUtils.java | 19 ++++++++----- .../editing/InputConnectionAdaptorTest.java | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index 845e8eb78ab01..c98e1acbba336 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -217,15 +217,22 @@ public int getOffsetAfter(CharSequence text, int offset) { // Flags if (isRegionalIndicatorSymbol(codePoint)) { - codePoint = Character.codePointAt(text, nextOffset); - nextOffset += Character.charCount(codePoint); + if (nextOffset >= len - 1 + || !isRegionalIndicatorSymbol(Character.codePointAt(text, nextOffset))) { + return offset + nextCharCount; + } + // In this case there are at least two regional indicator symbols ahead of + // offset. If those two regional indicator symbols are a pair that + // represent a region together, the next offset should be after both of + // them. int regionalIndicatorSymbolCount = 0; - while (nextOffset < len && isRegionalIndicatorSymbol(codePoint)) { - codePoint = Character.codePointAt(text, nextOffset); - nextOffset += Character.charCount(codePoint); + int regionOffset = offset; + while (regionOffset > 0 + && isRegionalIndicatorSymbol(Character.codePointBefore(text, offset))) { + regionOffset -= Character.charCount(Character.codePointBefore(text, offset)); regionalIndicatorSymbolCount++; } - if (regionalIndicatorSymbolCount > 0 && regionalIndicatorSymbolCount % 2 == 0) { + if (regionalIndicatorSymbolCount % 2 == 0) { nextCharCount += 2; } return offset + nextCharCount; 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 a8bd5c9375287..c2670f87ce52d 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -341,6 +341,33 @@ public void testSendKeyEvent_rightKeyMovesCaretRight() { assertEquals(selStart + 1, Selection.getSelectionEnd(editable)); } + @Test + public void testSendKeyEvent_rightKeyMovesCaretRightComplexRegion() { + int selStart = 0; + // Seven region indicator characters. The first six should be considered as + // three region indicators, and the final seventh character should be + // considered to be on its own because it has no partner. + String SAMPLE_REGION_TEXT = "🇷🇷🇷🇷🇷🇷🇷"; + Editable editable = sampleEditable(selStart, selStart, SAMPLE_REGION_TEXT); + InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); + + KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); + boolean didConsume; + + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 4); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 8); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 12); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 14); + } + @Test public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { int selStart = 0; From e496c51dadf60444e6dc6773b8be9265f5f6417d Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 6 Jul 2020 13:25:52 -0700 Subject: [PATCH 13/14] Formatting and link to Android code in docs --- .../android/io/flutter/plugin/editing/FlutterTextUtils.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java index c98e1acbba336..fd1e430841427 100644 --- a/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java +++ b/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java @@ -190,6 +190,9 @@ public int getOffsetBefore(CharSequence text, int offset) { /** * Gets the offset of the next character following the given offset, with consideration for * multi-byte characters. + * + * @see https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111 */ public int getOffsetAfter(CharSequence text, int offset) { final int len = text.length(); @@ -218,7 +221,7 @@ public int getOffsetAfter(CharSequence text, int offset) { // Flags if (isRegionalIndicatorSymbol(codePoint)) { if (nextOffset >= len - 1 - || !isRegionalIndicatorSymbol(Character.codePointAt(text, nextOffset))) { + || !isRegionalIndicatorSymbol(Character.codePointAt(text, nextOffset))) { return offset + nextCharCount; } // In this case there are at least two regional indicator symbols ahead of From bf5e516b60e1534b4b2716f9366dee4d2a0f2d3c Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 7 Jul 2020 10:03:49 -0700 Subject: [PATCH 14/14] Add a test for when the cursor is in the middle of a region indicator pair --- .../plugin/editing/InputConnectionAdaptorTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 c2670f87ce52d..d3afb22e5976d 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -354,6 +354,7 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexRegion() { KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); boolean didConsume; + // The cursor moves over two region indicators at a time. didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 4); @@ -363,9 +364,19 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexRegion() { didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 12); + + // When there is only one region indicator left with no pair, the cursor + // moves over that single region indicator. didConsume = adaptor.sendKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 14); + + // If the cursor is placed in the middle of a region indicator pair, it + // moves over only the second half of the pair. + adaptor.setSelection(6, 6); + didConsume = adaptor.sendKeyEvent(downKeyDown); + assertTrue(didConsume); + assertEquals(Selection.getSelectionStart(editable), 8); } @Test