Android text input autofill (flutter/engine#17465)

This commit is contained in:
LongCatIsLooong 2020-04-16 03:41:23 -07:00 committed by GitHub
parent fd18ae6aeb
commit 1afb95d156
5 changed files with 592 additions and 15 deletions

View File

@ -14,12 +14,15 @@ import android.os.Build;
import android.os.LocaleList;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewStructure;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
@ -283,6 +286,9 @@ public class FlutterView extends FrameLayout {
// FlutterView needs to be focusable so that the InputMethodManager can interact with it.
setFocusable(true);
setFocusableInTouchMode(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS);
}
}
/**
@ -898,6 +904,17 @@ public class FlutterView extends FrameLayout {
flutterEngine.getRenderer().setViewportMetrics(viewportMetrics);
}
@Override
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
super.onProvideAutofillVirtualStructure(structure, flags);
textInputPlugin.onProvideAutofillVirtualStructure(structure, flags);
}
@Override
public void autofill(SparseArray<AutofillValue> values) {
textInputPlugin.autofill(values);
}
/**
* Render modes for a {@link FlutterView}.
*

View File

@ -1,5 +1,7 @@
package io.flutter.embedding.engine.systemchannels;
import android.os.Build;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -10,6 +12,7 @@ import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -85,6 +88,22 @@ public class TextInputChannel {
result.error("error", exception.getMessage(), null);
}
break;
case "TextInput.setEditableSizeAndTransform":
try {
final JSONObject arguments = (JSONObject) args;
final double width = arguments.getDouble("width");
final double height = arguments.getDouble("height");
final JSONArray jsonMatrix = arguments.getJSONArray("transform");
final double[] matrix = new double[16];
for (int i = 0; i < 16; i++) {
matrix[i] = jsonMatrix.getDouble(i);
}
textInputMethodHandler.setEditableSizeAndTransform(width, height, matrix);
} catch (JSONException exception) {
result.error("error", exception.getMessage(), null);
}
break;
case "TextInput.clearClient":
textInputMethodHandler.clearClient();
result.success(null);
@ -119,6 +138,16 @@ public class TextInputChannel {
channel.invokeMethod("TextInputClient.requestExistingInputState", null);
}
private static HashMap<Object, Object> createEditingStateJSON(
String text, int selectionStart, int selectionEnd, int composingStart, int composingEnd) {
HashMap<Object, Object> state = new HashMap<>();
state.put("text", text);
state.put("selectionBase", selectionStart);
state.put("selectionExtent", selectionEnd);
state.put("composingBase", composingStart);
state.put("composingExtent", composingEnd);
return state;
}
/**
* Instructs Flutter to update its text input editing state to reflect the given configuration.
*/
@ -147,16 +176,31 @@ public class TextInputChannel {
+ "Composing end: "
+ composingEnd);
HashMap<Object, Object> state = new HashMap<>();
state.put("text", text);
state.put("selectionBase", selectionStart);
state.put("selectionExtent", selectionEnd);
state.put("composingBase", composingStart);
state.put("composingExtent", composingEnd);
final HashMap<Object, Object> state =
createEditingStateJSON(text, selectionStart, selectionEnd, composingStart, composingEnd);
channel.invokeMethod("TextInputClient.updateEditingState", Arrays.asList(inputClientId, state));
}
public void updateEditingStateWithTag(
int inputClientId, HashMap<String, TextEditState> editStates) {
Log.v(
TAG,
"Sending message to update editing state for "
+ String.valueOf(editStates.size())
+ " field(s).");
final HashMap<String, HashMap<Object, Object>> json = new HashMap<>();
for (Map.Entry<String, TextEditState> element : editStates.entrySet()) {
final TextEditState state = element.getValue();
json.put(
element.getKey(),
createEditingStateJSON(state.text, state.selectionStart, state.selectionEnd, -1, -1));
}
channel.invokeMethod(
"TextInputClient.updateEditingStateWithTag", Arrays.asList(inputClientId, json));
}
/** Instructs Flutter to execute a "newline" action. */
public void newline(int inputClientId) {
Log.v(TAG, "Sending 'newline' message.");
@ -229,6 +273,13 @@ public class TextInputChannel {
// TODO(mattcarroll): javadoc
void hide();
/**
* Requests that the autofill dropdown menu appear for the current client.
*
* <p>Has no effect if the current client does not support autofill.
*/
void requestAutofill();
// TODO(mattcarroll): javadoc
void setClient(int textInputClientId, @NonNull Configuration configuration);
@ -242,6 +293,16 @@ public class TextInputChannel {
*/
void setPlatformViewClient(int id);
/**
* Sets the size and the transform matrix of the current text input client.
*
* @param width the width of text input client. Must be finite.
* @param height the height of text input client. Must be finite.
* @param transform a 4x4 matrix that maps the local paint coordinate system to coordinate
* system of the FlutterView that owns the current client.
*/
void setEditableSizeAndTransform(double width, double height, double[] transform);
// TODO(mattcarroll): javadoc
void setEditingState(@NonNull TextEditState editingState);
@ -257,7 +318,14 @@ public class TextInputChannel {
if (inputActionName == null) {
throw new JSONException("Configuration JSON missing 'inputAction' property.");
}
Configuration[] fields = null;
if (!json.isNull("fields")) {
final JSONArray jsonFields = json.getJSONArray("fields");
fields = new Configuration[jsonFields.length()];
for (int i = 0; i < fields.length; i++) {
fields[i] = Configuration.fromJson(jsonFields.getJSONObject(i));
}
}
final Integer inputAction = inputActionFromTextInputAction(inputActionName);
return new Configuration(
json.optBoolean("obscureText"),
@ -266,7 +334,9 @@ public class TextInputChannel {
TextCapitalization.fromValue(json.getString("textCapitalization")),
InputType.fromJson(json.getJSONObject("inputType")),
inputAction,
json.isNull("actionLabel") ? null : json.getString("actionLabel"));
json.isNull("actionLabel") ? null : json.getString("actionLabel"),
json.isNull("autofill") ? null : Autofill.fromJson(json.getJSONObject("autofill")),
fields);
}
@NonNull
@ -296,6 +366,117 @@ public class TextInputChannel {
}
}
public static class Autofill {
public static Autofill fromJson(@NonNull JSONObject json)
throws JSONException, NoSuchFieldException {
final String uniqueIdentifier = json.getString("uniqueIdentifier");
final JSONArray hints = json.getJSONArray("hints");
final JSONObject editingState = json.getJSONObject("editingValue");
final String[] hintList = new String[hints.length()];
for (int i = 0; i < hintList.length; i++) {
hintList[i] = translateAutofillHint(hints.getString(i));
}
return new Autofill(uniqueIdentifier, hintList, TextEditState.fromJson(editingState));
}
public final String uniqueIdentifier;
public final String[] hints;
public final TextEditState editState;
@NonNull
private static String translateAutofillHint(@NonNull String hint) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return hint;
}
switch (hint) {
case "addressCity":
return "addressLocality";
case "addressState":
return "addressRegion";
case "birthday":
return "birthDateFull";
case "birthdayDay":
return "birthDateDay";
case "birthdayMonth":
return "birthDateMonth";
case "birthdayYear":
return "birthDateYear";
case "countryName":
return "addressCountry";
case "creditCardExpirationDate":
return View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE;
case "creditCardExpirationDay":
return View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY;
case "creditCardExpirationMonth":
return View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH;
case "creditCardExpirationYear":
return View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR;
case "creditCardNumber":
return View.AUTOFILL_HINT_CREDIT_CARD_NUMBER;
case "creditCardSecurityCode":
return View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE;
case "email":
return View.AUTOFILL_HINT_EMAIL_ADDRESS;
case "familyName":
return "personFamilyName";
case "fullStreetAddress":
return "streetAddress";
case "gender":
return "gender";
case "givenName":
return "personGivenName";
case "middleInitial":
return "personMiddleInitial";
case "middleName":
return "personMiddleName";
case "name":
return "personName";
case "namePrefix":
return "personNamePrefix";
case "nameSuffix":
return "personNameSuffix";
case "newPassword":
return "newPassword";
case "newUsername":
return "newUsername";
case "oneTimeCode":
return "smsOTPCode";
case "password":
return View.AUTOFILL_HINT_PASSWORD;
case "postalAddress":
return View.AUTOFILL_HINT_POSTAL_ADDRESS;
case "postalAddressExtended":
return "extendedAddress";
case "postalAddressExtendedPostalCode":
return "extendedPostalCode";
case "postalCode":
return View.AUTOFILL_HINT_POSTAL_CODE;
case "telephoneNumber":
return "phoneNumber";
case "telephoneNumberCountryCode":
return "phoneCountryCode";
case "telephoneNumberDevice":
return "phoneNumberDevice";
case "telephoneNumberNational":
return "phoneNational";
case "username":
return View.AUTOFILL_HINT_USERNAME;
default:
return hint;
}
}
public Autofill(
@NonNull String uniqueIdentifier,
@NonNull String[] hints,
@NonNull TextEditState editingState) {
this.uniqueIdentifier = uniqueIdentifier;
this.hints = hints;
this.editState = editingState;
}
}
public final boolean obscureText;
public final boolean autocorrect;
public final boolean enableSuggestions;
@ -303,6 +484,8 @@ public class TextInputChannel {
@NonNull public final InputType inputType;
@Nullable public final Integer inputAction;
@Nullable public final String actionLabel;
@Nullable public final Autofill autofill;
@Nullable public final Configuration[] fields;
public Configuration(
boolean obscureText,
@ -311,7 +494,9 @@ public class TextInputChannel {
@NonNull TextCapitalization textCapitalization,
@NonNull InputType inputType,
@Nullable Integer inputAction,
@Nullable String actionLabel) {
@Nullable String actionLabel,
@Nullable Autofill autofill,
@Nullable Configuration[] fields) {
this.obscureText = obscureText;
this.autocorrect = autocorrect;
this.enableSuggestions = enableSuggestions;
@ -319,6 +504,8 @@ public class TextInputChannel {
this.inputType = inputType;
this.inputAction = inputAction;
this.actionLabel = actionLabel;
this.autofill = autofill;
this.fields = fields;
}
}

View File

@ -6,12 +6,18 @@ package io.flutter.plugin.editing;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.provider.Settings;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@ -23,18 +29,22 @@ import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.platform.PlatformViewsController;
import java.util.HashMap;
/** Android implementation of the text input plugin. */
public class TextInputPlugin {
@NonNull private final View mView;
@NonNull private final InputMethodManager mImm;
@NonNull private final AutofillManager afm;
@NonNull private final TextInputChannel textInputChannel;
@NonNull private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
@Nullable private TextInputChannel.Configuration configuration;
@Nullable private SparseArray<TextInputChannel.Configuration> mAutofillConfigurations;
@Nullable private Editable mEditable;
private boolean mRestartInputPending;
@Nullable private InputConnection lastInputConnection;
@NonNull private PlatformViewsController platformViewsController;
@Nullable private Rect lastClientRect;
private final boolean restartAlwaysRequired;
// When true following calls to createInputConnection will return the cached lastInputConnection
@ -49,6 +59,11 @@ public class TextInputPlugin {
@NonNull PlatformViewsController platformViewsController) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
afm = view.getContext().getSystemService(AutofillManager.class);
} else {
afm = null;
}
textInputChannel = new TextInputChannel(dartExecutor);
textInputChannel.setTextInputMethodHandler(
@ -63,6 +78,11 @@ public class TextInputPlugin {
hideTextInput(mView);
}
@Override
public void requestAutofill() {
notifyViewEntered();
}
@Override
public void setClient(
int textInputClientId, TextInputChannel.Configuration configuration) {
@ -79,6 +99,11 @@ public class TextInputPlugin {
setTextInputEditingState(mView, editingState);
}
@Override
public void setEditableSizeAndTransform(double width, double height, double[] transform) {
saveEditableSizeAndTransform(width, height, transform);
}
@Override
public void clearClient() {
clearTextInputClient();
@ -268,6 +293,7 @@ public class TextInputPlugin {
}
private void hideTextInput(View view) {
notifyViewExited();
// Note: a race condition may lead to us hiding the keyboard here just after a platform view has
// shown it.
// This can only potentially happen when switching focus from a Flutter text field to a platform
@ -277,16 +303,51 @@ public class TextInputPlugin {
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
}
private void notifyViewEntered() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || afm == null || !needsAutofill()) {
return;
}
final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
final int[] offset = new int[2];
mView.getLocationOnScreen(offset);
Rect rect = new Rect(lastClientRect);
rect.offset(offset[0], offset[1]);
afm.notifyViewEntered(mView, triggerIdentifier.hashCode(), rect);
}
private void notifyViewExited() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|| afm == null
|| configuration == null
|| configuration.autofill == null) {
return;
}
final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
afm.notifyViewExited(mView, triggerIdentifier.hashCode());
}
private void notifyValueChanged(String newValue) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || afm == null || !needsAutofill()) {
return;
}
final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
afm.notifyValueChanged(mView, triggerIdentifier.hashCode(), AutofillValue.forText(newValue));
}
@VisibleForTesting
void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
this.configuration = configuration;
updateAutofillConfigurationIfNeeded(configuration);
mEditable = Editable.Factory.getInstance().newEditable("");
// setTextInputClient will be followed by a call to setTextInputEditingState.
// Do a restartInput at that time.
mRestartInputPending = true;
unlockPlatformViewInputConnection();
lastClientRect = null;
}
private void setPlatformViewTextInputClient(int platformViewId) {
@ -320,6 +381,7 @@ public class TextInputPlugin {
if (!state.text.equals(mEditable.toString())) {
mEditable.replace(0, mEditable.length(), state.text);
}
notifyValueChanged(mEditable.toString());
// Always apply state to selection which handles updating the selection if needed.
applyStateToSelection(state);
InputConnection connection = getLastInputConnection();
@ -342,6 +404,141 @@ public class TextInputPlugin {
}
}
private interface MinMax {
void inspect(double x, double y);
}
private void saveEditableSizeAndTransform(double width, double height, double[] matrix) {
final double[] minMax = new double[4]; // minX, maxX, minY, maxY.
final boolean isAffine = matrix[3] == 0 && matrix[7] == 0 && matrix[15] == 1;
minMax[0] = minMax[1] = matrix[12] / matrix[15]; // minX and maxX.
minMax[2] = minMax[3] = matrix[13] / matrix[15]; // minY and maxY.
final MinMax finder =
new MinMax() {
@Override
public void inspect(double x, double y) {
final double w = isAffine ? 1 : 1 / (matrix[3] * x + matrix[7] * y + matrix[15]);
final double tx = (matrix[0] * x + matrix[4] * y + matrix[12]) * w;
final double ty = (matrix[1] * x + matrix[5] * y + matrix[13]) * w;
if (tx < minMax[0]) {
minMax[0] = tx;
} else if (tx > minMax[1]) {
minMax[1] = tx;
}
if (ty < minMax[2]) {
minMax[2] = ty;
} else if (ty > minMax[3]) {
minMax[3] = ty;
}
}
};
finder.inspect(width, 0);
finder.inspect(width, height);
finder.inspect(0, height);
final Float density = mView.getContext().getResources().getDisplayMetrics().density;
lastClientRect =
new Rect(
(int) (minMax[0] * density),
(int) (minMax[2] * density),
(int) Math.ceil(minMax[1] * density),
(int) Math.ceil(minMax[3] * density));
}
private void updateAutofillConfigurationIfNeeded(TextInputChannel.Configuration configuration) {
notifyViewExited();
this.configuration = configuration;
final TextInputChannel.Configuration[] configurations = configuration.fields;
if (configuration.autofill == null) {
// Disables autofill if the configuration doesn't have an autofill field.
mAutofillConfigurations = null;
return;
}
mAutofillConfigurations = new SparseArray<>();
if (configurations == null) {
mAutofillConfigurations.put(
configuration.autofill.uniqueIdentifier.hashCode(), configuration);
} else {
for (TextInputChannel.Configuration config : configurations) {
TextInputChannel.Configuration.Autofill autofill = config.autofill;
if (autofill == null) {
continue;
}
mAutofillConfigurations.put(autofill.uniqueIdentifier.hashCode(), config);
}
}
}
private boolean needsAutofill() {
return mAutofillConfigurations != null;
}
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !needsAutofill()) {
return;
}
final String triggerIdentifier = configuration.autofill.uniqueIdentifier;
final AutofillId parentId = structure.getAutofillId();
for (int i = 0; i < mAutofillConfigurations.size(); i++) {
final int autofillId = mAutofillConfigurations.keyAt(i);
final TextInputChannel.Configuration config = mAutofillConfigurations.valueAt(i);
final TextInputChannel.Configuration.Autofill autofill = config.autofill;
if (autofill == null) {
continue;
}
structure.addChildCount(1);
final ViewStructure child = structure.newChild(i);
child.setAutofillId(parentId, autofillId);
child.setAutofillValue(AutofillValue.forText(autofill.editState.text));
child.setAutofillHints(autofill.hints);
child.setAutofillType(View.AUTOFILL_TYPE_TEXT);
child.setVisibility(View.VISIBLE);
}
}
public void autofill(SparseArray<AutofillValue> values) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
final TextInputChannel.Configuration.Autofill currentAutofill = configuration.autofill;
if (currentAutofill == null) {
return;
}
final HashMap<String, TextInputChannel.TextEditState> editingValues = new HashMap<>();
for (int i = 0; i < values.size(); i++) {
int virtualId = values.keyAt(i);
final TextInputChannel.Configuration config = mAutofillConfigurations.get(virtualId);
if (config == null || config.autofill == null) {
continue;
}
final TextInputChannel.Configuration.Autofill autofill = config.autofill;
final String value = values.valueAt(i).getTextValue().toString();
final TextInputChannel.TextEditState newState =
new TextInputChannel.TextEditState(value, value.length(), value.length());
// The value of the currently focused text field needs to be updated.
if (autofill.uniqueIdentifier.equals(currentAutofill.uniqueIdentifier)) {
setTextInputEditingState(mView, newState);
}
editingValues.put(autofill.uniqueIdentifier, newState);
}
textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues);
}
// Samsung's Korean keyboard has a bug where it always attempts to combine characters based on
// its internal state, ignoring if and when the cursor is moved programmatically. The same bug
// also causes non-korean keyboards to occasionally duplicate text when tapping in the middle
@ -394,6 +591,8 @@ public class TextInputPlugin {
}
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
unlockPlatformViewInputConnection();
notifyViewExited();
lastClientRect = null;
}
private static class InputTarget {

View File

@ -21,15 +21,18 @@ import android.os.LocaleList;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewStructure;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
@ -445,6 +448,17 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture
.checkInputConnectionProxy(view);
}
@Override
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
super.onProvideAutofillVirtualStructure(structure, flags);
mTextInputPlugin.onProvideAutofillVirtualStructure(structure, flags);
}
@Override
public void autofill(SparseArray<AutofillValue> values) {
mTextInputPlugin.autofill(values);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isAttached()) {

View File

@ -2,11 +2,15 @@ package io.flutter.plugin.editing;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.AssetManager;
@ -15,11 +19,13 @@ import android.provider.Settings;
import android.util.SparseIntArray;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewStructure;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@ -103,7 +109,15 @@ public class TextInputPluginTest {
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false, false, true, TextInputChannel.TextCapitalization.NONE, null, null, null));
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
testView, new TextInputChannel.TextEditState("", 0, 0));
@ -132,7 +146,15 @@ public class TextInputPluginTest {
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false, false, true, TextInputChannel.TextCapitalization.NONE, null, null, null));
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now. With
// changed text, we should
// always set the Editable contents.
@ -173,7 +195,15 @@ public class TextInputPluginTest {
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false, false, true, TextInputChannel.TextCapitalization.NONE, null, null, null));
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
testView, new TextInputChannel.TextEditState("", 0, 0));
@ -208,7 +238,15 @@ public class TextInputPluginTest {
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false, false, true, TextInputChannel.TextCapitalization.NONE, null, null, null));
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
testView, new TextInputChannel.TextEditState("", 0, 0));
@ -236,7 +274,15 @@ public class TextInputPluginTest {
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false, false, true, TextInputChannel.TextCapitalization.NONE, null, null, null));
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
testView, new TextInputChannel.TextEditState("", 0, 0));
@ -262,6 +308,8 @@ public class TextInputPluginTest {
TextInputChannel.TextCapitalization.NONE,
new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false),
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
@ -331,6 +379,8 @@ public class TextInputPluginTest {
TextInputChannel.TextCapitalization.NONE,
new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false),
null,
null,
null,
null));
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
@ -347,6 +397,116 @@ public class TextInputPluginTest {
}
}
@Test
public void autofill_onProvideVirtualViewStructure() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
FlutterView testView = new FlutterView(RuntimeEnvironment.application);
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, mock(DartExecutor.class), mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill1 =
new TextInputChannel.Configuration.Autofill(
"1", new String[] {"HINT1"}, new TextInputChannel.TextEditState("", 0, 0));
final TextInputChannel.Configuration.Autofill autofill2 =
new TextInputChannel.Configuration.Autofill(
"2", new String[] {"HINT2", "EXTRA"}, new TextInputChannel.TextEditState("", 0, 0));
final TextInputChannel.Configuration config1 =
new TextInputChannel.Configuration(
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
autofill1,
null);
final TextInputChannel.Configuration config2 =
new TextInputChannel.Configuration(
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
autofill2,
null);
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
autofill1,
new TextInputChannel.Configuration[] {config1, config2}));
final ViewStructure viewStructure = mock(ViewStructure.class);
final ViewStructure[] children = {mock(ViewStructure.class), mock(ViewStructure.class)};
when(viewStructure.newChild(anyInt()))
.thenAnswer(invocation -> children[invocation.getArgumentAt(0, int.class)]);
textInputPlugin.onProvideAutofillVirtualStructure(viewStructure, 0);
verify(viewStructure).newChild(0);
verify(viewStructure).newChild(1);
verify(children[0]).setAutofillId(any(), eq("1".hashCode()));
verify(children[0]).setAutofillHints(aryEq(new String[] {"HINT1"}));
verify(children[1]).setAutofillId(any(), eq("2".hashCode()));
verify(children[1]).setAutofillHints(aryEq(new String[] {"HINT2", "EXTRA"}));
}
@Test
public void autofill_onProvideVirtualViewStructure_single() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
FlutterView testView = new FlutterView(RuntimeEnvironment.application);
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, mock(DartExecutor.class), mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1", new String[] {"HINT1"}, new TextInputChannel.TextEditState("", 0, 0));
// Autofill should still work without AutofillGroup.
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
false,
false,
true,
TextInputChannel.TextCapitalization.NONE,
null,
null,
null,
autofill,
null));
final ViewStructure viewStructure = mock(ViewStructure.class);
final ViewStructure[] children = {mock(ViewStructure.class)};
when(viewStructure.newChild(anyInt()))
.thenAnswer(invocation -> children[invocation.getArgumentAt(0, int.class)]);
textInputPlugin.onProvideAutofillVirtualStructure(viewStructure, 0);
verify(viewStructure).newChild(0);
verify(children[0]).setAutofillId(any(), eq("1".hashCode()));
verify(children[0]).setAutofillHints(aryEq(new String[] {"HINT1"}));
}
@Implements(InputMethodManager.class)
public static class TestImm extends ShadowInputMethodManager {
private InputMethodSubtype currentInputMethodSubtype;