40 private static final String TAG =
"TextInputPlugin";
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;
50 private boolean mRestartInputPending;
51 @Nullable
private InputConnection lastInputConnection;
52 @NonNull
private PlatformViewsController platformViewsController;
53 @Nullable
private Rect lastClientRect;
57 private TextEditState mLastKnownFrameworkTextEditingState;
63 private boolean isInputConnectionLocked;
65 @SuppressLint(
"NewApi")
68 @NonNull TextInputChannel textInputChannel,
69 @NonNull PlatformViewsController platformViewsController) {
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);
83 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
88 this.textInputChannel = textInputChannel;
89 textInputChannel.setTextInputMethodHandler(
90 new TextInputChannel.TextInputMethodHandler() {
98 if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
101 hideTextInput(mView);
106 public void requestAutofill() {
111 public void finishAutofillContext(
boolean shouldSave) {
112 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26 || afm ==
null) {
123 public void setClient(
124 int textInputClientId, TextInputChannel.Configuration configuration) {
129 public void setPlatformViewClient(
int platformViewId,
boolean usesVirtualDisplay) {
130 setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
134 public void setEditingState(TextInputChannel.TextEditState editingState) {
144 public void clearClient() {
149 public void sendAppPrivateCommand(String
action, Bundle data) {
154 textInputChannel.requestExistingInputState();
156 this.platformViewsController = platformViewsController;
157 this.platformViewsController.attachTextInputPlugin(
this);
172 return imeSyncCallback;
189 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
190 isInputConnectionLocked =
true;
200 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
201 isInputConnectionLocked =
false;
210 @SuppressLint(
"NewApi")
212 platformViewsController.detachTextInputPlugin();
213 textInputChannel.setTextInputMethodHandler(
null);
215 mEditable.removeEditingStateListener(
this);
216 if (imeSyncCallback !=
null) {
217 imeSyncCallback.remove();
221 private static int inputTypeFromTextInputType(
222 TextInputChannel.InputType
type,
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;
233 textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
235 if (
type.isDecimal) {
236 textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
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;
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;
262 textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
263 textType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
265 if (autocorrect) textType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
266 if (!enableSuggestions) {
268 textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
269 textType |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
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;
286 @NonNull View view, @NonNull KeyboardManager keyboardManager, @NonNull EditorInfo outAttrs) {
287 if (inputTarget.type == InputTarget.Type.NO_TARGET) {
288 lastInputConnection =
null;
292 if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
296 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
297 if (isInputConnectionLocked) {
298 return lastInputConnection;
300 lastInputConnection =
301 platformViewsController
302 .getPlatformViewById(inputTarget.id)
303 .onCreateInputConnection(outAttrs);
304 return lastInputConnection;
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;
317 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_26
318 && !configuration.enableIMEPersonalizedLearning) {
319 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING;
323 if (configuration.inputAction ==
null) {
327 (InputType.TYPE_TEXT_FLAG_MULTI_LINE & outAttrs.inputType) != 0
328 ? EditorInfo.IME_ACTION_NONE
329 : EditorInfo.IME_ACTION_DONE;
331 enterAction = configuration.inputAction;
333 if (configuration.actionLabel !=
null) {
334 outAttrs.actionLabel = configuration.actionLabel;
335 outAttrs.actionId = enterAction;
337 outAttrs.imeOptions |= enterAction;
339 if (configuration.contentCommitMimeTypes !=
null) {
340 String[] imgTypeString = configuration.contentCommitMimeTypes;
341 EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString);
346 view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs);
347 outAttrs.initialSelStart = mEditable.getSelectionStart();
348 outAttrs.initialSelEnd = mEditable.getSelectionEnd();
350 lastInputConnection = connection;
351 return lastInputConnection;
356 return lastInputConnection;
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);
371 mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
372 mImm.restartInput(mView);
373 mRestartInputPending =
false;
378 mImm.sendAppPrivateCommand(mView,
action, data);
383 if (configuration ==
null
384 || configuration.inputType ==
null
385 || configuration.inputType.type != TextInputChannel.TextInputType.NONE) {
387 mImm.showSoftInput(view, 0);
393 private void hideTextInput(View view) {
401 mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
408 this.configuration = configuration;
409 inputTarget =
new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
411 mEditable.removeEditingStateListener(
this);
414 configuration.autofill !=
null ? configuration.autofill.editState :
null, mView);
415 updateAutofillConfigurationIfNeeded(configuration);
419 mRestartInputPending =
true;
420 unlockPlatformViewInputConnection();
421 lastClientRect =
null;
422 mEditable.addEditingStateListener(
this);
425 private void setPlatformViewTextInputClient(
int platformViewId,
boolean usesVirtualDisplay) {
426 if (usesVirtualDisplay) {
431 mView.requestFocus();
432 inputTarget =
new InputTarget(InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW, platformViewId);
433 mImm.restartInput(mView);
434 mRestartInputPending =
false;
437 new InputTarget(InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW, platformViewId);
438 lastInputConnection =
null;
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) {
448 for (
int index = 0; index < composingRegionLength; index++) {
449 if (before.text.charAt(index + before.composingStart)
450 != after.text.charAt(index + after.composingStart)) {
461 if (!mRestartInputPending
462 && mLastKnownFrameworkTextEditingState !=
null
463 && mLastKnownFrameworkTextEditingState.hasComposing()) {
468 mRestartInputPending = composingChanged(mLastKnownFrameworkTextEditingState,
state);
469 if (mRestartInputPending) {
470 Log.
i(
TAG,
"Composing region changed by the framework. Restarting the input method.");
474 mLastKnownFrameworkTextEditingState =
state;
475 mEditable.setEditingState(
state);
478 if (mRestartInputPending) {
479 mImm.restartInput(view);
480 mRestartInputPending =
false;
484 private interface MinMax {
488 private void saveEditableSizeAndTransform(
double width,
double height,
double[] matrix) {
489 final double[] minMax =
new double[4];
490 final boolean isAffine = matrix[3] == 0 && matrix[7] == 0 && matrix[15] == 1;
491 minMax[0] = minMax[1] = matrix[12] / matrix[15];
492 minMax[2] = minMax[3] = matrix[13] / matrix[15];
494 final MinMax finder =
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;
502 if (tx < minMax[0]) {
504 }
else if (tx > minMax[1]) {
508 if (ty < minMax[2]) {
510 }
else if (ty > minMax[3]) {
516 finder.inspect(
width, 0);
518 finder.inspect(0,
height);
519 final Float density = mView.getContext().getResources().getDisplayMetrics().density;
522 (
int) (minMax[0] * density),
523 (
int) (minMax[2] * density),
524 (
int) Math.ceil(minMax[1] * density),
525 (
int) Math.ceil(minMax[3] * density));
530 if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
547 mEditable.removeEditingStateListener(
this);
549 configuration =
null;
550 updateAutofillConfigurationIfNeeded(
null);
551 inputTarget =
new InputTarget(InputTarget.Type.NO_TARGET, 0);
552 unlockPlatformViewInputConnection();
553 lastClientRect =
null;
557 mImm.restartInput(mView);
560 private static class InputTarget {
574 public InputTarget(@NonNull
Type type,
int id) {
589 if (!getInputMethodManager().isAcceptingText() || lastInputConnection ==
null) {
600 : lastInputConnection.sendKeyEvent(keyEvent);
608 boolean textChanged,
boolean selectionChanged,
boolean composingRegionChanged) {
611 notifyValueChanged(mEditable.toString());
614 final int selectionStart = mEditable.getSelectionStart();
615 final int selectionEnd = mEditable.getSelectionEnd();
616 final int composingStart = mEditable.getComposingStart();
617 final int composingEnd = mEditable.getComposingEnd();
619 final ArrayList<TextEditingDelta> batchTextEditingDeltas =
620 mEditable.extractBatchTextEditingDeltas();
621 final boolean skipFrameworkUpdate =
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());
632 if (configuration.enableDeltaModel) {
633 textInputChannel.updateEditingStateWithDeltas(inputTarget.id, batchTextEditingDeltas);
634 mEditable.clearBatchDeltas();
636 textInputChannel.updateEditingState(
638 mEditable.toString(),
644 mLastKnownFrameworkTextEditingState =
646 mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd);
649 mEditable.clearBatchDeltas();
676 private boolean needsAutofill() {
677 return autofillConfiguration !=
null;
680 private void notifyViewEntered() {
681 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26 || afm ==
null || !needsAutofill()) {
685 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
686 final int[]
offset =
new int[2];
687 mView.getLocationOnScreen(
offset);
688 Rect rect =
new Rect(lastClientRect);
690 afm.notifyViewEntered(mView, triggerIdentifier.hashCode(), rect);
693 private void notifyViewExited() {
694 if (Build.VERSION.SDK_INT < API_LEVELS.API_26
696 || configuration ==
null
697 || configuration.autofill ==
null
698 || !needsAutofill()) {
702 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
703 afm.notifyViewExited(mView, triggerIdentifier.hashCode());
706 private void notifyValueChanged(String newValue) {
707 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26 || afm ==
null || !needsAutofill()) {
711 final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
712 afm.notifyValueChanged(mView, triggerIdentifier.hashCode(), AutofillValue.forText(newValue));
715 private void updateAutofillConfigurationIfNeeded(TextInputChannel.Configuration configuration) {
716 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26) {
720 if (configuration ==
null || configuration.autofill ==
null) {
722 autofillConfiguration =
null;
726 final TextInputChannel.Configuration[] configurations = configuration.fields;
727 autofillConfiguration =
new SparseArray<>();
729 if (configurations ==
null) {
730 autofillConfiguration.put(configuration.autofill.uniqueIdentifier.hashCode(), configuration);
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(
738 autofill.uniqueIdentifier.hashCode(),
739 AutofillValue.forText(autofill.editState.text));
746 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26 || !needsAutofill()) {
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) {
760 structure.addChildCount(1);
761 final ViewStructure child = structure.newChild(i);
762 child.setAutofillId(parentId, autofillId);
765 if (autofill.hints.length > 0) {
766 child.setAutofillHints(autofill.hints);
768 child.setAutofillType(View.AUTOFILL_TYPE_TEXT);
769 child.setVisibility(View.VISIBLE);
770 if (autofill.hintText !=
null) {
771 child.setHint(autofill.hintText);
777 if (triggerIdentifier.hashCode() == autofillId && lastClientRect !=
null) {
783 lastClientRect.width(),
784 lastClientRect.height());
785 child.setAutofillValue(AutofillValue.forText(mEditable));
787 child.setDimens(0, 0, 0, 0, 1, 1);
788 child.setAutofillValue(AutofillValue.forText(autofill.editState.text));
793 public void autofill(@NonNull SparseArray<AutofillValue> values) {
794 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26) {
798 if (configuration ==
null || autofillConfiguration ==
null || configuration.autofill ==
null) {
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);
807 final TextInputChannel.Configuration config = autofillConfiguration.get(virtualId);
808 if (config ==
null || config.autofill ==
null) {
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);
817 if (autofill.uniqueIdentifier.equals(currentAutofill.uniqueIdentifier)) {
821 mEditable.setEditingState(newState);
823 editingValues.put(autofill.uniqueIdentifier, newState);
826 textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues);