diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 4d384452682a7..5b5975bc423f5 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -4,9 +4,11 @@ package io.flutter.plugin.editing; +import android.content.Context; import android.text.Editable; import android.text.Selection; import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.InputMethodManager; import android.view.KeyEvent; import io.flutter.plugin.common.MethodChannel; @@ -17,63 +19,104 @@ import java.util.Map; class InputConnectionAdaptor extends BaseInputConnection { + private final FlutterView mFlutterView; private final int mClient; - private final TextInputPlugin mPlugin; private final MethodChannel mFlutterChannel; - private final Map mOutgoingState; + private final Editable mEditable; + private int mBatchCount; + private InputMethodManager mImm; public InputConnectionAdaptor(FlutterView view, int client, - TextInputPlugin plugin, MethodChannel flutterChannel) { + MethodChannel flutterChannel, Editable editable) { super(view, true); + mFlutterView = view; mClient = client; - mPlugin = plugin; mFlutterChannel = flutterChannel; - mOutgoingState = new HashMap<>(); + mEditable = editable; + mBatchCount = 0; + mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); } + // Send the current state of the editable to Flutter. private void updateEditingState() { - final Editable content = getEditable(); - mOutgoingState.put("text", content.toString()); - mOutgoingState.put("selectionBase", Selection.getSelectionStart(content)); - mOutgoingState.put("selectionExtent", Selection.getSelectionEnd(content)); - mOutgoingState.put("composingBase", BaseInputConnection.getComposingSpanStart(content)); - mOutgoingState.put("composingExtent", BaseInputConnection.getComposingSpanEnd(content)); - mFlutterChannel.invokeMethod("TextInputClient.updateEditingState", Arrays - .asList(mClient, mOutgoingState)); - mPlugin.setLatestEditingState(mOutgoingState); + // If the IME is in the middle of a batch edit, then wait until it completes. + if (mBatchCount > 0) + return; + + int selectionStart = Selection.getSelectionStart(mEditable); + int selectionEnd = Selection.getSelectionEnd(mEditable); + int composingStart = BaseInputConnection.getComposingSpanStart(mEditable); + int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable); + + mImm.updateSelection(mFlutterView, + selectionStart, selectionEnd, + composingStart, composingEnd); + + HashMap state = new HashMap(); + state.put("text", mEditable.toString()); + state.put("selectionBase", selectionStart); + state.put("selectionExtent", selectionEnd); + state.put("composingBase", composingStart); + state.put("composingExtent", composingEnd); + mFlutterChannel.invokeMethod("TextInputClient.updateEditingState", + Arrays.asList(mClient, state)); + } + + @Override + public Editable getEditable() { + return mEditable; + } + + @Override + public boolean beginBatchEdit() { + mBatchCount++; + return super.beginBatchEdit(); + } + + @Override + public boolean endBatchEdit() { + boolean result = super.endBatchEdit(); + mBatchCount--; + updateEditingState(); + return result; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - final boolean result = super.commitText(text, newCursorPosition); + boolean result = super.commitText(text, newCursorPosition); updateEditingState(); return result; } @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - final boolean result = super.deleteSurroundingText(beforeLength, afterLength); + boolean result = super.deleteSurroundingText(beforeLength, afterLength); updateEditingState(); return result; } @Override public boolean setComposingRegion(int start, int end) { - final boolean result = super.setComposingRegion(start, end); + boolean result = super.setComposingRegion(start, end); updateEditingState(); return result; } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { - final boolean result = super.setComposingText(text, newCursorPosition); + boolean result; + if (text.length() == 0) { + result = super.commitText(text, newCursorPosition); + } else { + result = super.setComposingText(text, newCursorPosition); + } updateEditingState(); return result; } @Override public boolean setSelection(int start, int end) { - final boolean result = super.setSelection(start, end); + boolean result = super.setSelection(start, end); updateEditingState(); return result; } @@ -82,26 +125,26 @@ public boolean setSelection(int start, int end) { public boolean sendKeyEvent(KeyEvent event) { final boolean result = super.sendKeyEvent(event); if (event.getAction() == KeyEvent.ACTION_UP) { - // Weird special case. This method is (sometimes) called for the backspace key in 2 - // situations: - // 1. There is no selection. In that case, we want to delete the previous character. - // 2. There is a selection. In that case, we want to delete the selection. - // event.getNumber() is 0, and commitText("", 1) will do what we want. - if (event.getKeyCode() == KeyEvent.KEYCODE_DEL && - optInt("selectionBase", -1) == optInt("selectionExtent", -1)) { - deleteSurroundingText(1, 0); + if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { + int selStart = Selection.getSelectionStart(mEditable); + int selEnd = Selection.getSelectionEnd(mEditable); + if (selEnd > selStart) { + // Delete the selection. + Selection.setSelection(mEditable, selStart); + deleteSurroundingText(0, selEnd - selStart); + } else if (selStart > 0) { + // Delete to the left of the cursor. + Selection.setSelection(mEditable, selStart - 1); + deleteSurroundingText(0, 1); + } } else { - String text = event.getNumber() == 0 ? "" : String.valueOf(event.getNumber()); - commitText(text, 1); + // Enter a character. + commitText(String.valueOf(event.getNumber()), 1); } } return result; } - private int optInt(String key, int defaultValue) { - return mOutgoingState.containsKey(key) ? (Integer) mOutgoingState.get(key) : defaultValue; - } - @Override public boolean performEditorAction(int actionCode) { // TODO(abarth): Support more actions. diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index c46e0ec89615c..9b7aaf43d0941 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -6,7 +6,10 @@ import android.app.Activity; import android.content.Context; +import android.text.Editable; import android.text.InputType; +import android.text.Selection; +import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; @@ -29,16 +32,16 @@ */ public class TextInputPlugin implements MethodCallHandler { - private final Activity mActivity; private final FlutterView mView; + private final InputMethodManager mImm; private final MethodChannel mFlutterChannel; private int mClient = 0; private JSONObject mConfiguration; - private JSONObject mLatestState; + private Editable mEditable; - public TextInputPlugin(Activity activity, FlutterView view) { - mActivity = activity; + public TextInputPlugin(FlutterView view) { mView = view; + mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); mFlutterChannel = new MethodChannel(view, "flutter/textinput", JSONMethodCodec.INSTANCE); mFlutterChannel.setMethodCallHandler(this); @@ -98,60 +101,53 @@ public InputConnection createInputConnection(FlutterView view, EditorInfo outAtt throws JSONException { if (mClient == 0) return null; + outAttrs.inputType = inputTypeFromTextInputType(mConfiguration.getString("inputType"), mConfiguration.optBoolean("obscureText")); if (!mConfiguration.isNull("actionLabel")) outAttrs.actionLabel = mConfiguration.getString("actionLabel"); outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_FULLSCREEN; - InputConnectionAdaptor connection = new InputConnectionAdaptor(view, mClient, this, - mFlutterChannel); - if (mLatestState != null) { - int selectionBase = (Integer) mLatestState.get("selectionBase"); - int selectionExtent = (Integer) mLatestState.get("selectionExtent"); - outAttrs.initialSelStart = selectionBase; - outAttrs.initialSelEnd = selectionExtent; - connection.getEditable().append((String) mLatestState.get("text")); - connection.setSelection(Math.max(selectionBase, 0), - Math.max(selectionExtent, 0)); - connection.setComposingRegion((Integer) mLatestState.get("composingBase"), - (Integer) mLatestState.get("composingExtent")); - } else { - outAttrs.initialSelStart = 0; - outAttrs.initialSelEnd = 0; - } + + InputConnectionAdaptor connection = new InputConnectionAdaptor(view, mClient, mFlutterChannel, mEditable); + outAttrs.initialSelStart = Math.max(Selection.getSelectionStart(mEditable), 0); + outAttrs.initialSelEnd = Math.max(Selection.getSelectionEnd(mEditable), 0); + return connection; } private void showTextInput(FlutterView view) { - InputMethodManager imm = - (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(view, 0); + mImm.showSoftInput(view, 0); } private void hideTextInput(FlutterView view) { - InputMethodManager imm = - (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); + mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); } private void setTextInputClient(FlutterView view, int client, JSONObject configuration) { - mLatestState = null; mClient = client; mConfiguration = configuration; - InputMethodManager imm = - (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.restartInput(view); - } + mEditable = Editable.Factory.getInstance().newEditable(""); - private void setTextInputEditingState(FlutterView view, JSONObject state) { - mLatestState = state; - InputMethodManager imm = - (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.restartInput(view); + mImm.restartInput(view); } - void setLatestEditingState(Map state) { - mLatestState = (JSONObject) JSONUtil.wrap(state); + private void setTextInputEditingState(FlutterView view, JSONObject state) + throws JSONException { + if (state.getString("text").equals(mEditable.toString())) { + Selection.setSelection(mEditable, state.getInt("selectionBase"), + state.getInt("selectionExtent")); + mImm.updateSelection( + mView, + Math.max(Selection.getSelectionStart(mEditable), 0), + Math.max(Selection.getSelectionEnd(mEditable), 0), + BaseInputConnection.getComposingSpanStart(mEditable), + BaseInputConnection.getComposingSpanEnd(mEditable)); + } else { + mEditable.replace(0, mEditable.length(), state.getString("text")); + Selection.setSelection(mEditable, state.getInt("selectionBase"), + state.getInt("selectionExtent")); + mImm.restartInput(view); + } } private void clearTextInputClient() { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 8602ba252f59b..b657aafb8e5db 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -174,7 +174,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { "flutter/platform", JSONMethodCodec.INSTANCE); flutterPlatformChannel.setMethodCallHandler(platformPlugin); addActivityLifecycleListener(platformPlugin); - mTextInputPlugin = new TextInputPlugin((Activity) getContext(), this); + mTextInputPlugin = new TextInputPlugin(this); setLocale(getResources().getConfiguration().locale);