| // 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.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.VisibleForTesting; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.chrome.browser.AppHooks; |
| import org.chromium.chrome.browser.SwipeRefreshHandler; |
| import org.chromium.chrome.browser.display_cutout.DisplayCutoutController; |
| import org.chromium.chrome.browser.fullscreen.FullscreenManager; |
| import org.chromium.chrome.browser.infobar.InfoBarContainer; |
| import org.chromium.chrome.browser.media.MediaCaptureNotificationService; |
| 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.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * WebContentsObserver used by Tab. |
| */ |
| public class TabWebContentsObserver extends TabWebContentsUserData { |
| // URL didFailLoad error code. Should match the value in net_error_list.h. |
| public static final int BLOCKED_BY_ADMINISTRATOR = -22; |
| |
| private static final Class<TabWebContentsObserver> USER_DATA_KEY = TabWebContentsObserver.class; |
| |
| /** 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({TabRendererExitStatus.OOM_PROTECTED_IN_RUNNING_APP, |
| TabRendererExitStatus.OOM_PROTECTED_IN_PAUSED_APP, |
| TabRendererExitStatus.OOM_PROTECTED_IN_BACKGROUND_APP, |
| TabRendererExitStatus.NOT_PROTECTED_IN_RUNNING_APP, |
| TabRendererExitStatus.NOT_PROTECTED_IN_PAUSED_APP, |
| TabRendererExitStatus.NOT_PROTECTED_IN_BACKGROUND_APP}) |
| @Retention(RetentionPolicy.SOURCE) |
| private @interface TabRendererExitStatus { |
| int OOM_PROTECTED_IN_RUNNING_APP = 0; |
| int OOM_PROTECTED_IN_PAUSED_APP = 1; |
| int OOM_PROTECTED_IN_BACKGROUND_APP = 2; |
| int NOT_PROTECTED_IN_RUNNING_APP = 3; |
| int NOT_PROTECTED_IN_PAUSED_APP = 4; |
| int NOT_PROTECTED_IN_BACKGROUND_APP = 5; |
| int NUM_ENTRIES = 6; |
| } |
| |
| private final Tab mTab; |
| private WebContentsObserver mObserver; |
| |
| public static void from(Tab tab) { |
| TabWebContentsObserver observer = get(tab); |
| if (observer == null) { |
| tab.getUserDataHost().setUserData(USER_DATA_KEY, new TabWebContentsObserver(tab)); |
| } |
| } |
| |
| @VisibleForTesting |
| public static TabWebContentsObserver get(Tab tab) { |
| return tab.getUserDataHost().getUserData(USER_DATA_KEY); |
| } |
| |
| private TabWebContentsObserver(Tab tab) { |
| super(tab); |
| mTab = tab; |
| } |
| |
| @Override |
| public void initWebContents(WebContents webContents) { |
| mObserver = new Observer(webContents); |
| } |
| |
| @Override |
| public void cleanupWebContents(WebContents webContents) { |
| mObserver.destroy(); |
| mObserver = null; |
| } |
| |
| @VisibleForTesting |
| public void simulateRendererKilledForTesting(boolean wasOomProtected) { |
| if (mObserver != null) mObserver.renderProcessGone(wasOomProtected); |
| } |
| |
| private class Observer extends WebContentsObserver { |
| public Observer(WebContents webContents) { |
| super(webContents); |
| } |
| |
| @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() || SadTab.isShowing(mTab)) 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; |
| if (processWasOomProtected) { |
| if (applicationRunning) { |
| rendererExitStatus = TabRendererExitStatus.OOM_PROTECTED_IN_RUNNING_APP; |
| } else if (applicationPaused) { |
| rendererExitStatus = TabRendererExitStatus.OOM_PROTECTED_IN_PAUSED_APP; |
| } else { |
| rendererExitStatus = TabRendererExitStatus.OOM_PROTECTED_IN_BACKGROUND_APP; |
| } |
| } else { |
| if (applicationRunning) { |
| rendererExitStatus = TabRendererExitStatus.NOT_PROTECTED_IN_RUNNING_APP; |
| } else if (applicationPaused) { |
| rendererExitStatus = TabRendererExitStatus.NOT_PROTECTED_IN_PAUSED_APP; |
| } else { |
| rendererExitStatus = TabRendererExitStatus.NOT_PROTECTED_IN_BACKGROUND_APP; |
| } |
| } |
| RecordHistogram.recordEnumeratedHistogram("Tab.RendererExitStatus", rendererExitStatus, |
| TabRendererExitStatus.NUM_ENTRIES); |
| |
| int activityState = ApplicationStatus.getStateForActivity( |
| mTab.getWindowAndroid().getActivity().get()); |
| int rendererCrashStatus = TAB_RENDERER_CRASH_STATUS_MAX; |
| if (mTab.isHidden() || activityState == ActivityState.PAUSED |
| || activityState == ActivityState.STOPPED |
| || activityState == ActivityState.DESTROYED) { |
| // The tab crashed in background or was killed by the OS out-of-memory killer. |
| mTab.setNeedsReload(); |
| 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; |
| SadTab.from(mTab).show(); |
| // This is necessary to correlate histogram data with stability counts. |
| RecordHistogram.recordBooleanHistogram("Stability.Android.RendererCrash", true); |
| } |
| RecordHistogram.recordEnumeratedHistogram( |
| "Tab.RendererCrashStatus", rendererCrashStatus, TAB_RENDERER_CRASH_STATUS_MAX); |
| |
| mTab.handleTabCrash(); |
| } |
| |
| @Override |
| public void didFinishLoad(long frameId, String validatedUrl, boolean isMainFrame) { |
| if (mTab.getNativePage() != null) { |
| mTab.pushNativePageStateToNavigationEntry(); |
| } |
| if (isMainFrame) mTab.didFinishPageLoad(validatedUrl); |
| 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) { |
| 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) { |
| assert description != null; |
| |
| 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, |
| long navigationHandleProxy) { |
| if (isInMainFrame && !isSameDocument) { |
| mTab.didStartPageLoad(url); |
| } |
| |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().onDidStartNavigation( |
| mTab, url, isInMainFrame, isSameDocument, navigationHandleProxy); |
| } |
| } |
| |
| @Override |
| public void didRedirectNavigation( |
| String url, boolean isInMainFrame, long navigationHandleProxy) { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().onDidRedirectNavigation( |
| mTab, url, isInMainFrame, navigationHandleProxy); |
| } |
| } |
| |
| @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) { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().onDidFinishNavigation(mTab, url, isInMainFrame, isErrorPage, |
| hasCommitted, isSameDocument, isFragmentNavigation, pageTransition, |
| errorCode, httpStatusCode); |
| } |
| |
| if (errorCode != 0) { |
| if (isInMainFrame) mTab.didFailPageLoad(errorCode); |
| |
| recordErrorInPolicyAuditor(url, errorDescription, errorCode); |
| } |
| |
| if (!hasCommitted) return; |
| |
| 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.exitPersistentFullscreenMode(); |
| } |
| |
| if (isInMainFrame) { |
| // Stop swipe-to-refresh animation. |
| SwipeRefreshHandler handler = SwipeRefreshHandler.get(mTab); |
| if (handler != null) handler.didStopRefreshing(); |
| } |
| } |
| |
| @Override |
| public void didFirstVisuallyNonEmptyPaint() { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().didFirstVisuallyNonEmptyPaint(mTab); |
| } |
| } |
| |
| @Override |
| public void didChangeThemeColor(int color) { |
| TabThemeColorHelper.get(mTab).updateIfNeeded(true); |
| } |
| |
| @Override |
| public void didAttachInterstitialPage() { |
| InfoBarContainer.get(mTab).setVisibility(View.INVISIBLE); |
| mTab.showRenderedPage(); |
| |
| 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() { |
| InfoBarContainer.get(mTab).setVisibility(View.VISIBLE); |
| |
| 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 navigationEntriesDeleted() { |
| mTab.notifyNavigationEntriesDeleted(); |
| } |
| |
| @Override |
| public void viewportFitChanged(@WebContentsObserver.ViewportFitType int value) { |
| DisplayCutoutController.from(mTab).setViewportFit(value); |
| } |
| |
| @Override |
| public void didReloadLoFiImages() { |
| RewindableIterator<TabObserver> observers = mTab.getTabObservers(); |
| while (observers.hasNext()) { |
| observers.next().didReloadLoFiImages(mTab); |
| } |
| } |
| |
| @Override |
| public void destroy() { |
| MediaCaptureNotificationService.updateMediaNotificationForTab( |
| mTab.getApplicationContext(), mTab.getId(), 0, mTab.getUrl()); |
| super.destroy(); |
| } |
| } |
| } |