Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +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;

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

@SuppressWarnings("deprecation")
public InputConnectionAdaptor(
View view,
Expand Down Expand Up @@ -76,15 +112,42 @@ 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);
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 && 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, mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd);
mClient,
currentValue.text,
currentValue.selectionStart,
currentValue.selectionEnd,
currentValue.composingStart,
currentValue.composingEnd);

mRepeatCheckNeeded = true;
mLastSentTextEditngValue = currentValue;
}

// 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
mRepeatCheckNeeded = false;
}

@Override
Expand All @@ -109,7 +172,7 @@ public boolean endBatchEdit() {
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
boolean result = super.commitText(text, newCursorPosition);
updateEditingState();
markDirty();
return result;
}

Expand All @@ -118,14 +181,21 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
if (Selection.getSelectionStart(mEditable) == -1) return true;

boolean result = super.deleteSurroundingText(beforeLength, afterLength);
updateEditingState();
markDirty();
return result;
}

@Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
markDirty();
return result;
}

@Override
public boolean setComposingRegion(int start, int end) {
boolean result = super.setComposingRegion(start, end);
updateEditingState();
markDirty();
return result;
}

Expand All @@ -137,7 +207,7 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) {
} else {
result = super.setComposingText(text, newCursorPosition);
}
updateEditingState();
markDirty();
return result;
}

Expand All @@ -159,7 +229,7 @@ public boolean finishComposingText() {
}
}

updateEditingState();
markDirty();
return result;
}

Expand All @@ -173,6 +243,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.
Expand All @@ -197,7 +274,7 @@ private boolean isSamsung() {
@Override
public boolean setSelection(int start, int end) {
boolean result = super.setSelection(start, end);
updateEditingState();
markDirty();
return result;
}

Expand All @@ -219,6 +296,7 @@ private static int clampIndexToEditable(int index, Editable editable) {

@Override
public boolean sendKeyEvent(KeyEvent event) {
markDirty();
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
Expand Down Expand Up @@ -344,6 +422,7 @@ public boolean sendKeyEvent(KeyEvent event) {

@Override
public boolean performContextMenuAction(int id) {
markDirty();
if (id == android.R.id.selectAll) {
setSelection(0, mEditable.length());
return true;
Expand Down Expand Up @@ -397,6 +476,7 @@ public boolean performContextMenuAction(int id) {

@Override
public boolean performEditorAction(int actionCode) {
markDirty();
switch (actionCode) {
case EditorInfo.IME_ACTION_NONE:
textInputChannel.newline(mClient);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.";

Expand All @@ -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++;
}
}
}