5package io.flutter.plugin.editing;
7import static io.flutter.Build.API_LEVELS;
9import android.annotation.SuppressLint;
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;
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;
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;
46 @NonNull
private InputTarget inputTarget =
new InputTarget(InputTarget.Type.NO_TARGET, 0);
47 @Nullable
private TextInputChannel.Configuration configuration;
50 private boolean mRestartInputPending;
51 @Nullable
private InputConnection lastInputConnection;
53 @Nullable
private Rect lastClientRect;
63 private boolean isInputConnectionLocked;
65 @SuppressLint(
"NewApi")
73 mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
75 afm = view.getContext().getSystemService(AutofillManager.class);
88 this.textInputChannel = textInputChannel;
89 textInputChannel.setTextInputMethodHandler(
98 if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
101 hideTextInput(mView);
106 public void requestAutofill() {
111 public void finishAutofillContext(
boolean shouldSave) {
123 public void setClient(
129 public void setPlatformViewClient(
int platformViewId,
boolean usesVirtualDisplay) {
130 setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
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(
225 boolean enableSuggestions,
226 boolean enableIMEPersonalizedLearning,
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;
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
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];
494 final MinMax finder =
497 public void inspect(
double x,
double y) {
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);
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));
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));
798 if (configuration ==
null || autofillConfiguration ==
null || configuration.autofill ==
null) {
802 final TextInputChannel.Configuration.Autofill currentAutofill = configuration.autofill;
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 =
817 if (autofill.uniqueIdentifier.equals(currentAutofill.uniqueIdentifier)) {
821 mEditable.setEditingState(newState);
823 editingValues.put(autofill.uniqueIdentifier, newState);
826 textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues);
static void v(@NonNull String tag, @NonNull String message)
static void i(@NonNull String tag, @NonNull String message)
void didChangeEditingState(boolean textChanged, boolean selectionChanged, boolean composingRegionChanged)
InputMethodManager getInputMethodManager()
void clearPlatformViewClient(int platformViewId)
void clearTextInputClient()
void sendTextInputAppPrivateCommand(@NonNull String action, @NonNull Bundle data)
void setTextInputClient(int client, TextInputChannel.Configuration configuration)
ImeSyncDeferringInsetsCallback getImeSyncCallback()
InputConnection getLastInputConnection()
boolean handleKeyEvent(@NonNull KeyEvent keyEvent)
void setTextInputEditingState(View view, TextInputChannel.TextEditState state)
void showTextInput(View view)
InputConnection createInputConnection( @NonNull View view, @NonNull KeyboardManager keyboardManager, @NonNull EditorInfo outAttrs)
void onProvideAutofillVirtualStructure(@NonNull ViewStructure structure, int flags)
void lockPlatformViewInputConnection()
void autofill(@NonNull SparseArray< AutofillValue > values)
void unlockPlatformViewInputConnection()
VIRTUAL_DISPLAY_PLATFORM_VIEW
PHYSICAL_DISPLAY_PLATFORM_VIEW
FlutterSemanticsFlag flags
static FlMethodResponse * hide(FlTextInputPlugin *self)
unsigned useCenter Optional< SkMatrix > matrix
sk_sp< SkBlender > blender SkRect rect
def Build(configs, env, options)
static SkColor4f transform(SkColor4f c, SkColorSpace *src, SkColorSpace *dst)
std::shared_ptr< const fml::Mapping > data