From 4adf8b86f9cbcdb407afbdb06bf75997014e5556 Mon Sep 17 00:00:00 2001 From: garyqian Date: Sun, 29 Mar 2020 13:56:12 -0700 Subject: [PATCH 01/13] Basic logging --- .../plugin/editing/InputConnectionAdaptor.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 93ec036b75ddf..be3d51748d482 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -80,6 +80,7 @@ private void updateEditingState() { int selectionEnd = Selection.getSelectionEnd(mEditable); int composingStart = BaseInputConnection.getComposingSpanStart(mEditable); int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable); + Log.e("flutter", " ## updateEditingState(" +selectionStart + "," +selectionEnd + "," +composingStart + "," +composingEnd + ")"); mImm.updateSelection(mFlutterView, selectionStart, selectionEnd, composingStart, composingEnd); @@ -95,6 +96,7 @@ public Editable getEditable() { @Override public boolean beginBatchEdit() { mBatchCount++; + Log.e("flutter", " # beg " + mBatchCount); return super.beginBatchEdit(); } @@ -102,12 +104,14 @@ public boolean beginBatchEdit() { public boolean endBatchEdit() { boolean result = super.endBatchEdit(); mBatchCount--; + Log.e("flutter", " # end " + mBatchCount); updateEditingState(); return result; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { + Log.e("flutter", "commitText"); boolean result = super.commitText(text, newCursorPosition); updateEditingState(); return result; @@ -115,6 +119,7 @@ public boolean commitText(CharSequence text, int newCursorPosition) { @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { + Log.e("flutter", "deleteSurroundingText"); if (Selection.getSelectionStart(mEditable) == -1) return true; boolean result = super.deleteSurroundingText(beforeLength, afterLength); @@ -124,6 +129,7 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { @Override public boolean setComposingRegion(int start, int end) { + Log.e("flutter", "setComposingRegion(" + start + "," + end + ")"); boolean result = super.setComposingRegion(start, end); updateEditingState(); return result; @@ -131,6 +137,7 @@ public boolean setComposingRegion(int start, int end) { @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { + Log.e("flutter", "setComposingText(" + text + "," + newCursorPosition + ")"); boolean result; if (text.length() == 0) { result = super.commitText(text, newCursorPosition); @@ -143,11 +150,13 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { @Override public boolean finishComposingText() { + Log.e("flutter", "finishComposingText"); boolean result = super.finishComposingText(); // Apply Samsung hacks. Samsung caches composing region data strangely, causing text // duplication. if (isSamsung) { + Log.e("flutter", "finishComposingText: Samsung hacks"); if (Build.VERSION.SDK_INT >= 21) { // Samsung keyboards don't clear the composing region on finishComposingText. // Update the keyboard with a reset/empty composing region. Critical on @@ -196,6 +205,7 @@ private boolean isSamsung() { @Override public boolean setSelection(int start, int end) { + Log.e("flutter", "setSelection"); boolean result = super.setSelection(start, end); updateEditingState(); return result; @@ -219,6 +229,7 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { + Log.e("flutter", "sendKeyEvent(" + event + ")"); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable); From 959941044e9881d01aa503b657ec8d5a6d3770b4 Mon Sep 17 00:00:00 2001 From: garyqian Date: Fri, 3 Apr 2020 17:54:19 -0700 Subject: [PATCH 02/13] Initial version --- .../editing/InputConnectionAdaptor.java | 53 ++++++++++++++++++- .../plugin/editing/TextInputPlugin.java | 4 ++ 2 files changed, 55 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 be3d51748d482..240ad26b9d256 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -38,6 +38,16 @@ class InputConnectionAdaptor extends BaseInputConnection { private InputMethodManager mImm; private final Layout mLayout; + // Used to store the last-sent values via updateEditingState to the framework. + // These are then compared against to prevent redundant messages with the same + // data before any valid operations were made to the contents. + private int mPreviousSelectionStart; + private int mPreviousSelectionEnd; + private int mPreviousComposingStart; + private int mPreviousComposingEnd; + private String mPreviousText; + private boolean repeatCheckNeeded = false; + // Used to determine if Samsung-specific hacks should be applied. private final boolean isSamsung; @@ -80,12 +90,41 @@ private void updateEditingState() { int selectionEnd = Selection.getSelectionEnd(mEditable); int composingStart = BaseInputConnection.getComposingSpanStart(mEditable); int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable); - Log.e("flutter", " ## updateEditingState(" +selectionStart + "," +selectionEnd + "," +composingStart + "," +composingEnd + ")"); + String text = mEditable.toString(); + + // Return if this data has already been sent and no meaningful changes have + // occurred to mark this as dirty. This prevents duplicate remote updates of + // the same data, which can break formatters that change the length of the + // contents. + if (repeatCheckNeeded && + selectionStart == mPreviousSelectionStart && + selectionEnd == mPreviousSelectionEnd && + composingStart == mPreviousComposingStart && + composingEnd == mPreviousComposingEnd && + text.equals(mPreviousText)) { + return; + } mImm.updateSelection(mFlutterView, selectionStart, selectionEnd, composingStart, composingEnd); textInputChannel.updateEditingState( - mClient, mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd); + mClient, text, selectionStart, selectionEnd, composingStart, composingEnd); + + repeatCheckNeeded = true; + mPreviousSelectionStart = selectionStart; + mPreviousSelectionEnd = selectionEnd; + mPreviousComposingStart = composingStart; + mPreviousComposingEnd = composingEnd; + mPreviousText = text; + } + + // This should be called whenever a change could have been made to + // the value of mEditable, which will make any call of updateEditingState() + // ineligible for repeat checking as we do not want to skip sending real changes + // to the framework. + public void markDirty() { + // Disable updateEditngState's repeat-update check + repeatCheckNeeded = false; } @Override @@ -113,6 +152,7 @@ public boolean endBatchEdit() { public boolean commitText(CharSequence text, int newCursorPosition) { Log.e("flutter", "commitText"); boolean result = super.commitText(text, newCursorPosition); + markDirty(); updateEditingState(); return result; } @@ -123,6 +163,7 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { if (Selection.getSelectionStart(mEditable) == -1) return true; boolean result = super.deleteSurroundingText(beforeLength, afterLength); + markDirty(); updateEditingState(); return result; } @@ -131,6 +172,7 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { public boolean setComposingRegion(int start, int end) { Log.e("flutter", "setComposingRegion(" + start + "," + end + ")"); boolean result = super.setComposingRegion(start, end); + markDirty(); updateEditingState(); return result; } @@ -144,6 +186,7 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { } else { result = super.setComposingText(text, newCursorPosition); } + markDirty(); updateEditingState(); return result; } @@ -168,6 +211,7 @@ public boolean finishComposingText() { } } + markDirty(); updateEditingState(); return result; } @@ -207,6 +251,7 @@ private boolean isSamsung() { public boolean setSelection(int start, int end) { Log.e("flutter", "setSelection"); boolean result = super.setSelection(start, end); + markDirty(); updateEditingState(); return result; } @@ -230,6 +275,7 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { Log.e("flutter", "sendKeyEvent(" + event + ")"); + markDirty(); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable); @@ -355,6 +401,8 @@ public boolean sendKeyEvent(KeyEvent event) { @Override public boolean performContextMenuAction(int id) { + Log.e("flutter", "performContextMenuAction(" + id + ")"); + markDirty(); if (id == android.R.id.selectAll) { setSelection(0, mEditable.length()); return true; @@ -408,6 +456,7 @@ public boolean performContextMenuAction(int id) { @Override public boolean performEditorAction(int actionCode) { + markDirty(); switch (actionCode) { case EditorInfo.IME_ACTION_NONE: textInputChannel.newline(mClient); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 9d41dc5b52902..91002fb64cc41 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -322,6 +322,10 @@ void setTextInputEditingState(View view, TextInputChannel.TextEditState state) { } // Always apply state to selection which handles updating the selection if needed. applyStateToSelection(state); + InputConnection connection = getLastInputConnection(); + if (connection != null && connection instanceof InputConnectionAdaptor) { + ((InputConnectionAdaptor)connection).markDirty(); + } // Use updateSelection to update imm on selection if it is not neccessary to restart. if (!restartAlwaysRequired && !mRestartInputPending) { mImm.updateSelection( From e43e8493a9ee4cc88927ff383285eabfacb32696 Mon Sep 17 00:00:00 2001 From: garyqian Date: Fri, 3 Apr 2020 18:02:29 -0700 Subject: [PATCH 03/13] remove logging --- .../plugin/editing/InputConnectionAdaptor.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 240ad26b9d256..beebedfe246e5 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -135,7 +135,6 @@ public Editable getEditable() { @Override public boolean beginBatchEdit() { mBatchCount++; - Log.e("flutter", " # beg " + mBatchCount); return super.beginBatchEdit(); } @@ -143,14 +142,12 @@ public boolean beginBatchEdit() { public boolean endBatchEdit() { boolean result = super.endBatchEdit(); mBatchCount--; - Log.e("flutter", " # end " + mBatchCount); updateEditingState(); return result; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - Log.e("flutter", "commitText"); boolean result = super.commitText(text, newCursorPosition); markDirty(); updateEditingState(); @@ -159,7 +156,6 @@ public boolean commitText(CharSequence text, int newCursorPosition) { @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - Log.e("flutter", "deleteSurroundingText"); if (Selection.getSelectionStart(mEditable) == -1) return true; boolean result = super.deleteSurroundingText(beforeLength, afterLength); @@ -170,7 +166,6 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { @Override public boolean setComposingRegion(int start, int end) { - Log.e("flutter", "setComposingRegion(" + start + "," + end + ")"); boolean result = super.setComposingRegion(start, end); markDirty(); updateEditingState(); @@ -179,7 +174,6 @@ public boolean setComposingRegion(int start, int end) { @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { - Log.e("flutter", "setComposingText(" + text + "," + newCursorPosition + ")"); boolean result; if (text.length() == 0) { result = super.commitText(text, newCursorPosition); @@ -193,13 +187,11 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { @Override public boolean finishComposingText() { - Log.e("flutter", "finishComposingText"); boolean result = super.finishComposingText(); // Apply Samsung hacks. Samsung caches composing region data strangely, causing text // duplication. if (isSamsung) { - Log.e("flutter", "finishComposingText: Samsung hacks"); if (Build.VERSION.SDK_INT >= 21) { // Samsung keyboards don't clear the composing region on finishComposingText. // Update the keyboard with a reset/empty composing region. Critical on @@ -249,7 +241,6 @@ private boolean isSamsung() { @Override public boolean setSelection(int start, int end) { - Log.e("flutter", "setSelection"); boolean result = super.setSelection(start, end); markDirty(); updateEditingState(); @@ -274,7 +265,6 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { - Log.e("flutter", "sendKeyEvent(" + event + ")"); markDirty(); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { @@ -401,7 +391,6 @@ public boolean sendKeyEvent(KeyEvent event) { @Override public boolean performContextMenuAction(int id) { - Log.e("flutter", "performContextMenuAction(" + id + ")"); markDirty(); if (id == android.R.id.selectAll) { setSelection(0, mEditable.length()); From 5d2e129e9bb98198ae17e5bc0c0231022c24ed63 Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 12:21:14 -0700 Subject: [PATCH 04/13] Implement non-empty methods to mark dirty --- .../plugin/editing/InputConnectionAdaptor.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index beebedfe246e5..95a642c779712 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -164,6 +164,14 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { return result; } + @Override + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength); + markDirty(); + updateEditingState(); + return result; + } + @Override public boolean setComposingRegion(int start, int end) { boolean result = super.setComposingRegion(start, end); @@ -218,6 +226,13 @@ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return extractedText; } + @Override + public boolean clearMetaKeyStates(int states) { + boolean result = super.clearMetaKeyStates(states); + markDirty(); + return result; + } + // Detect if the keyboard is a Samsung keyboard, where we apply Samsung-specific hacks to // fix critical bugs that make the keyboard otherwise unusable. See finishComposingText() for // more details. From 2fcc1d7c23bd957533868579d0d9437ea689965c Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 12:23:34 -0700 Subject: [PATCH 05/13] Format --- .../plugin/editing/InputConnectionAdaptor.java | 12 ++++++------ .../io/flutter/plugin/editing/TextInputPlugin.java | 2 +- 2 files changed, 7 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 95a642c779712..3c6992149270c 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -96,12 +96,12 @@ private void updateEditingState() { // occurred to mark this as dirty. This prevents duplicate remote updates of // the same data, which can break formatters that change the length of the // contents. - if (repeatCheckNeeded && - selectionStart == mPreviousSelectionStart && - selectionEnd == mPreviousSelectionEnd && - composingStart == mPreviousComposingStart && - composingEnd == mPreviousComposingEnd && - text.equals(mPreviousText)) { + if (repeatCheckNeeded + && selectionStart == mPreviousSelectionStart + && selectionEnd == mPreviousSelectionEnd + && composingStart == mPreviousComposingStart + && composingEnd == mPreviousComposingEnd + && text.equals(mPreviousText)) { return; } diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 91002fb64cc41..17ccea06728de 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -324,7 +324,7 @@ void setTextInputEditingState(View view, TextInputChannel.TextEditState state) { applyStateToSelection(state); InputConnection connection = getLastInputConnection(); if (connection != null && connection instanceof InputConnectionAdaptor) { - ((InputConnectionAdaptor)connection).markDirty(); + ((InputConnectionAdaptor) connection).markDirty(); } // Use updateSelection to update imm on selection if it is not neccessary to restart. if (!restartAlwaysRequired && !mRestartInputPending) { From 8a6d1283fb5e5334106e7511f031ebfbf9ff6033 Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 12:32:35 -0700 Subject: [PATCH 06/13] formatting --- .../io/flutter/plugin/editing/InputConnectionAdaptor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 3c6992149270c..ae0f0336fad66 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -226,12 +226,12 @@ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return extractedText; } - @Override - public boolean clearMetaKeyStates(int states) { + @Override + public boolean clearMetaKeyStates(int states) { boolean result = super.clearMetaKeyStates(states); markDirty(); return result; - } + } // Detect if the keyboard is a Samsung keyboard, where we apply Samsung-specific hacks to // fix critical bugs that make the keyboard otherwise unusable. See finishComposingText() for From 311ed698f8b35b7dec39b4c14ff56c4cbfaa4962 Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 12:39:45 -0700 Subject: [PATCH 07/13] Rename variable --- .../io/flutter/plugin/editing/InputConnectionAdaptor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index ae0f0336fad66..ee71623e2ce9e 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -46,7 +46,7 @@ class InputConnectionAdaptor extends BaseInputConnection { private int mPreviousComposingStart; private int mPreviousComposingEnd; private String mPreviousText; - private boolean repeatCheckNeeded = false; + private boolean mRepeatCheckNeeded = false; // Used to determine if Samsung-specific hacks should be applied. private final boolean isSamsung; @@ -96,7 +96,7 @@ private void updateEditingState() { // occurred to mark this as dirty. This prevents duplicate remote updates of // the same data, which can break formatters that change the length of the // contents. - if (repeatCheckNeeded + if (mRepeatCheckNeeded && selectionStart == mPreviousSelectionStart && selectionEnd == mPreviousSelectionEnd && composingStart == mPreviousComposingStart @@ -110,7 +110,7 @@ private void updateEditingState() { textInputChannel.updateEditingState( mClient, text, selectionStart, selectionEnd, composingStart, composingEnd); - repeatCheckNeeded = true; + mRepeatCheckNeeded = true; mPreviousSelectionStart = selectionStart; mPreviousSelectionEnd = selectionEnd; mPreviousComposingStart = composingStart; @@ -124,7 +124,7 @@ private void updateEditingState() { // to the framework. public void markDirty() { // Disable updateEditngState's repeat-update check - repeatCheckNeeded = false; + mRepeatCheckNeeded = false; } @Override From a83476463bbb6119e102a61fefd291b2a4a33efd Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 13:59:23 -0700 Subject: [PATCH 08/13] add tests --- .../editing/InputConnectionAdaptorTest.java | 74 +++++++++++++++++++ 1 file changed, 74 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 12d042c0237f6..bf0f513a0835e 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -270,6 +270,49 @@ public void testMethod_getExtractedText() { assertEquals(extractedText.selectionEnd, selStart); } + @Test + public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { + View testView = new View(RuntimeEnvironment.application); + FlutterJNI mockFlutterJni = mock(FlutterJNI.class); + DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); + int inputTargetId = 0; + TestTextInputChannel textInputChannel = new TestTextInputChannel(dartExecutor); + Editable mEditable = Editable.Factory.getInstance().newEditable(""); + Editable spyEditable = spy(mEditable); + EditorInfo outAttrs = new EditorInfo(); + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; + + InputConnectionAdaptor inputConnectionAdaptor = + new InputConnectionAdaptor( + testView, inputTargetId, textInputChannel, spyEditable, outAttrs); + + inputConnectionAdaptor.beginBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 0); + inputConnectionAdaptor.setComposingText("I do not fear computers. I fear the lack of them.", 1); + assertEquals(textInputChannel.text, null); + assertEquals(textInputChannel.updateEditingStateInvocations, 0); + inputConnectionAdaptor.endBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them."); + + inputConnectionAdaptor.beginBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + inputConnectionAdaptor.endBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + + inputConnectionAdaptor.beginBatchEdit(); + assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them."); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + inputConnectionAdaptor.setSelection(3, 4); + assertEquals(textInputChannel.updateEditingStateInvocations, 1); + assertEquals(textInputChannel.selectionStart, 49); + assertEquals(textInputChannel.selectionEnd, 49); + inputConnectionAdaptor.endBatchEdit(); + assertEquals(textInputChannel.updateEditingStateInvocations, 2); + assertEquals(textInputChannel.selectionStart, 3); + assertEquals(textInputChannel.selectionEnd, 4); + } + private static final String SAMPLE_TEXT = "Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit."; @@ -285,4 +328,35 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable edit TextInputChannel textInputChannel = mock(TextInputChannel.class); return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null); } + + private class TestTextInputChannel extends TextInputChannel { + public TestTextInputChannel(DartExecutor dartExecutor) { + super(dartExecutor); + } + + public int inputClientId; + public String text; + public int selectionStart; + public int selectionEnd; + public int composingStart; + public int composingEnd; + public int updateEditingStateInvocations = 0; + + @Override + public void updateEditingState( + int inputClientId, + String text, + int selectionStart, + int selectionEnd, + int composingStart, + int composingEnd) { + this.inputClientId = inputClientId; + this.text = text; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + this.composingStart = composingStart; + this.composingEnd = composingEnd; + updateEditingStateInvocations++; + } + } } From 1dd29edf9f9ddf23a3898c4dced196462eb7377b Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 18:35:48 -0700 Subject: [PATCH 09/13] Revert "remove logging" This reverts commit e43e8493a9ee4cc88927ff383285eabfacb32696. --- .../plugin/editing/InputConnectionAdaptor.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index ee71623e2ce9e..b013612e03316 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -135,6 +135,7 @@ public Editable getEditable() { @Override public boolean beginBatchEdit() { mBatchCount++; + Log.e("flutter", " # beg " + mBatchCount); return super.beginBatchEdit(); } @@ -142,12 +143,14 @@ public boolean beginBatchEdit() { public boolean endBatchEdit() { boolean result = super.endBatchEdit(); mBatchCount--; + Log.e("flutter", " # end " + mBatchCount); updateEditingState(); return result; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { + Log.e("flutter", "commitText"); boolean result = super.commitText(text, newCursorPosition); markDirty(); updateEditingState(); @@ -156,6 +159,7 @@ public boolean commitText(CharSequence text, int newCursorPosition) { @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { + Log.e("flutter", "deleteSurroundingText"); if (Selection.getSelectionStart(mEditable) == -1) return true; boolean result = super.deleteSurroundingText(beforeLength, afterLength); @@ -174,6 +178,7 @@ public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLeng @Override public boolean setComposingRegion(int start, int end) { + Log.e("flutter", "setComposingRegion(" + start + "," + end + ")"); boolean result = super.setComposingRegion(start, end); markDirty(); updateEditingState(); @@ -182,6 +187,7 @@ public boolean setComposingRegion(int start, int end) { @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { + Log.e("flutter", "setComposingText(" + text + "," + newCursorPosition + ")"); boolean result; if (text.length() == 0) { result = super.commitText(text, newCursorPosition); @@ -195,11 +201,13 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { @Override public boolean finishComposingText() { + Log.e("flutter", "finishComposingText"); boolean result = super.finishComposingText(); // Apply Samsung hacks. Samsung caches composing region data strangely, causing text // duplication. if (isSamsung) { + Log.e("flutter", "finishComposingText: Samsung hacks"); if (Build.VERSION.SDK_INT >= 21) { // Samsung keyboards don't clear the composing region on finishComposingText. // Update the keyboard with a reset/empty composing region. Critical on @@ -256,6 +264,7 @@ private boolean isSamsung() { @Override public boolean setSelection(int start, int end) { + Log.e("flutter", "setSelection"); boolean result = super.setSelection(start, end); markDirty(); updateEditingState(); @@ -280,6 +289,7 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { + Log.e("flutter", "sendKeyEvent(" + event + ")"); markDirty(); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { @@ -406,6 +416,7 @@ public boolean sendKeyEvent(KeyEvent event) { @Override public boolean performContextMenuAction(int id) { + Log.e("flutter", "performContextMenuAction(" + id + ")"); markDirty(); if (id == android.R.id.selectAll) { setSelection(0, mEditable.length()); From 6ac853e4ebacbda6085fbf1be1de158c4d835f1c Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 19:11:04 -0700 Subject: [PATCH 10/13] Revert "Revert "remove logging"" This reverts commit 1dd29edf9f9ddf23a3898c4dced196462eb7377b. --- .../plugin/editing/InputConnectionAdaptor.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index b013612e03316..ee71623e2ce9e 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -135,7 +135,6 @@ public Editable getEditable() { @Override public boolean beginBatchEdit() { mBatchCount++; - Log.e("flutter", " # beg " + mBatchCount); return super.beginBatchEdit(); } @@ -143,14 +142,12 @@ public boolean beginBatchEdit() { public boolean endBatchEdit() { boolean result = super.endBatchEdit(); mBatchCount--; - Log.e("flutter", " # end " + mBatchCount); updateEditingState(); return result; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - Log.e("flutter", "commitText"); boolean result = super.commitText(text, newCursorPosition); markDirty(); updateEditingState(); @@ -159,7 +156,6 @@ public boolean commitText(CharSequence text, int newCursorPosition) { @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - Log.e("flutter", "deleteSurroundingText"); if (Selection.getSelectionStart(mEditable) == -1) return true; boolean result = super.deleteSurroundingText(beforeLength, afterLength); @@ -178,7 +174,6 @@ public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLeng @Override public boolean setComposingRegion(int start, int end) { - Log.e("flutter", "setComposingRegion(" + start + "," + end + ")"); boolean result = super.setComposingRegion(start, end); markDirty(); updateEditingState(); @@ -187,7 +182,6 @@ public boolean setComposingRegion(int start, int end) { @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { - Log.e("flutter", "setComposingText(" + text + "," + newCursorPosition + ")"); boolean result; if (text.length() == 0) { result = super.commitText(text, newCursorPosition); @@ -201,13 +195,11 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { @Override public boolean finishComposingText() { - Log.e("flutter", "finishComposingText"); boolean result = super.finishComposingText(); // Apply Samsung hacks. Samsung caches composing region data strangely, causing text // duplication. if (isSamsung) { - Log.e("flutter", "finishComposingText: Samsung hacks"); if (Build.VERSION.SDK_INT >= 21) { // Samsung keyboards don't clear the composing region on finishComposingText. // Update the keyboard with a reset/empty composing region. Critical on @@ -264,7 +256,6 @@ private boolean isSamsung() { @Override public boolean setSelection(int start, int end) { - Log.e("flutter", "setSelection"); boolean result = super.setSelection(start, end); markDirty(); updateEditingState(); @@ -289,7 +280,6 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { - Log.e("flutter", "sendKeyEvent(" + event + ")"); markDirty(); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { @@ -416,7 +406,6 @@ public boolean sendKeyEvent(KeyEvent event) { @Override public boolean performContextMenuAction(int id) { - Log.e("flutter", "performContextMenuAction(" + id + ")"); markDirty(); if (id == android.R.id.selectAll) { setSelection(0, mEditable.length()); From 72bd9e170935332d18c93f79aac32dd945a8bbc5 Mon Sep 17 00:00:00 2001 From: garyqian Date: Mon, 6 Apr 2020 19:11:53 -0700 Subject: [PATCH 11/13] Remove extra updateEditingState --- .../io/flutter/plugin/editing/InputConnectionAdaptor.java | 7 ------- 1 file changed, 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 ee71623e2ce9e..c10d706a237b3 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -150,7 +150,6 @@ public boolean endBatchEdit() { public boolean commitText(CharSequence text, int newCursorPosition) { boolean result = super.commitText(text, newCursorPosition); markDirty(); - updateEditingState(); return result; } @@ -160,7 +159,6 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { boolean result = super.deleteSurroundingText(beforeLength, afterLength); markDirty(); - updateEditingState(); return result; } @@ -168,7 +166,6 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength); markDirty(); - updateEditingState(); return result; } @@ -176,7 +173,6 @@ public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLeng public boolean setComposingRegion(int start, int end) { boolean result = super.setComposingRegion(start, end); markDirty(); - updateEditingState(); return result; } @@ -189,7 +185,6 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { result = super.setComposingText(text, newCursorPosition); } markDirty(); - updateEditingState(); return result; } @@ -212,7 +207,6 @@ public boolean finishComposingText() { } markDirty(); - updateEditingState(); return result; } @@ -258,7 +252,6 @@ private boolean isSamsung() { public boolean setSelection(int start, int end) { boolean result = super.setSelection(start, end); markDirty(); - updateEditingState(); return result; } From 4e088383ebfe5fba15336642c9d8e71928ba1db9 Mon Sep 17 00:00:00 2001 From: garyqian Date: Tue, 7 Apr 2020 13:16:08 -0700 Subject: [PATCH 12/13] Use TextEditingValue class --- .../editing/InputConnectionAdaptor.java | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index c10d706a237b3..1d4a089f794f5 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -37,19 +37,46 @@ class InputConnectionAdaptor extends BaseInputConnection { private int mBatchCount; private InputMethodManager mImm; private final Layout mLayout; + // Used to determine if Samsung-specific hacks should be applied. + private final boolean isSamsung; - // Used to store the last-sent values via updateEditingState to the framework. - // These are then compared against to prevent redundant messages with the same - // data before any valid operations were made to the contents. - private int mPreviousSelectionStart; - private int mPreviousSelectionEnd; - private int mPreviousComposingStart; - private int mPreviousComposingEnd; - private String mPreviousText; private boolean mRepeatCheckNeeded = false; + private TextEditingValue mLastSentTextEditngValue; + // Data class used to get and store the last-sent values via updateEditingState to + // the framework. These are then compared against to prevent redundant messages + // with the same data before any valid operations were made to the contents. + private class TextEditingValue { + public int selectionStart; + public int selectionEnd; + public int composingStart; + public int composingEnd; + public String text; + + public TextEditingValue(Editable editable) { + selectionStart = Selection.getSelectionStart(editable); + selectionEnd = Selection.getSelectionEnd(editable); + composingStart = BaseInputConnection.getComposingSpanStart(editable); + composingEnd = BaseInputConnection.getComposingSpanEnd(editable); + text = editable.toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof TextEditingValue)) { + return false; + } + TextEditingValue value = (TextEditingValue) o; + return selectionStart == value.selectionStart + && selectionEnd == value.selectionEnd + && composingStart == value.composingStart + && composingEnd == value.composingEnd + && text.equals(value.text); + } + } - // Used to determine if Samsung-specific hacks should be applied. - private final boolean isSamsung; @SuppressWarnings("deprecation") public InputConnectionAdaptor( @@ -86,36 +113,33 @@ private void updateEditingState() { // If the IME is in the middle of a batch edit, then wait until it completes. if (mBatchCount > 0) return; - int selectionStart = Selection.getSelectionStart(mEditable); - int selectionEnd = Selection.getSelectionEnd(mEditable); - int composingStart = BaseInputConnection.getComposingSpanStart(mEditable); - int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable); - String text = mEditable.toString(); + TextEditingValue currentValue = new TextEditingValue(mEditable); // Return if this data has already been sent and no meaningful changes have // occurred to mark this as dirty. This prevents duplicate remote updates of // the same data, which can break formatters that change the length of the // contents. - if (mRepeatCheckNeeded - && selectionStart == mPreviousSelectionStart - && selectionEnd == mPreviousSelectionEnd - && composingStart == mPreviousComposingStart - && composingEnd == mPreviousComposingEnd - && text.equals(mPreviousText)) { + if (mRepeatCheckNeeded && currentValue.equals(mLastSentTextEditngValue)) { return; } - mImm.updateSelection(mFlutterView, selectionStart, selectionEnd, composingStart, composingEnd); + mImm.updateSelection( + mFlutterView, + currentValue.selectionStart, + currentValue.selectionEnd, + currentValue.composingStart, + currentValue.composingEnd); textInputChannel.updateEditingState( - mClient, text, selectionStart, selectionEnd, composingStart, composingEnd); + mClient, + currentValue.text, + currentValue.selectionStart, + currentValue.selectionEnd, + currentValue.composingStart, + currentValue.composingEnd); mRepeatCheckNeeded = true; - mPreviousSelectionStart = selectionStart; - mPreviousSelectionEnd = selectionEnd; - mPreviousComposingStart = composingStart; - mPreviousComposingEnd = composingEnd; - mPreviousText = text; + mLastSentTextEditngValue = currentValue; } // This should be called whenever a change could have been made to From ca9c5741a519b8aa45c76cdd7acb1b4e7fbefdd0 Mon Sep 17 00:00:00 2001 From: garyqian Date: Tue, 7 Apr 2020 15:40:56 -0700 Subject: [PATCH 13/13] formatting fixes --- .../flutter/plugin/editing/InputConnectionAdaptor.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 1d4a089f794f5..525e2efe8f847 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -70,14 +70,13 @@ public boolean equals(Object o) { } TextEditingValue value = (TextEditingValue) o; return selectionStart == value.selectionStart - && selectionEnd == value.selectionEnd - && composingStart == value.composingStart - && composingEnd == value.composingEnd - && text.equals(value.text); + && selectionEnd == value.selectionEnd + && composingStart == value.composingStart + && composingEnd == value.composingEnd + && text.equals(value.text); } } - @SuppressWarnings("deprecation") public InputConnectionAdaptor( View view,