Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
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;
Expand Down Expand Up @@ -283,6 +286,9 @@ private void init() {
// 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);
}
}

/**
Expand Down Expand Up @@ -898,6 +904,17 @@ private void sendViewportMetricsToFlutter() {
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}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,6 +12,7 @@
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;
Expand Down Expand Up @@ -85,6 +88,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
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);
Expand Down Expand Up @@ -119,6 +138,16 @@ public void requestExistingInputState() {
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.
*/
Expand Down Expand Up @@ -147,16 +176,31 @@ public void updateEditingState(
+ "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.");
Expand Down Expand Up @@ -229,6 +273,13 @@ public interface TextInputMethodHandler {
// 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);

Expand All @@ -242,6 +293,16 @@ public interface TextInputMethodHandler {
*/
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);

Expand All @@ -257,7 +318,14 @@ public static Configuration fromJson(@NonNull JSONObject json)
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"),
Expand All @@ -266,7 +334,9 @@ public static Configuration fromJson(@NonNull JSONObject json)
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
Expand Down Expand Up @@ -296,13 +366,126 @@ private static Integer inputActionFromTextInputAction(@NonNull String inputActio
}
}

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;
@NonNull public final TextCapitalization textCapitalization;
@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,
Expand All @@ -311,14 +494,18 @@ public Configuration(
@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;
this.textCapitalization = textCapitalization;
this.inputType = inputType;
this.inputAction = inputAction;
this.actionLabel = actionLabel;
this.autofill = autofill;
this.fields = fields;
}
}

Expand Down
Loading