// 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.customtabs;
import android.app.Application;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.SystemClock;
import android.support.customtabs.CustomTabsCallback;
import android.support.customtabs.CustomTabsSessionToken;
import android.text.TextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.prerender.ExternalPrerenderHandler;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.components.security_state.ConnectionSecurityLevel;
import org.chromium.content_public.browser.ContentBitmapCallback;
import org.chromium.content_public.browser.LoadUrlParams;
import java.util.concurrent.TimeUnit;
/**
* A {@link TabObserver} that also handles custom tabs specific logging and messaging.
*/
class CustomTabObserver extends EmptyTabObserver {
private final CustomTabsConnection mCustomTabsConnection;
private final CustomTabsSessionToken mSession;
private final boolean mOpenedByChrome;
private float mScaleForNavigationInfo = 1f;
private long mIntentReceivedTimestamp;
private long mPageLoadStartedTimestamp;
private boolean mScreenshotTakenForCurrentNavigation;
private static final int STATE_RESET = 0;
private static final int STATE_WAITING_LOAD_START = 1;
private static final int STATE_WAITING_LOAD_FINISH = 2;
private int mCurrentState;
public CustomTabObserver(
Application application, CustomTabsSessionToken session, boolean openedByChrome) {
if (openedByChrome) {
mCustomTabsConnection = null;
} else {
mCustomTabsConnection = CustomTabsConnection.getInstance(application);
}
mSession = session;
if (!openedByChrome && mCustomTabsConnection.shouldSendNavigationInfoForSession(mSession)) {
float desiredWidth = application.getResources().getDimensionPixelSize(
R.dimen.custom_tabs_screenshot_width);
float desiredHeight = application.getResources().getDimensionPixelSize(
R.dimen.custom_tabs_screenshot_height);
Rect bounds = ExternalPrerenderHandler.estimateContentSize(application, false);
mScaleForNavigationInfo = (bounds.width() == 0 || bounds.height() == 0) ? 1f :
Math.min(desiredWidth / bounds.width(), desiredHeight / bounds.height());
}
mOpenedByChrome = openedByChrome;
resetPageLoadTracking();
}
/**
* Tracks the next page load, with timestamp as the origin of time.
*/
public void trackNextPageLoadFromTimestamp(long timestamp) {
mIntentReceivedTimestamp = timestamp;
mCurrentState = STATE_WAITING_LOAD_START;
}
@Override
public void onLoadUrl(Tab tab, LoadUrlParams params, int loadType) {
if (mCustomTabsConnection != null) {
mCustomTabsConnection.registerLaunch(mSession, params.getUrl());
}
}
@Override
public void onPageLoadStarted(Tab tab, String url) {
if (mCurrentState == STATE_WAITING_LOAD_START) {
mPageLoadStartedTimestamp = SystemClock.elapsedRealtime();
mCurrentState = STATE_WAITING_LOAD_FINISH;
} else if (mCurrentState == STATE_WAITING_LOAD_FINISH) {
if (mCustomTabsConnection != null) {
mCustomTabsConnection.notifyNavigationEvent(
mSession, CustomTabsCallback.NAVIGATION_ABORTED);
mCustomTabsConnection.sendNavigationInfo(
mSession, tab.getUrl(), tab.getTitle(), null);
}
mPageLoadStartedTimestamp = SystemClock.elapsedRealtime();
}
if (mCustomTabsConnection != null) {
mCustomTabsConnection.notifyNavigationEvent(
mSession, CustomTabsCallback.NAVIGATION_STARTED);
mScreenshotTakenForCurrentNavigation = false;
}
}
@Override
public void onShown(Tab tab) {
if (mCustomTabsConnection != null) {
mCustomTabsConnection.notifyNavigationEvent(
mSession, CustomTabsCallback.TAB_SHOWN);
}
}
@Override
public void onHidden(Tab tab) {
if (!mScreenshotTakenForCurrentNavigation) captureNavigationInfo(tab);
}
@Override
public void onPageLoadFinished(Tab tab) {
long pageLoadFinishedTimestamp = SystemClock.elapsedRealtime();
if (mCustomTabsConnection != null) {
mCustomTabsConnection.notifyNavigationEvent(
mSession, CustomTabsCallback.NAVIGATION_FINISHED);
}
// Both histograms (commit and PLT) are reported here, to make sure
// that they are always recorded together, and that we only record
// commits for successful navigations.
if (mCurrentState == STATE_WAITING_LOAD_FINISH && mIntentReceivedTimestamp > 0) {
long timeToPageLoadStartedMs = mPageLoadStartedTimestamp - mIntentReceivedTimestamp;
long timeToPageLoadFinishedMs =
pageLoadFinishedTimestamp - mIntentReceivedTimestamp;
String histogramPrefix = mOpenedByChrome ? "ChromeGeneratedCustomTab" : "CustomTabs";
// Same bounds and bucket count as "Startup.FirstCommitNavigationTime"
RecordHistogram.recordCustomTimesHistogram(
histogramPrefix + ".IntentToFirstCommitNavigationTime", timeToPageLoadStartedMs,
1, TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS, 225);
// Same bounds and bucket count as PLT histograms.
RecordHistogram.recordCustomTimesHistogram(histogramPrefix + ".IntentToPageLoadedTime",
timeToPageLoadFinishedMs, 10, TimeUnit.MINUTES.toMillis(10),
TimeUnit.MILLISECONDS, 100);
}
resetPageLoadTracking();
captureNavigationInfo(tab);
}
@Override
public void onDidAttachInterstitialPage(Tab tab) {
if (tab.getSecurityLevel() != ConnectionSecurityLevel.DANGEROUS) return;
resetPageLoadTracking();
if (mCustomTabsConnection != null) {
mCustomTabsConnection.notifyNavigationEvent(
mSession, CustomTabsCallback.NAVIGATION_FAILED);
}
}
@Override
public void onPageLoadFailed(Tab tab, int errorCode) {
resetPageLoadTracking();
if (mCustomTabsConnection != null) {
mCustomTabsConnection.notifyNavigationEvent(
mSession, CustomTabsCallback.NAVIGATION_FAILED);
}
}
private void resetPageLoadTracking() {
mCurrentState = STATE_RESET;
mIntentReceivedTimestamp = -1;
}
private void captureNavigationInfo(final Tab tab) {
if (mCustomTabsConnection == null) return;
if (!mCustomTabsConnection.shouldSendNavigationInfoForSession(mSession)) return;
final ContentBitmapCallback callback = new ContentBitmapCallback() {
@Override
public void onFinishGetBitmap(Bitmap bitmap, int response) {
if (TextUtils.isEmpty(tab.getTitle()) && bitmap == null) return;
mCustomTabsConnection.sendNavigationInfo(
mSession, tab.getUrl(), tab.getTitle(), bitmap);
}
};
// Delay screenshot capture since the page might be doing post load tasks. And this also
// gives time to get rid of any redirects and avoid capturing screenshots for those.
ThreadUtils.postOnUiThreadDelayed(new Runnable() {
@Override
public void run() {
if (!tab.isHidden() && mCurrentState != STATE_RESET) return;
if (tab.getWebContents() == null) return;
tab.getWebContents().getContentBitmapAsync(
Bitmap.Config.ARGB_8888, mScaleForNavigationInfo, new Rect(), callback);
mScreenshotTakenForCurrentNavigation = true;
}
}, 1000);
}
}