mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Samsung keyboard duplication workaround: updateSelection (#16547)
This commit is contained in:
parent
42f18d9078
commit
c4c6ef6719
@ -4,8 +4,10 @@
|
||||
|
||||
package io.flutter.plugin.editing;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.text.DynamicLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.Layout;
|
||||
@ -17,6 +19,7 @@ import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.CursorAnchorInfo;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.inputmethod.InputMethodSubtype;
|
||||
import io.flutter.Log;
|
||||
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
|
||||
|
||||
@ -30,6 +33,9 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
private InputMethodManager mImm;
|
||||
private final Layout mLayout;
|
||||
|
||||
// Used to determine if Samsung-specific hacks should be applied.
|
||||
private final boolean isSamsung;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public InputConnectionAdaptor(
|
||||
View view,
|
||||
@ -56,6 +62,8 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
0.0f,
|
||||
false);
|
||||
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
isSamsung = isSamsung();
|
||||
}
|
||||
|
||||
// Send the current state of the editable to Flutter.
|
||||
@ -132,19 +140,64 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
public boolean finishComposingText() {
|
||||
boolean result = super.finishComposingText();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
// Update the keyboard with a reset/empty composing region. Critical on
|
||||
// Samsung keyboards to prevent punctuation duplication.
|
||||
CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
|
||||
builder.setComposingText(-1, "");
|
||||
CursorAnchorInfo anchorInfo = builder.build();
|
||||
mImm.updateCursorAnchorInfo(mFlutterView, anchorInfo);
|
||||
// Apply Samsung hacks. Samsung caches composing region data strangely, causing text
|
||||
// duplication.
|
||||
if (isSamsung) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
// Samsung keyboards don't clear the composing region on finishComposingText.
|
||||
// Update the keyboard with a reset/empty composing region. Critical on
|
||||
// Samsung keyboards to prevent punctuation duplication.
|
||||
CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
|
||||
builder.setComposingText(/*composingTextStart*/ -1, /*composingText*/ "");
|
||||
CursorAnchorInfo anchorInfo = builder.build();
|
||||
mImm.updateCursorAnchorInfo(mFlutterView, anchorInfo);
|
||||
}
|
||||
// TODO(garyq): There is still a duplication case that comes from hiding+showing the keyboard.
|
||||
// The exact behavior to cause it has so far been hard to pinpoint and it happens far more
|
||||
// rarely than the original bug.
|
||||
|
||||
// Temporarily indicate to the IME that the composing region selection should be reset.
|
||||
// The correct selection is then immediately set properly in the updateEditingState() call
|
||||
// in this method. This is a hack to trigger Samsung keyboard's internal cache to clear.
|
||||
// This prevents duplication on keyboard hide+show. See
|
||||
// https://github.com/flutter/flutter/issues/31512
|
||||
//
|
||||
// We only do this if the proper selection will be restored later, eg, when mBatchCount is 0.
|
||||
if (mBatchCount == 0) {
|
||||
mImm.updateSelection(
|
||||
mFlutterView,
|
||||
-1, /*selStart*/
|
||||
-1, /*selEnd*/
|
||||
-1, /*candidatesStart*/
|
||||
-1 /*candidatesEnd*/);
|
||||
}
|
||||
}
|
||||
|
||||
updateEditingState();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Detect if the keyboard is a Samsung keyboard, where we apply Samsung-specific hacks to
|
||||
// fix critical bugs that make the keyboard otherwise unusable. See finishComposingText() for
|
||||
// more details.
|
||||
@SuppressLint("NewApi") // New API guard is inline, the linter can't see it.
|
||||
@SuppressWarnings("deprecation")
|
||||
private boolean isSamsung() {
|
||||
InputMethodSubtype subtype = mImm.getCurrentInputMethodSubtype();
|
||||
// Impacted devices all shipped with Android Lollipop or newer.
|
||||
if (subtype == null
|
||||
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|
||||
|| !Build.MANUFACTURER.equals("samsung")) {
|
||||
return false;
|
||||
}
|
||||
String keyboardName =
|
||||
Settings.Secure.getString(
|
||||
mFlutterView.getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
|
||||
// The Samsung keyboard is called "com.sec.android.inputmethod/.SamsungKeypad" but look
|
||||
// for "Samsung" just in case Samsung changes the name of the keyboard.
|
||||
return keyboardName.contains("Samsung");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setSelection(int start, int end) {
|
||||
boolean result = super.setSelection(start, end);
|
||||
|
||||
@ -28,6 +28,9 @@ import io.flutter.plugin.common.JSONMethodCodec;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.platform.PlatformViewsController;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.junit.Test;
|
||||
@ -305,9 +308,17 @@ public class TextInputPluginTest {
|
||||
|
||||
@Test
|
||||
public void inputConnection_finishComposingTextUpdatesIMM() throws JSONException {
|
||||
ShadowBuild.setManufacturer("samsung");
|
||||
InputMethodSubtype inputMethodSubtype =
|
||||
new InputMethodSubtype(0, 0, /*locale=*/ "en", "", "", false, false);
|
||||
Settings.Secure.putString(
|
||||
RuntimeEnvironment.application.getContentResolver(),
|
||||
Settings.Secure.DEFAULT_INPUT_METHOD,
|
||||
"com.sec.android.inputmethod/.SamsungKeypad");
|
||||
TestImm testImm =
|
||||
Shadow.extract(
|
||||
RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE));
|
||||
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
|
||||
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
|
||||
View testView = new View(RuntimeEnvironment.application);
|
||||
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
|
||||
@ -338,13 +349,59 @@ public class TextInputPluginTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputConnection_samsungFinishComposingTextSetsSelection() throws JSONException {
|
||||
ShadowBuild.setManufacturer("samsung");
|
||||
InputMethodSubtype inputMethodSubtype =
|
||||
new InputMethodSubtype(0, 0, /*locale=*/ "en", "", "", false, false);
|
||||
Settings.Secure.putString(
|
||||
RuntimeEnvironment.application.getContentResolver(),
|
||||
Settings.Secure.DEFAULT_INPUT_METHOD,
|
||||
"com.sec.android.inputmethod/.SamsungKeypad");
|
||||
TestImm testImm =
|
||||
Shadow.extract(
|
||||
RuntimeEnvironment.application.getSystemService(Context.INPUT_METHOD_SERVICE));
|
||||
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
|
||||
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
|
||||
View testView = new View(RuntimeEnvironment.application);
|
||||
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
|
||||
TextInputPlugin textInputPlugin =
|
||||
new TextInputPlugin(testView, dartExecutor, mock(PlatformViewsController.class));
|
||||
textInputPlugin.setTextInputClient(
|
||||
0,
|
||||
new TextInputChannel.Configuration(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
TextInputChannel.TextCapitalization.NONE,
|
||||
new TextInputChannel.InputType(TextInputChannel.TextInputType.TEXT, false, false),
|
||||
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));
|
||||
InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo());
|
||||
|
||||
testImm.setTrackSelection(true);
|
||||
connection.finishComposingText();
|
||||
testImm.setTrackSelection(false);
|
||||
|
||||
List<Integer> expectedSelectionValues =
|
||||
Arrays.asList(0, 0, -1, -1, -1, -1, -1, -1, 0, 0, -1, -1);
|
||||
assertEquals(testImm.getSelectionUpdateValues(), expectedSelectionValues);
|
||||
}
|
||||
|
||||
@Implements(InputMethodManager.class)
|
||||
public static class TestImm extends ShadowInputMethodManager {
|
||||
private InputMethodSubtype currentInputMethodSubtype;
|
||||
private SparseIntArray restartCounter = new SparseIntArray();
|
||||
private CursorAnchorInfo cursorAnchorInfo;
|
||||
private ArrayList<Integer> selectionUpdateValues;
|
||||
private boolean trackSelection = false;
|
||||
|
||||
public TestImm() {}
|
||||
public TestImm() {
|
||||
selectionUpdateValues = new ArrayList<Integer>();
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public InputMethodSubtype getCurrentInputMethodSubtype() {
|
||||
@ -370,6 +427,28 @@ public class TextInputPluginTest {
|
||||
this.cursorAnchorInfo = cursorAnchorInfo;
|
||||
}
|
||||
|
||||
// We simply store the values to verify later.
|
||||
@Implementation
|
||||
public void updateSelection(
|
||||
View view, int selStart, int selEnd, int candidatesStart, int candidatesEnd) {
|
||||
if (trackSelection) {
|
||||
this.selectionUpdateValues.add(selStart);
|
||||
this.selectionUpdateValues.add(selEnd);
|
||||
this.selectionUpdateValues.add(candidatesStart);
|
||||
this.selectionUpdateValues.add(candidatesEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// only track values when enabled via this.
|
||||
public void setTrackSelection(boolean val) {
|
||||
trackSelection = val;
|
||||
}
|
||||
|
||||
// Returns true if the last updateSelection call passed the following values.
|
||||
public ArrayList<Integer> getSelectionUpdateValues() {
|
||||
return selectionUpdateValues;
|
||||
}
|
||||
|
||||
public CursorAnchorInfo getLastCursorAnchorInfo() {
|
||||
return cursorAnchorInfo;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user