Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2020)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java

Issue 2777223004: Migrate IME state update flow (Closed)
Patch Set: rebase Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java
diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java
index 73c07186b53d3b67e12a5104876c2e5b82aa8d4a..08686df7c33f5755b475353ad12845aad56b883f 100644
--- a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java
+++ b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java
@@ -4,8 +4,12 @@
package org.chromium.content.browser.input;
+import android.annotation.SuppressLint;
import android.content.res.Configuration;
+import android.graphics.Rect;
import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.text.SpannableString;
@@ -19,8 +23,10 @@ import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
import org.chromium.base.Log;
+import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
@@ -28,10 +34,16 @@ import org.chromium.blink_public.web.WebInputEventModifier;
import org.chromium.blink_public.web.WebInputEventType;
import org.chromium.blink_public.web.WebTextInputMode;
import org.chromium.content.browser.RenderCoordinates;
+import org.chromium.content.browser.ViewUtils;
import org.chromium.content.browser.picker.InputDialogContainer;
+import org.chromium.content_public.browser.ImeEventObserver;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ime.TextInputType;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Adapts and plumbs android IME service onto the chrome text input API.
* ImeAdapter provides an interface in both ways native <-> java:
@@ -60,36 +72,6 @@ public class ImeAdapter {
public static final int COMPOSITION_KEY_CODE = 229;
- /**
- * Interface for the delegate that needs to be notified of IME changes.
- */
- public interface ImeAdapterDelegate {
- /**
- * Called to notify the delegate about synthetic/real key events before sending to renderer.
- */
- void onImeEvent();
-
- /**
- * Called when the keyboard could not be shown due to the hardware keyboard being present.
- */
- void onKeyboardBoundsUnchanged();
-
- /**
- * @see BaseInputConnection#performContextMenuAction(int)
- */
- boolean performContextMenuAction(int id);
-
- /**
- * @return View that the keyboard should be attached to.
- */
- View getAttachedView();
-
- /**
- * @return Object that should be called for all keyboard show and hide requests.
- */
- ResultReceiver getNewShowKeyboardReceiver();
- }
-
static char[] sSingleCharArray = new char[1];
static KeyCharacterMap sKeyCharacterMap;
@@ -98,16 +80,30 @@ public class ImeAdapter {
private ChromiumBaseInputConnection mInputConnection;
private ChromiumBaseInputConnection.Factory mInputConnectionFactory;
- private final ImeAdapterDelegate mViewEmbedder;
+ // NOTE: This object will not be released by Android framework until the matching
+ // ResultReceiver in the InputMethodService (IME app) gets gc'ed.
+ private ShowKeyboardResultReceiver mShowKeyboardResultReceiver;
+
+ private final WebContents mWebContents;
+ private View mContainerView;
+
// This holds the information necessary for constructing CursorAnchorInfo, and notifies to
// InputMethodManager on appropriate timing, depending on how IME requested the information
// via InputConnection. The update request is per InputConnection, hence for each time it is
// re-created, the monitoring status will be reset.
private final CursorAnchorInfoController mCursorAnchorInfoController;
+ private final List<ImeEventObserver> mEventObservers = new ArrayList<>();
+
private int mTextInputType = TextInputType.NONE;
private int mTextInputFlags;
private int mTextInputMode = WebTextInputMode.kDefault;
+ private boolean mNodeEditable;
+ private boolean mNodePassword;
+
+ // Viewport rect before the OSK was brought up.
+ // Used to tell View#onSizeChanged to focus a form element.
+ private final Rect mFocusPreOSKViewportRect = new Rect();
// Keep the current configuration to detect the change when onConfigurationChanged() is called.
private Configuration mCurrentConfig;
@@ -123,18 +119,46 @@ public class ImeAdapter {
private boolean mIsConnected;
/**
+ * {@ResultReceiver} passed in InputMethodManager#showSoftInput}. We need this to scroll to the
+ * editable node at the right timing, which is after input method window shows up.
+ */
+ // TODO(crbug.com/635567): Fix this properly.
+ @SuppressLint("ParcelCreator")
+ private static class ShowKeyboardResultReceiver extends ResultReceiver {
+ // Unfortunately, the memory life cycle of ResultReceiver object, once passed in
+ // showSoftInput(), is in the control of Android's input method framework and IME app,
+ // so we use a weakref to avoid tying ImeAdapter's lifetime to that of ResultReceiver
+ // object.
+ private final WeakReference<ImeAdapter> mImeAdapter;
+
+ public ShowKeyboardResultReceiver(ImeAdapter imeAdapter, Handler handler) {
+ super(handler);
+ mImeAdapter = new WeakReference<>(imeAdapter);
+ }
+
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ ImeAdapter imeAdapter = mImeAdapter.get();
+ if (imeAdapter == null) return;
+ imeAdapter.onShowKeyboardReceiveResult(resultCode);
+ }
+ }
+
+ /**
* @param webContents WebContents instance with which this ImeAdapter is associated.
+ * @param containerView {@link View} instance which input events are posted on.
* @param wrapper InputMethodManagerWrapper that should receive all the call directed to
* InputMethodManager.
- * @param embedder The view that is used for callbacks from ImeAdapter.
*/
- public ImeAdapter(WebContents webContents, InputMethodManagerWrapper wrapper,
- ImeAdapterDelegate embedder) {
+ public ImeAdapter(
+ WebContents webContents, View containerView, InputMethodManagerWrapper wrapper) {
+ mWebContents = webContents;
+ mContainerView = containerView;
mInputMethodManagerWrapper = wrapper;
- mViewEmbedder = embedder;
+
// Deep copy newConfig so that we can notice the difference.
- mCurrentConfig = new Configuration(
- mViewEmbedder.getAttachedView().getResources().getConfiguration());
+ mCurrentConfig = new Configuration(mContainerView.getResources().getConfiguration());
+
// CursorAnchroInfo is supported only after L.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mCursorAnchorInfoController = CursorAnchorInfoController.create(wrapper,
@@ -166,6 +190,18 @@ public class ImeAdapter {
mNativeImeAdapterAndroid = nativeInit(webContents);
}
+ /**
+ * Set the container view.
+ * @param containerView {@link View} which this ImeAdapter works on.
+ */
+ public void setContainerView(View containerView) {
+ mContainerView = containerView;
+ }
+
+ public void addEventObserver(ImeEventObserver eventObserver) {
+ mEventObservers.add(eventObserver);
+ }
+
private void createInputConnectionFactory() {
if (mInputConnectionFactory != null) return;
mInputConnectionFactory = new ThreadedInputConnectionFactory(mInputMethodManagerWrapper);
@@ -194,15 +230,14 @@ public class ImeAdapter {
return null;
}
if (mInputConnectionFactory == null) return null;
- setInputConnection(mInputConnectionFactory.initializeAndGet(mViewEmbedder.getAttachedView(),
- this, mTextInputType, mTextInputFlags, mTextInputMode, mLastSelectionStart,
+ setInputConnection(mInputConnectionFactory.initializeAndGet(mContainerView, this,
+ mTextInputType, mTextInputFlags, mTextInputMode, mLastSelectionStart,
mLastSelectionEnd, outAttrs));
if (DEBUG_LOGS) Log.i(TAG, "onCreateInputConnection: " + mInputConnection);
if (mCursorAnchorInfoController != null) {
- mCursorAnchorInfoController.onRequestCursorUpdates(
- false /* not an immediate request */, false /* disable monitoring */,
- mViewEmbedder.getAttachedView());
+ mCursorAnchorInfoController.onRequestCursorUpdates(false /* not an immediate request */,
+ false /* disable monitoring */, mContainerView);
}
if (isValid()) {
nativeRequestCursorUpdate(mNativeImeAdapterAndroid,
@@ -288,54 +323,73 @@ public class ImeAdapter {
* selection.
* @param replyToRequest True when the update was requested by IME.
*/
- public void updateState(int textInputType, int textInputFlags, int textInputMode,
+ @CalledByNative
+ private void updateState(int textInputType, int textInputFlags, int textInputMode,
boolean showIfNeeded, String text, int selectionStart, int selectionEnd,
int compositionStart, int compositionEnd, boolean replyToRequest) {
- if (DEBUG_LOGS) {
- Log.i(TAG, "updateState: type [%d->%d], flags [%d], show [%b], ", mTextInputType,
- textInputType, textInputFlags, showIfNeeded);
- }
- boolean needsRestart = false;
- if (mRestartInputOnNextStateUpdate) {
- needsRestart = true;
- mRestartInputOnNextStateUpdate = false;
- }
-
- mTextInputFlags = textInputFlags;
- if (mTextInputMode != textInputMode) {
- mTextInputMode = textInputMode;
- needsRestart = true;
- }
- if (mTextInputType != textInputType) {
- mTextInputType = textInputType;
- needsRestart = true;
- }
- if (mCursorAnchorInfoController != null && (!TextUtils.equals(mLastText, text)
- || mLastSelectionStart != selectionStart || mLastSelectionEnd != selectionEnd
- || mLastCompositionStart != compositionStart
- || mLastCompositionEnd != compositionEnd)) {
- mCursorAnchorInfoController.invalidateLastCursorAnchorInfo();
- }
- mLastText = text;
- mLastSelectionStart = selectionStart;
- mLastSelectionEnd = selectionEnd;
- mLastCompositionStart = compositionStart;
- mLastCompositionEnd = compositionEnd;
-
- if (textInputType == TextInputType.NONE) {
- hideKeyboard();
- } else {
- if (needsRestart) restartInput();
- // There is no API for us to get notified of user's dismissal of keyboard.
- // Therefore, we should try to show keyboard even when text input type hasn't changed.
- if (showIfNeeded) showSoftKeyboard();
- }
+ TraceEvent.begin("ImeAdapter.updateState");
+ try {
+ if (DEBUG_LOGS) {
+ Log.i(TAG, "updateState: type [%d->%d], flags [%d], show [%b], ", mTextInputType,
+ textInputType, textInputFlags, showIfNeeded);
+ }
+ boolean needsRestart = false;
+ if (mRestartInputOnNextStateUpdate) {
+ needsRestart = true;
+ mRestartInputOnNextStateUpdate = false;
+ }
- if (mInputConnection == null) return;
- boolean singleLine = mTextInputType != TextInputType.TEXT_AREA
- && mTextInputType != TextInputType.CONTENT_EDITABLE;
- mInputConnection.updateStateOnUiThread(text, selectionStart, selectionEnd, compositionStart,
- compositionEnd, singleLine, replyToRequest);
+ mTextInputFlags = textInputFlags;
+ if (mTextInputMode != textInputMode) {
+ mTextInputMode = textInputMode;
+ needsRestart = true;
+ }
+ if (mTextInputType != textInputType) {
+ mTextInputType = textInputType;
+ needsRestart = true;
+
+ boolean editable = textInputType != TextInputType.NONE;
+ boolean password = textInputType == TextInputType.PASSWORD;
+ if (mNodeEditable != editable || mNodePassword != password) {
+ for (ImeEventObserver observer : mEventObservers) {
+ observer.onNodeAttributeUpdated(editable, password);
+ }
+ mNodeEditable = editable;
+ mNodePassword = password;
+ }
+ }
+ if (mCursorAnchorInfoController != null
+ && (!TextUtils.equals(mLastText, text) || mLastSelectionStart != selectionStart
+ || mLastSelectionEnd != selectionEnd
+ || mLastCompositionStart != compositionStart
+ || mLastCompositionEnd != compositionEnd)) {
+ mCursorAnchorInfoController.invalidateLastCursorAnchorInfo();
+ }
+ mLastText = text;
+ mLastSelectionStart = selectionStart;
+ mLastSelectionEnd = selectionEnd;
+ mLastCompositionStart = compositionStart;
+ mLastCompositionEnd = compositionEnd;
+
+ if (textInputType == TextInputType.NONE) {
+ hideKeyboard();
+ } else {
+ if (needsRestart) restartInput();
+ // There is no API for us to get notified of user's dismissal of keyboard.
+ // Therefore, we should try to show keyboard even when text input type hasn't
+ // changed.
+ if (showIfNeeded) showSoftKeyboard();
+ }
+
+ if (mInputConnection != null) {
+ boolean singleLine = mTextInputType != TextInputType.TEXT_AREA
+ && mTextInputType != TextInputType.CONTENT_EDITABLE;
+ mInputConnection.updateStateOnUiThread(text, selectionStart, selectionEnd,
+ compositionStart, compositionEnd, singleLine, replyToRequest);
+ }
+ } finally {
+ TraceEvent.end("ImeAdapter.updateState");
+ }
}
/**
@@ -343,20 +397,51 @@ public class ImeAdapter {
*/
private void showSoftKeyboard() {
if (DEBUG_LOGS) Log.i(TAG, "showSoftKeyboard");
- mInputMethodManagerWrapper.showSoftInput(
- mViewEmbedder.getAttachedView(), 0, mViewEmbedder.getNewShowKeyboardReceiver());
- if (mViewEmbedder.getAttachedView().getResources().getConfiguration().keyboard
+ mInputMethodManagerWrapper.showSoftInput(mContainerView, 0, getNewShowKeyboardReceiver());
+ if (mContainerView.getResources().getConfiguration().keyboard
!= Configuration.KEYBOARD_NOKEYS) {
- mViewEmbedder.onKeyboardBoundsUnchanged();
+ mWebContents.scrollFocusedEditableNodeIntoView();
}
}
/**
+ * Call this when we get result from ResultReceiver passed in calling showSoftInput().
+ * @param resultCode The result of showSoftInput() as defined in InputMethodManager.
+ */
+ public void onShowKeyboardReceiveResult(int resultCode) {
+ if (resultCode == InputMethodManager.RESULT_SHOWN) {
+ // If OSK is newly shown, delay the form focus until
+ // the onSizeChanged (in order to adjust relative to the
+ // new size).
+ // TODO(jdduke): We should not assume that onSizeChanged will
+ // always be called, crbug.com/294908.
+ mContainerView.getWindowVisibleDisplayFrame(mFocusPreOSKViewportRect);
+ } else if (ViewUtils.hasFocus(mContainerView)
+ && resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN) {
+ // If the OSK was already there, focus the form immediately.
+ mWebContents.scrollFocusedEditableNodeIntoView();
+ }
+ }
+
+ public Rect getFocusPreOSKViewportRect() {
+ return mFocusPreOSKViewportRect;
+ }
+
+ @VisibleForTesting
+ public ResultReceiver getNewShowKeyboardReceiver() {
+ if (mShowKeyboardResultReceiver == null) {
+ // Note: the returned object will get leaked by Android framework.
+ mShowKeyboardResultReceiver = new ShowKeyboardResultReceiver(this, new Handler());
+ }
+ return mShowKeyboardResultReceiver;
+ }
+
+ /**
* Hide soft keyboard.
*/
private void hideKeyboard() {
if (DEBUG_LOGS) Log.i(TAG, "hideKeyboard");
- View view = mViewEmbedder.getAttachedView();
+ View view = mContainerView;
if (mInputMethodManagerWrapper.isActive(view)) {
// NOTE: we should not set ResultReceiver here. Otherwise, IMM will own ContentViewCore
// and ImeAdapter even after input method goes away and result gets received.
@@ -498,8 +583,8 @@ public class ImeAdapter {
*/
void updateSelection(
int selectionStart, int selectionEnd, int compositionStart, int compositionEnd) {
- mInputMethodManagerWrapper.updateSelection(mViewEmbedder.getAttachedView(),
- selectionStart, selectionEnd, compositionStart, compositionEnd);
+ mInputMethodManagerWrapper.updateSelection(
+ mContainerView, selectionStart, selectionEnd, compositionStart, compositionEnd);
}
/**
@@ -507,7 +592,7 @@ public class ImeAdapter {
*/
void restartInput() {
// This will eventually cause input method manager to call View#onCreateInputConnection().
- mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
+ mInputMethodManagerWrapper.restartInput(mContainerView);
if (mInputConnection != null) mInputConnection.onRestartInputOnUiThread();
}
@@ -516,7 +601,22 @@ public class ImeAdapter {
*/
boolean performContextMenuAction(int id) {
if (DEBUG_LOGS) Log.i(TAG, "performContextMenuAction: id [%d]", id);
- return mViewEmbedder.performContextMenuAction(id);
+ switch (id) {
+ case android.R.id.selectAll:
+ mWebContents.selectAll();
+ return true;
+ case android.R.id.cut:
+ mWebContents.cut();
+ return true;
+ case android.R.id.copy:
+ mWebContents.copy();
+ return true;
+ case android.R.id.paste:
+ mWebContents.paste();
+ return true;
+ default:
+ return false;
+ }
}
boolean performEditorAction(int actionCode) {
@@ -550,6 +650,11 @@ public class ImeAdapter {
flags));
}
+ private void onImeEvent() {
+ for (ImeEventObserver observer : mEventObservers) observer.onImeEvent();
+ if (mNodeEditable) mWebContents.dismissTextHandles();
+ }
+
boolean sendCompositionToNative(
CharSequence text, int newCursorPosition, boolean isCommit, int unicodeFromKeyEvent) {
if (!isValid()) return false;
@@ -561,7 +666,7 @@ public class ImeAdapter {
return true;
}
- mViewEmbedder.onImeEvent();
+ onImeEvent();
long timestampMs = SystemClock.uptimeMillis();
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.kRawKeyDown, 0,
timestampMs, COMPOSITION_KEY_CODE, 0, false, unicodeFromKeyEvent);
@@ -601,7 +706,7 @@ public class ImeAdapter {
// sends ACTION_DOWN), so it's fine to silently drop it.
return false;
}
- mViewEmbedder.onImeEvent();
+ onImeEvent();
return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, type,
getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
@@ -617,7 +722,7 @@ public class ImeAdapter {
* @return Whether the native counterpart of ImeAdapter received the call.
*/
boolean deleteSurroundingText(int beforeLength, int afterLength) {
- mViewEmbedder.onImeEvent();
+ onImeEvent();
if (!isValid()) return false;
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.kRawKeyDown, 0,
SystemClock.uptimeMillis(), COMPOSITION_KEY_CODE, 0, false, 0);
@@ -636,7 +741,7 @@ public class ImeAdapter {
* @return Whether the native counterpart of ImeAdapter received the call.
*/
boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
- mViewEmbedder.onImeEvent();
+ onImeEvent();
if (!isValid()) return false;
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.kRawKeyDown, 0,
SystemClock.uptimeMillis(), COMPOSITION_KEY_CODE, 0, false, 0);
@@ -712,8 +817,8 @@ public class ImeAdapter {
nativeRequestCursorUpdate(mNativeImeAdapterAndroid, immediateRequest, monitorRequest);
}
if (mCursorAnchorInfoController == null) return false;
- return mCursorAnchorInfoController.onRequestCursorUpdates(immediateRequest, monitorRequest,
- mViewEmbedder.getAttachedView());
+ return mCursorAnchorInfoController.onRequestCursorUpdates(
+ immediateRequest, monitorRequest, mContainerView);
}
/**
@@ -734,7 +839,7 @@ public class ImeAdapter {
if (mCursorAnchorInfoController == null) return;
mCursorAnchorInfoController.onUpdateFrameInfo(renderCoordinates, hasInsertionMarker,
isInsertionMarkerVisible, insertionMarkerHorizontal, insertionMarkerTop,
- insertionMarkerBottom, mViewEmbedder.getAttachedView());
+ insertionMarkerBottom, mContainerView);
}
@CalledByNative
@@ -768,8 +873,7 @@ public class ImeAdapter {
@CalledByNative
private void setCharacterBounds(float[] characterBounds) {
if (mCursorAnchorInfoController == null) return;
- mCursorAnchorInfoController.setCompositionCharacterBounds(characterBounds,
- mViewEmbedder.getAttachedView());
+ mCursorAnchorInfoController.setCompositionCharacterBounds(characterBounds, mContainerView);
}
@CalledByNative

Powered by Google App Engine
This is Rietveld 408576698