// 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.ViewGroup; 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.contextualsearch.ContextualSearchManager; import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler; import org.chromium.chrome.browser.tab.Tab; import org.chromium.components.navigation_interception.InterceptNavigationDelegate; import org.chromium.components.navigation_interception.NavigationParams; import org.chromium.components.web_contents_delegate_android.WebContentsDelegateAndroid; import org.chromium.content.browser.ContentVideoViewEmbedder; import org.chromium.content.browser.ContentView; import org.chromium.content.browser.ContentViewClient; import org.chromium.content.browser.ContentViewCore; import org.chromium.content_public.browser.LoadUrlParams; 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 * ContentViewCore displayed inside of a panel and exposes a simple API relevant to actions a * panel has. */ public class OverlayPanelContent { /** The ContentViewCore that this panel will display. */ private ContentViewCore mContentViewCore; /** 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 ContentViewCore has started loading a URL. */ private boolean mDidStartLoadingUrl; /** * Whether the ContentViewCore 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 ContentViewCore responsible for displaying content. */ private ContentViewClient mContentViewClient; /** 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; // ============================================================================================ // InterceptNavigationDelegateImpl // ============================================================================================ // Used to intercept intent navigations. // TODO(jeremycho): Consider creating a Tab with the Panel's ContentViewCore, // 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.getContentViewCore() != 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. */ public OverlayPanelContent(OverlayContentDelegate contentDelegate, OverlayContentProgressObserver progressObserver, ChromeActivity activity) { mNativeOverlayPanelContentPtr = nativeInit(); mContentDelegate = contentDelegate; mProgressObserver = progressObserver; mActivity = activity; mWebContentsDelegate = new WebContentsDelegateAndroid() { private boolean mIsFullscreen; @Override public void loadingStateChanged(boolean toDifferentDocument) { boolean isLoading = mContentViewCore != null && mContentViewCore.getWebContents() != null && mContentViewCore.getWebContents().isLoading(); if (isLoading) { mProgressObserver.onProgressBarStarted(); } else { mProgressObserver.onProgressBarFinished(); } } @Override public void onLoadProgressChanged(int progress) { mProgressObserver.onProgressBarUpdated(progress); } @Override public void toggleFullscreenModeForTab(boolean enterFullscreen) { mIsFullscreen = enterFullscreen; } @Override public boolean isFullscreenForTabOrPending() { return mIsFullscreen; } @Override public ContentVideoViewEmbedder getContentVideoViewEmbedder() { return null; // Have a no-op embedder be used. } }; } // ============================================================================================ // ContentViewCore related // ============================================================================================ /** * Load a URL; this will trigger creation of a new ContentViewCore 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 { createNewContentView(); mLoadedUrl = url; mDidStartLoadingUrl = true; mIsProcessingPendingNavigation = true; if (!mContentDelegate.handleInterceptLoadUrl(mContentViewCore, url)) { mContentViewCore.getWebContents().getNavigationController().loadUrl( new LoadUrlParams(url)); } } } /** * Makes the content visible, causing it to be rendered. */ public void showContent() { setVisibility(true); } /** * Creates a ContentViewCore. This method will be overridden by tests. * @param activity The ChromeActivity. * @return The newly created ContentViewCore. */ protected ContentViewCore createContentViewCore(ChromeActivity activity) { return new ContentViewCore(activity, ChromeVersionInfo.getProductVersion()); } /** * Create a new ContentViewCore that will be managed by this panel. */ private void createNewContentView() { if (mContentViewCore != null) { // If the ContentViewCore has already been created, but never used, // then there's no need to create a new one. if (!mDidStartLoadingUrl) return; destroyContentView(); } mContentViewCore = createContentViewCore(mActivity); if (mContentViewClient == null) { mContentViewClient = new ContentViewClient(); } mContentViewCore.setContentViewClient(mContentViewClient); ContentView cv = ContentView.createContentView(mActivity, mContentViewCore); // Creates an initially hidden WebContents which gets shown when the panel is opened. WebContents panelWebContents = WebContentsFactory.createWebContents(false, true); // Dummny ViewAndroidDelegate since the container view for overlay panel is // never added to the view hierarchy. ViewAndroidDelegate delegate = new ViewAndroidDelegate() { private ViewGroup mContainerView; private ViewAndroidDelegate init(ViewGroup containerView) { mContainerView = containerView; return this; } @Override public View acquireView() { assert false : "Shold not reach here"; return null; } @Override public void setViewPosition(View anchorView, float x, float y, float width, float height, float scale, int leftMargin, int topMargin) { } @Override public void removeView(View anchorView) { } @Override public ViewGroup getContainerView() { return mContainerView; } }.init(cv); mContentViewCore.initialize(delegate, cv, panelWebContents, mActivity.getWindowAndroid()); // Transfers the ownership of the WebContents to the native OverlayPanelContent. nativeSetWebContents(mNativeOverlayPanelContentPtr, panelWebContents, mWebContentsDelegate); mWebContentsObserver = new WebContentsObserver(panelWebContents) { @Override public void didStartLoading(String url) { mContentDelegate.onContentLoadStarted(url); } @Override public void navigationEntryCommitted() { mContentDelegate.onNavigationEntryCommitted(); } @Override public void didStartProvisionalLoadForFrame(long frameId, long parentFrameId, boolean isMainFrame, String validatedUrl, boolean isErrorPage, boolean isIframeSrcdoc) { if (isMainFrame) { mContentDelegate.onMainFrameLoadStarted(validatedUrl, !TextUtils.equals(validatedUrl, mLoadedUrl)); } } @Override public void didNavigateMainFrame(String url, String baseUrl, boolean isNavigationToDifferentPage, boolean isNavigationInPage, int httpResultCode) { mIsProcessingPendingNavigation = false; mContentDelegate.onMainFrameNavigation(url, !TextUtils.equals(url, mLoadedUrl), isHttpFailureCode(httpResultCode)); } @Override public void didFinishLoad(long frameId, String validatedUrl, boolean isMainFrame) { mContentDelegate.onContentLoadFinished(); } }; mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(); nativeSetInterceptNavigationDelegate(mNativeOverlayPanelContentPtr, mInterceptNavigationDelegate, panelWebContents); mContentDelegate.onContentViewCreated(mContentViewCore); } /** * Destroy this panel's ContentViewCore. */ private void destroyContentView() { if (mContentViewCore != null) { // Native destroy will call up to destroy the Java WebContents. nativeDestroyWebContents(mNativeOverlayPanelContentPtr); mContentViewCore.destroy(); mContentViewCore = null; if (mWebContentsObserver != null) { mWebContentsObserver.destroy(); mWebContentsObserver = null; } mDidStartLoadingUrl = false; mIsProcessingPendingNavigation = false; setVisibility(false); // After everything has been disposed, notify the observer. mContentDelegate.onContentViewDestroyed(); } } // ============================================================================================ // Utilities // ============================================================================================ /** * Calls updateTopControlsState on the ContentViewCore. * @param enableHiding Enable the toolbar's ability to hide. * @param enableShowing If the toolbar is allowed to show. * @param animate If the toolbar should animate when showing/hiding. */ public void updateTopControlsState(boolean enableHiding, boolean enableShowing, boolean animate) { if (mContentViewCore != null && mContentViewCore.getWebContents() != null) { mContentViewCore.getWebContents().updateTopControlsState(enableHiding, enableShowing, animate); } } /** * @return Whether a pending navigation if being processed. */ public boolean isProcessingPendingNavigation() { return mIsProcessingPendingNavigation; } /** * Reset the ContentViewCore's scroll position to (0, 0). */ public void resetContentViewScroll() { if (mContentViewCore != null) { mContentViewCore.scrollTo(0, 0); } } /** * @return The Y scroll position. */ public float getContentVerticalScroll() { return mContentViewCore != null ? mContentViewCore.computeVerticalScrollOffset() : -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 CVC is created with the search request, but if none was made we'll need // one in order to display an empty panel. if (mContentViewCore == null) { createNewContentView(); } // NOTE(pedrosimonetti): Calling onShow() on the ContentViewCore 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 (mContentViewCore != null) mContentViewCore.onShow(); mContentDelegate.onContentViewSeen(); } else { if (mContentViewCore != null) mContentViewCore.onHide(); } mContentDelegate.onVisibilityChanged(isVisible); } /** * @return Whether the given HTTP result code represents a failure or not. */ private boolean isHttpFailureCode(int httpResultCode) { return httpResultCode <= 0 || httpResultCode >= 400; } /** * Set a ContentViewClient for this panel to use (will be reused for each new ContentViewCore). * @param viewClient The ContentViewClient to use. */ public void setContentViewClient(ContentViewClient viewClient) { mContentViewClient = viewClient; if (mContentViewCore != null) { mContentViewCore.setContentViewClient(mContentViewClient); } } /** * @return true if the ContentViewCore is visible on the page. */ public boolean isContentShowing() { return mIsContentViewShowing; } // ============================================================================================ // Methods for managing this panel's ContentViewCore. // ============================================================================================ /** * Reset this object's native pointer to 0; */ @CalledByNative private void clearNativePanelContentPtr() { assert mNativeOverlayPanelContentPtr != 0; mNativeOverlayPanelContentPtr = 0; } /** * @return This panel's ContentViewCore. */ @VisibleForTesting public ContentViewCore getContentViewCore() { return mContentViewCore; } /** * 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 (mContentViewCore != null) { destroyContentView(); } // 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 nativeSetWebContents(long nativeOverlayPanelContent, WebContents webContents, WebContentsDelegateAndroid delegate); private native void nativeDestroyWebContents(long nativeOverlayPanelContent); private native void nativeSetInterceptNavigationDelegate(long nativeOverlayPanelContent, InterceptNavigationDelegate delegate, WebContents webContents); }