Index: chrome/android/java_staging/src/org/chromium/chrome/browser/tab/BackgroundContentViewHelper.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/tab/BackgroundContentViewHelper.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/tab/BackgroundContentViewHelper.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e9c95a3fc1fcaffc33587afe4f5f51424a9bf7e8 |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/tab/BackgroundContentViewHelper.java |
@@ -0,0 +1,897 @@ |
+// 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.tab; |
+ |
+import android.app.Activity; |
+import android.net.Uri; |
+import android.os.Handler; |
+import android.text.TextUtils; |
+ |
+import org.chromium.base.CalledByNative; |
+import org.chromium.base.FieldTrialList; |
+import org.chromium.base.metrics.RecordHistogram; |
+import org.chromium.base.metrics.RecordUserAction; |
+import org.chromium.chrome.browser.ChromeVersionInfo; |
+import org.chromium.chrome.browser.ContentViewUtil; |
+import org.chromium.chrome.browser.EmptyTabObserver; |
+import org.chromium.chrome.browser.Tab; |
+import org.chromium.chrome.browser.device.DeviceClassManager; |
+import org.chromium.components.web_contents_delegate_android.WebContentsDelegateAndroid; |
+import org.chromium.content.browser.ContentView; |
+import org.chromium.content.browser.ContentViewClient; |
+import org.chromium.content.browser.ContentViewCore; |
+import org.chromium.content.browser.RenderCoordinates; |
+import org.chromium.content_public.browser.GestureStateListener; |
+import org.chromium.content_public.browser.LoadUrlParams; |
+import org.chromium.content_public.browser.WebContents; |
+import org.chromium.content_public.browser.WebContentsObserver; |
+import org.chromium.content_public.common.Referrer; |
+import org.chromium.ui.base.WindowAndroid; |
+ |
+import java.net.MalformedURLException; |
+import java.net.URL; |
+import java.util.concurrent.TimeUnit; |
+ |
+/** |
+ * Responsible for background content view creation and management. |
+ */ |
+public class BackgroundContentViewHelper extends EmptyTabObserver { |
+ private static final String GOOGLE_PREVIEW_SERVER_PREFIX = "icl"; |
+ |
+ // Google preview staging and canary servers. Only to be used in dev builds. |
+ private static final String GOOGLE_PREVIEW_SERVER_SUFFIX = ".googleusercontent.com"; |
+ |
+ private static final String GWS_CHROME_JOINT_EXPERIMENT_ID_STRING = "gcjeid"; |
+ |
+ private static final String GOOGLE_DOMAIN_PREFIX = "www.google."; |
+ |
+ // Instant search clicks swap reasons. |
+ // IMPORTANT: Do not renumber any existing constants (except the BOUNDARY ones). Keep these in |
+ // sync with histograms.xml file. |
+ private static final int SWAP_REASON_REGULAR = 0; |
+ private static final int SWAP_REASON_TIMEOUT = 1; |
+ private static final int SWAP_REASON_ABORT_ON_NAVIGATE = 2; |
+ private static final int SWAP_REASON_ON_PREVIEW_FAIL = 3; |
+ private static final int SWAP_REASON_ORIGINAL_FAIL = 4; |
+ private static final int SWAP_REASON_FORCE = 5; |
+ private static final int SWAP_REASON_BOUNDARY = 6; |
+ |
+ // Instant search clicks scroll state while swapping. |
+ // IMPORTANT: Do not renumber any existing constants (except the BOUNDARY ones). Keep these in |
+ // sync with histograms.xml file. |
+ private static final int PREVIEW_SCROLL_STATE_NO_SCROLL = 0; |
+ private static final int PREVIEW_SCROLL_STATE_SCROLL = 1; |
+ private static final int PREVIEW_SCROLL_STATE_SCROLL_AT_BOTTOM = 2; |
+ private static final int PREVIEW_SCROLL_STATE_BOUNDARY = 3; |
+ |
+ // Instant search clicks experiment state. |
+ private static final int INSTANT_SEARCH_CLICKS_EXPERIMENT_DISABLED = 0; |
+ private static final int INSTANT_SEARCH_CLICKS_EXPERIMENT_ENABLED = 1; |
+ private static final int INSTANT_SEARCH_CLICKS_EXPERIMENT_CONTROL = 2; |
+ |
+ /** |
+ * The maximum amount of time to wait for the background page to swap. We force the swap after |
+ * this timeout. |
+ */ |
+ private final int mSwapTimeoutMs; |
+ |
+ /** |
+ * The minimum number of renderer frames needed before swap is triggered. |
+ */ |
+ private final int mMinRendererFramesNeededForSwap; |
+ |
+ // Native pointer corresponding to the current object. |
+ private long mNativeBackgroundContentViewHelperPtr; |
+ |
+ // The content view core created to load url in background. |
+ private ContentViewCore mContentViewCore; |
+ |
+ // Android window used to created content view. |
+ private final WindowAndroid mWindowAndroid; |
+ |
+ // The webcontents observer on current content view of the tab. |
+ private WebContentsObserver mCurrentViewWebContentsObserver; |
+ |
+ // The webcontents observer that listens for events to swap in the new WebContents. |
+ private WebContentsObserver mWebContentsObserver; |
+ |
+ // The webcontents delegate that gets notified when background view is ready to be swapped. |
+ private final WebContentsDelegateAndroid mWebContentsDelegate; |
+ |
+ // Whether or not the preview page finished loading. |
+ private boolean mPreviewLoaded = false; |
+ |
+ // Whether or not the background view has painted anything non-empty. |
+ private boolean mPaintedNonEmpty = false; |
+ |
+ private int mNumRendererFramesReceived = 0; |
+ |
+ // Whether or not the swap is in progress. We will be in this state while syncing scroll offsets |
+ // between content views. |
+ private boolean mSwapInProgress = false; |
+ |
+ // Whether or not the background view started loading content. |
+ private boolean mDidStartLoad = false; |
+ |
+ // Whether or not the background view finished loading. |
+ private boolean mDidFinishLoad = false; |
+ |
+ // The tab associated with the background view. |
+ private final Tab mTab; |
+ |
+ // The delegate who is notified when background content view is ready. |
+ private final BackgroundContentViewDelegate mDelegate; |
+ |
+ // GestureStateListener attached to the background contentView. |
+ private final GestureStateListener mBackgroundGestureStateListener; |
+ |
+ // GestureStateListener attached to the current contentView of tab. |
+ private final GestureStateListener mCurrentViewGestureStateListener; |
+ |
+ private final Handler mHandler; |
+ |
+ private final Runnable mSwapOnTimeout = new Runnable() { |
+ @Override |
+ public void run() { |
+ forceSwappingContentViews(); |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.ReasonForSwap", |
+ SWAP_REASON_TIMEOUT, SWAP_REASON_BOUNDARY); |
+ } |
+ }; |
+ |
+ private long mBackgroundLoadStartTimeStampMs; |
+ |
+ /** |
+ * Whether or not to record 'instant search clicks' uma for the current loaded page in tab. |
+ * This is set to true, for instant search clicks enabled urls(preview urls) and also for the |
+ * 'would have been' instant search click enabled urls. |
+ */ |
+ private boolean mRecordUma = false; |
+ |
+ private final int mExperimentGroup; |
+ |
+ /** |
+ * The recent load progress before swapping. This will be used to return the progress after |
+ * swapping the page views. |
+ */ |
+ private int mLoadProgress; |
+ |
+ /** |
+ * Whether we transferred pagescale from preview to original. |
+ */ |
+ private boolean mOriginalPageZoomed = false; |
+ |
+ /** |
+ * The delegate that is notified when BackgroundContentView is ready to be swapped in. |
+ */ |
+ public interface BackgroundContentViewDelegate { |
+ /** |
+ * Called when the background content view has drawn non-empty screen.. |
+ * @param cvc The background ContentViewCore that is ready to be swapped in. |
+ * @param didStartLoad Whether WebContentsObserver::DidStartProvisionalLoadForFrame() has |
+ * been called for background ContentViewCore. |
+ * @param didFinishLoad Whether WebContentsObserver::DidFinishLoad() has already been |
+ * called for background {@link ContentViewCore}. |
+ * @param progress The recent load progress that was received for the |
+ * {@link ContentView}. |
+ */ |
+ void onBackgroundViewReady( |
+ ContentViewCore cvc, boolean didStartLoad, boolean didFinishLoad, int progress); |
+ |
+ /** |
+ * Called to notify the load progress of backgrounc content view. |
+ * @param progress The recent load progress that was received for the |
+ * {@link ContentView}. |
+ */ |
+ void onLoadProgressChanged(int progress); |
+ } |
+ |
+ /** |
+ * Creates an instance of BackgroundContentViewHelper. |
+ * @param window An instance of a {@link WindowAndroid}. |
+ * @param tab The {@link ChromeTab} associated with current backgruond view. |
+ * @param delegate The {@link BackgroundContentViewDelegate} to notify when backgruond view is |
+ * ready to swap in. |
+ */ |
+ public BackgroundContentViewHelper(WindowAndroid windowAndroid, Tab tab, |
+ BackgroundContentViewDelegate delegate) { |
+ mWindowAndroid = windowAndroid; |
+ mNativeBackgroundContentViewHelperPtr = nativeInit(); |
+ |
+ mTab = tab; |
+ assert delegate != null; |
+ mDelegate = delegate; |
+ mTab.addObserver(this); |
+ mBackgroundGestureStateListener = new GestureStateListener() { |
+ @Override |
+ public void onScrollOffsetOrExtentChanged(int scrollOffsetY, int scrollExtentY) { |
+ syncPageStateAndSwapIfReady(); |
+ } |
+ }; |
+ |
+ mCurrentViewGestureStateListener = new GestureStateListener() { |
+ @Override |
+ public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) { |
+ syncPageStateAndSwapIfReady(); |
+ } |
+ |
+ @Override |
+ public void onScrollEnded(int scrollOffsetY, int scrollExtentY) { |
+ syncPageStateAndSwapIfReady(); |
+ } |
+ |
+ @Override |
+ public void onSingleTap(boolean consumed, int x, int y) { |
+ recordSingleTap(); |
+ } |
+ }; |
+ |
+ mWebContentsDelegate = new WebContentsDelegateAndroid() { |
+ @Override |
+ public void onLoadProgressChanged(int progress) { |
+ mLoadProgress = progress; |
+ mDelegate.onLoadProgressChanged(progress); |
+ trySwappingBackgroundView(); |
+ } |
+ }; |
+ mHandler = new Handler(); |
+ |
+ mSwapTimeoutMs = nativeGetSwapTimeoutMs(mNativeBackgroundContentViewHelperPtr); |
+ mMinRendererFramesNeededForSwap = |
+ nativeGetNumFramesNeededForSwap(mNativeBackgroundContentViewHelperPtr); |
+ mBackgroundLoadStartTimeStampMs = System.currentTimeMillis(); |
+ mExperimentGroup = getExperimentGroup(); |
+ } |
+ |
+ /* |
+ * @return Experiment group that the current user fall into. |
+ */ |
+ private int getExperimentGroup() { |
+ if (!DeviceClassManager.enableInstantSearchClicks()) { |
+ return INSTANT_SEARCH_CLICKS_EXPERIMENT_DISABLED; |
+ } |
+ |
+ String instantSearchClicksFieldTrialValue = |
+ FieldTrialList.findFullName("InstantSearchClicks"); |
+ if (TextUtils.equals(instantSearchClicksFieldTrialValue, "Enabled") |
+ || TextUtils.equals(instantSearchClicksFieldTrialValue, |
+ "InstantSearchClicksEnabled")) { |
+ return INSTANT_SEARCH_CLICKS_EXPERIMENT_ENABLED; |
+ } else if (TextUtils.equals(instantSearchClicksFieldTrialValue, "Control")) { |
+ return INSTANT_SEARCH_CLICKS_EXPERIMENT_CONTROL; |
+ } |
+ return INSTANT_SEARCH_CLICKS_EXPERIMENT_DISABLED; |
+ } |
+ |
+ /** |
+ * @return True iff content height of original page is greater than preview page. |
+ */ |
+ private boolean hasBackgroundPageLoadedMoreThanPreview() { |
+ if (!mPaintedNonEmpty || mTab.getContentViewCore() == null) return false; |
+ return getContentViewCore().getContentHeightCss() |
+ > mTab.getContentViewCore().getContentHeightCss(); |
+ } |
+ |
+ /** |
+ * @return True iff original page is loading in background content view which is not swapped in. |
+ */ |
+ public boolean hasPendingBackgroundPage() { |
+ return mContentViewCore != null; |
+ } |
+ |
+ /** |
+ * @return True iff swapping is in progress. This is true till the content view is completely |
+ * swapped in. |
+ */ |
+ public boolean isPageSwappingInProgress() { |
+ return mSwapInProgress; |
+ } |
+ |
+ /** |
+ * @return The {@link ContentViewCore} associated with the current background page. |
+ */ |
+ public ContentViewCore getContentViewCore() { |
+ return mContentViewCore; |
+ } |
+ |
+ /** @return The recent progress of background content view. */ |
+ public int getProgress() { |
+ return mLoadProgress; |
+ } |
+ |
+ /** |
+ * Loads the specified URL in the background content view. |
+ * @param url the url to load. |
+ */ |
+ private void loadUrl(String url) { |
+ if (mNativeBackgroundContentViewHelperPtr == 0) return; |
+ |
+ if (mContentViewCore == null) return; |
+ |
+ mContentViewCore.onShow(); |
+ |
+ LoadUrlParams loadUrlParams = new LoadUrlParams(url); |
+ // Use the referrer of preview url as referrer for original url. |
+ // We are always loading url during onDidStartProvisionalLoadForFrame of preview URL, where |
+ // load is not yet committed and so is the navigation entry. The current active |
+ // navigation entry is still the referrer for preview url. |
+ String referrerString = mTab.getWebContents() |
+ .getNavigationController().getOriginalUrlForVisibleNavigationEntry(); |
+ Uri referrer = getUri(referrerString); |
+ String referrerHost = referrer.getHost(); |
+ if (referrerHost == null |
+ || TextUtils.indexOf(referrerHost, GOOGLE_DOMAIN_PREFIX) != 0) { |
+ referrerHost = "www.google.com"; |
+ } |
+ // We come here only through google.com and google.com always sends the referrer with |
+ // value at least equal to the origin (and full url in http requests). |
+ // Add 'gcjeid' param to referrer so that the PLT metrics are logged in a different bucket. |
+ // PLT metrics are logged in a different bucket if the referer contains 'gcjeid' param. |
+ // For Experiment: gcjeid=19 |
+ String referrerTrimmed = "http://" + referrerHost + "/?" |
+ + GWS_CHROME_JOINT_EXPERIMENT_ID_STRING + "=19"; |
+ loadUrlParams.setReferrer(new Referrer(referrerTrimmed, 0 /* WebReferrerPolicyAlways */)); |
+ mContentViewCore.getWebContents().getNavigationController().loadUrl(loadUrlParams); |
+ } |
+ |
+ /** |
+ * Releases the ownership of the content view created. Also, resets the whole state of the |
+ * current object. |
+ */ |
+ private ContentViewCore releaseContentViewCore() { |
+ mTab.detachOverlayContentViewCore(mContentViewCore); |
+ |
+ ContentViewCore originalContentViewCore = mContentViewCore; |
+ if (mContentViewCore != null && mNativeBackgroundContentViewHelperPtr != 0) { |
+ nativeReleaseWebContents(mNativeBackgroundContentViewHelperPtr); |
+ } |
+ reset(); |
+ return originalContentViewCore; |
+ } |
+ |
+ /** |
+ * Destroys the contentview and the corresponding native object. |
+ */ |
+ private void destroy() { |
+ if (mCurrentViewWebContentsObserver != null) { |
+ mCurrentViewWebContentsObserver.destroy(); |
+ mCurrentViewWebContentsObserver = null; |
+ } |
+ destroyContentViewCore(); |
+ |
+ if (mNativeBackgroundContentViewHelperPtr == 0) return; |
+ |
+ nativeDestroy(mNativeBackgroundContentViewHelperPtr); |
+ mNativeBackgroundContentViewHelperPtr = 0; |
+ } |
+ |
+ /** |
+ * Creates a new {@link ContentViewCore} and loads the given url. |
+ * @param url Url to load. |
+ */ |
+ private void createContentViewCoreWithUrl(String url) { |
+ if (url == null || mNativeBackgroundContentViewHelperPtr == 0) return; |
+ |
+ destroyContentViewCore(); |
+ |
+ Activity activity = mWindowAndroid.getActivity().get(); |
+ mContentViewCore = new ContentViewCore(activity); |
+ ContentView cv = new ContentView(activity, mContentViewCore); |
+ mContentViewCore.initialize(cv, cv, |
+ ContentViewUtil.createWebContents(mTab.isIncognito(), false), mWindowAndroid); |
+ |
+ // The renderer should be set with the height considering the top controls(non-fullscreen |
+ // mode). |
+ mContentViewCore.setTopControlsHeight(mContentViewCore.getTopControlsHeightPix(), true); |
+ mWebContentsObserver = new WebContentsObserver(mContentViewCore.getWebContents()) { |
+ @Override |
+ public void didFirstVisuallyNonEmptyPaint() { |
+ mPaintedNonEmpty = true; |
+ trySwappingBackgroundView(); |
+ } |
+ @Override |
+ public void didStartProvisionalLoadForFrame(long frameId, long parentFrameId, |
+ boolean isMainFrame, String validatedUrl, boolean isErrorPage, |
+ boolean isIframeSrcdoc) { |
+ if (isMainFrame) mDidStartLoad = true; |
+ } |
+ |
+ @Override |
+ public void didFinishLoad(long frameId, String validatedUrl, boolean isMainFrame) { |
+ if (isMainFrame) { |
+ mDidFinishLoad = true; |
+ trySwappingBackgroundView(); |
+ } |
+ } |
+ |
+ @Override |
+ public void didFailLoad(boolean isProvisionalLoad, |
+ boolean isMainFrame, int errorCode, String description, String failingUrl) { |
+ if (isMainFrame) { |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.ReasonForSwap", |
+ SWAP_REASON_ORIGINAL_FAIL, SWAP_REASON_BOUNDARY); |
+ mDidFinishLoad = true; |
+ trySwappingBackgroundView(); |
+ } |
+ } |
+ }; |
+ |
+ mContentViewCore.setContentViewClient(new ContentViewClient() { |
+ @Override |
+ public void onOffsetsForFullscreenChanged( |
+ float topControlsOffsetYPix, |
+ float contentOffsetYPix, |
+ float overdrawBottomHeightPix) { |
+ // Using onOffsetsForFullscreenChanged, as it is called on every frame update. |
+ // Here we know that we have an update to frame. Ideally we need a function like |
+ // onUpdateFrame. |
+ if (mPaintedNonEmpty |
+ // We need to make sure the frame we got has some content. Sometimes |
+ // didFirstVisuallyNonEmptyPaint is called, but still the frame is empty and |
+ // has the default width of Chrome '980'px. So waiting for the contentWidth |
+ // of both preview and original to become same. Ideally in all cases they |
+ // should be same as they both are same content. Also if we get hung here |
+ // for some pages in the worst cases we swap on full load of original, or |
+ // our hard timeout will kick in. |
+ && getContentViewCore().getContentWidthCss() |
+ == mTab.getContentViewCore().getContentWidthCss() |
+ && !mSwapInProgress) { |
+ mNumRendererFramesReceived++; |
+ trySwappingBackgroundView(); |
+ } |
+ } |
+ }); |
+ |
+ if (mSwapTimeoutMs > 0) mHandler.postDelayed(mSwapOnTimeout, mSwapTimeoutMs); |
+ |
+ nativeSetWebContents(mNativeBackgroundContentViewHelperPtr, |
+ mContentViewCore, mWebContentsDelegate); |
+ |
+ mBackgroundLoadStartTimeStampMs = System.currentTimeMillis(); |
+ |
+ loadUrl(url); |
+ // Keep the toolbar always show in background view. |
+ mContentViewCore.getWebContents().updateTopControlsState(false, true, false); |
+ mTab.attachOverlayContentViewCore(mContentViewCore, false); |
+ } |
+ |
+ /** |
+ * Helper to delete {@link ContentViewCore} and it's native web contents object. |
+ */ |
+ private void destroyContentViewCore() { |
+ if (mContentViewCore != null && mNativeBackgroundContentViewHelperPtr != 0) { |
+ RecordHistogram.recordTimesHistogram( |
+ "InstantSearchClicks.TimeInPreview", |
+ System.currentTimeMillis() - mBackgroundLoadStartTimeStampMs, |
+ TimeUnit.MILLISECONDS); |
+ mTab.detachOverlayContentViewCore(mContentViewCore); |
+ mContentViewCore.destroy(); |
+ nativeDestroyWebContents(mNativeBackgroundContentViewHelperPtr); |
+ } |
+ reset(); |
+ // Note: We cannot set these in reset(), as we call reset() from releaseContentView and |
+ // these values are used after that. |
+ mSwapInProgress = false; |
+ mLoadProgress = 0; |
+ mDidStartLoad = false; |
+ mDidFinishLoad = false; |
+ } |
+ |
+ /** |
+ * @return True iff Background view has finished loading or all the following conditions are |
+ * met. |
+ * 1. Preview page has loaded completely. |
+ * 2. Background view has loaded atleast of preview page height. |
+ * 3. Background view has painted anything non-empty. |
+ */ |
+ private boolean isBackgroundViewReady() { |
+ if (mDidFinishLoad) return true; |
+ |
+ if (!mPreviewLoaded) return false; |
+ |
+ if (!mPaintedNonEmpty) return false; |
+ |
+ if (mNumRendererFramesReceived < mMinRendererFramesNeededForSwap) return false; |
+ |
+ if (!hasBackgroundPageLoadedMoreThanPreview()) return false; |
+ |
+ return true; |
+ } |
+ |
+ /** Called when any of the conditions needed for background view swap are met. */ |
+ private void trySwappingBackgroundView() { |
+ if (mSwapInProgress) return; |
+ // This function is asynchronously called from various places. |
+ // So make sure we have background content view before doing anything. |
+ if (!hasPendingBackgroundPage()) return; |
+ if (isBackgroundViewReady()) backgroundViewReady(); |
+ } |
+ |
+ /** Called when we are ready to swap in background view to foreground. */ |
+ private void backgroundViewReady() { |
+ if (mDelegate == null) return; |
+ mSwapInProgress = true; |
+ swapInBackgroundViewAfterScroll(); |
+ } |
+ |
+ private boolean isGooglePreviewServerHost(String host) { |
+ if (host == null) return false; |
+ return TextUtils.equals(host, GOOGLE_PREVIEW_SERVER_PREFIX + GOOGLE_PREVIEW_SERVER_SUFFIX) |
+ || (ChromeVersionInfo.isDevBuild() && host.endsWith(GOOGLE_PREVIEW_SERVER_SUFFIX) |
+ && host.contains("promise")); |
+ } |
+ |
+ private Uri getUri(String url) { |
+ return Uri.parse(url == null ? "" : url); |
+ } |
+ |
+ /** |
+ * @return original url if the passed in url is a preview url, else null. |
+ */ |
+ private String getOriginalUrlForPreviewUrl(String previewUrl) { |
+ Uri previewUri = getUri(previewUrl); |
+ String originalUrl = null; |
+ if (isGooglePreviewServerHost(previewUri.getHost())) { |
+ try { |
+ originalUrl = (new URL(previewUri.getQueryParameter("url"))).toString(); |
+ } catch (MalformedURLException mue) { |
+ originalUrl = null; |
+ } |
+ } |
+ return originalUrl; |
+ } |
+ |
+ |
+ /** |
+ * Checks if the incoming url is instant search clicks eligible url. Only urls with referer |
+ * containing google search domain and 'gcjeid' param are instant search clicks eligible url. |
+ * @param url Url string. |
+ */ |
+ private boolean isInstantSearchClicksControlUrl(String url) { |
+ String referrer = mTab.getContentViewCore().getWebContents() |
+ .getNavigationController().getOriginalUrlForVisibleNavigationEntry(); |
+ Uri referrerUri = getUri(referrer); |
+ Uri previewUri = getUri(url); |
+ if (referrerUri.getHost() == null || previewUri.getHost() == null) return false; |
+ // Check if the url is coming from search page and has gcjeid param. |
+ // TODO(ksimbili): Make this check stricter. |
+ return (TextUtils.indexOf(referrerUri.getHost(), GOOGLE_DOMAIN_PREFIX) == 0 |
+ && referrerUri.getQueryParameter(GWS_CHROME_JOINT_EXPERIMENT_ID_STRING) != null |
+ && TextUtils.indexOf(previewUri.getHost(), GOOGLE_DOMAIN_PREFIX) == -1); |
+ } |
+ |
+ private void setLogUma() { |
+ // We ideally need to listen for gestures on current tab only while swapping to detect when |
+ // scrolling ends(if one is active). But for UMA logging, we are listening for it always. |
+ mTab.getContentViewCore().addGestureStateListener(mCurrentViewGestureStateListener); |
+ mRecordUma = true; |
+ } |
+ |
+ private void resetLogUma() { |
+ if (mTab.getContentViewCore() != null) { |
+ mTab.getContentViewCore().removeGestureStateListener(mCurrentViewGestureStateListener); |
+ } |
+ mRecordUma = false; |
+ } |
+ |
+ /** |
+ * Loads the original url in the background view, if the passed in url is a preview url. |
+ * @param previewUrl Preview url string. |
+ */ |
+ public void loadOriginalUrlIfPreview(String previewUrl) { |
+ if (mExperimentGroup == INSTANT_SEARCH_CLICKS_EXPERIMENT_DISABLED) { |
+ // If feature is disabled return; |
+ return; |
+ } |
+ |
+ if (mExperimentGroup == INSTANT_SEARCH_CLICKS_EXPERIMENT_CONTROL |
+ && isInstantSearchClicksControlUrl(previewUrl)) { |
+ setLogUma(); |
+ return; |
+ } |
+ |
+ String originalUrl = getOriginalUrlForPreviewUrl(previewUrl); |
+ if (originalUrl == null) return; |
+ |
+ if (hasPendingBackgroundPage()) { |
+ // The following condition is safe, as browser reload will always be on original url. |
+ // Hence we will never encounter a situation where user is clicking on 'Reload' button |
+ // and the page is not reloading. |
+ if (originalUrl.equals(mContentViewCore.getWebContents().getUrl())) return; |
+ |
+ // Log uma for the previous preview getting aborted |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.ReasonForSwap", |
+ SWAP_REASON_ABORT_ON_NAVIGATE, SWAP_REASON_BOUNDARY); |
+ } |
+ |
+ // Build content view. |
+ createContentViewCoreWithUrl(originalUrl); |
+ |
+ setLogUma(); |
+ } |
+ |
+ /** |
+ * Transfers zoom level from current cvc to the background cvc. |
+ * @return Whether zoom level of background cvc had to be changed. It happens only when it is |
+ * different from current cvc. |
+ */ |
+ private boolean transferPageScaleFactor() { |
+ float currentViewPageScaleFactor = |
+ mTab.getContentViewCore().getRenderCoordinates().getPageScaleFactor(); |
+ |
+ ContentViewCore backgroundViewCore = getContentViewCore(); |
+ RenderCoordinates rc = backgroundViewCore.getRenderCoordinates(); |
+ |
+ // Clamp the input value. |
+ float newPageScaleFactor = Math.max(rc.getMinPageScaleFactor(), |
+ Math.min(currentViewPageScaleFactor, rc.getMaxPageScaleFactor())); |
+ if (newPageScaleFactor == rc.getPageScaleFactor()) return false; |
+ |
+ float pageScaleFactorDelta = (newPageScaleFactor / rc.getPageScaleFactor()); |
+ backgroundViewCore.pinchByDelta(pageScaleFactorDelta); |
+ mOriginalPageZoomed = true; |
+ return true; |
+ } |
+ |
+ /** |
+ * Transfers scroll offset from current cvc to the background cvc. |
+ * @return Whether scroll offset of background cvc had to be changed. It happens only when it |
+ * is different from current cvc. |
+ */ |
+ private boolean transferScrollOffset() { |
+ ContentViewCore currentViewCore = mTab.getContentViewCore(); |
+ RenderCoordinates currentViewRenderCoordinates = currentViewCore.getRenderCoordinates(); |
+ float currentViewScrollX = currentViewRenderCoordinates.getScrollXPix(); |
+ float currentViewScrollY = currentViewRenderCoordinates.getScrollYPix(); |
+ |
+ ContentViewCore backgroundViewCore = getContentViewCore(); |
+ RenderCoordinates backgroundViewRenderCoordinates = |
+ backgroundViewCore.getRenderCoordinates(); |
+ float maxHorizontalScroll = backgroundViewRenderCoordinates.getMaxHorizontalScrollPix(); |
+ float maxVerticalScroll = backgroundViewRenderCoordinates.getMaxVerticalScrollPix(); |
+ |
+ // Bound the scroll offsets to max scroll values. |
+ float newScrollX = Math.min(currentViewScrollX, maxHorizontalScroll); |
+ float newScrollY = Math.min(currentViewScrollY, maxVerticalScroll); |
+ |
+ float backgroundViewScrollX = backgroundViewRenderCoordinates.getScrollXPix(); |
+ float backgroundViewScrollY = backgroundViewRenderCoordinates.getScrollYPix(); |
+ int scrollByX = Math.round(backgroundViewRenderCoordinates.fromPixToLocalCss( |
+ newScrollX - backgroundViewScrollX)); |
+ int scrollByY = Math.round(backgroundViewRenderCoordinates.fromPixToLocalCss( |
+ newScrollY - backgroundViewScrollY)); |
+ |
+ if (Math.abs(scrollByX) <= 1 && Math.abs(scrollByY) <= 1) return false; |
+ |
+ if (!mOriginalPageZoomed) { |
+ backgroundViewCore.scrollBy(Math.round(newScrollX - backgroundViewScrollX), |
+ Math.round(newScrollY - backgroundViewScrollY)); |
+ } else { |
+ backgroundViewCore.scrollTo(Math.round(newScrollX), Math.round(newScrollY)); |
+ } |
+ return true; |
+ } |
+ |
+ private void swapInBackgroundViewAfterScroll() { |
+ getContentViewCore().addGestureStateListener(mBackgroundGestureStateListener); |
+ syncPageStateAndSwapIfReady(); |
+ } |
+ |
+ // Set scroll offset and zoom values from current cvc and swap the cvc if the both are synced. |
+ private void syncPageStateAndSwapIfReady() { |
+ // This function is asynchronously called from various places. |
+ // So make sure we have background content view before doing anything. |
+ if (!hasPendingBackgroundPage() || !isPageSwappingInProgress()) return; |
+ |
+ ContentViewCore currentViewCore = mTab.getContentViewCore(); |
+ if (currentViewCore == null || currentViewCore.isScrollInProgress()) { |
+ // We'll comeback here on scrollEnded and flingEndGesture. |
+ return; |
+ } |
+ |
+ if (transferPageScaleFactor()) return; |
+ |
+ if (transferScrollOffset()) return; |
+ |
+ RenderCoordinates currentViewRenderCoordinates = currentViewCore.getRenderCoordinates(); |
+ int currentViewScrollY = currentViewRenderCoordinates.getScrollYPixInt(); |
+ |
+ int state; |
+ if (currentViewScrollY == 0) { |
+ state = PREVIEW_SCROLL_STATE_NO_SCROLL; |
+ } else if (currentViewScrollY >= currentViewRenderCoordinates.getMaxVerticalScrollPix()) { |
+ state = PREVIEW_SCROLL_STATE_SCROLL_AT_BOTTOM; |
+ } else { |
+ state = PREVIEW_SCROLL_STATE_SCROLL; |
+ } |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.PreviewScrollState", state, PREVIEW_SCROLL_STATE_BOUNDARY); |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.ReasonForSwap", SWAP_REASON_REGULAR, SWAP_REASON_BOUNDARY); |
+ |
+ // We are ready swap the view now. |
+ swapInBackgroundView(); |
+ } |
+ |
+ private void swapInBackgroundView() { |
+ // This function is asynchronously called from various places. |
+ // So make sure we have background content view before doing anything. |
+ if (!hasPendingBackgroundPage()) return; |
+ |
+ if (mTab.getContentViewCore() != null) { |
+ mTab.getContentViewCore().removeGestureStateListener(mCurrentViewGestureStateListener); |
+ } |
+ getContentViewCore().removeGestureStateListener(mBackgroundGestureStateListener); |
+ assert mDelegate != null; |
+ |
+ // Make sure the ContentViewCore is visible. |
+ getContentViewCore().setDrawsContent(true); |
+ |
+ // Merge history stack before swap. |
+ nativeMergeHistoryFrom(mNativeBackgroundContentViewHelperPtr, mTab.getWebContents()); |
+ ContentViewCore cvc = releaseContentViewCore(); |
+ mDelegate.onBackgroundViewReady(cvc, mDidStartLoad, mDidFinishLoad, mLoadProgress); |
+ |
+ RecordHistogram.recordTimesHistogram("InstantSearchClicks.TimeToSwap", |
+ System.currentTimeMillis() - mBackgroundLoadStartTimeStampMs, |
+ TimeUnit.MILLISECONDS); |
+ |
+ // Content view is released already, but still calling this to clear some state. |
+ destroyContentViewCore(); |
+ |
+ // We need to include metrics from orignal page after swap too. For example, single tap |
+ // metric should include taps in both preview and original page after swap. This we reset |
+ // when tab commits a new navigation. |
+ setLogUma(); |
+ } |
+ |
+ /** Stop the current navigation. */ |
+ public void stopLoading() { |
+ destroyContentViewCore(); |
+ } |
+ |
+ /** |
+ * Forces swapping of content views. |
+ */ |
+ public void forceSwappingContentViews() { |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.ReasonForSwap", SWAP_REASON_FORCE, SWAP_REASON_BOUNDARY); |
+ swapInBackgroundView(); |
+ } |
+ |
+ /** |
+ * Calls 'unload' handler and deletes the webContents. |
+ * @param webContents The webcontents to be deleted. |
+ */ |
+ public void unloadAndDeleteWebContents(WebContents webContents) { |
+ nativeUnloadAndDeleteWebContents(mNativeBackgroundContentViewHelperPtr, webContents); |
+ } |
+ |
+ /** |
+ * Helper to reset the state of the object. |
+ */ |
+ private void reset() { |
+ mContentViewCore = null; |
+ if (mWebContentsObserver != null) { |
+ mWebContentsObserver.destroy(); |
+ mWebContentsObserver = null; |
+ } |
+ |
+ mPreviewLoaded = false; |
+ mPaintedNonEmpty = false; |
+ mNumRendererFramesReceived = 0; |
+ resetLogUma(); |
+ mHandler.removeCallbacks(mSwapOnTimeout); |
+ mOriginalPageZoomed = false; |
+ } |
+ |
+ @Override |
+ public void onLoadProgressChanged(Tab tab, int progress) { |
+ if (!hasPendingBackgroundPage()) return; |
+ if (progress >= 100) { |
+ mPreviewLoaded = true; |
+ trySwappingBackgroundView(); |
+ } |
+ } |
+ |
+ @Override |
+ public void onContentChanged(Tab tab) { |
+ if (mCurrentViewWebContentsObserver != null) { |
+ mCurrentViewWebContentsObserver.destroy(); |
+ } |
+ if (tab.getContentViewCore() == null) return; |
+ |
+ loadOriginalUrlIfPreview(mTab.getUrl()); |
+ |
+ mCurrentViewWebContentsObserver = new WebContentsObserver(mTab.getWebContents()) { |
+ @Override |
+ public void didFailLoad(boolean isProvisionalLoad, |
+ boolean isMainFrame, int errorCode, String description, String failingUrl) { |
+ if (isMainFrame && hasPendingBackgroundPage()) { |
+ if (TextUtils.equals(mContentViewCore.getWebContents().getUrl(), |
+ getOriginalUrlForPreviewUrl(failingUrl))) { |
+ forceSwappingContentViews(); |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "InstantSearchClicks.ReasonForSwap", |
+ SWAP_REASON_ON_PREVIEW_FAIL, SWAP_REASON_BOUNDARY); |
+ } |
+ } |
+ } |
+ |
+ @Override |
+ public void didStartProvisionalLoadForFrame(long frameId, long parentFrameId, |
+ boolean isMainFrame, String validatedUrl, boolean isErrorPage, |
+ boolean isIframeSrcdoc) { |
+ if (isMainFrame) { |
+ if (mTab.getContentViewCore() != null) { |
+ loadOriginalUrlIfPreview(validatedUrl); |
+ } else { |
+ destroyContentViewCore(); |
+ } |
+ } |
+ } |
+ |
+ @Override |
+ public void didCommitProvisionalLoadForFrame( |
+ long frameId, boolean isMainFrame, String url, int transitionType) { |
+ if (!isMainFrame) return; |
+ |
+ if (!hasPendingBackgroundPage() && !isInstantSearchClicksControlUrl(url)) { |
+ resetLogUma(); |
+ } |
+ |
+ if (hasPendingBackgroundPage() && getOriginalUrlForPreviewUrl(url) == null) { |
+ // We don't destroy the contentView in onDidStartProvisionalLoadForFrame, if the |
+ // url is navigating to non-preview URL. We instead do that here. Some urls |
+ // which are handled using intents, the url is never commited. In which case we |
+ // still want to swap to happen. |
+ destroyContentViewCore(); |
+ } |
+ } |
+ }; |
+ } |
+ |
+ @Override |
+ public void onDestroyed(Tab tab) { |
+ destroy(); |
+ } |
+ |
+ @CalledByNative |
+ private long getNativePtr() { |
+ return mNativeBackgroundContentViewHelperPtr; |
+ } |
+ |
+ public void recordBack() { |
+ if (mRecordUma) RecordUserAction.record("MobilePreviewPageBack"); |
+ } |
+ |
+ public void recordReload() { |
+ if (mRecordUma) RecordUserAction.record("MobilePreviewPageReload"); |
+ } |
+ |
+ public void recordTabClose() { |
+ if (mRecordUma) RecordUserAction.record("MobilePreviewPageTabClose"); |
+ } |
+ |
+ public void recordSingleTap() { |
+ if (mRecordUma) RecordUserAction.record("MobilePreviewPageSingleTap"); |
+ } |
+ |
+ private native long nativeInit(); |
+ private native void nativeDestroy(long nativeBackgroundContentViewHelper); |
+ private native void nativeSetWebContents(long nativeBackgroundContentViewHelper, |
+ ContentViewCore contentViewCore, WebContentsDelegateAndroid delegate); |
+ private native void nativeDestroyWebContents(long nativeBackgroundContentViewHelper); |
+ private native void nativeReleaseWebContents(long nativeBackgroundContentViewHelper); |
+ private native void nativeMergeHistoryFrom(long nativeBackgroundContentViewHelper, |
+ WebContents webContents); |
+ private native void nativeUnloadAndDeleteWebContents(long nativeBackgroundContentViewHelper, |
+ WebContents webContents); |
+ private native int nativeGetSwapTimeoutMs(long nativeBackgroundContentViewHelper); |
+ private native int nativeGetNumFramesNeededForSwap(long nativeBackgroundContentViewHelper); |
+} |