Commit 707d8cc3 authored by Changwan Ryu's avatar Changwan Ryu Committed by Commit Bot

Support IME for multi-display in Chrome

Currently, Chrome uses application context when it creates
ContentView because it needs to reparent the view across different
activities.

Since it is not using activity context, and it seems quite challenging
to refactor the codebase to do so, we are landing this as a short term
workaround. The workaround consists of the following:

1) Pass WindowAndroid into IMMWI, and try to get activity context, only
   for Chrome.
2) Call setLocalFocus(true, true) only for showing keyboard.
   (jinsukkim@'s idea)
3) Wait for input connection to be established
   (yukawa@'s idea)

The rest are just plumbing work.

Manually tested the following on physical device and emulator:

1) activate IMEs between omnibox and content
2) activate and deactivate IMEs by touching inside and outside input
   box
3) move ChromePublic between displays and activate IMEs

Also, manually tested that this works on WebView.

Bug: 1021403

Change-Id: Ia952f76e2fb1afef073825435ab4569bcded6762
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1994393
Commit-Queue: Changwan Ryu <changwan@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarYohei Yukawa <yukawa@chromium.org>
Cr-Commit-Position: refs/heads/master@{#737537}
parent 26d47f91
......@@ -502,7 +502,7 @@ public class VrShell extends GvrLayout
// Use application context here to avoid leaking the activity context.
imeAdapter.setInputMethodManagerWrapper(ImeAdapter.createDefaultInputMethodManagerWrapper(
mActivity.getApplicationContext()));
mActivity.getApplicationContext(), mContentVrWindowAndroid, null));
mInputMethodManagerWrapper = null;
}
......
......@@ -16,6 +16,7 @@ import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.ui.base.WindowAndroid;
/**
* A fake wrapper around Android's InputMethodManager that doesn't really talk to the
......@@ -115,4 +116,10 @@ public class VrInputMethodManagerWrapper implements InputMethodManagerWrapper {
@Override
public void notifyUserAction() {}
@Override
public void onWindowAndroidChanged(WindowAndroid newWindowAndroid) {}
@Override
public void onInputConnectionCreated() {}
}
......@@ -150,6 +150,13 @@ public class ShareIntentTest {
public ObservableSupplier<ShareDelegate> getShareDelegateSupplier() {
return mActivity.getShareDelegateSupplier();
}
@Override
public Object getSystemService(String name) {
// Prevents a scenario where InputMethodManager#hideSoftInput()
// gets called before Activity#onCreate() gets called in this test.
return null;
}
}
@Test
......
......@@ -516,6 +516,7 @@ junit_binary("content_junit_tests") {
"junit/src/org/chromium/content/browser/UiThreadTaskTraitsImplTest.java",
"junit/src/org/chromium/content/browser/accessibility/BrowserAccessibilityStateTest.java",
"junit/src/org/chromium/content/browser/androidoverlay/DialogOverlayCoreTest.java",
"junit/src/org/chromium/content/browser/input/InputMethodManagerWrapperImplTest.java",
"junit/src/org/chromium/content/browser/input/RangeTest.java",
"junit/src/org/chromium/content/browser/input/TextInputStateTest.java",
"junit/src/org/chromium/content/browser/input/ThreadedInputConnectionFactoryTest.java",
......
......@@ -54,6 +54,7 @@ import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.base.ime.TextInputAction;
import org.chromium.ui.base.ime.TextInputType;
......@@ -86,7 +87,8 @@ import java.util.List;
* lifetime of the object.
*/
@JNINamespace("content")
public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData {
public class ImeAdapterImpl
implements ImeAdapter, WindowEventObserver, UserData, InputMethodManagerWrapper.Delegate {
private static final String TAG = "Ime";
private static final boolean DEBUG_LOGS = false;
......@@ -108,6 +110,7 @@ public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData
private final WebContentsImpl mWebContents;
private ViewAndroidDelegate mViewDelegate;
private WindowAndroid mWindowAndroid;
// This holds the information necessary for constructing CursorAnchorInfo, and notifies to
// InputMethodManager on appropriate timing, depending on how IME requested the information
......@@ -183,9 +186,9 @@ public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData
/**
* Returns an instance of the default {@link InputMethodManagerWrapper}
*/
public static InputMethodManagerWrapper createDefaultInputMethodManagerWrapper(
Context context) {
return new InputMethodManagerWrapperImpl(context);
public static InputMethodManagerWrapper createDefaultInputMethodManagerWrapper(Context context,
WindowAndroid windowAndroid, InputMethodManagerWrapper.Delegate delegate) {
return new InputMethodManagerWrapperImpl(context, windowAndroid, delegate);
}
/**
......@@ -196,9 +199,10 @@ public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData
mWebContents = (WebContentsImpl) webContents;
mViewDelegate = mWebContents.getViewAndroidDelegate();
assert mViewDelegate != null;
// Use application context here to avoid leaking the activity context.
InputMethodManagerWrapper wrapper =
createDefaultInputMethodManagerWrapper(ContextUtils.getApplicationContext());
InputMethodManagerWrapper wrapper = createDefaultInputMethodManagerWrapper(
ContextUtils.getApplicationContext(), mWebContents.getTopLevelNativeWindow(), this);
// Deep copy newConfig so that we can notice the difference.
mCurrentConfig = new Configuration(getContainerView().getResources().getConfiguration());
......@@ -327,6 +331,8 @@ public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData
ImeAdapterImpl.this, false /* not an immediate request */,
false /* disable monitoring */);
}
if (mInputConnection != null) mInputMethodManagerWrapper.onInputConnectionCreated();
return mInputConnection;
}
......@@ -337,6 +343,11 @@ public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData
mInputConnection = inputConnection;
}
@Override
public boolean hasInputConnection() {
return mInputConnection != null;
}
@Override
public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) {
mInputMethodManagerWrapper = immw;
......@@ -620,6 +631,13 @@ public class ImeAdapterImpl implements ImeAdapter, WindowEventObserver, UserData
}
}
@Override
public void onWindowAndroidChanged(WindowAndroid windowAndroid) {
if (mInputMethodManagerWrapper != null) {
mInputMethodManagerWrapper.onWindowAndroidChanged(windowAndroid);
}
}
@Override
public void onAttachedToWindow() {
if (mInputConnectionFactory != null) {
......
......@@ -4,6 +4,7 @@
package org.chromium.content.browser.input;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.IBinder;
......@@ -13,9 +14,16 @@ import android.view.View;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.base.task.PostTask;
import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
......@@ -28,13 +36,55 @@ public class InputMethodManagerWrapperImpl implements InputMethodManagerWrapper
private final Context mContext;
public InputMethodManagerWrapperImpl(Context context) {
private WindowAndroid mWindowAndroid;
private Delegate mDelegate;
private Runnable mPendingRunnableOnInputConnection;
public InputMethodManagerWrapperImpl(
Context context, WindowAndroid windowAndroid, Delegate delegate) {
if (DEBUG_LOGS) Log.i(TAG, "Constructor");
mContext = context;
mWindowAndroid = windowAndroid;
mDelegate = delegate;
}
@Override
public void onWindowAndroidChanged(WindowAndroid windowAndroid) {
mWindowAndroid = windowAndroid;
}
private Context getContextForMultiDisplay() {
Activity activity = getActivityFromWindowAndroid(mWindowAndroid);
if (DEBUG_LOGS) {
if (activity == null) Log.i(TAG, "activity is null.");
}
return activity == null ? mContext : activity;
}
/**
* Get an Activity from WindowAndroid.
*
* @param windowAndroid
* @return The Activity. May return null if it fails.
*/
private static Activity getActivityFromWindowAndroid(WindowAndroid windowAndroid) {
if (windowAndroid == null) return null;
// Unwrap this when we actually need it.
WeakReference<Activity> weakRef = windowAndroid.getActivity();
if (weakRef == null) return null;
return weakRef.get();
}
private InputMethodManager getInputMethodManager() {
return (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
// For multi-display case, we need to have the correct activity context.
// However, for Chrome, we wrap application context as container view's
// context in order to prevent leakage. (e.g. see TabImpl.java).
// This is a workaround to update mContext with the activity from
// ActivityWindowAndroid. https://crbug.com/1021403
Context context = getContextForMultiDisplay();
return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
}
@Override
......@@ -45,9 +95,54 @@ public class InputMethodManagerWrapperImpl implements InputMethodManagerWrapper
manager.restartInput(view);
}
@VisibleForTesting
protected boolean hasCorrectDisplayId(Context context, Activity activity) {
// We did not support multi-display before O.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true;
int contextDisplayId = getDisplayId(context);
int activityDisplayId = getDisplayId(activity);
if (activityDisplayId != contextDisplayId) {
Log.w(TAG,
"Activity's display ID(%d) does not match context's display ID(%d). "
+ "Using a workaround to show soft input on the correct display...",
activityDisplayId, contextDisplayId);
return false;
}
return true;
}
@VisibleForTesting
protected int getDisplayId(Context context) {
return DisplayAndroid.getNonMultiDisplay(context).getDisplayId();
}
@Override
public void showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
if (DEBUG_LOGS) Log.i(TAG, "showSoftInput");
mPendingRunnableOnInputConnection = null;
Activity activity = getActivityFromWindowAndroid(mWindowAndroid);
if (activity != null && !hasCorrectDisplayId(mContext, activity)) {
// https://crbug.com/1021403
// This is a workaround for multi-display case. We need this as long as
// Chrome uses the application context for creating the content view.
// Note that this will create a few ms delay in showing keyboard.
activity.getWindow().setLocalFocus(true, true);
if (mDelegate != null && !mDelegate.hasInputConnection()) {
// Delay keyboard showing until input connection is established.
mPendingRunnableOnInputConnection = () -> {
if (isActive(view)) showSoftInputInternal(view, flags, resultReceiver);
};
return;
}
// If we already have InputConnection, then show soft input now.
}
showSoftInputInternal(view, flags, resultReceiver);
}
private void showSoftInputInternal(View view, int flags, ResultReceiver resultReceiver) {
if (DEBUG_LOGS) Log.i(TAG, "showSoftInputInternal");
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); // crbug.com/616283
try {
InputMethodManager manager = getInputMethodManager();
......@@ -69,6 +164,7 @@ public class InputMethodManagerWrapperImpl implements InputMethodManagerWrapper
public boolean hideSoftInputFromWindow(
IBinder windowToken, int flags, ResultReceiver resultReceiver) {
if (DEBUG_LOGS) Log.i(TAG, "hideSoftInputFromWindow");
mPendingRunnableOnInputConnection = null;
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); // crbug.com/616283
try {
InputMethodManager manager = getInputMethodManager();
......@@ -126,4 +222,12 @@ public class InputMethodManagerWrapperImpl implements InputMethodManagerWrapper
return;
}
}
@Override
public void onInputConnectionCreated() {
if (mPendingRunnableOnInputConnection == null) return;
Runnable runnable = mPendingRunnableOnInputConnection;
mPendingRunnableOnInputConnection = null;
PostTask.postTask(UiThreadTaskTraits.DEFAULT, runnable);
}
}
......@@ -12,6 +12,7 @@ import android.view.inputmethod.InputConnection;
import androidx.annotation.VisibleForTesting;
import org.chromium.content.browser.input.ImeAdapterImpl;
import org.chromium.ui.base.WindowAndroid;
/**
* Adapts and plumbs android IME service onto the chrome text input API.
......@@ -34,8 +35,10 @@ public interface ImeAdapter {
* @return the default {@link InputMethodManagerWrapper} that the ImeAdapter uses to
* make calls to the InputMethodManager.
*/
static InputMethodManagerWrapper createDefaultInputMethodManagerWrapper(Context context) {
return ImeAdapterImpl.createDefaultInputMethodManagerWrapper(context);
static InputMethodManagerWrapper createDefaultInputMethodManagerWrapper(Context context,
WindowAndroid windowAndroid, InputMethodManagerWrapper.Delegate delegate) {
return ImeAdapterImpl.createDefaultInputMethodManagerWrapper(
context, windowAndroid, delegate);
}
/**
......
......@@ -11,10 +11,18 @@ import android.os.ResultReceiver;
import android.view.View;
import android.view.inputmethod.CursorAnchorInfo;
import org.chromium.ui.base.WindowAndroid;
/**
* Wrapper around Android's InputMethodManager so that the implementation can be swapped out.
*/
public interface InputMethodManagerWrapper {
/** An embedder may implement this for multi-display support. */
public interface Delegate {
/** Whether the delegate has established an input connection. */
boolean hasInputConnection();
}
/**
* @see android.view.inputmethod.InputMethodManager#restartInput(View)
*/
......@@ -60,4 +68,15 @@ public interface InputMethodManagerWrapper {
* an input method app may wait longer when the user switches methods within the app.
*/
void notifyUserAction();
/**
* Call this when WindowAndroid object has changed.
* @param newWindowAndroid The new WindowAndroid object.
*/
void onWindowAndroidChanged(WindowAndroid newWindowAndroid);
/**
* Call this when non-null InputConnection has been created.
*/
void onInputConnectionCreated();
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.input;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.ui.base.WindowAndroid;
import java.lang.ref.WeakReference;
/**
* A robolectric test for {@link InputMethodManagerWrapperImpl} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
// Any VERSION_CODE >= O is fine.
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.O)
public class InputMethodManagerWrapperImplTest {
private static final boolean DEBUG = false;
private class TestInputMethodManagerWrapperImpl extends InputMethodManagerWrapperImpl {
public TestInputMethodManagerWrapperImpl(
Context context, WindowAndroid windowAndroid, Delegate delegate) {
super(context, windowAndroid, delegate);
}
@Override
protected int getDisplayId(Context context) {
if (context == mContext) {
assert mContextDisplayId != -1;
return mContextDisplayId;
}
if (context == mActivity) {
assert mActivityDisplayId != -1;
return mActivityDisplayId;
}
return super.getDisplayId(context);
}
}
@Mock
private Context mContext;
@Mock
private Activity mActivity;
@Mock
private Window mWindow;
@Mock
private WindowAndroid mWindowAndroid;
@Mock
private InputMethodManagerWrapper.Delegate mDelegate;
@Mock
private View mView;
@Mock
private InputMethodManager mInputMethodManager;
@Mock
private WindowManager mContextWindowManager;
@Mock
private WindowManager mActivityWindowManager;
private int mContextDisplayId = -1; // uninitialized
private int mActivityDisplayId = -1; // uninitialized
private InOrder mInOrder;
private InputMethodManagerWrapperImpl mImmw;
public InputMethodManagerWrapperImplTest() {
if (DEBUG) ShadowLog.stream = System.out;
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mImmw = new TestInputMethodManagerWrapperImpl(mContext, mWindowAndroid, mDelegate);
when(mContext.getSystemService(Context.INPUT_METHOD_SERVICE))
.thenReturn(mInputMethodManager);
when(mActivity.getSystemService(Context.INPUT_METHOD_SERVICE))
.thenReturn(mInputMethodManager);
when(mActivity.getWindow()).thenReturn(mWindow);
mInOrder = inOrder(mInputMethodManager, mWindow);
}
@After
public void tearDown() throws Exception {
mInOrder.verifyNoMoreInteractions();
}
@Test
public void testWebViewHasNoActivity() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(null);
mImmw.showSoftInput(mView, 0, null);
mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
}
private void setDisplayIds(int contextDisplayId, int activityDisplayId) {
mContextDisplayId = contextDisplayId;
mActivityDisplayId = activityDisplayId;
}
@Test
public void testSingleDisplay() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
setDisplayIds(0, 0);
mImmw.showSoftInput(mView, 0, null);
mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
}
@Test
public void testMultiDisplaysWithInputConnection() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
setDisplayIds(0, 1); // context and activity have different display IDs
when(mDelegate.hasInputConnection()).thenReturn(true);
mImmw.showSoftInput(mView, 0, null);
// Run a workaround.
mInOrder.verify(mWindow).setLocalFocus(true, true);
// When InputConnection is available, then show soft input immediately.
mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
}
@Test
public void testMultiDisplaysWithoutInputConnection() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
setDisplayIds(0, 1); // context and activity have different display Ids
when(mDelegate.hasInputConnection()).thenReturn(false);
when(mInputMethodManager.isActive(mView)).thenReturn(true);
mImmw.showSoftInput(mView, 0, null);
// Run a workaround.
mInOrder.verify(mWindow).setLocalFocus(true, true);
// InputConnection is not available, then wait for onInputConnectionCreated().
mInOrder.verifyNoMoreInteractions();
mImmw.onInputConnectionCreated();
// Post task: note that PostTask actually does not require
// Robolectric.getForegroundThreadScheduler().runOneTask() to be called.
// Check first if input method is still valid on the current view.
mInOrder.verify(mInputMethodManager).isActive(mView);
mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
}
@Test
public void testMultiDisplaysWithoutInputConnection_hideKeyboard() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
setDisplayIds(0, 1); // context and activity have different display Ids
when(mDelegate.hasInputConnection()).thenReturn(false);
when(mInputMethodManager.isActive(mView)).thenReturn(true);
mImmw.showSoftInput(mView, 0, null);
// Run a workaround.
mInOrder.verify(mWindow).setLocalFocus(true, true);
// InputConnection is not available, then wait for onInputConnectionCreated().
mInOrder.verifyNoMoreInteractions();
// Hide called before input connection is created.
mImmw.hideSoftInputFromWindow(null, 0, null);
mImmw.onInputConnectionCreated();
mInOrder.verify(mInputMethodManager).hideSoftInputFromWindow(null, 0, null);
// Do not call showSoftInput.
}
@Test
public void testMultiDisplaysWithoutInputConnection_notActive() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
setDisplayIds(0, 1); // context and activity have different display Ids
when(mDelegate.hasInputConnection()).thenReturn(false);
when(mInputMethodManager.isActive(mView)).thenReturn(false);
mImmw.showSoftInput(mView, 0, null);
// Run a workaround.
mInOrder.verify(mWindow).setLocalFocus(true, true);
// InputConnection is not available, then wait for onInputConnectionCreated().
mInOrder.verifyNoMoreInteractions();
// Another showSoftInput before input connection gets created.
mImmw.showSoftInput(mView, 1, null);
mImmw.onInputConnectionCreated();
// Post task: note that PostTask actually does not require
// Robolectric.getForegroundThreadScheduler().runOneTask() to be called.
// Check first if input method is still valid on the current view.
mInOrder.verify(mInputMethodManager).isActive(mView);
// Do not call showSoftInput since it is not active.
}
@Test
public void testMultiDisplaysWithoutInputConnection_showSoftInputAgain() throws Exception {
when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
setDisplayIds(0, 1); // context and activity have different display Ids
when(mDelegate.hasInputConnection()).thenReturn(false);
when(mInputMethodManager.isActive(mView)).thenReturn(true);
mImmw.showSoftInput(mView, 0, null);
// Run a workaround.
mInOrder.verify(mWindow).setLocalFocus(true, true);
// InputConnection is not available, then wait for onInputConnectionCreated().
mInOrder.verifyNoMoreInteractions();
// Another showSoftInput before input connection gets created.
mImmw.showSoftInput(mView, 1, null);
mImmw.onInputConnectionCreated();
// Post task: note that PostTask actually does not require
// Robolectric.getForegroundThreadScheduler().runOneTask() to be called.
// Check first if input method is still valid on the current view.
mInOrder.verify(mInputMethodManager).isActive(mView);
// Note that the first call to showSoftInput was ignored.
mInOrder.verify(mInputMethodManager).showSoftInput(mView, 1, null);
}
}
......@@ -134,7 +134,7 @@ public class ThreadedInputConnectionFactoryTest {
mImeAdapter = Mockito.mock(ImeAdapterImpl.class);
mInputMethodManager = Mockito.mock(InputMethodManager.class);
mFactory = new TestFactory(new InputMethodManagerWrapperImpl(mContext));
mFactory = new TestFactory(new InputMethodManagerWrapperImpl(mContext, null, null));
mFactory.onWindowFocusChanged(true);
mImeHandler = mFactory.getHandler();
mImeShadowLooper = (ShadowLooper) Shadow.extract(mImeHandler.getLooper());
......
......@@ -17,6 +17,7 @@ import org.chromium.content.browser.input.ImeAdapterImpl;
import org.chromium.content.browser.input.Range;
import org.chromium.content_public.browser.ImeAdapter;
import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.ui.base.WindowAndroid;
import java.util.ArrayList;
import java.util.List;
......@@ -215,4 +216,10 @@ public class TestInputMethodManagerWrapper implements InputMethodManagerWrapper
public void onUpdateSelection(Range oldSel, Range oldComp, Range newSel, Range newComp) {}
public void expectsSelectionOutsideComposition() {}
@Override
public void onWindowAndroidChanged(WindowAndroid windowAndroid) {}
@Override
public void onInputConnectionCreated() {}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment