// Copyright 2012 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; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; import android.provider.Settings; import android.provider.Settings.Secure; import android.text.Editable; import android.util.Log; import android.util.Pair; import android.view.ActionMode; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.AbsoluteLayout; import android.widget.FrameLayout; import com.google.common.annotations.VisibleForTesting; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.base.WeakContext; import org.chromium.content.R; import org.chromium.content.browser.ContentViewGestureHandler.MotionEventDelegate; import org.chromium.content.browser.accessibility.AccessibilityInjector; import org.chromium.content.browser.accessibility.BrowserAccessibilityManager; import org.chromium.content.browser.input.AdapterInputConnection; import org.chromium.content.browser.input.HandleView; import org.chromium.content.browser.input.ImeAdapter; import org.chromium.content.browser.input.InputMethodManagerWrapper; import org.chromium.content.browser.input.ImeAdapter.AdapterInputConnectionFactory; import org.chromium.content.browser.input.InsertionHandleController; import org.chromium.content.browser.input.SelectPopupDialog; import org.chromium.content.browser.input.SelectionHandleController; import org.chromium.content.common.TraceEvent; import org.chromium.ui.ViewAndroid; import org.chromium.ui.ViewAndroidDelegate; import org.chromium.ui.WindowAndroid; import org.chromium.ui.gfx.DeviceDisplayInfo; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; /** * Provides a Java-side 'wrapper' around a WebContent (native) instance. * Contains all the major functionality necessary to manage the lifecycle of a ContentView without * being tied to the view system. */ @JNINamespace("content") public class ContentViewCore implements MotionEventDelegate, NavigationClient, AccessibilityStateChangeListener { /** * Indicates that input events are batched together and delivered just before vsync. */ public static final int INPUT_EVENTS_DELIVERED_AT_VSYNC = 1; /** * Opposite of INPUT_EVENTS_DELIVERED_AT_VSYNC. */ public static final int INPUT_EVENTS_DELIVERED_IMMEDIATELY = 0; private static final String TAG = "ContentViewCore"; // Used to avoid enabling zooming in / out if resulting zooming will // produce little visible difference. private static final float ZOOM_CONTROLS_EPSILON = 0.007f; // Used to represent gestures for long press and long tap. private static final int IS_LONG_PRESS = 1; private static final int IS_LONG_TAP = 2; // Length of the delay (in ms) before fading in handles after the last page movement. private static final int TEXT_HANDLE_FADE_IN_DELAY = 300; // If the embedder adds a JavaScript interface object that contains an indirect reference to // the ContentViewCore, then storing a strong ref to the interface object on the native // side would prevent garbage collection of the ContentViewCore (as that strong ref would // create a new GC root). // For that reason, we store only a weak reference to the interface object on the // native side. However we still need a strong reference on the Java side to // prevent garbage collection if the embedder doesn't maintain their own ref to the // interface object - the Java side ref won't create a new GC root. // This map stores those refernces. We put into the map on addJavaScriptInterface() // and remove from it in removeJavaScriptInterface(). private final Map mJavaScriptInterfaces = new HashMap(); // Additionally, we keep track of all Java bound JS objects that are in use on the // current page to ensure that they are not garbage collected until the page is // navigated. This includes interface objects that have been removed // via the removeJavaScriptInterface API and transient objects returned from methods // on the interface object. Note we use HashSet rather than Set as the native side // expects HashSet (no bindings for interfaces). private final HashSet mRetainedJavaScriptObjects = new HashSet(); /** * Interface that consumers of {@link ContentViewCore} must implement to allow the proper * dispatching of view methods through the containing view. * *

