Index: content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java |
diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java |
index 68d484b08d3fa8644d8b3d7f837068f86a9549ff..c1795c6bb116b04909059022d43449d1410e6635 100644 |
--- a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java |
+++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java |
@@ -17,6 +17,8 @@ import android.graphics.Bitmap; |
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.util.Pair; |
import android.view.ActionMode; |
@@ -35,6 +37,7 @@ import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeL |
import android.view.accessibility.AccessibilityNodeProvider; |
import android.view.inputmethod.EditorInfo; |
import android.view.inputmethod.InputConnection; |
+import android.view.inputmethod.InputMethodManager; |
import org.chromium.base.ObserverList; |
import org.chromium.base.ObserverList.RewindableIterator; |
@@ -58,7 +61,6 @@ import org.chromium.content_public.browser.AccessibilitySnapshotCallback; |
import org.chromium.content_public.browser.AccessibilitySnapshotNode; |
import org.chromium.content_public.browser.ActionModeCallbackHelper; |
import org.chromium.content_public.browser.GestureStateListener; |
-import org.chromium.content_public.browser.ImeEventObserver; |
import org.chromium.content_public.browser.WebContents; |
import org.chromium.content_public.browser.WebContentsObserver; |
import org.chromium.device.gamepad.GamepadList; |
@@ -66,6 +68,7 @@ import org.chromium.ui.base.DeviceFormFactor; |
import org.chromium.ui.base.EventForwarder; |
import org.chromium.ui.base.ViewAndroidDelegate; |
import org.chromium.ui.base.WindowAndroid; |
+import org.chromium.ui.base.ime.TextInputType; |
import org.chromium.ui.display.DisplayAndroid; |
import org.chromium.ui.display.DisplayAndroid.DisplayAndroidObserver; |
@@ -89,7 +92,7 @@ import java.util.Map; |
public class ContentViewCore |
implements AccessibilityStateChangeListener, DisplayAndroidObserver, |
SystemCaptioningBridge.SystemCaptioningBridgeListener, WindowAndroidProvider, |
- JoystickZoomProvider.PinchZoomHandler, ImeEventObserver { |
+ JoystickZoomProvider.PinchZoomHandler { |
private static final String TAG = "cr_ContentViewCore"; |
// Used to avoid enabling zooming in / out if resulting zooming will |
@@ -171,6 +174,32 @@ public class ContentViewCore |
} |
/** |
+ * {@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 CVC's lifetime to that of ResultReceiver object. |
+ private final WeakReference<ContentViewCore> mContentViewCore; |
+ |
+ public ShowKeyboardResultReceiver(ContentViewCore contentViewCore, Handler handler) { |
+ super(handler); |
+ mContentViewCore = new WeakReference<>(contentViewCore); |
+ } |
+ |
+ @Override |
+ public void onReceiveResult(int resultCode, Bundle resultData) { |
+ ContentViewCore contentViewCore = mContentViewCore.get(); |
+ if (contentViewCore == null) return; |
+ contentViewCore.onShowKeyboardReceiveResult(resultCode); |
+ } |
+ } |
+ |
+ /** |
* Interface that consumers of {@link ContentViewCore} must implement to allow the proper |
* dispatching of view methods through the containing view. |
* |
@@ -223,6 +252,8 @@ public class ContentViewCore |
private WebContents mWebContents; |
private WebContentsObserver mWebContentsObserver; |
+ private ContentViewClient mContentViewClient; |
+ |
// Native pointer to C++ ContentViewCoreImpl object which will be set by nativeInit(). |
private long mNativeContentViewCore; |
@@ -291,6 +322,10 @@ public class ContentViewCore |
// WebView. |
private boolean mShouldSetAccessibilityFocusOnPageLoad; |
+ // Temporary notification to tell onSizeChanged to focus a form element, |
+ // because the OSK was just brought up. |
+ private final Rect mFocusPreOSKViewportRect = new Rect(); |
+ |
// Whether a touch scroll sequence is active, used to hide text selection |
// handles. Note that a scroll sequence will *always* bound a pinch |
// sequence, so this will also be true for the duration of a pinch gesture. |
@@ -321,6 +356,10 @@ public class ContentViewCore |
// A ViewAndroidDelegate that delegates to the current container view. |
private ViewAndroidDelegate mViewAndroidDelegate; |
+ // 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 Boolean mHasViewFocus; |
// The list of observers that are notified when ContentViewCore changes its WindowAndroid. |
@@ -435,10 +474,6 @@ public class ContentViewCore |
if (mNativeContentViewCore != 0) nativeWasResized(mNativeContentViewCore); |
} |
- public void addImeEventObserver(ImeEventObserver imeEventObserver) { |
- mImeAdapter.addEventObserver(imeEventObserver); |
- } |
- |
@VisibleForTesting |
public void setImeAdapterForTest(ImeAdapter imeAdapter) { |
mImeAdapter = imeAdapter; |
@@ -449,6 +484,55 @@ public class ContentViewCore |
return mImeAdapter; |
} |
+ private ImeAdapter createImeAdapter() { |
+ return new ImeAdapter( |
+ new InputMethodManagerWrapper(mContext), new ImeAdapter.ImeAdapterDelegate() { |
+ @Override |
+ public void onImeEvent() { |
+ mPopupZoomer.hide(true); |
+ getContentViewClient().onImeEvent(); |
+ if (isFocusedNodeEditable()) mWebContents.dismissTextHandles(); |
+ } |
+ |
+ @Override |
+ public void onKeyboardBoundsUnchanged() { |
+ assert mWebContents != null; |
+ mWebContents.scrollFocusedEditableNodeIntoView(); |
+ } |
+ |
+ @Override |
+ public boolean performContextMenuAction(int id) { |
+ assert mWebContents != null; |
+ 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; |
+ } |
+ } |
+ |
+ @Override |
+ public View getAttachedView() { |
+ return mContainerView; |
+ } |
+ |
+ @Override |
+ public ResultReceiver getNewShowKeyboardReceiver() { |
+ return ContentViewCore.this.getNewShowKeyboardReceiver(); |
+ } |
+ }); |
+ } |
+ |
/** |
* |
* @param viewDelegate Delegate to add/remove anchor views. |
@@ -486,14 +570,13 @@ public class ContentViewCore |
setContainerViewInternals(internalDispatcher); |
initPopupZoomer(mContext); |
- mImeAdapter = new ImeAdapter( |
- mWebContents, mContainerView, new InputMethodManagerWrapper(mContext)); |
- mImeAdapter.addEventObserver(this); |
+ mImeAdapter = createImeAdapter(); |
+ attachImeAdapter(); |
- mSelectionPopupController = new SelectionPopupController( |
- mContext, windowAndroid, webContents, mContainerView, mRenderCoordinates); |
+ mSelectionPopupController = new SelectionPopupController(mContext, windowAndroid, |
+ webContents, viewDelegate.getContainerView(), mRenderCoordinates, mImeAdapter); |
mSelectionPopupController.setCallback(ActionModeCallbackHelper.EMPTY_CALLBACK); |
- mSelectionPopupController.setContainerView(mContainerView); |
+ mSelectionPopupController.setContainerView(getContainerView()); |
mWebContentsObserver = new ContentViewWebContentsObserver(this); |
@@ -581,7 +664,6 @@ public class ContentViewCore |
if (mContainerView != null) { |
hideSelectPopupWithCancelMessage(); |
mPopupZoomer.hide(false); |
- mImeAdapter.setContainerView(containerView); |
} |
mContainerView = containerView; |
@@ -687,6 +769,12 @@ public class ContentViewCore |
mWebContentsObserver.destroy(); |
mWebContentsObserver = null; |
mImeAdapter.resetAndHideKeyboard(); |
+ // TODO(igsolla): address TODO in ContentViewClient because ContentViewClient is not |
+ // currently a real Null Object. |
+ // |
+ // Instead of deleting the client we use the Null Object pattern to avoid null checks |
+ // in this class. |
+ mContentViewClient = new ContentViewClient(); |
mWebContents = null; |
mNativeContentViewCore = 0; |
mJavaScriptInterfaces.clear(); |
@@ -709,6 +797,27 @@ public class ContentViewCore |
return mNativeContentViewCore != 0; |
} |
+ public void setContentViewClient(ContentViewClient client) { |
+ if (client == null) { |
+ throw new IllegalArgumentException("The client can't be null."); |
+ } |
+ mContentViewClient = client; |
+ } |
+ |
+ @VisibleForTesting |
+ public ContentViewClient getContentViewClient() { |
+ if (mContentViewClient == null) { |
+ // We use the Null Object pattern to avoid having to perform a null check in this class. |
+ // We create it lazily because most of the time a client will be set almost immediately |
+ // after ContentView is created. |
+ mContentViewClient = new ContentViewClient(); |
+ // We don't set the native ContentViewClient pointer here on purpose. The native |
+ // implementation doesn't mind a null delegate and using one is better than passing a |
+ // Null Object, since we cut down on the number of JNI calls. |
+ } |
+ return mContentViewClient; |
+ } |
+ |
/** |
* @return Viewport width in physical pixels as set from onSizeChanged. |
*/ |
@@ -726,6 +835,14 @@ public class ContentViewCore |
} |
/** |
+ * @return Viewport height when the OSK is hidden in physical pixels as set from onSizeChanged. |
+ */ |
+ @CalledByNative |
+ public int getViewportHeightWithOSKHiddenPix() { |
+ return mViewportHeightPix + getContentViewClient().getSystemWindowInsetBottom(); |
+ } |
+ |
+ /** |
* @return Width of underlying physical surface. |
*/ |
@CalledByNative |
@@ -1179,13 +1296,12 @@ public class ContentViewCore |
// Execute a delayed form focus operation because the OSK was brought |
// up earlier. |
- Rect focusPreOSKViewportRect = mImeAdapter.getFocusPreOSKViewportRect(); |
- if (!focusPreOSKViewportRect.isEmpty()) { |
+ if (!mFocusPreOSKViewportRect.isEmpty()) { |
Rect rect = new Rect(); |
getContainerView().getWindowVisibleDisplayFrame(rect); |
- if (!rect.equals(focusPreOSKViewportRect)) { |
+ if (!rect.equals(mFocusPreOSKViewportRect)) { |
// Only assume the OSK triggered the onSizeChanged if width was preserved. |
- if (rect.width() == focusPreOSKViewportRect.width()) { |
+ if (rect.width() == mFocusPreOSKViewportRect.width()) { |
assert mWebContents != null; |
mWebContents.scrollFocusedEditableNodeIntoView(); |
} |
@@ -1197,7 +1313,7 @@ public class ContentViewCore |
private void cancelRequestToScrollFocusedEditableNodeIntoView() { |
// Zero-ing the rect will prevent |updateAfterSizeChanged()| from |
// issuing the delayed form focus event. |
- mImeAdapter.getFocusPreOSKViewportRect().setEmpty(); |
+ mFocusPreOSKViewportRect.setEmpty(); |
} |
/** |
@@ -1676,17 +1792,38 @@ public class ContentViewCore |
TraceEvent.end("ContentViewCore:updateFrameInfo"); |
} |
- // ImeEventObserver |
- |
- @Override |
- public void onImeEvent() { |
- mPopupZoomer.hide(true); |
+ @CalledByNative |
+ private void updateImeAdapter(long nativeImeAdapterAndroid, int textInputType, |
+ int textInputFlags, int textInputMode, String text, int selectionStart, |
+ int selectionEnd, int compositionStart, int compositionEnd, boolean showImeIfNeeded, |
+ boolean replyToRequest) { |
+ try { |
+ TraceEvent.begin("ContentViewCore.updateImeAdapter"); |
+ boolean focusedNodeEditable = (textInputType != TextInputType.NONE); |
+ boolean focusedNodeIsPassword = (textInputType == TextInputType.PASSWORD); |
+ |
+ mImeAdapter.attach(nativeImeAdapterAndroid); |
+ mImeAdapter.updateState(textInputType, textInputFlags, textInputMode, showImeIfNeeded, |
+ text, selectionStart, selectionEnd, compositionStart, compositionEnd, |
+ replyToRequest); |
+ |
+ boolean editableToggled = (focusedNodeEditable != isFocusedNodeEditable()); |
+ mSelectionPopupController.updateSelectionState(focusedNodeEditable, |
+ focusedNodeIsPassword); |
+ if (editableToggled) { |
+ if (mJoystickScrollProvider != null) { |
+ mJoystickScrollProvider.setEnabled(!focusedNodeEditable); |
+ } |
+ getContentViewClient().onFocusedNodeEditabilityChanged(focusedNodeEditable); |
+ } |
+ } finally { |
+ TraceEvent.end("ContentViewCore.updateImeAdapter"); |
+ } |
} |
- @Override |
- public void onNodeAttributeUpdated(boolean editable, boolean password) { |
- if (mJoystickScrollProvider != null) mJoystickScrollProvider.setEnabled(!editable); |
- mSelectionPopupController.updateSelectionState(editable, password); |
+ @CalledByNative |
+ private void forceUpdateImeAdapter(long nativeImeAdapterAndroid) { |
+ mImeAdapter.attach(nativeImeAdapterAndroid); |
} |
/** |
@@ -1799,16 +1936,29 @@ public class ContentViewCore |
@SuppressWarnings("unused") |
@CalledByNative |
private void onRenderProcessChange() { |
+ attachImeAdapter(); |
// Immediately sync closed caption settings to the new render process. |
mSystemCaptioningBridge.syncToListener(this); |
} |
/** |
+ * Attaches the native ImeAdapter object to the java ImeAdapter to allow communication via JNI. |
+ */ |
+ public void attachImeAdapter() { |
+ if (mImeAdapter != null && mNativeContentViewCore != 0) { |
+ mImeAdapter.attach(nativeGetNativeImeAdapter(mNativeContentViewCore)); |
+ } |
+ } |
+ |
+ /** |
* @see View#hasFocus() |
*/ |
@CalledByNative |
private boolean hasFocus() { |
- return ViewUtils.hasFocus(mContainerView); |
+ // If the container view is not focusable, we consider it always focused from |
+ // Chromium's point of view. |
+ if (!mContainerView.isFocusable()) return true; |
+ return mContainerView.hasFocus(); |
} |
/** |
@@ -2432,6 +2582,35 @@ public class ContentViewCore |
mSelectionPopupController.setSelectionClient(selectionClient); |
} |
+ /** |
+ * 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. |
+ getContainerView().getWindowVisibleDisplayFrame(mFocusPreOSKViewportRect); |
+ } else if (hasFocus() && resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN) { |
+ // If the OSK was already there, focus the form immediately. |
+ if (mWebContents != null) { |
+ mWebContents.scrollFocusedEditableNodeIntoView(); |
+ } |
+ } |
+ } |
+ |
+ @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; |
+ } |
+ |
private native long nativeInit(WebContents webContents, ViewAndroidDelegate viewAndroidDelegate, |
long windowAndroidPtr, float dipScale, HashSet<Object> retainedObjectSet); |
private static native ContentViewCore nativeFromWebContentsAndroid(WebContents webContents); |
@@ -2492,6 +2671,9 @@ public class ContentViewCore |
private native void nativeSelectPopupMenuItems(long nativeContentViewCoreImpl, |
long nativeSelectPopupSourceFrame, int[] indices); |
+ |
+ private native long nativeGetNativeImeAdapter(long nativeContentViewCoreImpl); |
+ |
private native int nativeGetCurrentRenderProcessId(long nativeContentViewCoreImpl); |
private native void nativeSetAllowJavascriptInterfacesInspection( |