blob: 562656b8fe23fddd2f5f5e2a0ccc01ba065d39ea [file] [log] [blame]
// 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.os.Handler;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.ObserverList.RewindableIterator;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.SwipeRefreshHandler;
import org.chromium.chrome.browser.display_cutout.DisplayCutoutTabHelper;
import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherImpl;
import org.chromium.chrome.browser.media.MediaCaptureNotificationServiceImpl;
import org.chromium.chrome.browser.policy.PolicyAuditor;
import org.chromium.chrome.browser.policy.PolicyAuditor.AuditEvent;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.content_public.browser.GlobalRenderFrameHostId;
import org.chromium.content_public.browser.LifecycleState;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.net.NetError;
import org.chromium.url.GURL;
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 TabImpl mTab;
private final ObserverList<Callback<WebContents>> mInitObservers = new ObserverList<>();
private final Handler mHandler = new Handler();
private WebContentsObserver mObserver;
private GURL mLastUrl;
public static TabWebContentsObserver from(Tab tab) {
TabWebContentsObserver observer = get(tab);
if (observer == null) {
observer = new TabWebContentsObserver(tab);
tab.getUserDataHost().setUserData(USER_DATA_KEY, observer);
}
return observer;
}
@VisibleForTesting
public static TabWebContentsObserver get(Tab tab) {
return tab.getUserDataHost().getUserData(USER_DATA_KEY);
}
private TabWebContentsObserver(Tab tab) {
super(tab);
mTab = (TabImpl) tab;
}
/**
* Adds an observer triggered when |initWebContents| is invoked.
* <p>A newly created tab adding this observer misses the event because
* |TabObserver.onContentChanged| -&gt; |TabWebContentsObserver.initWebContents|
* occurs before the observer is added. Manually trigger it here.
* @param observer Observer to add.
*/
public void addInitWebContentsObserver(Callback<WebContents> observer) {
if (mInitObservers.addObserver(observer) && mTab.getWebContents() != null) {
observer.onResult(mTab.getWebContents());
}
}
/**
* Remove the InitWebContents observer from the list.
*/
public void removeInitWebContentsObserver(Callback<WebContents> observer) {
mInitObservers.removeObserver(observer);
}
@Override
public void destroyInternal() {
mInitObservers.clear();
}
@Override
public void initWebContents(WebContents webContents) {
mObserver = new Observer(webContents);
// For browser tabs, we want to set accessibility focus to the page when it loads. This
// is not the default behavior for embedded web views.
WebContentsAccessibility.fromWebContents(webContents).setShouldFocusOnPageLoad(true);
// Enable image descriptions feature normally, but not for Chrome Custom Tabs.
WebContentsAccessibility.fromWebContents(webContents)
.setAllowImageDescriptions(!mTab.isCustomTab());
for (Callback<WebContents> callback : mInitObservers) callback.onResult(webContents);
}
@Override
public void cleanupWebContents(WebContents webContents) {
if (mObserver != null) {
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;
// TODO(crbug.com/1074078): Remove the Handler and call SadTab directly when
// WebContentsObserverProxy observers' iterator concurrency issue is fixed.
// Showing the SadTab will cause the content view hosting WebContents to lose focus.
// Post the show in order to avoid immediately triggering
// {@link WebContentsObserver#onWebContentsLostFocus}. This will ensure all
// observers in {@link WebContentsObserverProxy} receive callbacks for
// {@link WebContentsObserver#renderProcessGone} first.
SadTab sadTab = SadTab.from(mTab);
(new Handler()).post(() -> {
sadTab.show(mTab.getThemedApplicationContext(),
/* suggestionAction= */ () -> {
Activity activity = mTab.getWindowAndroid().getActivity().get();
assert activity != null;
HelpAndFeedbackLauncherImpl.getInstance().show(activity,
activity.getString(R.string.help_context_sad_tab),
Profile.fromWebContents(mTab.getWebContents()), null);
},
/* buttonAction= */ () -> {
if (sadTab.showSendFeedbackView()) {
mTab.getActivity().startHelpAndFeedback(mTab.getUrl().getSpec(),
"MobileSadTabFeedback",
Profile.fromWebContents(mTab.getWebContents()));
} else {
mTab.reload();
}
});
});
// 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(GlobalRenderFrameHostId frameId, GURL url, boolean isKnownValid,
boolean isInPrimaryMainFrame, @LifecycleState int frameLifecycleState) {
assert isKnownValid;
if (frameLifecycleState == LifecycleState.ACTIVE) {
if (mTab.getNativePage() != null) {
mTab.pushNativePageStateToNavigationEntry();
}
if (isInPrimaryMainFrame) mTab.didFinishPageLoad(url);
}
PolicyAuditor auditor = AppHooks.get().getPolicyAuditor();
auditor.notifyAuditEvent(ContextUtils.getApplicationContext(),
AuditEvent.OPEN_URL_SUCCESS, url.getSpec(), "");
}
@Override
public void didFailLoad(boolean isInPrimaryMainFrame, int errorCode, GURL failingGurl,
@LifecycleState int frameLifecycleState) {
if (isInPrimaryMainFrame) {
mTab.didFailPageLoad(errorCode);
}
recordErrorInPolicyAuditor(failingGurl.getSpec(), "net error: " + errorCode, errorCode);
}
private void recordErrorInPolicyAuditor(
String failingUrl, String description, int errorCode) {
assert description != null;
PolicyAuditor auditor = AppHooks.get().getPolicyAuditor();
auditor.notifyAuditEvent(ContextUtils.getApplicationContext(),
AuditEvent.OPEN_URL_FAILURE, failingUrl, description);
if (errorCode == BLOCKED_BY_ADMINISTRATOR) {
auditor.notifyAuditEvent(ContextUtils.getApplicationContext(),
AuditEvent.OPEN_URL_BLOCKED, failingUrl, "");
}
}
@Override
public void titleWasSet(String title) {
mTab.updateTitle(title);
}
@Override
public void didStartNavigation(NavigationHandle navigation) {
if (navigation.isInPrimaryMainFrame() && !navigation.isSameDocument()) {
mTab.didStartPageLoad(navigation.getUrl());
}
RewindableIterator<TabObserver> observers = mTab.getTabObservers();
while (observers.hasNext()) {
observers.next().onDidStartNavigation(mTab, navigation);
}
}
@Override
public void didRedirectNavigation(NavigationHandle navigation) {
RewindableIterator<TabObserver> observers = mTab.getTabObservers();
while (observers.hasNext()) {
observers.next().onDidRedirectNavigation(mTab, navigation);
}
}
@Override
public void didFinishNavigation(NavigationHandle navigation) {
RewindableIterator<TabObserver> observers = mTab.getTabObservers();
while (observers.hasNext()) {
observers.next().onDidFinishNavigation(mTab, navigation);
}
if (navigation.errorCode() != NetError.OK) {
if (navigation.isInPrimaryMainFrame()) mTab.didFailPageLoad(navigation.errorCode());
recordErrorInPolicyAuditor(navigation.getUrl().getSpec(),
navigation.errorDescription(), navigation.errorCode());
}
mLastUrl = navigation.getUrl();
if (!navigation.hasCommitted()) return;
if (navigation.isInPrimaryMainFrame()) {
if (!mTab.isDestroyed()) {
TabStateAttributes.from(mTab).setIsTabStateDirty(true);
}
mTab.updateTitle();
mTab.handleDidFinishNavigation(navigation.getUrl(), navigation.pageTransition());
mTab.setIsShowingErrorPage(navigation.isErrorPage());
observers.rewind();
while (observers.hasNext()) {
observers.next().onUrlUpdated(mTab);
}
}
if (navigation.isInPrimaryMainFrame()) {
// Stop swipe-to-refresh animation.
SwipeRefreshHandler handler = SwipeRefreshHandler.get(mTab);
if (handler != null) handler.didStopRefreshing();
}
}
@Override
public void loadProgressChanged(float progress) {
if (!mTab.isLoading()) return;
mTab.notifyLoadProgress(progress);
}
@Override
public void didFirstVisuallyNonEmptyPaint() {
RewindableIterator<TabObserver> observers = mTab.getTabObservers();
while (observers.hasNext()) {
observers.next().didFirstVisuallyNonEmptyPaint(mTab);
}
}
@Override
public void didChangeThemeColor() {
mTab.updateThemeColor(mTab.getWebContents().getThemeColor());
}
@Override
public void navigationEntriesDeleted() {
mTab.notifyNavigationEntriesDeleted();
}
@Override
public void navigationEntriesChanged() {
if (!mTab.isDestroyed()) {
TabStateAttributes.from(mTab).setIsTabStateDirty(true);
}
}
@Override
public void viewportFitChanged(@WebContentsObserver.ViewportFitType int value) {
DisplayCutoutTabHelper.from(mTab).setViewportFit(value);
}
@Override
public void destroy() {
MediaCaptureNotificationServiceImpl.updateMediaNotificationForTab(
ContextUtils.getApplicationContext(), mTab.getId(), null, mLastUrl);
super.destroy();
}
}
}