* All methods with the "super_" prefix should be routed to the parent of the * implementing container view. */ @SuppressWarnings("javadoc") public interface InternalAccessDelegate { /** * @see View#drawChild(Canvas, View, long) */ boolean drawChild(Canvas canvas, View child, long drawingTime); /** * @see View#onKeyUp(keyCode, KeyEvent) */ boolean super_onKeyUp(int keyCode, KeyEvent event); /** * @see View#dispatchKeyEventPreIme(KeyEvent) */ boolean super_dispatchKeyEventPreIme(KeyEvent event); /** * @see View#dispatchKeyEvent(KeyEvent) */ boolean super_dispatchKeyEvent(KeyEvent event); /** * @see View#onGenericMotionEvent(MotionEvent) */ boolean super_onGenericMotionEvent(MotionEvent event); /** * @see View#onConfigurationChanged(Configuration) */ void super_onConfigurationChanged(Configuration newConfig); /** * @see View#awakenScrollBars() */ boolean awakenScrollBars(); /** * @see View#awakenScrollBars(int, boolean) */ boolean super_awakenScrollBars(int startDelay, boolean invalidate); } /** * An interface that allows the embedder to be notified when the pinch gesture starts and * stops. */ public interface GestureStateListener { /** * Called when the pinch gesture starts. */ void onPinchGestureStart(); /** * Called when the pinch gesture ends. */ void onPinchGestureEnd(); /** * Called when the fling gesture is sent. */ void onFlingStartGesture(int vx, int vy); /** * Called when the fling cancel gesture is sent. */ void onFlingCancelGesture(); /** * Called when a fling event was not handled by the renderer. */ void onUnhandledFlingStartEvent(); } /** * An interface for controlling visibility and state of embedder-provided zoom controls. */ public interface ZoomControlsDelegate { /** * Called when it's reasonable to show zoom controls. */ void invokeZoomPicker(); /** * Called when zoom controls need to be hidden (e.g. when the view hides). */ void dismissZoomPicker(); /** * Called when page scale has been changed, so the controls can update their state. */ void updateZoomControls(); } /** * An interface that allows the embedder to be notified of changes to the parameters of the * currently displayed contents. * These notifications are consistent with respect to the UI thread (the size is the size of * the contents currently displayed on screen). */ public interface UpdateFrameInfoListener { /** * Called each time any of the parameters are changed. * * @param widthCss The content width in logical (CSS) pixels. * @param heightCss The content height in logical (CSS) pixels. * @param pageScaleFactor The page scale. */ void onFrameInfoUpdated(float widthCss, float heightCss, float pageScaleFactor); } private VSyncManager.Provider mVSyncProvider; private VSyncManager.Listener mVSyncListener; private int mVSyncSubscriberCount; private boolean mVSyncListenerRegistered; // To avoid IPC delay we use input events to directly trigger a vsync signal in the renderer. // When we do this, we also need to avoid sending the real vsync signal for the current // frame to avoid double-ticking. This flag is used to inhibit the next vsync notification. private boolean mDidSignalVSyncUsingInputEvent; public VSyncManager.Listener getVSyncListener(VSyncManager.Provider vsyncProvider) { if (mVSyncProvider != null && mVSyncListenerRegistered) { mVSyncProvider.unregisterVSyncListener(mVSyncListener); mVSyncListenerRegistered = false; } mVSyncProvider = vsyncProvider; mVSyncListener = new VSyncManager.Listener() { @Override public void updateVSync(long tickTimeMicros, long intervalMicros) { if (mNativeContentViewCore != 0) { nativeUpdateVSyncParameters(mNativeContentViewCore, tickTimeMicros, intervalMicros); } } @Override public void onVSync(long frameTimeMicros) { animateIfNecessary(frameTimeMicros); if (mDidSignalVSyncUsingInputEvent) { TraceEvent.instant("ContentViewCore::onVSync ignored"); mDidSignalVSyncUsingInputEvent = false; return; } if (mNativeContentViewCore != 0) { nativeOnVSync(mNativeContentViewCore, frameTimeMicros); } } }; if (mVSyncSubscriberCount > 0) { // setVSyncNotificationEnabled(true) is called before getVSyncListener. vsyncProvider.registerVSyncListener(mVSyncListener); mVSyncListenerRegistered = true; } return mVSyncListener; } @CalledByNative void setVSyncNotificationEnabled(boolean enabled) { if (!isVSyncNotificationEnabled() && enabled) { mDidSignalVSyncUsingInputEvent = false; } if (mVSyncProvider != null) { if (!mVSyncListenerRegistered && enabled) { mVSyncProvider.registerVSyncListener(mVSyncListener); mVSyncListenerRegistered = true; } else if (mVSyncSubscriberCount == 1 && !enabled) { assert mVSyncListenerRegistered; mVSyncProvider.unregisterVSyncListener(mVSyncListener); mVSyncListenerRegistered = false; } } mVSyncSubscriberCount += enabled ? 1 : -1; assert mVSyncSubscriberCount >= 0; } @CalledByNative private void resetVSyncNotification() { while (isVSyncNotificationEnabled()) setVSyncNotificationEnabled(false); mVSyncSubscriberCount = 0; mVSyncListenerRegistered = false; mNeedAnimate = false; } private boolean isVSyncNotificationEnabled() { return mVSyncProvider != null && mVSyncListenerRegistered; } @CalledByNative private void setNeedsAnimate() { if (!mNeedAnimate) { mNeedAnimate = true; setVSyncNotificationEnabled(true); } } private final Context mContext; private ViewGroup mContainerView; private InternalAccessDelegate mContainerViewInternals; private WebContentsObserverAndroid mWebContentsObserver; private ContentViewClient mContentViewClient; private ContentSettings mContentSettings; // Native pointer to C++ ContentViewCoreImpl object which will be set by nativeInit(). private int mNativeContentViewCore = 0; private boolean mAttachedToWindow = false; // Pid of the renderer process backing this ContentViewCore. private int mPid = 0; private ContentViewGestureHandler mContentViewGestureHandler; private GestureStateListener mGestureStateListener; private UpdateFrameInfoListener mUpdateFrameInfoListener; private ZoomManager mZoomManager; private ZoomControlsDelegate mZoomControlsDelegate; private PopupZoomer mPopupZoomer; private Runnable mFakeMouseMoveRunnable = null; // Only valid when focused on a text / password field. private ImeAdapter mImeAdapter; private ImeAdapter.AdapterInputConnectionFactory mAdapterInputConnectionFactory; private AdapterInputConnection mInputConnection; private SelectionHandleController mSelectionHandleController; private InsertionHandleController mInsertionHandleController; private Runnable mDeferredHandleFadeInRunnable; // Size of the viewport in physical pixels as set from onSizeChanged. private int mViewportWidthPix; private int mViewportHeightPix; private int mPhysicalBackingWidthPix; private int mPhysicalBackingHeightPix; private int mOverdrawBottomHeightPix; private int mViewportSizeOffsetWidthPix; private int mViewportSizeOffsetHeightPix; // Cached copy of all positions and scales as reported by the renderer. private final RenderCoordinates mRenderCoordinates; private final RenderCoordinates.NormalizedPoint mStartHandlePoint; private final RenderCoordinates.NormalizedPoint mEndHandlePoint; private final RenderCoordinates.NormalizedPoint mInsertionHandlePoint; // Tracks whether a selection is currently active. When applied to selected text, indicates // whether the last selected text is still highlighted. private boolean mHasSelection; private String mLastSelectedText; private boolean mSelectionEditable; private ActionMode mActionMode; private boolean mUnselectAllOnActionModeDismiss; // Delegate that will handle GET downloads, and be notified of completion of POST downloads. private ContentViewDownloadDelegate mDownloadDelegate; // The AccessibilityInjector that handles loading Accessibility scripts into the web page. private AccessibilityInjector mAccessibilityInjector; // Handles native accessibility, i.e. without any script injection. private BrowserAccessibilityManager mBrowserAccessibilityManager; // System accessibility service. private final AccessibilityManager mAccessibilityManager; // Allows us to dynamically respond when the accessibility script injection flag changes. private ContentObserver mAccessibilityScriptInjectionObserver; // Temporary notification to tell onSizeChanged to focus a form element, // because the OSK was just brought up. private boolean mUnfocusOnNextSizeChanged = false; private final Rect mFocusPreOSKViewportRect = new Rect(); private boolean mNeedUpdateOrientationChanged; // Used to keep track of whether we should try to undo the last zoom-to-textfield operation. private boolean mScrolledAndZoomedFocusedEditableNode = false; // Whether we use hardware-accelerated drawing. private boolean mHardwareAccelerated = false; // Whether we received a new frame since consumePendingRendererFrame() was last called. private boolean mPendingRendererFrame = false; // Whether we should animate at the next vsync tick. private boolean mNeedAnimate = false; private ViewAndroid mViewAndroid; /** * Constructs a new ContentViewCore. Embedders must call initialize() after constructing * a ContentViewCore and before using it. * * @param context The context used to create this. */ public ContentViewCore(Context context) { mContext = context; WeakContext.initializeWeakContext(context); HeapStatsLogger.init(mContext.getApplicationContext()); mAdapterInputConnectionFactory = new AdapterInputConnectionFactory(); mRenderCoordinates = new RenderCoordinates(); mRenderCoordinates.setDeviceScaleFactor( getContext().getResources().getDisplayMetrics().density); mStartHandlePoint = mRenderCoordinates.createNormalizedPoint(); mEndHandlePoint = mRenderCoordinates.createNormalizedPoint(); mInsertionHandlePoint = mRenderCoordinates.createNormalizedPoint(); mAccessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); } /** * @return The context used for creating this ContentViewCore. */ @CalledByNative public Context getContext() { return mContext; } /** * @return The ViewGroup that all view actions of this ContentViewCore should interact with. */ public ViewGroup getContainerView() { return mContainerView; } /** * Specifies how much smaller the WebKit layout size should be relative to the size of this * view. * @param offsetXPix The X amount in pixels to shrink the viewport by. * @param offsetYPix The Y amount in pixels to shrink the viewport by. */ public void setViewportSizeOffset(int offsetXPix, int offsetYPix) { if (offsetXPix != mViewportSizeOffsetWidthPix || offsetYPix != mViewportSizeOffsetHeightPix) { mViewportSizeOffsetWidthPix = offsetXPix; mViewportSizeOffsetHeightPix = offsetYPix; if (mNativeContentViewCore != 0) nativeWasResized(mNativeContentViewCore); } } /** * Returns a delegate that can be used to add and remove views from the ContainerView. * * NOTE: Use with care, as not all ContentViewCore users setup their ContainerView in the same * way. In particular, the Android WebView has limitations on what implementation details can * be provided via a child view, as they are visible in the API and could introduce * compatibility breaks with existing applications. If in doubt, contact the * android_webview/OWNERS * * @return A ViewAndroidDelegate that can be used to add and remove views. */ @VisibleForTesting public ViewAndroidDelegate getViewAndroidDelegate() { return new ViewAndroidDelegate() { @Override public View acquireAnchorView() { View anchorView = new View(getContext()); mContainerView.addView(anchorView); return anchorView; } @Override @SuppressWarnings("deprecation") // AbsoluteLayout.LayoutParams public void setAnchorViewPosition( View view, float x, float y, float width, float height) { assert(view.getParent() == mContainerView); float scale = (float) DeviceDisplayInfo.create(getContext()).getDIPScale(); // The anchor view should not go outside the bounds of the ContainerView. int leftMargin = Math.round(x * scale); int topMargin = Math.round(mRenderCoordinates.getContentOffsetYPix() + y * scale); // ContentViewCore currently only supports these two container view types. if (mContainerView instanceof FrameLayout) { int scaledWidth = Math.round(width * scale); if (scaledWidth + leftMargin > mContainerView.getWidth()) { scaledWidth = mContainerView.getWidth() - leftMargin; } FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( scaledWidth, Math.round(height * scale)); lp.leftMargin = leftMargin; lp.topMargin = topMargin; view.setLayoutParams(lp); } else if (mContainerView instanceof AbsoluteLayout) { // This fixes the offset due to a difference in // scrolling model of WebView vs. Chrome. // TODO(sgurun) fix this to use mContainerView.getScroll[X/Y]() // as it naturally accounts for scroll differences between // these models. leftMargin += mRenderCoordinates.getScrollXPixInt(); topMargin += mRenderCoordinates.getScrollYPixInt(); android.widget.AbsoluteLayout.LayoutParams lp = new android.widget.AbsoluteLayout.LayoutParams((int)width, (int)(height * scale), leftMargin, topMargin); view.setLayoutParams(lp); } else { Log.e(TAG, "Unknown layout " + mContainerView.getClass().getName()); } } @Override public void releaseAnchorView(View anchorView) { mContainerView.removeView(anchorView); } }; } @VisibleForTesting public ImeAdapter getImeAdapterForTest() { return mImeAdapter; } @VisibleForTesting public void setAdapterInputConnectionFactory(AdapterInputConnectionFactory factory) { mAdapterInputConnectionFactory = factory; } @VisibleForTesting public AdapterInputConnection getInputConnectionForTest() { return mInputConnection; } private ImeAdapter createImeAdapter(Context context) { return new ImeAdapter(new InputMethodManagerWrapper(context), new ImeAdapter.ImeAdapterDelegate() { @Override public void onImeEvent(boolean isFinish) { getContentViewClient().onImeEvent(); if (!isFinish) { hideHandles(); undoScrollFocusedEditableNodeIntoViewIfNeeded(false); } } @Override public void onSetFieldValue() { scrollFocusedEditableNodeIntoView(); } @Override public void onDismissInput() { getContentViewClient().onImeStateChangeRequested(false); } @Override public View getAttachedView() { return mContainerView; } @Override public ResultReceiver getNewShowKeyboardReceiver() { return new ResultReceiver(new Handler()) { @Override public void onReceiveResult(int resultCode, Bundle resultData) { getContentViewClient().onImeStateChangeRequested( resultCode == InputMethodManager.RESULT_SHOWN || resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN); 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). getContainerView().getWindowVisibleDisplayFrame( mFocusPreOSKViewportRect); } else if (resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN) { // If the OSK was already there, focus the form immediately. scrollFocusedEditableNodeIntoView(); } else { undoScrollFocusedEditableNodeIntoViewIfNeeded(false); } } }; } } ); } /** * Returns true if the given Activity has hardware acceleration enabled * in its manifest, or in its foreground window. * * TODO(husky): Remove when initialize() is refactored (see TODO there) * TODO(dtrainor) This is still used by other classes. Make sure to pull some version of this * out before removing it. */ public static boolean hasHardwareAcceleration(Activity activity) { // Has HW acceleration been enabled manually in the current window? Window window = activity.getWindow(); if (window != null) { if ((window.getAttributes().flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0) { return true; } } // Has HW acceleration been enabled in the manifest? try { ActivityInfo info = activity.getPackageManager().getActivityInfo( activity.getComponentName(), 0); if ((info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0) { return true; } } catch (PackageManager.NameNotFoundException e) { Log.e("Chrome", "getActivityInfo(self) should not fail"); } return false; } /** * Returns true if the given Context is a HW-accelerated Activity. * * TODO(husky): Remove when initialize() is refactored (see TODO there) */ private static boolean hasHardwareAcceleration(Context context) { if (context instanceof Activity) { return hasHardwareAcceleration((Activity) context); } return false; } /** * * @param containerView The view that will act as a container for all views created by this. * @param internalDispatcher Handles dispatching all hidden or super methods to the * containerView. * @param nativeWebContents A pointer to the native web contents. * @param windowAndroid An instance of the WindowAndroid. */ // Perform important post-construction set up of the ContentViewCore. // We do not require the containing view in the constructor to allow embedders to create a // ContentViewCore without having fully created its containing view. The containing view // is a vital component of the ContentViewCore, so embedders must exercise caution in what // they do with the ContentViewCore before calling initialize(). // We supply the nativeWebContents pointer here rather than in the constructor to allow us // to set the private browsing mode at a later point for the WebView implementation. // Note that the caller remains the owner of the nativeWebContents and is responsible for // deleting it after destroying the ContentViewCore. public void initialize(ViewGroup containerView, InternalAccessDelegate internalDispatcher, int nativeWebContents, WindowAndroid windowAndroid, int inputEventDeliveryMode) { // Check whether to use hardware acceleration. This is a bit hacky, and // only works if the Context is actually an Activity (as it is in the // Chrome application). // // What we're doing here is checking whether the app has *requested* // hardware acceleration by setting the appropriate flags. This does not // necessarily mean we're going to *get* hardware acceleration -- that's // up to the Android framework. // // TODO(husky): Once the native code has been updated so that the // HW acceleration flag can be set dynamically (Grace is doing this), // move this check into onAttachedToWindow(), where we can test for // HW support directly. mHardwareAccelerated = hasHardwareAcceleration(mContext); mContainerView = containerView; int windowNativePointer = windowAndroid != null ? windowAndroid.getNativePointer() : 0; int viewAndroidNativePointer = 0; if (windowNativePointer != 0) { mViewAndroid = new ViewAndroid(windowAndroid, getViewAndroidDelegate()); viewAndroidNativePointer = mViewAndroid.getNativePointer(); } mNativeContentViewCore = nativeInit(mHardwareAccelerated, nativeWebContents, viewAndroidNativePointer, windowNativePointer); mContentSettings = new ContentSettings(this, mNativeContentViewCore); initializeContainerView(internalDispatcher, inputEventDeliveryMode); mAccessibilityInjector = AccessibilityInjector.newInstance(this); String contentDescription = "Web View"; if (R.string.accessibility_content_view == 0) { Log.w(TAG, "Setting contentDescription to 'Web View' as no value was specified."); } else { contentDescription = mContext.getResources().getString( R.string.accessibility_content_view); } mContainerView.setContentDescription(contentDescription); mWebContentsObserver = new WebContentsObserverAndroid(this) { @Override public void didStartLoading(String url) { hidePopupDialog(); resetGestureDetectors(); } }; mPid = nativeGetCurrentRenderProcessId(mNativeContentViewCore); } @CalledByNative void onNativeContentViewCoreDestroyed(int nativeContentViewCore) { assert nativeContentViewCore == mNativeContentViewCore; mNativeContentViewCore = 0; } /** * Initializes the View that will contain all Views created by the ContentViewCore. * * @param internalDispatcher Handles dispatching all hidden or super methods to the * containerView. */ private void initializeContainerView(InternalAccessDelegate internalDispatcher, int inputEventDeliveryMode) { TraceEvent.begin(); mContainerViewInternals = internalDispatcher; mContainerView.setWillNotDraw(false); mContainerView.setFocusable(true); mContainerView.setFocusableInTouchMode(true); mContainerView.setClickable(true); mZoomManager = new ZoomManager(mContext, this); mContentViewGestureHandler = new ContentViewGestureHandler(mContext, this, mZoomManager, inputEventDeliveryMode); mZoomControlsDelegate = new ZoomControlsDelegate() { @Override public void invokeZoomPicker() {} @Override public void dismissZoomPicker() {} @Override public void updateZoomControls() {} }; mRenderCoordinates.reset(); initPopupZoomer(mContext); mImeAdapter = createImeAdapter(mContext); TraceEvent.end(); } private void initPopupZoomer(Context context){ mPopupZoomer = new PopupZoomer(context); mPopupZoomer.setOnVisibilityChangedListener(new PopupZoomer.OnVisibilityChangedListener() { @Override public void onPopupZoomerShown(final PopupZoomer zoomer) { mContainerView.post(new Runnable() { @Override public void run() { if (mContainerView.indexOfChild(zoomer) == -1) { mContainerView.addView(zoomer); } else { assert false : "PopupZoomer should never be shown without being hidden"; } } }); } @Override public void onPopupZoomerHidden(final PopupZoomer zoomer) { mContainerView.post(new Runnable() { @Override public void run() { if (mContainerView.indexOfChild(zoomer) != -1) { mContainerView.removeView(zoomer); mContainerView.invalidate(); } else { assert false : "PopupZoomer should never be hidden without being shown"; } } }); } }); // TODO(yongsheng): LONG_TAP is not enabled in PopupZoomer. So need to dispatch a LONG_TAP // gesture if a user completes a tap on PopupZoomer UI after a LONG_PRESS gesture. PopupZoomer.OnTapListener listener = new PopupZoomer.OnTapListener() { @Override public boolean onSingleTap(View v, MotionEvent e) { mContainerView.requestFocus(); if (mNativeContentViewCore != 0) { nativeSingleTap(mNativeContentViewCore, e.getEventTime(), e.getX(), e.getY(), true); } return true; } @Override public boolean onLongPress(View v, MotionEvent e) { if (mNativeContentViewCore != 0) { nativeLongPress(mNativeContentViewCore, e.getEventTime(), e.getX(), e.getY(), true); } return true; } }; mPopupZoomer.setOnTapListener(listener); } /** * Destroy the internal state of the ContentView. This method may only be * called after the ContentView has been removed from the view system. No * other methods may be called on this ContentView after this method has * been called. */ public void destroy() { if (mNativeContentViewCore != 0) { nativeOnJavaContentViewCoreDestroyed(mNativeContentViewCore); } resetVSyncNotification(); mVSyncProvider = null; if (mViewAndroid != null) mViewAndroid.destroy(); mNativeContentViewCore = 0; mContentSettings = null; mJavaScriptInterfaces.clear(); mRetainedJavaScriptObjects.clear(); unregisterAccessibilityContentObserver(); } private void unregisterAccessibilityContentObserver() { if (mAccessibilityScriptInjectionObserver == null) { return; } getContext().getContentResolver().unregisterContentObserver( mAccessibilityScriptInjectionObserver); mAccessibilityScriptInjectionObserver = null; } /** * Returns true initially, false after destroy() has been called. * It is illegal to call any other public method after destroy(). */ public boolean isAlive() { return mNativeContentViewCore != 0; } /** * This is only useful for passing over JNI to native code that requires ContentViewCore*. * @return native ContentViewCore pointer. */ @CalledByNative public int getNativeContentViewCore() { return mNativeContentViewCore; } /** * For internal use. Throws IllegalStateException if mNativeContentView is 0. * Use this to ensure we get a useful Java stack trace, rather than a native * crash dump, from use-after-destroy bugs in Java code. */ void checkIsAlive() throws IllegalStateException { if (!isAlive()) { throw new IllegalStateException("ContentView used after destroy() was called"); } } public void setContentViewClient(ContentViewClient client) { if (client == null) { throw new IllegalArgumentException("The client can't be null."); } mContentViewClient = client; } 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; } public int getBackgroundColor() { if (mNativeContentViewCore != 0) { return nativeGetBackgroundColor(mNativeContentViewCore); } return Color.WHITE; } @CalledByNative private void onBackgroundColorChanged(int color) { getContentViewClient().onBackgroundColorChanged(color); } /** * Load url without fixing up the url string. Consumers of ContentView are responsible for * ensuring the URL passed in is properly formatted (i.e. the scheme has been added if left * off during user input). * * @param params Parameters for this load. */ public void loadUrl(LoadUrlParams params) { if (mNativeContentViewCore == 0) return; nativeLoadUrl(mNativeContentViewCore, params.mUrl, params.mLoadUrlType, params.mTransitionType, params.mUaOverrideOption, params.getExtraHeadersString(), params.mPostData, params.mBaseUrlForDataUrl, params.mVirtualUrlForDataUrl, params.mCanLoadLocalResources); } /** * Stops loading the current web contents. */ public void stopLoading() { if (mNativeContentViewCore != 0) nativeStopLoading(mNativeContentViewCore); } /** * Get the URL of the current page. * * @return The URL of the current page. */ public String getUrl() { if (mNativeContentViewCore != 0) return nativeGetURL(mNativeContentViewCore); return null; } /** * Get the title of the current page. * * @return The title of the current page. */ public String getTitle() { if (mNativeContentViewCore != 0) return nativeGetTitle(mNativeContentViewCore); return null; } /** * Shows an interstitial page driven by the passed in delegate. * * @param url The URL being blocked by the interstitial. * @param delegate The delegate handling the interstitial. */ @VisibleForTesting public void showInterstitialPage( String url, InterstitialPageDelegateAndroid delegate) { if (mNativeContentViewCore == 0) return; nativeShowInterstitialPage(mNativeContentViewCore, url, delegate.getNative()); } /** * @return Whether the page is currently showing an interstitial, such as a bad HTTPS page. */ public boolean isShowingInterstitialPage() { return mNativeContentViewCore == 0 ? false : nativeIsShowingInterstitialPage(mNativeContentViewCore); } /** * Mark any new frames that have arrived since this function was last called as non-pending. * * @return Whether there was a pending frame from the renderer. */ public boolean consumePendingRendererFrame() { boolean hadPendingFrame = mPendingRendererFrame; mPendingRendererFrame = false; return hadPendingFrame; } /** * @return Viewport width in physical pixels as set from onSizeChanged. */ @CalledByNative public int getViewportWidthPix() { return mViewportWidthPix; } /** * @return Viewport height in physical pixels as set from onSizeChanged. */ @CalledByNative public int getViewportHeightPix() { return mViewportHeightPix; } /** * @return Width of underlying physical surface. */ @CalledByNative public int getPhysicalBackingWidthPix() { return mPhysicalBackingWidthPix; } /** * @return Height of underlying physical surface. */ @CalledByNative public int getPhysicalBackingHeightPix() { return mPhysicalBackingHeightPix; } /** * @return Amount the output surface extends past the bottom of the window viewport. */ @CalledByNative public int getOverdrawBottomHeightPix() { return mOverdrawBottomHeightPix; } /** * @return The amount to shrink the viewport relative to {@link #getViewportWidthPix()}. */ @CalledByNative public int getViewportSizeOffsetWidthPix() { return mViewportSizeOffsetWidthPix; } /** * @return The amount to shrink the viewport relative to {@link #getViewportHeightPix()}. */ @CalledByNative public int getViewportSizeOffsetHeightPix() { return mViewportSizeOffsetHeightPix; } /** * @see android.webkit.WebView#getContentHeight() */ public float getContentHeightCss() { return mRenderCoordinates.getContentHeightCss(); } /** * @see android.webkit.WebView#getContentWidth() */ public float getContentWidthCss() { return mRenderCoordinates.getContentWidthCss(); } public Bitmap getBitmap() { return getBitmap(getViewportWidthPix(), getViewportHeightPix()); } public Bitmap getBitmap(int width, int height) { if (width == 0 || height == 0 || getViewportWidthPix() == 0 || getViewportHeightPix() == 0) { return null; } Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); if (mNativeContentViewCore != 0 && nativePopulateBitmapFromCompositor(mNativeContentViewCore, b)) { // If we successfully grabbed a bitmap, check if we have to draw the Android overlay // components as well. if (mContainerView.getChildCount() > 0) { Canvas c = new Canvas(b); c.scale(width / (float) getViewportWidthPix(), height / (float) getViewportHeightPix()); mContainerView.draw(c); } return b; } return null; } /** * Generates a bitmap of the content that is performance optimized based on capture time. * *

