-
Notifications
You must be signed in to change notification settings - Fork 6k
Android Autofill #17465
Android Autofill #17465
Changes from 15 commits
e6a7d23
ff05c8d
056a393
34283bf
d3f71da
4954bdf
2f31a97
0eaa510
f121560
e87f57f
a1967fd
7b2ae2c
086ab38
7197492
e1c4334
7423c08
10a7a2b
480f843
0ca379f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,15 @@ | |
| import android.view.inputmethod.EditorInfo; | ||
| import androidx.annotation.NonNull; | ||
| import androidx.annotation.Nullable; | ||
| import androidx.autofill.HintConstants; | ||
| import io.flutter.Log; | ||
| import io.flutter.embedding.engine.dart.DartExecutor; | ||
| import io.flutter.plugin.common.JSONMethodCodec; | ||
| 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 +87,19 @@ 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); | ||
|
|
@@ -119,6 +134,16 @@ public void requestExistingInputState() { | |
| channel.invokeMethod("TextInputClient.requestExistingInputState", null); | ||
| } | ||
|
|
||
| private HashMap<Object, Object> createEditingStateJSON( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this method be static? |
||
| 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 +172,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."); | ||
|
|
@@ -229,6 +269,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); | ||
|
|
||
|
|
@@ -242,6 +289,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); | ||
|
|
||
|
|
@@ -257,7 +314,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 fields = json.getJSONArray("fields"); | ||
| fields = new Configuration[fields.length()]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two declarations of |
||
| for (int i = 0; i < fields.length; i++) { | ||
| fields[i] = Configuration.fromJson(fields.getJSONObject(i)); | ||
| } | ||
| } | ||
| final Integer inputAction = inputActionFromTextInputAction(inputActionName); | ||
| return new Configuration( | ||
| json.optBoolean("obscureText"), | ||
|
|
@@ -266,7 +330,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 | ||
|
|
@@ -296,13 +362,123 @@ 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) { | ||
| switch (hint) { | ||
| case "addressCity": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are very sensitive to adding dependencies to the embedding. This can cause conflict in the future if a plugin adds this dependency. |
||
| case "addressState": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_REGION; | ||
| case "birthday": | ||
| return HintConstants.AUTOFILL_HINT_BIRTH_DATE_FULL; | ||
| case "birthdayDay": | ||
| return HintConstants.AUTOFILL_HINT_BIRTH_DATE_DAY; | ||
| case "birthdayMonth": | ||
| return HintConstants.AUTOFILL_HINT_BIRTH_DATE_MONTH; | ||
| case "birthdayYear": | ||
| return HintConstants.AUTOFILL_HINT_BIRTH_DATE_YEAR; | ||
| case "countryName": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY; | ||
| case "creditCardExpirationDate": | ||
| return HintConstants.AUTOFILL_HINT_CREDIT_CARD_NUMBER; | ||
| case "creditCardExpirationDay": | ||
| return HintConstants.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY; | ||
| case "creditCardExpirationMonth": | ||
| return HintConstants.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH; | ||
| case "creditCardExpirationYear": | ||
| return HintConstants.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR; | ||
| case "creditCardNumber": | ||
| return HintConstants.AUTOFILL_HINT_CREDIT_CARD_NUMBER; | ||
| case "creditCardSecurityCode": | ||
| return HintConstants.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE; | ||
| case "email": | ||
| return HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS; | ||
| case "familyName": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME_FAMILY; | ||
| case "fullStreetAddress": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS; | ||
| case "gender": | ||
| return HintConstants.AUTOFILL_HINT_GENDER; | ||
| case "givenName": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME_GIVEN; | ||
| case "middleInitial": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL; | ||
| case "middleName": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME_MIDDLE; | ||
| case "name": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME; | ||
| case "namePrefix": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME_PREFIX; | ||
| case "nameSuffix": | ||
| return HintConstants.AUTOFILL_HINT_PERSON_NAME_SUFFIX; | ||
| case "newPassword": | ||
| return HintConstants.AUTOFILL_HINT_NEW_PASSWORD; | ||
| case "newUsername": | ||
| return HintConstants.AUTOFILL_HINT_NEW_USERNAME; | ||
| case "oneTimeCode": | ||
| return HintConstants.AUTOFILL_HINT_SMS_OTP; | ||
| case "password": | ||
| return HintConstants.AUTOFILL_HINT_PASSWORD; | ||
| case "postalAddress": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS; | ||
| case "postalAddressExtended": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS; | ||
| case "postalAddressExtendedPostalCode": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE; | ||
| case "postalCode": | ||
| return HintConstants.AUTOFILL_HINT_POSTAL_CODE; | ||
| case "telephoneNumber": | ||
| return HintConstants.AUTOFILL_HINT_PHONE_NUMBER; | ||
| case "telephoneNumberCountryCode": | ||
| return HintConstants.AUTOFILL_HINT_PHONE_COUNTRY_CODE; | ||
| case "telephoneNumberDevice": | ||
| return HintConstants.AUTOFILL_HINT_PHONE_NUMBER_DEVICE; | ||
| case "telephoneNumberNational": | ||
| return HintConstants.AUTOFILL_HINT_PHONE_NATIONAL; | ||
| case "username": | ||
| return HintConstants.AUTOFILL_HINT_NEW_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, | ||
|
|
@@ -311,14 +487,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; | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.