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

Commit 7d9865c

Browse files
implement updateCursorAnchorInfo and updateExtractedText
1 parent 810d53a commit 7d9865c

File tree

4 files changed

+188
-76
lines changed

4 files changed

+188
-76
lines changed

shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,12 @@ public static TextEditState fromJson(@NonNull JSONObject textEditState) throws J
686686
public final int composingStart;
687687
public final int composingEnd;
688688

689-
public TextEditState(@NonNull String text, int selectionStart, int selectionEnd, int composingStart, int composingEnd) {
689+
public TextEditState(
690+
@NonNull String text,
691+
int selectionStart,
692+
int selectionEnd,
693+
int composingStart,
694+
int composingEnd) {
690695
this.text = text;
691696
this.selectionStart = selectionStart;
692697
this.selectionEnd = selectionEnd;

shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java

Lines changed: 140 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,25 @@
3131
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
3232

3333
class InputConnectionAdaptor extends BaseInputConnection {
34+
private static final String TAG = "flutter";
35+
3436
private final View mFlutterView;
3537
private final int mClient;
3638
private final TextInputChannel textInputChannel;
3739
private final Editable mEditable;
3840
private final EditorInfo mEditorInfo;
41+
private ExtractedTextRequest mExtractRequest;
42+
private boolean mMonitorCursorUpdate = false;
43+
private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
44+
private ExtractedText mExtractedText = new ExtractedText();
3945
private int mBatchCount;
4046
private InputMethodManager mImm;
4147
private final Layout mLayout;
4248
private FlutterTextUtils flutterTextUtils;
4349
// Used to determine if Samsung-specific hacks should be applied.
4450
private final boolean isSamsung;
4551

52+
private TextEditingValue mLastUpdatedImmEditingValue;
4653
private TextEditingValue mLastKnownTextEditingValue;
4754
// Data class used to get and store the last-sent values via updateEditingState to
4855
// the framework. These are then compared against to prevent redundant messages
@@ -114,6 +121,9 @@ public InputConnectionAdaptor(
114121
mEditable = editable;
115122
mEditorInfo = editorInfo;
116123
mBatchCount = 0;
124+
// Initialize the "last seen" text editing values to a non-null value.
125+
mLastUpdatedImmEditingValue = new TextEditingValue(mEditable);
126+
mLastKnownTextEditingValue = mLastUpdatedImmEditingValue;
117127
this.flutterTextUtils = new FlutterTextUtils(flutterJNI);
118128
// We create a dummy Layout with max width so that the selection
119129
// shifting acts as if all text were in one line.
@@ -143,7 +153,9 @@ public InputConnectionAdaptor(
143153
// Send the current state of the editable to Flutter.
144154
private void updateEditingState() {
145155
// If the IME is in the middle of a batch edit, then wait until it completes.
146-
if (mBatchCount > 0) return;
156+
if (mBatchCount > 0) {
157+
return;
158+
}
147159

148160
TextEditingValue currentValue = new TextEditingValue(mEditable);
149161

@@ -152,28 +164,88 @@ private void updateEditingState() {
152164
return;
153165
}
154166

155-
mImm.updateSelection(
156-
mFlutterView,
167+
Log.v(TAG, "send EditingState to flutter: " + currentValue.toString());
168+
textInputChannel.updateEditingState(
169+
mClient,
170+
currentValue.text,
157171
currentValue.selectionStart,
158172
currentValue.selectionEnd,
159173
currentValue.composingStart,
160174
currentValue.composingEnd);
161175

162-
textInputChannel.updateEditingState(
163-
mClient,
164-
currentValue.text,
176+
mLastKnownTextEditingValue = currentValue;
177+
}
178+
179+
private ExtractedText getExtractedText(TextEditingValue editingValue) {
180+
mExtractedText.startOffset = 0;
181+
mExtractedText.partialStartOffset = -1;
182+
mExtractedText.partialEndOffset = -1;
183+
mExtractedText.selectionStart = editingValue.selectionStart;
184+
mExtractedText.selectionEnd = editingValue.selectionEnd;
185+
mExtractedText.text = editingValue.text;
186+
return mExtractedText;
187+
}
188+
189+
private CursorAnchorInfo getCursorAnchorInfo(TextEditingValue editingValue) {
190+
if (mCursorAnchorInfoBuilder == null) {
191+
mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
192+
} else {
193+
mCursorAnchorInfoBuilder.reset();
194+
}
195+
196+
mCursorAnchorInfoBuilder.setSelectionRange(
197+
editingValue.selectionStart, editingValue.selectionEnd);
198+
final int composingStart = editingValue.composingStart;
199+
final int composingEnd = editingValue.composingEnd;
200+
if (composingStart >= 0 && composingEnd > composingStart) {
201+
mCursorAnchorInfoBuilder.setComposingText(
202+
composingStart, editingValue.text.subSequence(composingStart, composingEnd));
203+
} else {
204+
mCursorAnchorInfoBuilder.setComposingText(-1, "");
205+
}
206+
return mCursorAnchorInfoBuilder.build();
207+
}
208+
209+
private void updateIMMIfNeeded() {
210+
if (mBatchCount > 0) {
211+
return;
212+
}
213+
214+
TextEditingValue currentValue = new TextEditingValue(mEditable);
215+
216+
// Always send selection update. InputMethodManager#updateSelection skips sending the message
217+
// if none of the parameters have changed since the last time we called it.
218+
mImm.updateSelection(
219+
mFlutterView,
165220
currentValue.selectionStart,
166221
currentValue.selectionEnd,
167222
currentValue.composingStart,
168223
currentValue.composingEnd);
169224

170-
mLastKnownTextEditingValue = currentValue;
225+
if (currentValue == mLastUpdatedImmEditingValue) {
226+
return;
227+
}
228+
229+
if (mExtractRequest != null) {
230+
mImm.updateExtractedText(mFlutterView, mExtractRequest.token, getExtractedText(currentValue));
231+
}
232+
233+
if (mMonitorCursorUpdate) {
234+
final CursorAnchorInfo info = getCursorAnchorInfo(currentValue);
235+
mImm.updateCursorAnchorInfo(mFlutterView, info);
236+
Log.v(TAG, "update CursorAnchorInfo: " + info.toString());
237+
}
238+
239+
mLastUpdatedImmEditingValue = currentValue;
171240
}
172241

173-
// Called when the current text editing state held by the text input plugin is overwritten by a
174-
// newly received value from the framework.
242+
// Called when the current text editing state held by the text input plugin (in mEditable) is
243+
// overwritten by a newly received value from the framework.
175244
public void didUpdateEditingValue() {
176245
mLastKnownTextEditingValue = new TextEditingValue(mEditable);
246+
// Try to update the input method immediately after the internal state change. Or defer it to
247+
// endBatchEdit if we're in a nested edit.
248+
updateIMMIfNeeded();
177249
}
178250

179251
@Override
@@ -191,78 +263,102 @@ public boolean beginBatchEdit() {
191263
public boolean endBatchEdit() {
192264
boolean result = super.endBatchEdit();
193265
mBatchCount--;
266+
// These 2 methods do nothing if mBatchCount > 0.
194267
updateEditingState();
268+
updateIMMIfNeeded();
195269
return result;
196270
}
197271

198272
@Override
199273
public boolean commitText(CharSequence text, int newCursorPosition) {
274+
beginBatchEdit();
200275
boolean result = super.commitText(text, newCursorPosition);
276+
endBatchEdit();
201277
return result;
202278
}
203279

204280
@Override
205281
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
206282
if (Selection.getSelectionStart(mEditable) == -1) return true;
207283

284+
beginBatchEdit();
208285
boolean result = super.deleteSurroundingText(beforeLength, afterLength);
286+
endBatchEdit();
209287
return result;
210288
}
211289

212290
@Override
213291
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
292+
beginBatchEdit();
214293
boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
294+
endBatchEdit();
215295
return result;
216296
}
217297

218298
@Override
219299
public boolean setComposingRegion(int start, int end) {
300+
beginBatchEdit();
301+
Log.i("flutter", "engine: set CR: " + String.valueOf(start) + " - " + String.valueOf(end));
220302
boolean result = super.setComposingRegion(start, end);
303+
endBatchEdit();
221304
return result;
222305
}
223306

224307
@Override
225308
public boolean setComposingText(CharSequence text, int newCursorPosition) {
226309
boolean result;
310+
beginBatchEdit();
311+
Log.i("flutter", "engine: set CT: " + text + ", " + String.valueOf(newCursorPosition));
227312
if (text.length() == 0) {
228313
result = super.commitText(text, newCursorPosition);
229314
} else {
230315
result = super.setComposingText(text, newCursorPosition);
231316
}
317+
endBatchEdit();
232318
return result;
233319
}
234320

235321
@Override
236322
public boolean finishComposingText() {
323+
Log.i("flutter", "engine: finish composing");
324+
beginBatchEdit();
237325
boolean result = super.finishComposingText();
238-
239-
// Apply Samsung hacks. Samsung caches composing region data strangely, causing text
240-
// duplication.
241-
if (isSamsung) {
242-
if (Build.VERSION.SDK_INT >= 21) {
243-
// Samsung keyboards don't clear the composing region on finishComposingText.
244-
// Update the keyboard with a reset/empty composing region. Critical on
245-
// Samsung keyboards to prevent punctuation duplication.
246-
CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
247-
builder.setComposingText(/*composingTextStart*/ -1, /*composingText*/ "");
248-
CursorAnchorInfo anchorInfo = builder.build();
249-
mImm.updateCursorAnchorInfo(mFlutterView, anchorInfo);
250-
}
251-
}
252-
326+
endBatchEdit();
253327
return result;
254328
}
255329

256330
// TODO(garyq): Implement a more feature complete version of getExtractedText
257331
@Override
258332
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
259-
ExtractedText extractedText = new ExtractedText();
260-
extractedText.selectionStart = Selection.getSelectionStart(mEditable);
261-
extractedText.selectionEnd = Selection.getSelectionEnd(mEditable);
262-
extractedText.text = mEditable.toString();
333+
// Input methods may use this method to get the current content of the
334+
final boolean textMonitor = (flags & GET_EXTRACTED_TEXT_MONITOR) != 0;
335+
if (textMonitor == (mExtractRequest == null)) {
336+
Log.d(TAG, "The input method toggled text monitoring " + (textMonitor ? "on" : "off"));
337+
}
338+
if (textMonitor) {
339+
// Enables text monitoring. See updateIMMIfNeeded.
340+
mExtractRequest = request;
341+
} else {
342+
mExtractRequest = null;
343+
}
344+
ExtractedText extractedText = getExtractedText(new TextEditingValue(mEditable));
263345
return extractedText;
264346
}
265347

348+
@Override
349+
public boolean requestCursorUpdates(int cursorUpdateMode) {
350+
//
351+
352+
if ((cursorUpdateMode & CURSOR_UPDATE_IMMEDIATE) != 0) {
353+
mImm.updateCursorAnchorInfo(
354+
mFlutterView, getCursorAnchorInfo(new TextEditingValue(mEditable)));
355+
}
356+
357+
// Enables cursor monitoring.
358+
mMonitorCursorUpdate = (cursorUpdateMode & CURSOR_UPDATE_MONITOR) != 0;
359+
return true;
360+
}
361+
266362
@Override
267363
public boolean clearMetaKeyStates(int states) {
268364
boolean result = super.clearMetaKeyStates(states);
@@ -292,8 +388,9 @@ private boolean isSamsung() {
292388

293389
@Override
294390
public boolean setSelection(int start, int end) {
391+
beginBatchEdit();
295392
boolean result = super.setSelection(start, end);
296-
updateEditingState();
393+
endBatchEdit();
297394
return result;
298395
}
299396

@@ -315,6 +412,13 @@ private static int clampIndexToEditable(int index, Editable editable) {
315412

316413
@Override
317414
public boolean sendKeyEvent(KeyEvent event) {
415+
beginBatchEdit();
416+
final boolean result = doSendKeyEvent(event);
417+
endBatchEdit();
418+
return result;
419+
}
420+
421+
private boolean doSendKeyEvent(KeyEvent event) {
318422
if (event.getAction() == KeyEvent.ACTION_DOWN) {
319423
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
320424
int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
@@ -327,7 +431,6 @@ public boolean sendKeyEvent(KeyEvent event) {
327431
// Delete the selection.
328432
Selection.setSelection(mEditable, selStart);
329433
mEditable.delete(selStart, selEnd);
330-
updateEditingState();
331434
return true;
332435
}
333436
return false;
@@ -418,6 +521,13 @@ public boolean sendKeyEvent(KeyEvent event) {
418521

419522
@Override
420523
public boolean performContextMenuAction(int id) {
524+
beginBatchEdit();
525+
final boolean result = doPerformContextMenuAction(id);
526+
endBatchEdit();
527+
return result;
528+
}
529+
530+
private boolean doPerformContextMenuAction(int id) {
421531
if (id == android.R.id.selectAll) {
422532
setSelection(0, mEditable.length());
423533
return true;

shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,13 @@ BaseInputConnection getFakeBaseInputConnection() {
172172
if (mFakeBaseInputConnection != null) {
173173
return mFakeBaseInputConnection;
174174
}
175-
mFakeBaseInputConnection = new BaseInputConnection(mView, false) {
176-
@Override
177-
public Editable getEditable() {
178-
return mEditable;
179-
}
180-
};
175+
mFakeBaseInputConnection =
176+
new BaseInputConnection(mView, true) {
177+
@Override
178+
public Editable getEditable() {
179+
return mEditable;
180+
}
181+
};
181182
return mFakeBaseInputConnection;
182183
}
183184

@@ -459,31 +460,25 @@ void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
459460
// Always apply state to selection which handles updating the selection if needed.
460461
applyStateToSelection(state);
461462

462-
if (state.composingEnd < 0)
463+
if (state.composingEnd < 0) {
463464
BaseInputConnection.removeComposingSpans(mEditable);
464-
else
465-
getFakeBaseInputConnection().setComposingRegion(state.composingStart, state.composingEnd);
466-
467-
// Use updateSelection to update imm on selection if it is not necessary to restart.
468-
if (!restartAlwaysRequired && !mRestartInputPending) {
469-
mImm.updateSelection(
470-
mView,
471-
Math.max(Selection.getSelectionStart(mEditable), 0),
472-
Math.max(Selection.getSelectionEnd(mEditable), 0),
473-
BaseInputConnection.getComposingSpanStart(mEditable),
474-
BaseInputConnection.getComposingSpanEnd(mEditable));
475-
// Restart if there is a pending restart or the device requires a force restart
476-
// (see isRestartAlwaysRequired). Restarting will also update the selection.
477465
} else {
478-
mImm.restartInput(view);
479-
mRestartInputPending = false;
466+
getFakeBaseInputConnection().setComposingRegion(state.composingStart, state.composingEnd);
480467
}
481468

482-
// Notify the connection adaptor that the last known remote editing state has been updated.
469+
// Notify the connection adaptor that the mEditable has been updated. The InputConnection is
470+
// responsible for further notifying the input method.
483471
InputConnection connection = getLastInputConnection();
484472
if (connection != null && connection instanceof InputConnectionAdaptor) {
485473
((InputConnectionAdaptor) connection).didUpdateEditingValue();
486474
}
475+
476+
// Restart if there is a pending restart or the device requires a force restart
477+
// (see isRestartAlwaysRequired). Restarting will also update the selection.
478+
if (restartAlwaysRequired || mRestartInputPending) {
479+
mImm.restartInput(view);
480+
mRestartInputPending = false;
481+
}
487482
}
488483

489484
private interface MinMax {

0 commit comments

Comments
 (0)