* To have a consistent capture time across devices, we will scale down the captured bitmap * where necessary to reduce the time to generate the bitmap. * * @param width The width of the content to be captured. * @param height The height of the content to be captured. * @return A pair of the generated bitmap, and the scale that needs to be applied to return the * bitmap to it's original size (i.e. if the bitmap is scaled down 50%, this * will be 2). */ public Pair getScaledPerformanceOptimizedBitmap(int width, int height) { float scale = 1f; // On tablets, always scale down to MDPI for performance reasons. if (DeviceUtils.isTablet(getContext())) { scale = getContext().getResources().getDisplayMetrics().density; } return Pair.create( getBitmap((int) (width / scale), (int) (height / scale)), scale); } /** * @return Whether the current WebContents has a previous navigation entry. */ public boolean canGoBack() { return mNativeContentViewCore != 0 && nativeCanGoBack(mNativeContentViewCore); } /** * @return Whether the current WebContents has a navigation entry after the current one. */ public boolean canGoForward() { return mNativeContentViewCore != 0 && nativeCanGoForward(mNativeContentViewCore); } /** * @param offset The offset into the navigation history. * @return Whether we can move in history by given offset */ public boolean canGoToOffset(int offset) { return mNativeContentViewCore != 0 && nativeCanGoToOffset(mNativeContentViewCore, offset); } /** * Navigates to the specified offset from the "current entry". Does nothing if the offset is out * of bounds. * @param offset The offset into the navigation history. */ public void goToOffset(int offset) { if (mNativeContentViewCore != 0) nativeGoToOffset(mNativeContentViewCore, offset); } @Override public void goToNavigationIndex(int index) { if (mNativeContentViewCore != 0) nativeGoToNavigationIndex(mNativeContentViewCore, index); } /** * Goes to the navigation entry before the current one. */ public void goBack() { if (mNativeContentViewCore != 0) nativeGoBack(mNativeContentViewCore); } /** * Goes to the navigation entry following the current one. */ public void goForward() { if (mNativeContentViewCore != 0) nativeGoForward(mNativeContentViewCore); } /** * Reload the current page. */ public void reload() { mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); if (mNativeContentViewCore != 0) nativeReload(mNativeContentViewCore); } /** * Cancel the pending reload. */ public void cancelPendingReload() { if (mNativeContentViewCore != 0) nativeCancelPendingReload(mNativeContentViewCore); } /** * Continue the pending reload. */ public void continuePendingReload() { if (mNativeContentViewCore != 0) nativeContinuePendingReload(mNativeContentViewCore); } /** * Clears the ContentViewCore's page history in both the backwards and * forwards directions. */ public void clearHistory() { if (mNativeContentViewCore != 0) nativeClearHistory(mNativeContentViewCore); } String getSelectedText() { return mHasSelection ? mLastSelectedText : ""; } // End FrameLayout overrides. /** * @see {@link android.webkit.WebView#flingScroll(int, int)} */ public void flingScroll(int vx, int vy) { // Notes: // (1) Use large negative values for the x/y parameters so we don't accidentally scroll a // nested frame. // (2) vx and vy are inverted to match WebView behavior. mContentViewGestureHandler.fling( System.currentTimeMillis(), -Integer.MAX_VALUE, -Integer.MIN_VALUE, -vx, -vy); } /** * @see View#onTouchEvent(MotionEvent) */ public boolean onTouchEvent(MotionEvent event) { undoScrollFocusedEditableNodeIntoViewIfNeeded(false); return mContentViewGestureHandler.onTouchEvent(event); } /** * @return ContentViewGestureHandler for all MotionEvent and gesture related calls. */ ContentViewGestureHandler getContentViewGestureHandler() { return mContentViewGestureHandler; } @Override public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts) { if (mNativeContentViewCore != 0) { return nativeSendTouchEvent(mNativeContentViewCore, timeMs, action, pts); } return false; } @SuppressWarnings("unused") @CalledByNative private void hasTouchEventHandlers(boolean hasTouchHandlers) { mContentViewGestureHandler.hasTouchEventHandlers(hasTouchHandlers); } @SuppressWarnings("unused") @CalledByNative private void confirmTouchEvent(int ackResult) { mContentViewGestureHandler.confirmTouchEvent(ackResult); } @SuppressWarnings("unused") @CalledByNative private void unhandledFlingStartEvent() { if (mGestureStateListener != null) { mGestureStateListener.onUnhandledFlingStartEvent(); } } @Override public boolean sendGesture(int type, long timeMs, int x, int y, boolean lastInputEventForVSync, Bundle b) { if (offerGestureToEmbedder(type)) return false; if (mNativeContentViewCore == 0) return false; updateTextHandlesForGesture(type); updateGestureStateListener(type, b); if (lastInputEventForVSync && isVSyncNotificationEnabled()) { assert type == ContentViewGestureHandler.GESTURE_SCROLL_BY || type == ContentViewGestureHandler.GESTURE_PINCH_BY; mDidSignalVSyncUsingInputEvent = true; } switch (type) { case ContentViewGestureHandler.GESTURE_SHOW_PRESSED_STATE: nativeShowPressState(mNativeContentViewCore, timeMs, x, y); return true; case ContentViewGestureHandler.GESTURE_SHOW_PRESS_CANCEL: nativeShowPressCancel(mNativeContentViewCore, timeMs, x, y); return true; case ContentViewGestureHandler.GESTURE_DOUBLE_TAP: nativeDoubleTap(mNativeContentViewCore, timeMs, x, y); return true; case ContentViewGestureHandler.GESTURE_SINGLE_TAP_UP: nativeSingleTap(mNativeContentViewCore, timeMs, x, y, false); return true; case ContentViewGestureHandler.GESTURE_SINGLE_TAP_CONFIRMED: handleTapOrPress(timeMs, x, y, 0, b.getBoolean(ContentViewGestureHandler.SHOW_PRESS, false)); return true; case ContentViewGestureHandler.GESTURE_SINGLE_TAP_UNCONFIRMED: nativeSingleTapUnconfirmed(mNativeContentViewCore, timeMs, x, y); return true; case ContentViewGestureHandler.GESTURE_LONG_PRESS: handleTapOrPress(timeMs, x, y, IS_LONG_PRESS, false); return true; case ContentViewGestureHandler.GESTURE_LONG_TAP: handleTapOrPress(timeMs, x, y, IS_LONG_TAP, false); return true; case ContentViewGestureHandler.GESTURE_SCROLL_START: nativeScrollBegin(mNativeContentViewCore, timeMs, x, y); return true; case ContentViewGestureHandler.GESTURE_SCROLL_BY: { int dx = b.getInt(ContentViewGestureHandler.DISTANCE_X); int dy = b.getInt(ContentViewGestureHandler.DISTANCE_Y); nativeScrollBy(mNativeContentViewCore, timeMs, x, y, dx, dy, lastInputEventForVSync); return true; } case ContentViewGestureHandler.GESTURE_SCROLL_END: nativeScrollEnd(mNativeContentViewCore, timeMs); return true; case ContentViewGestureHandler.GESTURE_FLING_START: nativeFlingStart(mNativeContentViewCore, timeMs, x, y, b.getInt(ContentViewGestureHandler.VELOCITY_X, 0), b.getInt(ContentViewGestureHandler.VELOCITY_Y, 0)); return true; case ContentViewGestureHandler.GESTURE_FLING_CANCEL: nativeFlingCancel(mNativeContentViewCore, timeMs); return true; case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: nativePinchBegin(mNativeContentViewCore, timeMs, x, y); return true; case ContentViewGestureHandler.GESTURE_PINCH_BY: nativePinchBy(mNativeContentViewCore, timeMs, x, y, b.getFloat(ContentViewGestureHandler.DELTA, 0), lastInputEventForVSync); return true; case ContentViewGestureHandler.GESTURE_PINCH_END: nativePinchEnd(mNativeContentViewCore, timeMs); return true; default: return false; } } public void setGestureStateListener(GestureStateListener pinchGestureStateListener) { mGestureStateListener = pinchGestureStateListener; } void updateGestureStateListener(int gestureType, Bundle b) { if (mGestureStateListener == null) return; switch (gestureType) { case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: mGestureStateListener.onPinchGestureStart(); break; case ContentViewGestureHandler.GESTURE_PINCH_END: mGestureStateListener.onPinchGestureEnd(); break; case ContentViewGestureHandler.GESTURE_FLING_START: mGestureStateListener.onFlingStartGesture( b.getInt(ContentViewGestureHandler.VELOCITY_X, 0), b.getInt(ContentViewGestureHandler.VELOCITY_Y, 0)); break; case ContentViewGestureHandler.GESTURE_FLING_CANCEL: mGestureStateListener.onFlingCancelGesture(); break; default: break; } } public interface JavaScriptCallback { void handleJavaScriptResult(String jsonResult); } /** * Injects the passed Javascript code in the current page and evaluates it. * If a result is required, pass in a callback. * Used in automation tests. * * @param script The Javascript to execute. * @param callback The callback to be fired off when a result is ready. The script's * result will be json encoded and passed as the parameter, and the call * will be made on the main thread. * If no result is required, pass null. * @throws IllegalStateException If the ContentView has been destroyed. */ public void evaluateJavaScript( String script, JavaScriptCallback callback) throws IllegalStateException { checkIsAlive(); nativeEvaluateJavaScript(mNativeContentViewCore, script, callback); } /** * This method should be called when the containing activity is paused. */ public void onActivityPause() { TraceEvent.begin(); hidePopupDialog(); nativeOnHide(mNativeContentViewCore); TraceEvent.end(); } /** * This method should be called when the containing activity is resumed. */ public void onActivityResume() { nativeOnShow(mNativeContentViewCore); setAccessibilityState(mAccessibilityManager.isEnabled()); } /** * To be called when the ContentView is shown. */ public void onShow() { nativeOnShow(mNativeContentViewCore); setAccessibilityState(mAccessibilityManager.isEnabled()); } /** * To be called when the ContentView is hidden. */ public void onHide() { hidePopupDialog(); setInjectedAccessibility(false); nativeOnHide(mNativeContentViewCore); } /** * Return the ContentSettings object used to retrieve the settings for this * ContentViewCore. For modifications, ChromeNativePreferences is to be used. * @return A ContentSettings object that can be used to retrieve this * ContentViewCore's settings. */ public ContentSettings getContentSettings() { return mContentSettings; } @Override public boolean didUIStealScroll(float x, float y) { return getContentViewClient().shouldOverrideScroll( x, y, computeHorizontalScrollOffset(), computeVerticalScrollOffset()); } @Override public boolean hasFixedPageScale() { return mRenderCoordinates.hasFixedPageScale(); } private void hidePopupDialog() { SelectPopupDialog.hide(this); hideHandles(); hideSelectActionBar(); } void hideSelectActionBar() { if (mActionMode != null) { mActionMode.finish(); mActionMode = null; } } private void resetGestureDetectors() { mContentViewGestureHandler.resetGestureHandlers(); } /** * @see View#onAttachedToWindow() */ @SuppressWarnings("javadoc") public void onAttachedToWindow() { mAttachedToWindow = true; if (mNativeContentViewCore != 0) { assert mPid == nativeGetCurrentRenderProcessId(mNativeContentViewCore); ChildProcessLauncher.bindAsHighPriority(mPid); // Normally the initial binding is removed in onRenderProcessSwap(), but it is possible // to construct WebContents and spawn the renderer before passing it to ContentViewCore. // In this case there will be no onRenderProcessSwap() call and the initial binding will // be removed here. ChildProcessLauncher.removeInitialBinding(mPid); } setAccessibilityState(mAccessibilityManager.isEnabled()); } /** * @see View#onDetachedFromWindow() */ @SuppressWarnings("javadoc") public void onDetachedFromWindow() { mAttachedToWindow = false; if (mNativeContentViewCore != 0) { assert mPid == nativeGetCurrentRenderProcessId(mNativeContentViewCore); ChildProcessLauncher.unbindAsHighPriority(mPid); } setInjectedAccessibility(false); hidePopupDialog(); mZoomControlsDelegate.dismissZoomPicker(); unregisterAccessibilityContentObserver(); } /** * @see View#onVisibilityChanged(android.view.View, int) */ public void onVisibilityChanged(View changedView, int visibility) { if (visibility != View.VISIBLE) { mZoomControlsDelegate.dismissZoomPicker(); } } /** * @see View#onCreateInputConnection(EditorInfo) */ public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (!mImeAdapter.hasTextInputType()) { // Although onCheckIsTextEditor will return false in this case, the EditorInfo // is still used by the InputMethodService. Need to make sure the IME doesn't // enter fullscreen mode. outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; } mInputConnection = mAdapterInputConnectionFactory.get(mContainerView, mImeAdapter, outAttrs); return mInputConnection; } public Editable getEditableForTest() { return mInputConnection.getEditable(); } /** * @see View#onCheckIsTextEditor() */ public boolean onCheckIsTextEditor() { return mImeAdapter.hasTextInputType(); } /** * @see View#onConfigurationChanged(Configuration) */ @SuppressWarnings("javadoc") public void onConfigurationChanged(Configuration newConfig) { TraceEvent.begin(); if (newConfig.keyboard != Configuration.KEYBOARD_NOKEYS) { mImeAdapter.attach(nativeGetNativeImeAdapter(mNativeContentViewCore), ImeAdapter.getTextInputTypeNone(), AdapterInputConnection.INVALID_SELECTION, AdapterInputConnection.INVALID_SELECTION); InputMethodManager manager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); manager.restartInput(mContainerView); } mContainerViewInternals.super_onConfigurationChanged(newConfig); mNeedUpdateOrientationChanged = true; TraceEvent.end(); } /** * @see View#onSizeChanged(int, int, int, int) */ @SuppressWarnings("javadoc") public void onSizeChanged(int wPix, int hPix, int owPix, int ohPix) { if (getViewportWidthPix() == wPix && getViewportHeightPix() == hPix) return; mViewportWidthPix = wPix; mViewportHeightPix = hPix; if (mNativeContentViewCore != 0) { nativeWasResized(mNativeContentViewCore); } updateAfterSizeChanged(); } /** * Called when the underlying surface the compositor draws to changes size. * This may be larger than the viewport size. */ public void onPhysicalBackingSizeChanged(int wPix, int hPix) { if (mPhysicalBackingWidthPix == wPix && mPhysicalBackingHeightPix == hPix) return; mPhysicalBackingWidthPix = wPix; mPhysicalBackingHeightPix = hPix; if (mNativeContentViewCore != 0) { nativeWasResized(mNativeContentViewCore); } } /** * Called when the amount the surface is overdrawing off the bottom has changed. * @param overdrawHeightPix The overdraw height. */ public void onOverdrawBottomHeightChanged(int overdrawHeightPix) { if (mOverdrawBottomHeightPix == overdrawHeightPix) return; mOverdrawBottomHeightPix = overdrawHeightPix; if (mNativeContentViewCore != 0) { nativeWasResized(mNativeContentViewCore); } } private void updateAfterSizeChanged() { mPopupZoomer.hide(false); // Execute a delayed form focus operation because the OSK was brought // up earlier. if (!mFocusPreOSKViewportRect.isEmpty()) { Rect rect = new Rect(); getContainerView().getWindowVisibleDisplayFrame(rect); if (!rect.equals(mFocusPreOSKViewportRect)) { scrollFocusedEditableNodeIntoView(); mFocusPreOSKViewportRect.setEmpty(); } } else if (mUnfocusOnNextSizeChanged) { undoScrollFocusedEditableNodeIntoViewIfNeeded(true); mUnfocusOnNextSizeChanged = false; } if (mNeedUpdateOrientationChanged) { sendOrientationChangeEvent(); mNeedUpdateOrientationChanged = false; } } private void scrollFocusedEditableNodeIntoView() { if (mNativeContentViewCore != 0) { Runnable scrollTask = new Runnable() { @Override public void run() { if (mNativeContentViewCore != 0) { nativeScrollFocusedEditableNodeIntoView(mNativeContentViewCore); } } }; scrollTask.run(); // The native side keeps track of whether the zoom and scroll actually occurred. It is // more efficient to do it this way and sometimes fire an unnecessary message rather // than synchronize with the renderer and always have an additional message. mScrolledAndZoomedFocusedEditableNode = true; } } private void undoScrollFocusedEditableNodeIntoViewIfNeeded(boolean backButtonPressed) { // The only call to this function that matters is the first call after the // scrollFocusedEditableNodeIntoView function call. // If the first call to this function is a result of a back button press we want to undo the // preceding scroll. If the call is a result of some other action we don't want to perform // an undo. // All subsequent calls are ignored since only the scroll function sets // mScrolledAndZoomedFocusedEditableNode to true. if (mScrolledAndZoomedFocusedEditableNode && backButtonPressed && mNativeContentViewCore != 0) { Runnable scrollTask = new Runnable() { @Override public void run() { if (mNativeContentViewCore != 0) { nativeUndoScrollFocusedEditableNodeIntoView(mNativeContentViewCore); } } }; scrollTask.run(); } mScrolledAndZoomedFocusedEditableNode = false; } public void onFocusChanged(boolean gainFocus) { if (!gainFocus) getContentViewClient().onImeStateChangeRequested(false); if (mNativeContentViewCore != 0) nativeSetFocus(mNativeContentViewCore, gainFocus); } /** * @see View#onKeyUp(int, KeyEvent) */ public boolean onKeyUp(int keyCode, KeyEvent event) { if (mPopupZoomer.isShowing() && keyCode == KeyEvent.KEYCODE_BACK) { mPopupZoomer.hide(true); return true; } return mContainerViewInternals.super_onKeyUp(keyCode, event); } /** * @see View#dispatchKeyEventPreIme(KeyEvent) */ public boolean dispatchKeyEventPreIme(KeyEvent event) { try { TraceEvent.begin(); if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && mImeAdapter.isActive()) { mUnfocusOnNextSizeChanged = true; } else { undoScrollFocusedEditableNodeIntoViewIfNeeded(false); } return mContainerViewInternals.super_dispatchKeyEventPreIme(event); } finally { TraceEvent.end(); } } /** * @see View#dispatchKeyEvent(KeyEvent) */ public boolean dispatchKeyEvent(KeyEvent event) { if (getContentViewClient().shouldOverrideKeyEvent(event)) { return mContainerViewInternals.super_dispatchKeyEvent(event); } if (mImeAdapter.dispatchKeyEvent(event)) return true; return mContainerViewInternals.super_dispatchKeyEvent(event); } /** * @see View#onHoverEvent(MotionEvent) * Mouse move events are sent on hover enter, hover move and hover exit. * They are sent on hover exit because sometimes it acts as both a hover * move and hover exit. */ public boolean onHoverEvent(MotionEvent event) { TraceEvent.begin("onHoverEvent"); mContainerView.removeCallbacks(mFakeMouseMoveRunnable); if (mBrowserAccessibilityManager != null) { return mBrowserAccessibilityManager.onHoverEvent(event); } if (mNativeContentViewCore != 0) { nativeSendMouseMoveEvent(mNativeContentViewCore, event.getEventTime(), event.getX(), event.getY()); } TraceEvent.end("onHoverEvent"); return true; } /** * @see View#onGenericMotionEvent(MotionEvent) */ public boolean onGenericMotionEvent(MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { switch (event.getAction()) { case MotionEvent.ACTION_SCROLL: nativeSendMouseWheelEvent(mNativeContentViewCore, event.getEventTime(), event.getX(), event.getY(), event.getAxisValue(MotionEvent.AXIS_VSCROLL)); mContainerView.removeCallbacks(mFakeMouseMoveRunnable); // Send a delayed onMouseMove event so that we end // up hovering over the right position after the scroll. final MotionEvent eventFakeMouseMove = MotionEvent.obtain(event); mFakeMouseMoveRunnable = new Runnable() { @Override public void run() { onHoverEvent(eventFakeMouseMove); } }; mContainerView.postDelayed(mFakeMouseMoveRunnable, 250); return true; } } return mContainerViewInternals.super_onGenericMotionEvent(event); } /** * @see View#scrollBy(int, int) * Currently the ContentView scrolling happens in the native side. In * the Java view system, it is always pinned at (0, 0). scrollBy() and scrollTo() * are overridden, so that View's mScrollX and mScrollY will be unchanged at * (0, 0). This is critical for drawing ContentView correctly. */ public void scrollBy(int xPix, int yPix) { if (mNativeContentViewCore != 0) { nativeScrollBy(mNativeContentViewCore, System.currentTimeMillis(), 0, 0, xPix, yPix, false); } } /** * @see View#scrollTo(int, int) */ public void scrollTo(int xPix, int yPix) { if (mNativeContentViewCore == 0) return; final float xCurrentPix = mRenderCoordinates.getScrollXPix(); final float yCurrentPix = mRenderCoordinates.getScrollYPix(); final float dxPix = xPix - xCurrentPix; final float dyPix = yPix - yCurrentPix; if (dxPix != 0 || dyPix != 0) { long time = System.currentTimeMillis(); nativeScrollBegin(mNativeContentViewCore, time, xCurrentPix, yCurrentPix); nativeScrollBy(mNativeContentViewCore, time, xCurrentPix, yCurrentPix, dxPix, dyPix, false); nativeScrollEnd(mNativeContentViewCore, time); } } // NOTE: this can go away once ContentView.getScrollX() reports correct values. // see: b/6029133 public int getNativeScrollXForTest() { return mRenderCoordinates.getScrollXPixInt(); } // NOTE: this can go away once ContentView.getScrollY() reports correct values. // see: b/6029133 public int getNativeScrollYForTest() { return mRenderCoordinates.getScrollYPixInt(); } /** * @see View#computeHorizontalScrollExtent() */ @SuppressWarnings("javadoc") public int computeHorizontalScrollExtent() { return mRenderCoordinates.getLastFrameViewportWidthPixInt(); } /** * @see View#computeHorizontalScrollOffset() */ @SuppressWarnings("javadoc") public int computeHorizontalScrollOffset() { return mRenderCoordinates.getScrollXPixInt(); } /** * @see View#computeHorizontalScrollRange() */ @SuppressWarnings("javadoc") public int computeHorizontalScrollRange() { return mRenderCoordinates.getContentWidthPixInt(); } /** * @see View#computeVerticalScrollExtent() */ @SuppressWarnings("javadoc") public int computeVerticalScrollExtent() { return mRenderCoordinates.getLastFrameViewportHeightPixInt(); } /** * @see View#computeVerticalScrollOffset() */ @SuppressWarnings("javadoc") public int computeVerticalScrollOffset() { return mRenderCoordinates.getScrollYPixInt(); } /** * @see View#computeVerticalScrollRange() */ @SuppressWarnings("javadoc") public int computeVerticalScrollRange() { return mRenderCoordinates.getContentHeightPixInt(); } // End FrameLayout overrides. /** * @see View#awakenScrollBars(int, boolean) */ @SuppressWarnings("javadoc") public boolean awakenScrollBars(int startDelay, boolean invalidate) { // For the default implementation of ContentView which draws the scrollBars on the native // side, calling this function may get us into a bad state where we keep drawing the // scrollBars, so disable it by always returning false. if (mContainerView.getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { return false; } else { return mContainerViewInternals.super_awakenScrollBars(startDelay, invalidate); } } @SuppressWarnings("unused") @CalledByNative private void onTabCrash() { assert mPid != 0; getContentViewClient().onRendererCrash(ChildProcessLauncher.isOomProtected(mPid)); mPid = 0; } private void handleTapOrPress( long timeMs, float xPix, float yPix, int isLongPressOrTap, boolean showPress) { if (!mContainerView.isFocused()) mContainerView.requestFocus(); if (!mPopupZoomer.isShowing()) mPopupZoomer.setLastTouch(xPix, yPix); if (isLongPressOrTap == IS_LONG_PRESS) { getInsertionHandleController().allowAutomaticShowing(); getSelectionHandleController().allowAutomaticShowing(); if (mNativeContentViewCore != 0) { nativeLongPress(mNativeContentViewCore, timeMs, xPix, yPix, false); } } else if (isLongPressOrTap == IS_LONG_TAP) { getInsertionHandleController().allowAutomaticShowing(); getSelectionHandleController().allowAutomaticShowing(); if (mNativeContentViewCore != 0) { nativeLongTap(mNativeContentViewCore, timeMs, xPix, yPix, false); } } else { if (!showPress && mNativeContentViewCore != 0) { nativeShowPressState(mNativeContentViewCore, timeMs, xPix, yPix); } if (mSelectionEditable) getInsertionHandleController().allowAutomaticShowing(); if (mNativeContentViewCore != 0) { nativeSingleTap(mNativeContentViewCore, timeMs, xPix, yPix, false); } } } public void setZoomControlsDelegate(ZoomControlsDelegate zoomControlsDelegate) { mZoomControlsDelegate = zoomControlsDelegate; } public void updateMultiTouchZoomSupport(boolean supportsMultiTouchZoom) { mZoomManager.updateMultiTouchSupport(supportsMultiTouchZoom); } public void selectPopupMenuItems(int[] indices) { if (mNativeContentViewCore != 0) { nativeSelectPopupMenuItems(mNativeContentViewCore, indices); } } /** * Get the screen orientation from the OS and push it to WebKit. * * TODO(husky): Add a hook for mock orientations. * * TODO(husky): Currently each new tab starts with an orientation of 0 until you actually * rotate the device. This is wrong if you actually started in landscape mode. To fix this, we * need to push the correct orientation, but only after WebKit's Frame object has been fully * initialized. Need to find a good time to do that. onPageFinished() would probably work but * it isn't implemented yet. */ private void sendOrientationChangeEvent() { if (mNativeContentViewCore == 0) return; WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); switch (windowManager.getDefaultDisplay().getRotation()) { case Surface.ROTATION_90: nativeSendOrientationChangeEvent(mNativeContentViewCore, 90); break; case Surface.ROTATION_180: nativeSendOrientationChangeEvent(mNativeContentViewCore, 180); break; case Surface.ROTATION_270: nativeSendOrientationChangeEvent(mNativeContentViewCore, -90); break; case Surface.ROTATION_0: nativeSendOrientationChangeEvent(mNativeContentViewCore, 0); break; default: Log.w(TAG, "Unknown rotation!"); break; } } /** * Register the delegate to be used when content can not be handled by * the rendering engine, and should be downloaded instead. This will replace * the current delegate, if any. * @param delegate An implementation of ContentViewDownloadDelegate. */ public void setDownloadDelegate(ContentViewDownloadDelegate delegate) { mDownloadDelegate = delegate; } // Called by DownloadController. ContentViewDownloadDelegate getDownloadDelegate() { return mDownloadDelegate; } private SelectionHandleController getSelectionHandleController() { if (mSelectionHandleController == null) { mSelectionHandleController = new SelectionHandleController(getContainerView()) { @Override public void selectBetweenCoordinates(int x1, int y1, int x2, int y2) { if (mNativeContentViewCore != 0 && !(x1 == x2 && y1 == y2)) { nativeSelectBetweenCoordinates(mNativeContentViewCore, x1, y1 - mRenderCoordinates.getContentOffsetYPix(), x2, y2 - mRenderCoordinates.getContentOffsetYPix()); } } @Override public void showHandles(int startDir, int endDir) { super.showHandles(startDir, endDir); showSelectActionBar(); } }; mSelectionHandleController.hideAndDisallowAutomaticShowing(); } return mSelectionHandleController; } private InsertionHandleController getInsertionHandleController() { if (mInsertionHandleController == null) { mInsertionHandleController = new InsertionHandleController(getContainerView()) { private static final int AVERAGE_LINE_HEIGHT = 14; @Override public void setCursorPosition(int x, int y) { if (mNativeContentViewCore != 0) { nativeMoveCaret(mNativeContentViewCore, x, y - mRenderCoordinates.getContentOffsetYPix()); } } @Override public void paste() { mImeAdapter.paste(); hideHandles(); } @Override public int getLineHeight() { return (int) Math.ceil( mRenderCoordinates.fromLocalCssToPix(AVERAGE_LINE_HEIGHT)); } @Override public void showHandle() { super.showHandle(); } }; mInsertionHandleController.hideAndDisallowAutomaticShowing(); } return mInsertionHandleController; } public InsertionHandleController getInsertionHandleControllerForTest() { return mInsertionHandleController; } private void updateHandleScreenPositions() { if (isSelectionHandleShowing()) { mSelectionHandleController.setStartHandlePosition( mStartHandlePoint.getXPix(), mStartHandlePoint.getYPix()); mSelectionHandleController.setEndHandlePosition( mEndHandlePoint.getXPix(), mEndHandlePoint.getYPix()); } if (isInsertionHandleShowing()) { mInsertionHandleController.setHandlePosition( mInsertionHandlePoint.getXPix(), mInsertionHandlePoint.getYPix()); } } private void hideHandles() { if (mSelectionHandleController != null) { mSelectionHandleController.hideAndDisallowAutomaticShowing(); } if (mInsertionHandleController != null) { mInsertionHandleController.hideAndDisallowAutomaticShowing(); } } private void showSelectActionBar() { if (mActionMode != null) { mActionMode.invalidate(); return; } // Start a new action mode with a SelectActionModeCallback. SelectActionModeCallback.ActionHandler actionHandler = new SelectActionModeCallback.ActionHandler() { @Override public boolean selectAll() { return mImeAdapter.selectAll(); } @Override public boolean cut() { return mImeAdapter.cut(); } @Override public boolean copy() { return mImeAdapter.copy(); } @Override public boolean paste() { return mImeAdapter.paste(); } @Override public boolean isSelectionEditable() { return mSelectionEditable; } @Override public String getSelectedText() { return ContentViewCore.this.getSelectedText(); } @Override public void onDestroyActionMode() { mActionMode = null; if (mUnselectAllOnActionModeDismiss) mImeAdapter.unselect(); getContentViewClient().onContextualActionBarHidden(); } }; mActionMode = null; // On ICS, startActionMode throws an NPE when getParent() is null. if (mContainerView.getParent() != null) { mActionMode = mContainerView.startActionMode( getContentViewClient().getSelectActionModeCallback(getContext(), actionHandler, nativeIsIncognito(mNativeContentViewCore))); } mUnselectAllOnActionModeDismiss = true; if (mActionMode == null) { // There is no ActionMode, so remove the selection. mImeAdapter.unselect(); } else { getContentViewClient().onContextualActionBarShown(); } } public boolean getUseDesktopUserAgent() { if (mNativeContentViewCore != 0) { return nativeGetUseDesktopUserAgent(mNativeContentViewCore); } return false; } /** * Set whether or not we're using a desktop user agent for the currently loaded page. * @param override If true, use a desktop user agent. Use a mobile one otherwise. * @param reloadOnChange Reload the page if the UA has changed. */ public void setUseDesktopUserAgent(boolean override, boolean reloadOnChange) { if (mNativeContentViewCore != 0) { nativeSetUseDesktopUserAgent(mNativeContentViewCore, override, reloadOnChange); } } public void clearSslPreferences() { nativeClearSslPreferences(mNativeContentViewCore); } /** * @return Whether the native ContentView has crashed. */ public boolean isCrashed() { if (mNativeContentViewCore == 0) return false; return nativeCrashed(mNativeContentViewCore); } private boolean isSelectionHandleShowing() { return mSelectionHandleController != null && mSelectionHandleController.isShowing(); } private boolean isInsertionHandleShowing() { return mInsertionHandleController != null && mInsertionHandleController.isShowing(); } private void updateTextHandlesForGesture(int type) { switch(type) { case ContentViewGestureHandler.GESTURE_DOUBLE_TAP: case ContentViewGestureHandler.GESTURE_SCROLL_START: case ContentViewGestureHandler.GESTURE_FLING_START: case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: temporarilyHideTextHandles(); break; default: break; } } // Makes the insertion/selection handles invisible. They will fade back in shortly after the // last call to scheduleTextHandleFadeIn (or temporarilyHideTextHandles). private void temporarilyHideTextHandles() { if (isSelectionHandleShowing()) { mSelectionHandleController.setHandleVisibility(HandleView.INVISIBLE); } if (isInsertionHandleShowing()) { mInsertionHandleController.setHandleVisibility(HandleView.INVISIBLE); } scheduleTextHandleFadeIn(); } private boolean allowTextHandleFadeIn() { if (mContentViewGestureHandler.isNativeScrolling() || mContentViewGestureHandler.isNativePinching()) { return false; } if (mPopupZoomer.isShowing()) return false; return true; } // Cancels any pending fade in and schedules a new one. private void scheduleTextHandleFadeIn() { if (!isInsertionHandleShowing() && !isSelectionHandleShowing()) return; if (mDeferredHandleFadeInRunnable == null) { mDeferredHandleFadeInRunnable = new Runnable() { @Override public void run() { if (!allowTextHandleFadeIn()) { // Delay fade in until it is allowed. scheduleTextHandleFadeIn(); } else { if (isSelectionHandleShowing()) { mSelectionHandleController.beginHandleFadeIn(); } if (isInsertionHandleShowing()) { mInsertionHandleController.beginHandleFadeIn(); } } } }; } mContainerView.removeCallbacks(mDeferredHandleFadeInRunnable); mContainerView.postDelayed(mDeferredHandleFadeInRunnable, TEXT_HANDLE_FADE_IN_DELAY); } /** * Shows the IME if the focused widget could accept text input. */ public void showImeIfNeeded() { if (mNativeContentViewCore != 0) nativeShowImeIfNeeded(mNativeContentViewCore); } public void setUpdateFrameInfoListener(UpdateFrameInfoListener updateFrameInfoListener) { mUpdateFrameInfoListener = updateFrameInfoListener; } @SuppressWarnings("unused") @CalledByNative private void updateFrameInfo( float scrollOffsetX, float scrollOffsetY, float pageScaleFactor, float minPageScaleFactor, float maxPageScaleFactor, float contentWidth, float contentHeight, float viewportWidth, float viewportHeight, float controlsOffsetYCss, float contentOffsetYCss, float overdrawBottomHeightCss) { TraceEvent.instant("ContentViewCore:updateFrameInfo"); // Adjust contentWidth/Height to be always at least as big as // the actual viewport (as set by onSizeChanged). contentWidth = Math.max(contentWidth, mRenderCoordinates.fromPixToLocalCss(mViewportWidthPix)); contentHeight = Math.max(contentHeight, mRenderCoordinates.fromPixToLocalCss(mViewportHeightPix)); final float contentOffsetYPix = mRenderCoordinates.fromDipToPix(contentOffsetYCss); final boolean contentSizeChanged = contentWidth != mRenderCoordinates.getContentWidthCss() || contentHeight != mRenderCoordinates.getContentHeightCss(); final boolean scaleLimitsChanged = minPageScaleFactor != mRenderCoordinates.getMinPageScaleFactor() || maxPageScaleFactor != mRenderCoordinates.getMaxPageScaleFactor(); final boolean pageScaleChanged = pageScaleFactor != mRenderCoordinates.getPageScaleFactor(); final boolean scrollChanged = pageScaleChanged || scrollOffsetX != mRenderCoordinates.getScrollX() || scrollOffsetY != mRenderCoordinates.getScrollY(); final boolean contentOffsetChanged = contentOffsetYPix != mRenderCoordinates.getContentOffsetYPix(); final boolean needHidePopupZoomer = contentSizeChanged || scrollChanged; final boolean needUpdateZoomControls = scaleLimitsChanged || scrollChanged; final boolean needTemporarilyHideHandles = scrollChanged; if (needHidePopupZoomer) mPopupZoomer.hide(true); if (pageScaleChanged) { // This function should be called back from native as soon // as the scroll is applied to the backbuffer. We should only // update mNativeScrollX/Y here for consistency. getContentViewClient().onScaleChanged( mRenderCoordinates.getPageScaleFactor(), pageScaleFactor); } mRenderCoordinates.updateFrameInfo( scrollOffsetX, scrollOffsetY, contentWidth, contentHeight, viewportWidth, viewportHeight, pageScaleFactor, minPageScaleFactor, maxPageScaleFactor, contentOffsetYPix); if ((contentSizeChanged || pageScaleChanged) && mUpdateFrameInfoListener != null) { mUpdateFrameInfoListener.onFrameInfoUpdated( contentWidth, contentHeight, pageScaleFactor); } if (needTemporarilyHideHandles) temporarilyHideTextHandles(); if (needUpdateZoomControls) mZoomControlsDelegate.updateZoomControls(); if (contentOffsetChanged) updateHandleScreenPositions(); // Update offsets for fullscreen. final float deviceScale = mRenderCoordinates.getDeviceScaleFactor(); final float controlsOffsetPix = controlsOffsetYCss * deviceScale; final float overdrawBottomHeightPix = overdrawBottomHeightCss * deviceScale; getContentViewClient().onOffsetsForFullscreenChanged( controlsOffsetPix, contentOffsetYPix, overdrawBottomHeightPix); mPendingRendererFrame = true; if (mBrowserAccessibilityManager != null) { mBrowserAccessibilityManager.notifyFrameInfoInitialized(); } // Update geometry for external video surface. getContentViewClient().onGeometryChanged(-1, null); } @SuppressWarnings("unused") @CalledByNative private void updateImeAdapter(int nativeImeAdapterAndroid, int textInputType, String text, int selectionStart, int selectionEnd, int compositionStart, int compositionEnd, boolean showImeIfNeeded) { TraceEvent.begin(); mSelectionEditable = (textInputType != ImeAdapter.getTextInputTypeNone()); if (mActionMode != null) mActionMode.invalidate(); mImeAdapter.attachAndShowIfNeeded(nativeImeAdapterAndroid, textInputType, selectionStart, selectionEnd, showImeIfNeeded); if (mInputConnection != null) { mInputConnection.setEditableText(text, selectionStart, selectionEnd, compositionStart, compositionEnd); } TraceEvent.end(); } @SuppressWarnings("unused") @CalledByNative private void processImeBatchStateAck(boolean isBegin) { if (mInputConnection == null) return; mInputConnection.setIgnoreTextInputStateUpdates(isBegin); } @SuppressWarnings("unused") @CalledByNative private void setTitle(String title) { getContentViewClient().onUpdateTitle(title); } /** * Called (from native) when the