blob: d27ed85463e7e2f91655c7579ad136d5731d3820 [file] [log] [blame]
// Copyright 2015 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.chrome.browser.compositor.bottombar;
import android.text.TextUtils;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeVersionInfo;
import org.chromium.chrome.browser.WebContentsFactory;
import org.chromium.chrome.browser.content.ContentUtils;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager;
import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.embedder_support.delegate.WebContentsDelegateAndroid;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.components.navigation_interception.InterceptNavigationDelegate;
import org.chromium.components.navigation_interception.NavigationParams;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.RenderCoordinates;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.ViewAndroidDelegate;
/**
* Content container for an OverlayPanel. This class is responsible for the management of the
* WebContents displayed inside of a panel and exposes a simple API relevant to actions a
* panel has.
*/
public class OverlayPanelContent {
/** The WebContents that this panel will display. */
private WebContents mWebContents;
/** The container view that this panel uses. */
private ViewGroup mContainerView;
/** The pointer to the native version of this class. */
private long mNativeOverlayPanelContentPtr;
/** Used for progress bar events. */
private final WebContentsDelegateAndroid mWebContentsDelegate;
/** The activity that this content is contained in. */
private ChromeActivity mActivity;
/** Observer used for tracking loading and navigation. */
private WebContentsObserver mWebContentsObserver;
/** The URL that was directly loaded using the {@link #loadUrl(String)} method. */
private String mLoadedUrl;
/** Whether the content has started loading a URL. */
private boolean mDidStartLoadingUrl;
/**
* Whether we should reuse any existing WebContents instead of deleting and recreating.
* See crbug.com/682953 for details.
*/
private boolean mShouldReuseWebContents;
/**
* Whether the WebContents is processing a pending navigation.
* NOTE(pedrosimonetti): This is being used to prevent redirections on the SERP to be
* interpreted as a regular navigation, which should cause the Contextual Search Panel
* to be promoted as a Tab. This was added to work around a server bug that has been fixed.
* Just checking for whether the Content has been touched is enough to determine whether a
* navigation should be promoted (assuming it was caused by the touch), as done in
* {@link ContextualSearchManager#shouldPromoteSearchNavigation()}.
* For more details, see crbug.com/441048
* TODO(pedrosimonetti): remove this from M48 or move it to Contextual Search Panel.
*/
private boolean mIsProcessingPendingNavigation;
/** Whether the content view is currently being displayed. */
private boolean mIsContentViewShowing;
/** The observer used by this object to inform implementers of different events. */
private OverlayContentDelegate mContentDelegate;
/** Used to observe progress bar events. */
private OverlayContentProgressObserver mProgressObserver;
/** If a URL is set to delayed load (load on user interaction), it will be stored here. */
private String mPendingUrl;
// http://crbug.com/522266 : An instance of InterceptNavigationDelegateImpl should be kept in
// java layer. Otherwise, the instance could be garbage-collected unexpectedly.
private InterceptNavigationDelegate mInterceptNavigationDelegate;
/** Set to {@code True} if opened for an incognito tab. */
private boolean mIsIncognito;
/** The desired size of the {@link ContentView} associated with this panel content. */
private int mContentViewWidth;
private int mContentViewHeight;
private boolean mSubtractBarHeight;
/** The height of the bar at the top of the OverlayPanel in pixels. */
private final int mBarHeightPx;
/** Sets the top offset of the overlay panel in pixel. 0 when fully expanded. */
private int mPanelTopOffsetPx;
private class OverlayViewDelegate extends ViewAndroidDelegate {
public OverlayViewDelegate(ViewGroup v) {
super(v);
}
@Override
public void setViewPosition(View view, float x, float y, float width, float height,
int leftMargin, int topMargin) {
super.setViewPosition(view, x, y, width, height, leftMargin, topMargin);
// Applies top offset depending on the overlay panel state.
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
lp.topMargin += mPanelTopOffsetPx + mBarHeightPx;
}
}
// ============================================================================================
// InterceptNavigationDelegateImpl
// ============================================================================================
// Used to intercept intent navigations.
// TODO(jeremycho): Consider creating a Tab with the Panel's WebContents.
// which would also handle functionality like long-press-to-paste.
private class InterceptNavigationDelegateImpl implements InterceptNavigationDelegate {
final ExternalNavigationHandler mExternalNavHandler;
public InterceptNavigationDelegateImpl() {
Tab tab = mActivity.getActivityTab();
mExternalNavHandler = (tab != null && tab.getWebContents() != null)
? new ExternalNavigationHandler(tab)
: null;
}
@Override
public boolean shouldIgnoreNavigation(NavigationParams navigationParams) {
// If either of the required params for the delegate are null, do not call the
// delegate and ignore the navigation.
if (mExternalNavHandler == null || navigationParams == null) return true;
// TODO(mdjones): Rather than passing the two navigation params, instead consider
// passing a boolean to make this API simpler.
return !mContentDelegate.shouldInterceptNavigation(mExternalNavHandler,
navigationParams);
}
}
// ============================================================================================
// Constructor
// ============================================================================================
/**
* @param contentDelegate An observer for events that occur on this content. If null is passed
* for this parameter, the default one will be used.
* @param progressObserver An observer for progress related events.
* @param activity The ChromeActivity that contains this object.
* @param isIncognito {@True} if opened for an incognito tab
* @param barHeight The height of the bar at the top of the OverlayPanel in dp.
*/
public OverlayPanelContent(OverlayContentDelegate contentDelegate,
OverlayContentProgressObserver progressObserver, ChromeActivity activity,
boolean isIncognito, float barHeight) {
mNativeOverlayPanelContentPtr = nativeInit();
mContentDelegate = contentDelegate;
mProgressObserver = progressObserver;
mActivity = activity;
mIsIncognito = isIncognito;
mBarHeightPx = (int) (barHeight * mActivity.getResources().getDisplayMetrics().density);
mWebContentsDelegate = new WebContentsDelegateAndroid() {
private boolean mIsFullscreen;
@Override
public void loadingStateChanged(boolean toDifferentDocument) {
boolean isLoading = mWebContents != null && mWebContents.isLoading();
if (isLoading) {
mProgressObserver.onProgressBarStarted();
} else {
mProgressObserver.onProgressBarFinished();
}
}
@Override
public void onLoadProgressChanged(int progress) {
mProgressObserver.onProgressBarUpdated(progress);
}
@Override
public void enterFullscreenModeForTab(boolean prefersNavigationBar) {
mIsFullscreen = true;
}
@Override
public void exitFullscreenModeForTab() {
mIsFullscreen = false;
}
@Override
public boolean isFullscreenForTabOrPending() {
return mIsFullscreen;
}
@Override
public int getTopControlsHeight() {
return (int) (mBarHeightPx
/ mActivity.getWindowAndroid().getDisplay().getDipScale());
}
@Override
public int getBottomControlsHeight() {
return 0;
}
@Override
public boolean controlsResizeView() {
return false;
}
};
}
// ============================================================================================
// WebContents related
// ============================================================================================
/**
* Load a URL; this will trigger creation of a new WebContents if being loaded immediately,
* otherwise one is created when the panel's content becomes visible.
* @param url The URL that should be loaded.
* @param shouldLoadImmediately If a URL should be loaded immediately or wait until visibility
* changes.
*/
public void loadUrl(String url, boolean shouldLoadImmediately) {
mPendingUrl = null;
if (!shouldLoadImmediately) {
mPendingUrl = url;
} else {
createNewWebContents();
mLoadedUrl = url;
mDidStartLoadingUrl = true;
mIsProcessingPendingNavigation = true;
mWebContents.getNavigationController().loadUrl(new LoadUrlParams(url));
}
}
/**
* Call this when a loadUrl request has failed to notify the panel that the WebContents can
* be reused. See crbug.com/682953 for details.
*/
void onLoadUrlFailed() {
mShouldReuseWebContents = true;
}
/**
* Set the desired size of the underlying {@link ContentView}. This is determined
* by the {@link OverlayPanel} before the creation of the content view.
* @param width The width of the content view.
* @param height The height of the content view.
* @param subtractBarHeight if {@code true} view height should be smaller by {@code mBarHeight}.
*/
void setContentViewSize(int width, int height, boolean subtractBarHeight) {
mContentViewWidth = width;
mContentViewHeight = height;
mSubtractBarHeight = subtractBarHeight;
}
/**
* Makes the content visible, causing it to be rendered.
*/
public void showContent() {
setVisibility(true);
}
/**
* Sets the top offset of the overlay panel that varies as the panel state changes.
* @param offset Top offset in pixel.
*/
public void setPanelTopOffset(int offset) {
mPanelTopOffsetPx = offset;
}
/**
* Create a new WebContents that will be managed by this panel.
*/
private void createNewWebContents() {
if (mWebContents != null) {
// If the WebContents has already been created, but never used,
// then there's no need to create a new one.
if (!mDidStartLoadingUrl || mShouldReuseWebContents) return;
destroyWebContents();
}
// Creates an initially hidden WebContents which gets shown when the panel is opened.
mWebContents = WebContentsFactory.createWebContents(mIsIncognito, true);
ContentView cv = ContentView.createContentView(mActivity, mWebContents);
if (mContentViewWidth != 0 || mContentViewHeight != 0) {
int width = mContentViewWidth == 0 ? ContentView.DEFAULT_MEASURE_SPEC
: MeasureSpec.makeMeasureSpec(mContentViewWidth, MeasureSpec.EXACTLY);
int height = mContentViewHeight == 0 ? ContentView.DEFAULT_MEASURE_SPEC
: MeasureSpec.makeMeasureSpec(mContentViewHeight, MeasureSpec.EXACTLY);
cv.setDesiredMeasureSpec(width, height);
}
OverlayViewDelegate delegate = new OverlayViewDelegate(cv);
mWebContents.initialize(ChromeVersionInfo.getProductVersion(), delegate, cv,
mActivity.getWindowAndroid(), WebContents.createDefaultInternalsHolder());
ContentUtils.setUserAgentOverride(mWebContents);
// Transfers the ownership of the WebContents to the native OverlayPanelContent.
nativeSetWebContents(mNativeOverlayPanelContentPtr, mWebContents, mWebContentsDelegate);
mWebContentsObserver =
new WebContentsObserver(mWebContents) {
@Override
public void didStartLoading(String url) {
mContentDelegate.onContentLoadStarted(url);
}
@Override
public void navigationEntryCommitted() {
mContentDelegate.onNavigationEntryCommitted();
}
@Override
public void didStartNavigation(String url, boolean isInMainFrame,
boolean isSameDocument, long navigationHandleProxy) {
if (isInMainFrame && !isSameDocument) {
mContentDelegate.onMainFrameLoadStarted(
url, !TextUtils.equals(url, mLoadedUrl));
}
}
@Override
public void didFinishNavigation(String url, boolean isInMainFrame,
boolean isErrorPage, boolean hasCommitted, boolean isSameDocument,
boolean isFragmentNavigation, boolean isRendererInitiated,
boolean isDownload, Integer pageTransition, int errorCode,
String errorDescription, int httpStatusCode) {
if (hasCommitted && isInMainFrame) {
mIsProcessingPendingNavigation = false;
mContentDelegate.onMainFrameNavigation(url,
!TextUtils.equals(url, mLoadedUrl),
isHttpFailureCode(httpStatusCode));
}
}
};
mContainerView = cv;
mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl();
nativeSetInterceptNavigationDelegate(
mNativeOverlayPanelContentPtr, mInterceptNavigationDelegate, mWebContents);
mContentDelegate.onContentViewCreated();
resizePanelContentView();
mActivity.getCompositorViewHolder().addView(mContainerView, 1);
}
/**
* Destroy this panel's WebContents.
*/
private void destroyWebContents() {
if (mWebContents != null) {
// Native destroy will call up to destroy the Java WebContents.
nativeDestroyWebContents(mNativeOverlayPanelContentPtr);
mWebContents = null;
if (mWebContentsObserver != null) {
mWebContentsObserver.destroy();
mWebContentsObserver = null;
}
mDidStartLoadingUrl = false;
mIsProcessingPendingNavigation = false;
mShouldReuseWebContents = false;
setVisibility(false);
// After everything has been disposed, notify the observer.
mContentDelegate.onContentViewDestroyed();
}
}
// ============================================================================================
// Utilities
// ============================================================================================
/**
* Calls updateBrowserControlsState on the WebContents.
* @param areControlsHidden Whether the browser controls are hidden for the web contents. If
* false, the web contents viewport always accounts for the controls.
* Otherwise the web contents never accounts for them.
*/
public void updateBrowserControlsState(boolean areControlsHidden) {
nativeUpdateBrowserControlsState(mNativeOverlayPanelContentPtr, areControlsHidden);
}
/**
* @return Whether a pending navigation if being processed.
*/
public boolean isProcessingPendingNavigation() {
return mIsProcessingPendingNavigation;
}
/**
* Reset the content's scroll position to (0, 0).
*/
public void resetContentViewScroll() {
if (mWebContents != null) {
mWebContents.getEventForwarder().scrollTo(0, 0);
}
}
/**
* @return The Y scroll position.
*/
public float getContentVerticalScroll() {
return mWebContents != null
? RenderCoordinates.fromWebContents(mWebContents).getScrollYPixInt()
: -1.f;
}
/**
* Sets the visibility of the Search Content View.
* @param isVisible True to make it visible.
*/
private void setVisibility(boolean isVisible) {
if (mIsContentViewShowing == isVisible) return;
mIsContentViewShowing = isVisible;
if (isVisible) {
// If the last call to loadUrl was specified to be delayed, load it now.
if (!TextUtils.isEmpty(mPendingUrl)) loadUrl(mPendingUrl, true);
// The WebContents is created with the search request, but if none was made we'll need
// one in order to display an empty panel.
if (mWebContents == null) createNewWebContents();
// NOTE(pedrosimonetti): Calling onShow() on the WebContents will cause the page
// to be rendered. This has a side effect of causing the page to be included in
// your Web History (if enabled). For this reason, onShow() should only be called
// when we know for sure the page will be seen by the user.
if (mWebContents != null) mWebContents.onShow();
mContentDelegate.onContentViewSeen();
} else {
if (mWebContents != null) mWebContents.onHide();
}
mContentDelegate.onVisibilityChanged(isVisible);
}
/**
* @return Whether the given HTTP result code represents a failure or not.
*/
private static boolean isHttpFailureCode(int httpResultCode) {
return httpResultCode <= 0 || httpResultCode >= 400;
}
/**
* @return true if the content is visible on the page.
*/
public boolean isContentShowing() {
return mIsContentViewShowing;
}
// ============================================================================================
// Methods for managing this panel's WebContents.
// ============================================================================================
/**
* Reset this object's native pointer to 0;
*/
@CalledByNative
private void clearNativePanelContentPtr() {
assert mNativeOverlayPanelContentPtr != 0;
mNativeOverlayPanelContentPtr = 0;
}
protected WebContents getWebContents() {
return mWebContents;
}
ViewGroup getContainerView() {
return mContainerView;
}
void resizePanelContentView() {
WebContents webContents = getWebContents();
if (webContents == null) return;
int viewHeight = mContentViewHeight - (mSubtractBarHeight ? mBarHeightPx : 0);
nativeOnPhysicalBackingSizeChanged(
mNativeOverlayPanelContentPtr, webContents, mContentViewWidth, viewHeight);
mWebContents.setSize(mContentViewWidth, viewHeight);
}
/**
* Remove the list history entry from this panel if it was within a certain timeframe.
* @param historyUrl The URL to remove.
* @param urlTimeMs The time the URL was navigated to.
*/
public void removeLastHistoryEntry(String historyUrl, long urlTimeMs) {
nativeRemoveLastHistoryEntry(mNativeOverlayPanelContentPtr, historyUrl, urlTimeMs);
}
/**
* Destroy the native component of this class.
*/
@VisibleForTesting
public void destroy() {
if (mWebContents != null) destroyWebContents();
// Tests will not create the native pointer, so we need to check if it's not zero
// otherwise calling nativeDestroy with zero will make Chrome crash.
if (mNativeOverlayPanelContentPtr != 0L) {
nativeDestroy(mNativeOverlayPanelContentPtr);
}
}
// Native calls.
private native long nativeInit();
private native void nativeDestroy(long nativeOverlayPanelContent);
private native void nativeRemoveLastHistoryEntry(
long nativeOverlayPanelContent, String historyUrl, long urlTimeMs);
private native void nativeOnPhysicalBackingSizeChanged(
long nativeOverlayPanelContent, WebContents webContents, int width, int height);
private native void nativeSetWebContents(long nativeOverlayPanelContent,
WebContents webContents, WebContentsDelegateAndroid delegate);
private native void nativeDestroyWebContents(long nativeOverlayPanelContent);
private native void nativeSetInterceptNavigationDelegate(long nativeOverlayPanelContent,
InterceptNavigationDelegate delegate, WebContents webContents);
private native void nativeUpdateBrowserControlsState(
long nativeOverlayPanelContent, boolean areControlsHidden);
}