Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
TextInputPlugin.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package io.flutter.plugin.editing;
6
7import static io.flutter.Build.API_LEVELS;
8
9import android.annotation.SuppressLint;
10import android.content.Context;
11import android.graphics.Rect;
12import android.os.Build;
13import android.os.Bundle;
14import android.text.Editable;
15import android.text.InputType;
16import android.util.SparseArray;
17import android.view.KeyEvent;
18import android.view.View;
19import android.view.ViewStructure;
20import android.view.autofill.AutofillId;
21import android.view.autofill.AutofillManager;
22import android.view.autofill.AutofillValue;
23import android.view.inputmethod.EditorInfo;
24import android.view.inputmethod.InputConnection;
25import android.view.inputmethod.InputMethodManager;
26import androidx.annotation.NonNull;
27import androidx.annotation.Nullable;
28import androidx.annotation.VisibleForTesting;
29import androidx.core.view.inputmethod.EditorInfoCompat;
30import io.flutter.Log;
31import io.flutter.embedding.android.KeyboardManager;
32import io.flutter.embedding.engine.systemchannels.TextInputChannel;
33import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
34import io.flutter.plugin.platform.PlatformViewsController;
35import java.util.ArrayList;
36import java.util.HashMap;
37
38/** Android implementation of the text input plugin. */
39public class TextInputPlugin implements ListenableEditingState.EditingStateWatcher {
40 private static final String TAG = "TextInputPlugin";
41
42 @NonNull private final View mView;
43 @NonNull private final InputMethodManager mImm;
44 @NonNull private final AutofillManager afm;
45 @NonNull private final TextInputChannel textInputChannel;
46 @NonNull private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
47 @Nullable private TextInputChannel.Configuration configuration;
48 @Nullable private SparseArray<TextInputChannel.Configuration> autofillConfiguration;
49 @NonNull private ListenableEditingState mEditable;
50 private boolean mRestartInputPending;
51 @Nullable private InputConnection lastInputConnection;
52 @NonNull private PlatformViewsController platformViewsController;
53 @Nullable private Rect lastClientRect;
54 private ImeSyncDeferringInsetsCallback imeSyncCallback;
55
56 // Initialize the "last seen" text editing values to a non-null value.
57 private TextEditState mLastKnownFrameworkTextEditingState;
58
59 // When true following calls to createInputConnection will return the cached lastInputConnection
60 // if the input
61 // target is a platform view. See the comments on lockPlatformViewInputConnection for more
62 // details.
63 private boolean isInputConnectionLocked;
64
65 @SuppressLint("NewApi")
67 @NonNull View view,
68 @NonNull TextInputChannel textInputChannel,
69 @NonNull PlatformViewsController platformViewsController) {
70 mView = view;
71 // Create a default object.
72 mEditable = new ListenableEditingState(null, mView);
73 mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
74 if (Build.VERSION.SDK_INT >= API_LEVELS.API_26) {
75 afm = view.getContext().getSystemService(AutofillManager.class);
76 } else {
77 afm = null;
78 }
79
80 // Sets up syncing ime insets with the framework, allowing
81 // the Flutter view to grow and shrink to accommodate Android
82 // controlled keyboard animations.
83 if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
84 imeSyncCallback = new ImeSyncDeferringInsetsCallback(view);
85 imeSyncCallback.install();
86 }
87
88 this.textInputChannel = textInputChannel;
89 textInputChannel.setTextInputMethodHandler(
90 new TextInputChannel.TextInputMethodHandler() {
91 @Override
92 public void show() {
93 showTextInput(mView);
94 }
95
96 @Override
97 public void hide() {
98 if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
99 notifyViewExited();
100 } else {
101 hideTextInput(mView);
102 }
103 }
104
105 @Override
106 public void requestAutofill() {
107 notifyViewEntered();
108 }
109
110 @Override
111 public void finishAutofillContext(boolean shouldSave) {
112 if (Build.VERSION.SDK_INT < API_LEVELS.API_26 || afm == null) {
113 return;
114 }
115 if (shouldSave) {
116 afm.commit();
117 } else {
118 afm.cancel();
119 }
120 }
121
122 @Override
123 public void setClient(
124 int textInputClientId, TextInputChannel.Configuration configuration) {
125 setTextInputClient(textInputClientId, configuration);
126 }
127
128 @Override
129 public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
130 setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
131 }
132
133 @Override
134 public void setEditingState(TextInputChannel.TextEditState editingState) {
135 setTextInputEditingState(mView, editingState);
136 }
137
138 @Override
139 public void setEditableSizeAndTransform(double width, double height, double[] transform) {
140 saveEditableSizeAndTransform(width, height, transform);
141 }
142
143 @Override
144 public void clearClient() {
146 }
147
148 @Override
149 public void sendAppPrivateCommand(String action, Bundle data) {
151 }
152 });
153
154 textInputChannel.requestExistingInputState();
155
156 this.platformViewsController = platformViewsController;
157 this.platformViewsController.attachTextInputPlugin(this);
158 }
159
160 @NonNull
161 public InputMethodManager getInputMethodManager() {
162 return mImm;
163 }
164
165 @VisibleForTesting
166 Editable getEditable() {
167 return mEditable;
168 }
169
170 @VisibleForTesting
172 return imeSyncCallback;
173 }
174
175 /**
176 * Use the current platform view input connection until unlockPlatformViewInputConnection is
177 * called.
178 *
179 * <p>The current input connection instance is cached and any following call to @{link
180 * createInputConnection} returns the cached connection until unlockPlatformViewInputConnection is
181 * called.
182 *
183 * <p>This is a no-op if the current input target isn't a platform view.
184 *
185 * <p>This is used to preserve an input connection when moving a platform view from one virtual
186 * display to another.
187 */
189 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
190 isInputConnectionLocked = true;
191 }
192 }
193
194 /**
195 * Unlocks the input connection.
196 *
197 * <p>See also: @{link lockPlatformViewInputConnection}.
198 */
200 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
201 isInputConnectionLocked = false;
202 }
203 }
204
205 /**
206 * Detaches the text input plugin from the platform views controller.
207 *
208 * <p>The TextInputPlugin instance should not be used after calling this.
209 */
210 @SuppressLint("NewApi")
211 public void destroy() {
212 platformViewsController.detachTextInputPlugin();
213 textInputChannel.setTextInputMethodHandler(null);
214 notifyViewExited();
215 mEditable.removeEditingStateListener(this);
216 if (imeSyncCallback != null) {
217 imeSyncCallback.remove();
218 }
219 }
220
221 private static int inputTypeFromTextInputType(
222 TextInputChannel.InputType type,
223 boolean obscureText,
224 boolean autocorrect,
225 boolean enableSuggestions,
226 boolean enableIMEPersonalizedLearning,
227 TextInputChannel.TextCapitalization textCapitalization) {
228 if (type.type == TextInputChannel.TextInputType.DATETIME) {
229 return InputType.TYPE_CLASS_DATETIME;
230 } else if (type.type == TextInputChannel.TextInputType.NUMBER) {
231 int textType = InputType.TYPE_CLASS_NUMBER;
232 if (type.isSigned) {
233 textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
234 }
235 if (type.isDecimal) {
236 textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
237 }
238 return textType;
239 } else if (type.type == TextInputChannel.TextInputType.PHONE) {
240 return InputType.TYPE_CLASS_PHONE;
241 } else if (type.type == TextInputChannel.TextInputType.NONE) {
242 return InputType.TYPE_NULL;
243 }
244
245 int textType = InputType.TYPE_CLASS_TEXT;
246 if (type.type == TextInputChannel.TextInputType.MULTILINE) {
247 textType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
248 } else if (type.type == TextInputChannel.TextInputType.EMAIL_ADDRESS) {
249 textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
250 } else if (type.type == TextInputChannel.TextInputType.URL) {
251 textType |= InputType.TYPE_TEXT_VARIATION_URI;
252 } else if (type.type == TextInputChannel.TextInputType.VISIBLE_PASSWORD) {
253 textType |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
254 } else if (type.type == TextInputChannel.TextInputType.NAME) {
255 textType |= InputType.TYPE_TEXT_VARIATION_PERSON_NAME;
256 } else if (type.type == TextInputChannel.TextInputType.POSTAL_ADDRESS) {
257 textType |= InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS;
258 }
259
260 if (obscureText) {
261 // Note: both required. Some devices ignore TYPE_TEXT_FLAG_NO_SUGGESTIONS.
262 textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
263 textType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
264 } else {
265 if (autocorrect) textType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
266 if (!enableSuggestions) {
267 // Note: both required. Some devices ignore TYPE_TEXT_FLAG_NO_SUGGESTIONS.
268 textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
269 textType |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
270 }
271 }
272
273 if (textCapitalization == TextInputChannel.TextCapitalization.CHARACTERS) {
274 textType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
275 } else if (textCapitalization == TextInputChannel.TextCapitalization.WORDS) {
276 textType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
277 } else if (textCapitalization == TextInputChannel.TextCapitalization.SENTENCES) {
278 textType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
279 }
280
281 return textType;
282 }
283
284 @Nullable
285 public InputConnection createInputConnection(
286 @NonNull View view, @NonNull KeyboardManager keyboardManager, @NonNull EditorInfo outAttrs) {
287 if (inputTarget.type == InputTarget.Type.NO_TARGET) {
288 lastInputConnection = null;
289 return null;
290 }
291
292 if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
293 return null;
294 }
295
296 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
297 if (isInputConnectionLocked) {
298 return lastInputConnection;
299 }
300 lastInputConnection =
301 platformViewsController
302 .getPlatformViewById(inputTarget.id)
303 .onCreateInputConnection(outAttrs);
304 return lastInputConnection;
305 }
306
307 outAttrs.inputType =
308 inputTypeFromTextInputType(
309 configuration.inputType,
310 configuration.obscureText,
311 configuration.autocorrect,
312 configuration.enableSuggestions,
313 configuration.enableIMEPersonalizedLearning,
314 configuration.textCapitalization);
315 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
316
317 if (Build.VERSION.SDK_INT >= API_LEVELS.API_26
318 && !configuration.enableIMEPersonalizedLearning) {
319 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING;
320 }
321
322 int enterAction;
323 if (configuration.inputAction == null) {
324 // If an explicit input action isn't set, then default to none for multi-line fields
325 // and done for single line fields.
326 enterAction =
327 (InputType.TYPE_TEXT_FLAG_MULTI_LINE & outAttrs.inputType) != 0
328 ? EditorInfo.IME_ACTION_NONE
329 : EditorInfo.IME_ACTION_DONE;
330 } else {
331 enterAction = configuration.inputAction;
332 }
333 if (configuration.actionLabel != null) {
334 outAttrs.actionLabel = configuration.actionLabel;
335 outAttrs.actionId = enterAction;
336 }
337 outAttrs.imeOptions |= enterAction;
338
339 if (configuration.contentCommitMimeTypes != null) {
340 String[] imgTypeString = configuration.contentCommitMimeTypes;
341 EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString);
342 }
343
344 InputConnectionAdaptor connection =
346 view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs);
347 outAttrs.initialSelStart = mEditable.getSelectionStart();
348 outAttrs.initialSelEnd = mEditable.getSelectionEnd();
349
350 lastInputConnection = connection;
351 return lastInputConnection;
352 }
353
354 @Nullable
355 public InputConnection getLastInputConnection() {
356 return lastInputConnection;
357 }
358
359 /**
360 * Clears a platform view text input client if it is the current input target.
361 *
362 * <p>This is called when a platform view is disposed to make sure we're not hanging to a stale
363 * input connection.
364 */
365 public void clearPlatformViewClient(int platformViewId) {
366 if ((inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW
367 || inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW)
368 && inputTarget.id == platformViewId) {
369 inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
370 notifyViewExited();
371 mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
372 mImm.restartInput(mView);
373 mRestartInputPending = false;
374 }
375 }
376
377 public void sendTextInputAppPrivateCommand(@NonNull String action, @NonNull Bundle data) {
378 mImm.sendAppPrivateCommand(mView, action, data);
379 }
380
381 @VisibleForTesting
382 void showTextInput(View view) {
383 if (configuration == null
384 || configuration.inputType == null
385 || configuration.inputType.type != TextInputChannel.TextInputType.NONE) {
386 view.requestFocus();
387 mImm.showSoftInput(view, 0);
388 } else {
389 hideTextInput(view);
390 }
391 }
392
393 private void hideTextInput(View view) {
394 notifyViewExited();
395 // Note: when a virtual display is used, a race condition may lead to us hiding the keyboard
396 // here just after a platform view has shown it.
397 // This can only potentially happen when switching focus from a Flutter text field to a platform
398 // view's text
399 // field(by text field here I mean anything that keeps the keyboard open).
400 // See: https://github.com/flutter/flutter/issues/34169
401 mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
402 }
403
404 @VisibleForTesting
405 void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
406 // Call notifyViewExited on the previous field.
407 notifyViewExited();
408 this.configuration = configuration;
409 inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
410
411 mEditable.removeEditingStateListener(this);
412 mEditable =
414 configuration.autofill != null ? configuration.autofill.editState : null, mView);
415 updateAutofillConfigurationIfNeeded(configuration);
416
417 // setTextInputClient will be followed by a call to setTextInputEditingState.
418 // Do a restartInput at that time.
419 mRestartInputPending = true;
420 unlockPlatformViewInputConnection();
421 lastClientRect = null;
422 mEditable.addEditingStateListener(this);
423 }
424
425 private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
426 if (usesVirtualDisplay) {
427 // We need to make sure that the Flutter view is focused so that no imm operations get short
428 // circuited.
429 // Not asking for focus here specifically manifested in a bug on API 28 devices where the
430 // platform view's request to show a keyboard was ignored.
431 mView.requestFocus();
432 inputTarget = new InputTarget(InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW, platformViewId);
433 mImm.restartInput(mView);
434 mRestartInputPending = false;
435 } else {
436 inputTarget =
437 new InputTarget(InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW, platformViewId);
438 lastInputConnection = null;
439 }
440 }
441
442 private static boolean composingChanged(
443 TextInputChannel.TextEditState before, TextInputChannel.TextEditState after) {
444 final int composingRegionLength = before.composingEnd - before.composingStart;
445 if (composingRegionLength != after.composingEnd - after.composingStart) {
446 return true;
447 }
448 for (int index = 0; index < composingRegionLength; index++) {
449 if (before.text.charAt(index + before.composingStart)
450 != after.text.charAt(index + after.composingStart)) {
451 return true;
452 }
453 }
454 return false;
455 }
456
457 // Called by the text input channel to update the text input plugin with the
458 // latest TextEditState from the framework.
459 @VisibleForTesting
460 void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
461 if (!mRestartInputPending
462 && mLastKnownFrameworkTextEditingState != null
463 && mLastKnownFrameworkTextEditingState.hasComposing()) {
464 // Also restart input if the framework (or the developer) decides to
465 // change the composing region by itself (which is discouraged). Many IMEs
466 // don't expect editors to commit composing text, so a restart is needed
467 // to reset their internal states.
468 mRestartInputPending = composingChanged(mLastKnownFrameworkTextEditingState, state);
469 if (mRestartInputPending) {
470 Log.i(TAG, "Composing region changed by the framework. Restarting the input method.");
471 }
472 }
473
474 mLastKnownFrameworkTextEditingState = state;
475 mEditable.setEditingState(state);
476
477 // Restart if needed. Restarting will also update the selection.
478 if (mRestartInputPending) {
479 mImm.restartInput(view);
480 mRestartInputPending = false;
481 }
482 }
483
484 private interface MinMax {
485 void inspect(double x, double y);
486 }
487
488 private void saveEditableSizeAndTransform(double width, double height, double[] matrix) {
489 final double[] minMax = new double[4]; // minX, maxX, minY, maxY.
490 final boolean isAffine = matrix[3] == 0 && matrix[7] == 0 && matrix[15] == 1;
491 minMax[0] = minMax[1] = matrix[12] / matrix[15]; // minX and maxX.
492 minMax[2] = minMax[3] = matrix[13] / matrix[15]; // minY and maxY.
493
494 final MinMax finder =
495 new MinMax() {
496 @Override
497 public void inspect(double x, double y) {
498 final double w = isAffine ? 1 : 1 / (matrix[3] * x + matrix[7] * y + matrix[15]);
499 final double tx = (matrix[0] * x + matrix[4] * y + matrix[12]) * w;
500 final double ty = (matrix[1] * x + matrix[5] * y + matrix[13]) * w;
501
502 if (tx < minMax[0]) {
503 minMax[0] = tx;
504 } else if (tx > minMax[1]) {
505 minMax[1] = tx;
506 }
507
508 if (ty < minMax[2]) {
509 minMax[2] = ty;
510 } else if (ty > minMax[3]) {
511 minMax[3] = ty;
512 }
513 }
514 };
515
516 finder.inspect(width, 0);
517 finder.inspect(width, height);
518 finder.inspect(0, height);
519 final Float density = mView.getContext().getResources().getDisplayMetrics().density;
520 lastClientRect =
521 new Rect(
522 (int) (minMax[0] * density),
523 (int) (minMax[2] * density),
524 (int) Math.ceil(minMax[1] * density),
525 (int) Math.ceil(minMax[3] * density));
526 }
527
528 @VisibleForTesting
530 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
531 // This only applies to platform views that use a virtual display.
532 // Focus changes in the framework tree have no guarantees on the order focus nodes are
533 // notified. A node that lost focus may be notified before or after a node that gained focus.
534 // When moving the focus from a Flutter text field to an AndroidView, it is possible that the
535 // Flutter text field's focus node will be notified that it lost focus after the AndroidView
536 // was notified that it gained focus. When this happens the text field will send a
537 // clearTextInput command which we ignore.
538 // By doing this we prevent the framework from clearing a platform view input client (the only
539 // way to do so is to set a new framework text client). I don't see an obvious use case for
540 // "clearing" a platform view's text input client, and it may be error prone as we don't know
541 // how the platform view manages the input connection and we probably shouldn't interfere.
542 // If we ever want to allow the framework to clear a platform view text client we should
543 // probably consider changing the focus manager such that focus nodes that lost focus are
544 // notified before focus nodes that gained focus as part of the same focus event.
545 return;
546 }
547 mEditable.removeEditingStateListener(this);
548 notifyViewExited();
549 configuration = null;
550 updateAutofillConfigurationIfNeeded(null);
551 inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
552 unlockPlatformViewInputConnection();
553 lastClientRect = null;
554
555 // Call restartInput to reset IME internal states. Otherwise some IMEs (Gboard for instance)
556 // keep reacting based on the previous input configuration until a new configuration is set.
557 mImm.restartInput(mView);
558 }
559
560 private static class InputTarget {
561 enum Type {
563 // InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter
564 // framework.
566 // InputConnection is managed by a platform view that is presented on a virtual display.
568 // InputConnection is managed by a platform view that is embedded in the activity's view
569 // hierarchy. This view hierarchy is displayed in a physical display within the aplication
570 // display area.
572 }
573
574 public InputTarget(@NonNull Type type, int id) {
575 this.type = type;
576 this.id = id;
577 }
578
579 @NonNull Type type;
580 // The ID of the input target.
581 //
582 // For framework clients this is the framework input connection client ID.
583 // For platform views this is the platform view's ID.
584 int id;
585 }
586
587 // -------- Start: KeyboardManager Synchronous Responder -------
588 public boolean handleKeyEvent(@NonNull KeyEvent keyEvent) {
589 if (!getInputMethodManager().isAcceptingText() || lastInputConnection == null) {
590 return false;
591 }
592
593 // Send the KeyEvent as an IME KeyEvent. If the input connection is an
594 // InputConnectionAdaptor then call its handleKeyEvent method (because
595 // this method will be called by the keyboard manager, and
596 // InputConnectionAdaptor#sendKeyEvent forwards the key event back to the
597 // keyboard manager).
598 return (lastInputConnection instanceof InputConnectionAdaptor)
599 ? ((InputConnectionAdaptor) lastInputConnection).handleKeyEvent(keyEvent)
600 : lastInputConnection.sendKeyEvent(keyEvent);
601 }
602 // -------- End: KeyboardManager Synchronous Responder -------
603
604 // -------- Start: ListenableEditingState watcher implementation -------
605
606 @Override
608 boolean textChanged, boolean selectionChanged, boolean composingRegionChanged) {
609 if (textChanged) {
610 // Notify the autofill manager of the value change.
611 notifyValueChanged(mEditable.toString());
612 }
613
614 final int selectionStart = mEditable.getSelectionStart();
615 final int selectionEnd = mEditable.getSelectionEnd();
616 final int composingStart = mEditable.getComposingStart();
617 final int composingEnd = mEditable.getComposingEnd();
618
619 final ArrayList<TextEditingDelta> batchTextEditingDeltas =
620 mEditable.extractBatchTextEditingDeltas();
621 final boolean skipFrameworkUpdate =
622 // The framework needs to send its editing state first.
623 mLastKnownFrameworkTextEditingState == null
624 || (mEditable.toString().equals(mLastKnownFrameworkTextEditingState.text)
625 && selectionStart == mLastKnownFrameworkTextEditingState.selectionStart
626 && selectionEnd == mLastKnownFrameworkTextEditingState.selectionEnd
627 && composingStart == mLastKnownFrameworkTextEditingState.composingStart
628 && composingEnd == mLastKnownFrameworkTextEditingState.composingEnd);
629 if (!skipFrameworkUpdate) {
630 Log.v(TAG, "send EditingState to flutter: " + mEditable.toString());
631
632 if (configuration.enableDeltaModel) {
633 textInputChannel.updateEditingStateWithDeltas(inputTarget.id, batchTextEditingDeltas);
634 mEditable.clearBatchDeltas();
635 } else {
636 textInputChannel.updateEditingState(
637 inputTarget.id,
638 mEditable.toString(),
639 selectionStart,
640 selectionEnd,
641 composingStart,
642 composingEnd);
643 }
644 mLastKnownFrameworkTextEditingState =
645 new TextEditState(
646 mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd);
647 } else {
648 // Don't accumulate deltas if they are not sent to the framework.
649 mEditable.clearBatchDeltas();
650 }
651 }
652
653 // -------- End: ListenableEditingState watcher implementation -------
654
655 // -------- Start: Autofill -------
656 // ### Setup and provide the initial text values and hints.
657 //
658 // The TextInputConfiguration used to set up the current client is also used for populating
659 // "AutofillVirtualStructure" when requested by the autofill manager (AFM), See
660 // #onProvideAutofillVirtualStructure.
661 //
662 // ### Keep the AFM updated
663 //
664 // The autofill session connected to the AFM keeps a copy of the current state for each reported
665 // field in "AutofillVirtualStructure" (instead of holding a reference to those fields), so the
666 // AFM needs to be notified when text changes if the client was part of the
667 // "AutofillVirtualStructure" previously reported to the AFM. This step is essential for
668 // triggering autofill save. This is done in #didChangeEditingState by calling
669 // #notifyValueChanged.
670 //
671 // Additionally when the text input plugin receives a new TextInputConfiguration,
672 // AutofillManager#notifyValueChanged will be called on all the autofillable fields contained in
673 // the TextInputConfiguration, in case some of them are tracked by the session and their values
674 // have changed. However if the value of an unfocused EditableText is changed in the framework,
675 // such change will not be sent to the text input plugin until the next TextInput.attach call.
676 private boolean needsAutofill() {
677 return autofillConfiguration != null;
678 }
679
680 private void notifyViewEntered() {
681 if (Build.VERSION.SDK_INT < API_LEVELS.API_26 || afm == null || !needsAutofill()) {
682 return;
683 }
684
685 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
686 final int[] offset = new int[2];
687 mView.getLocationOnScreen(offset);
688 Rect rect = new Rect(lastClientRect);
689 rect.offset(offset[0], offset[1]);
690 afm.notifyViewEntered(mView, triggerIdentifier.hashCode(), rect);
691 }
692
693 private void notifyViewExited() {
694 if (Build.VERSION.SDK_INT < API_LEVELS.API_26
695 || afm == null
696 || configuration == null
697 || configuration.autofill == null
698 || !needsAutofill()) {
699 return;
700 }
701
702 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
703 afm.notifyViewExited(mView, triggerIdentifier.hashCode());
704 }
705
706 private void notifyValueChanged(String newValue) {
707 if (Build.VERSION.SDK_INT < API_LEVELS.API_26 || afm == null || !needsAutofill()) {
708 return;
709 }
710
711 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
712 afm.notifyValueChanged(mView, triggerIdentifier.hashCode(), AutofillValue.forText(newValue));
713 }
714
715 private void updateAutofillConfigurationIfNeeded(TextInputChannel.Configuration configuration) {
716 if (Build.VERSION.SDK_INT < API_LEVELS.API_26) {
717 return;
718 }
719
720 if (configuration == null || configuration.autofill == null) {
721 // Disables autofill if the configuration doesn't have an autofill field.
722 autofillConfiguration = null;
723 return;
724 }
725
726 final TextInputChannel.Configuration[] configurations = configuration.fields;
727 autofillConfiguration = new SparseArray<>();
728
729 if (configurations == null) {
730 autofillConfiguration.put(configuration.autofill.uniqueIdentifier.hashCode(), configuration);
731 } else {
732 for (TextInputChannel.Configuration config : configurations) {
733 TextInputChannel.Configuration.Autofill autofill = config.autofill;
734 if (autofill != null) {
735 autofillConfiguration.put(autofill.uniqueIdentifier.hashCode(), config);
736 afm.notifyValueChanged(
737 mView,
738 autofill.uniqueIdentifier.hashCode(),
739 AutofillValue.forText(autofill.editState.text));
740 }
741 }
742 }
743 }
744
745 public void onProvideAutofillVirtualStructure(@NonNull ViewStructure structure, int flags) {
746 if (Build.VERSION.SDK_INT < API_LEVELS.API_26 || !needsAutofill()) {
747 return;
748 }
749
750 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
751 final AutofillId parentId = structure.getAutofillId();
752 for (int i = 0; i < autofillConfiguration.size(); i++) {
753 final int autofillId = autofillConfiguration.keyAt(i);
754 final TextInputChannel.Configuration config = autofillConfiguration.valueAt(i);
755 final TextInputChannel.Configuration.Autofill autofill = config.autofill;
756 if (autofill == null) {
757 continue;
758 }
759
760 structure.addChildCount(1);
761 final ViewStructure child = structure.newChild(i);
762 child.setAutofillId(parentId, autofillId);
763 // Don't set hints when there's none.
764 // See https://github.com/flutter/flutter/issues/98505.
765 if (autofill.hints.length > 0) {
766 child.setAutofillHints(autofill.hints);
767 }
768 child.setAutofillType(View.AUTOFILL_TYPE_TEXT);
769 child.setVisibility(View.VISIBLE);
770 if (autofill.hintText != null) {
771 child.setHint(autofill.hintText);
772 }
773
774 // For some autofill services, only visible input fields are eligible for autofill.
775 // Reports the real size of the child if it's the current client, or 1x1 if we don't
776 // know the real dimensions of the child.
777 if (triggerIdentifier.hashCode() == autofillId && lastClientRect != null) {
778 child.setDimens(
779 lastClientRect.left,
780 lastClientRect.top,
781 0,
782 0,
783 lastClientRect.width(),
784 lastClientRect.height());
785 child.setAutofillValue(AutofillValue.forText(mEditable));
786 } else {
787 child.setDimens(0, 0, 0, 0, 1, 1);
788 child.setAutofillValue(AutofillValue.forText(autofill.editState.text));
789 }
790 }
791 }
792
793 public void autofill(@NonNull SparseArray<AutofillValue> values) {
794 if (Build.VERSION.SDK_INT < API_LEVELS.API_26) {
795 return;
796 }
797
798 if (configuration == null || autofillConfiguration == null || configuration.autofill == null) {
799 return;
800 }
801
802 final TextInputChannel.Configuration.Autofill currentAutofill = configuration.autofill;
803 final HashMap<String, TextInputChannel.TextEditState> editingValues = new HashMap<>();
804 for (int i = 0; i < values.size(); i++) {
805 int virtualId = values.keyAt(i);
806
807 final TextInputChannel.Configuration config = autofillConfiguration.get(virtualId);
808 if (config == null || config.autofill == null) {
809 continue;
810 }
811
812 final TextInputChannel.Configuration.Autofill autofill = config.autofill;
813 final String value = values.valueAt(i).getTextValue().toString();
814 final TextInputChannel.TextEditState newState =
815 new TextInputChannel.TextEditState(value, value.length(), value.length(), -1, -1);
816
817 if (autofill.uniqueIdentifier.equals(currentAutofill.uniqueIdentifier)) {
818 // Autofilling the current client is the same as handling user input
819 // from the virtual keyboard. Setting the editable to newState and an
820 // update will be sent to the framework.
821 mEditable.setEditingState(newState);
822 } else {
823 editingValues.put(autofill.uniqueIdentifier, newState);
824 }
825 }
826 textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues);
827 }
828 // -------- End: Autofill -------
829}
static void v(@NonNull String tag, @NonNull String message)
Definition Log.java:40
static void i(@NonNull String tag, @NonNull String message)
Definition Log.java:52
void didChangeEditingState(boolean textChanged, boolean selectionChanged, boolean composingRegionChanged)
void sendTextInputAppPrivateCommand(@NonNull String action, @NonNull Bundle data)
void setTextInputClient(int client, TextInputChannel.Configuration configuration)
ImeSyncDeferringInsetsCallback getImeSyncCallback()
boolean handleKeyEvent(@NonNull KeyEvent keyEvent)
void setTextInputEditingState(View view, TextInputChannel.TextEditState state)
InputConnection createInputConnection( @NonNull View view, @NonNull KeyboardManager keyboardManager, @NonNull EditorInfo outAttrs)
void onProvideAutofillVirtualStructure(@NonNull ViewStructure structure, int flags)
void autofill(@NonNull SparseArray< AutofillValue > values)
AtkStateType state
FlutterSemanticsFlag flags
uint8_t value
static FlMethodResponse * hide(FlTextInputPlugin *self)
double y
double x
Build(configs, env, options)
Definition build.py:232
TRect< Scalar > Rect
Definition rect.h:746
static SkColor4f transform(SkColor4f c, SkColorSpace *src, SkColorSpace *dst)
Definition p3.cpp:47
SkScalar w
#define TAG()
int32_t height
int32_t width
Point offset
const uintptr_t id
int_closure destroy