// 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.os.SystemClock; import android.support.annotation.IntDef; import android.view.View; import org.chromium.base.ActivityState; import org.chromium.base.ApplicationState; import org.chromium.base.ApplicationStatus; import org.chromium.base.Log; import org.chromium.base.ObserverList.RewindableIterator; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.AppHooks; import org.chromium.chrome.browser.fullscreen.FullscreenManager; import org.chromium.chrome.browser.media.MediaCaptureNotificationService; import org.chromium.chrome.browser.metrics.UmaSessionStats; import org.chromium.chrome.browser.metrics.UmaUtils; import org.chromium.chrome.browser.policy.PolicyAuditor; import org.chromium.chrome.browser.policy.PolicyAuditor.AuditEvent; import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContentsObserver; import java.util.concurrent.TimeUnit; /** * WebContentsObserver used by Tab. */ public class TabWebContentsObserver extends WebContentsObserver { // URL didFailLoad error code. Should match the value in net_error_list.h. public static final int BLOCKED_BY_ADMINISTRATOR = -22; /** Used for logging. */ private static final String TAG = "TabWebContentsObs"; // TabRendererCrashStatus defined in tools/metrics/histograms/histograms.xml. private static final int TAB_RENDERER_CRASH_STATUS_SHOWN_IN_FOREGROUND_APP = 0; private static final int TAB_RENDERER_CRASH_STATUS_HIDDEN_IN_FOREGROUND_APP = 1; private static final int TAB_RENDERER_CRASH_STATUS_HIDDEN_IN_BACKGROUND_APP = 2; private static final int TAB_RENDERER_CRASH_STATUS_MAX = 3; // TabRendererExitStatus defined in tools/metrics/histograms/histograms.xml. // Designed to replace TabRendererCrashStatus if numbers line up. @IntDef({TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_RUNNING_APP, TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_PAUSED_APP, TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_BACKGROUND_APP, TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_RUNNING_APP, TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_PAUSED_APP, TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_BACKGROUND_APP, TAB_RENDERER_EXIT_STATUS_MAX}) private @interface TabRendererExitStatus {} private static final int TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_RUNNING_APP = 0; private static final int TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_PAUSED_APP = 1; private static final int TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_BACKGROUND_APP = 2; private static final int TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_RUNNING_APP = 3; private static final int TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_PAUSED_APP = 4; private static final int TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_BACKGROUND_APP = 5; private static final int TAB_RENDERER_EXIT_STATUS_MAX = 6; private final Tab mTab; public TabWebContentsObserver(WebContents webContents, Tab tab) { super(webContents); mTab = tab; } @Override public void renderProcessGone(boolean processWasOomProtected) { Log.i(TAG, "renderProcessGone() for tab id: " + mTab.getId() + ", oom protected: " + Boolean.toString(processWasOomProtected) + ", already needs reload: " + Boolean.toString(mTab.needsReload())); // Do nothing for subsequent calls that happen while the tab remains crashed. This // can occur when the tab is in the background and it shares the renderer with other // tabs. After the renderer crashes, the WebContents of its tabs are still around // and they still share the RenderProcessHost. When one of the tabs reloads spawning // a new renderer for the shared RenderProcessHost and the new renderer crashes // again, all tabs sharing this renderer will be notified about the crash (including // potential background tabs that did not reload yet). if (mTab.needsReload() || mTab.isShowingSadTab()) return; // This will replace TabRendererCrashStatus if numbers line up. int appState = ApplicationStatus.getStateForApplication(); boolean applicationRunning = (appState == ApplicationState.HAS_RUNNING_ACTIVITIES); boolean applicationPaused = (appState == ApplicationState.HAS_PAUSED_ACTIVITIES); @TabRendererExitStatus int rendererExitStatus = TAB_RENDERER_EXIT_STATUS_MAX; if (processWasOomProtected) { if (applicationRunning) { rendererExitStatus = TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_RUNNING_APP; } else if (applicationPaused) { rendererExitStatus = TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_PAUSED_APP; } else { rendererExitStatus = TAB_RENDERER_EXIT_STATUS_OOM_PROTECTED_IN_BACKGROUND_APP; } } else { if (applicationRunning) { rendererExitStatus = TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_RUNNING_APP; } else if (applicationPaused) { rendererExitStatus = TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_PAUSED_APP; } else { rendererExitStatus = TAB_RENDERER_EXIT_STATUS_NOT_PROTECTED_IN_BACKGROUND_APP; } } RecordHistogram.recordEnumeratedHistogram( "Tab.RendererExitStatus", rendererExitStatus, TAB_RENDERER_EXIT_STATUS_MAX); int activityState = ApplicationStatus.getStateForActivity( mTab.getWindowAndroid().getActivity().get()); int rendererCrashStatus = TAB_RENDERER_CRASH_STATUS_MAX; if (!processWasOomProtected || activityState == ActivityState.PAUSED || activityState == ActivityState.STOPPED || activityState == ActivityState.DESTROYED) { // The tab crashed in background or was killed by the OS out-of-memory killer. //setNeedsReload(true); mTab.setNeedsReload(true); if (applicationRunning) { rendererCrashStatus = TAB_RENDERER_CRASH_STATUS_HIDDEN_IN_FOREGROUND_APP; } else { rendererCrashStatus = TAB_RENDERER_CRASH_STATUS_HIDDEN_IN_BACKGROUND_APP; } } else { rendererCrashStatus = TAB_RENDERER_CRASH_STATUS_SHOWN_IN_FOREGROUND_APP; mTab.showSadTab(); // This is necessary to correlate histogram data with stability counts. UmaSessionStats.logRendererCrash(); } RecordHistogram.recordEnumeratedHistogram( "Tab.RendererCrashStatus", rendererCrashStatus, TAB_RENDERER_CRASH_STATUS_MAX); mTab.handleTabCrash(); } @Override public void navigationEntryCommitted() { if (mTab.getNativePage() != null) { mTab.pushNativePageStateToNavigationEntry(); } } @Override public void didFinishLoad(long frameId, String validatedUrl, boolean isMainFrame) { if (isMainFrame) mTab.didFinishPageLoad(); PolicyAuditor auditor = AppHooks.get().getPolicyAuditor(); auditor.notifyAuditEvent( mTab.getApplicationContext(), AuditEvent.OPEN_URL_SUCCESS, validatedUrl, ""); } @Override public void didFailLoad( boolean isMainFrame, int errorCode, String description, String failingUrl) { mTab.updateThemeColorIfNeeded(true); RewindableIterator<TabObserver> observers = mTab.getTabObservers(); while (observers.hasNext()) { observers.next().onDidFailLoad(mTab, isMainFrame, errorCode, description, failingUrl); } if (isMainFrame) mTab.didFailPageLoad(errorCode); recordErrorInPolicyAuditor(failingUrl, description, errorCode); } private void recordErrorInPolicyAuditor(String failingUrl, String description, int errorCode) { PolicyAuditor auditor = AppHooks.get().getPolicyAuditor(); auditor.notifyAuditEvent(mTab.getApplicationContext(), AuditEvent.OPEN_URL_FAILURE, failingUrl, description); if (errorCode == BLOCKED_BY_ADMINISTRATOR) { auditor.notifyAuditEvent( mTab.getApplicationContext(), AuditEvent.OPEN_URL_BLOCKED, failingUrl, ""); } } @Override public void titleWasSet(String title) { mTab.updateTitle(title); } @Override public void didStartNavigation( String url, boolean isInMainFrame, boolean isSameDocument, boolean isErrorPage) { if (isInMainFrame && !isSameDocument) { mTab.didStartPageLoad(url, isErrorPage); } RewindableIterator<TabObserver> observers = mTab.getTabObservers(); while (observers.hasNext()) { observers.next().onDidStartNavigation( mTab, url, isInMainFrame, isSameDocument, isErrorPage); } } @Override public void didFinishNavigation(String url, boolean isInMainFrame, boolean isErrorPage, boolean hasCommitted, boolean isSameDocument, boolean isFragmentNavigation, Integer pageTransition, int errorCode, String errorDescription, int httpStatusCode) { RewindableIterator<TabObserver> observers = mTab.getTabObservers(); while (observers.hasNext()) { observers.next().onDidFinishNavigation(mTab, url, isInMainFrame, isErrorPage, hasCommitted, isSameDocument, isFragmentNavigation, pageTransition, errorCode, httpStatusCode); } if (errorCode != 0) { mTab.updateThemeColorIfNeeded(true); if (isInMainFrame) mTab.didFailPageLoad(errorCode); recordErrorInPolicyAuditor(url, errorDescription, errorCode); } if (!hasCommitted) return; if (isInMainFrame && UmaUtils.isRunningApplicationStart()) { // Current median is 550ms, and long tail is very long. ZoomedIn gives good view of the // median and ZoomedOut gives a good overview. RecordHistogram.recordCustomTimesHistogram( "Startup.FirstCommitNavigationTime2.ZoomedIn", SystemClock.uptimeMillis() - UmaUtils.getForegroundStartTime(), 200, 1000, TimeUnit.MILLISECONDS, 100); // For ZoomedOut very rarely is it under 50ms and this range matches // CustomTabs.IntentToFirstCommitNavigationTime2.ZoomedOut. RecordHistogram.recordCustomTimesHistogram( "Startup.FirstCommitNavigationTime2.ZoomedOut", SystemClock.uptimeMillis() - UmaUtils.getForegroundStartTime(), 50, TimeUnit.MINUTES.toMillis(10), TimeUnit.MILLISECONDS, 50); UmaUtils.setRunningApplicationStart(false); } if (isInMainFrame) { mTab.setIsTabStateDirty(true); mTab.updateTitle(); mTab.handleDidFinishNavigation(url, pageTransition); mTab.setIsShowingErrorPage(isErrorPage); } observers.rewind(); while (observers.hasNext()) { observers.next().onUrlUpdated(mTab); } FullscreenManager fullscreenManager = mTab.getFullscreenManager(); if (isInMainFrame && !isSameDocument && fullscreenManager != null) { fullscreenManager.setPersistentFullscreenMode(false); } if (isInMainFrame) { mTab.stopSwipeRefreshHandler(); } } @Override public void didFirstVisuallyNonEmptyPaint() { RewindableIterator<TabObserver> observers = mTab.getTabObservers(); while (observers.hasNext()) { observers.next().didFirstVisuallyNonEmptyPaint(mTab); } } @Override public void didChangeThemeColor(int color) { mTab.updateThemeColorIfNeeded(true); } @Override public void didAttachInterstitialPage() { mTab.getInfoBarContainer().setVisibility(View.INVISIBLE); mTab.showRenderedPage(); mTab.updateThemeColorIfNeeded(false); RewindableIterator<TabObserver> observers = mTab.getTabObservers(); while (observers.hasNext()) { observers.next().onDidAttachInterstitialPage(mTab); } mTab.notifyLoadProgress(mTab.getProgress()); mTab.updateFullscreenEnabledState(); PolicyAuditor auditor = AppHooks.get().getPolicyAuditor(); auditor.notifyCertificateFailure( PolicyAuditor.nativeGetCertificateFailure(mTab.getWebContents()), mTab.getApplicationContext()); } @Override public void didDetachInterstitialPage() { mTab.getInfoBarContainer().setVisibility(View.VISIBLE); mTab.updateThemeColorIfNeeded(false); RewindableIterator<TabObserver> observers = mTab.getTabObservers(); while (observers.hasNext()) { observers.next().onDidDetachInterstitialPage(mTab); } mTab.notifyLoadProgress(mTab.getProgress()); mTab.updateFullscreenEnabledState(); if (!mTab.maybeShowNativePage(mTab.getUrl(), false)) { mTab.showRenderedPage(); } } @Override public void destroy() { MediaCaptureNotificationService.updateMediaNotificationForTab( mTab.getApplicationContext(), mTab.getId(), 0, mTab.getUrl()); super.destroy(); } }