diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 6f045eb0c79c1..ea188d3722626 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -418,6 +418,8 @@ private void setPlatformViewTextInputClient(int platformViewId) { mRestartInputPending = false; } + // Called by the text input channel to update the text input plugin with the + // latest TextEditState from the framework. @VisibleForTesting void setTextInputEditingState(View view, TextInputChannel.TextEditState state) { mLastKnownFrameworkTextEditingState = state; @@ -591,7 +593,7 @@ public void didChangeEditingState( final int selectionEnd = mEditable.getSelectionEnd(); final int composingStart = mEditable.getComposingStart(); final int composingEnd = mEditable.getComposingEnd(); - // Framework needs to sent value first. + // The framework needs to send value first. final boolean skipFrameworkUpdate = mLastKnownFrameworkTextEditingState == null || (mEditable.toString().equals(mLastKnownFrameworkTextEditingState.text) @@ -773,11 +775,14 @@ public void autofill(SparseArray values) { final TextInputChannel.TextEditState newState = new TextInputChannel.TextEditState(value, value.length(), value.length(), -1, -1); - // The value of the currently focused text field needs to be updated. if (autofill.uniqueIdentifier.equals(currentAutofill.uniqueIdentifier)) { - setTextInputEditingState(mView, newState); + // Autofilling the current client is the same as handling user input + // from the virtual keyboard. Setting the editable to newState and an + // update will be sent to the framework. + mEditable.setEditingState(newState); + } else { + editingValues.put(autofill.uniqueIdentifier, newState); } - editingValues.put(autofill.uniqueIdentifier, newState); } textInputChannel.updateEditingStateWithTag(inputTarget.id, editingValues); diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 38c557d147b4d..2bf7ae7584941 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -27,6 +27,7 @@ import android.provider.Settings; import android.text.InputType; import android.text.Selection; +import android.util.SparseArray; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.View; @@ -55,6 +56,7 @@ import io.flutter.plugin.platform.PlatformViewsController; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import org.json.JSONArray; import org.json.JSONException; @@ -927,6 +929,92 @@ public void autofill_testLifeCycle() { assertEquals("1".hashCode(), testAfm.exitId); } + @Test + public void autofill_testAutofillUpdatesTheFramework() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + TestAfm testAfm = + Shadow.extract(RuntimeEnvironment.application.getSystemService(AutofillManager.class)); + FlutterView testView = new FlutterView(RuntimeEnvironment.application); + TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + TextInputPlugin textInputPlugin = + new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + + // Set up an autofill scenario with 2 fields. + final TextInputChannel.Configuration.Autofill autofill1 = + new TextInputChannel.Configuration.Autofill( + "1", + new String[] {"HINT1"}, + new TextInputChannel.TextEditState("field 1", 0, 0, -1, -1)); + final TextInputChannel.Configuration.Autofill autofill2 = + new TextInputChannel.Configuration.Autofill( + "2", + new String[] {"HINT2", "EXTRA"}, + new TextInputChannel.TextEditState("field 2", 0, 0, -1, -1)); + + 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); + + final TextInputChannel.Configuration autofillConfiguration = + new TextInputChannel.Configuration( + false, + false, + true, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + new TextInputChannel.Configuration[] {config1, config2}); + + textInputPlugin.setTextInputClient(0, autofillConfiguration); + textInputPlugin.setTextInputEditingState( + testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + + final SparseArray autofillValues = new SparseArray(); + autofillValues.append("1".hashCode(), AutofillValue.forText("focused field")); + autofillValues.append("2".hashCode(), AutofillValue.forText("unfocused field")); + + // Autofill both fields. + textInputPlugin.autofill(autofillValues); + + // Verify the Editable has been updated. + assertTrue(textInputPlugin.getEditable().toString().equals("focused field")); + + // The autofill value of the focused field is sent via updateEditingState. + verify(textInputChannel, times(1)) + .updateEditingState(anyInt(), eq("focused field"), eq(13), eq(13), eq(-1), eq(-1)); + + final ArgumentCaptor mapCaptor = ArgumentCaptor.forClass(HashMap.class); + + verify(textInputChannel, times(1)).updateEditingStateWithTag(anyInt(), mapCaptor.capture()); + final TextInputChannel.TextEditState editState = + (TextInputChannel.TextEditState) mapCaptor.getValue().get("2"); + assertEquals(editState.text, "unfocused field"); + } + @Test public void autofill_testSetTextIpnutClientUpdatesSideFields() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {