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 |