3131import io .flutter .embedding .engine .systemchannels .TextInputChannel ;
3232
3333class 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 ;
0 commit comments