| 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
|
|
|