blob: e6ef840e269fa7a133d81e18b941b506ea8b3fb8 [file] [log] [blame]
// Copyright 2014 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.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.text.TextUtils;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
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.TraceEvent;
import org.chromium.base.UserDataHost;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityUtils;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.WebContentsFactory;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.compositor.CompositorViewHolder;
import org.chromium.chrome.browser.content.ContentUtils;
import org.chromium.chrome.browser.contextmenu.ContextMenuPopulatorFactory;
import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.incognito.IncognitoUtils;
import org.chromium.chrome.browser.native_page.NativePageAssassin;
import org.chromium.chrome.browser.night_mode.NightModeUtils;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.paint_preview.StartupPaintPreviewHelper;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.rlz.RevenueStats;
import org.chromium.chrome.browser.tab.state.CriticalPersistedTabData;
import org.chromium.chrome.browser.ui.TabObscuringHandler;
import org.chromium.chrome.browser.ui.native_page.FrozenNativePage;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.components.security_state.ConnectionSecurityLevel;
import org.chromium.components.security_state.SecurityStateModel;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.components.version_info.VersionInfo;
import org.chromium.content_public.browser.ChildProcessImportance;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.content_public.browser.navigation_controller.UserAgentOverrideOption;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;
import java.nio.ByteBuffer;
/**
* Implementation of the interface {@link Tab}. Contains and manages a {@link ContentView}.
* This class is not intended to be extended.
*/
public class TabImpl implements Tab, TabObscuringHandler.Observer {
private static final long INVALID_TIMESTAMP = -1;
/** Used for logging. */
private static final String TAG = "Tab";
private static final String PRODUCT_VERSION = VersionInfo.getProductVersion();
private static final String REQUEST_DESKTOP_ENABLED_PARAM = "enabled";
private long mNativeTabAndroid;
/** Unique id of this tab (within its container). */
private final int mId;
/** Whether or not this tab is an incognito tab. */
private final boolean mIncognito;
/**
* An Application {@link Context}. Unlike {@link #mActivity}, this is the only one that is
* publicly exposed to help prevent leaking the {@link Activity}.
*/
private final Context mThemedApplicationContext;
/** Gives {@link Tab} a way to interact with the Android window. */
private WindowAndroid mWindowAndroid;
/** The current native page (e.g. chrome-native://newtab), or {@code null} if there is none. */
private NativePage mNativePage;
/** {@link WebContents} showing the current page, or {@code null} if the tab is frozen. */
private WebContents mWebContents;
/** The parent view of the ContentView and the InfoBarContainer. */
private ContentView mContentView;
/** The view provided by {@link TabViewManager} to be shown on top of Content view. */
private View mCustomView;
/**
* The {@link TabViewManager} associated with this Tab that is responsible for managing custom
* views.
*/
private TabViewManagerImpl mTabViewManager;
/** A list of Tab observers. These are used to broadcast Tab events to listeners. */
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
protected final ObserverList<TabObserver> mObservers = new ObserverList<>();
// Content layer Delegates
private TabWebContentsDelegateAndroidImpl mWebContentsDelegate;
/**
* Tab id to be used as a source tab in SyncedTabDelegate.
*/
private int mSourceTabId = INVALID_TAB_ID;
private boolean mIsClosing;
private boolean mIsShowingErrorPage;
/**
* Saves how this tab was launched (from a link, external app, etc) so that
* we can determine the different circumstances in which it should be
* closed. For example, a tab opened from an external app should be closed
* when the back stack is empty and the user uses the back hardware key. A
* standard tab however should be kept open and the entire activity should
* be moved to the background.
*/
private final @Nullable @TabLaunchType Integer mLaunchType;
private @Nullable @TabCreationState Integer mCreationState;
/**
* URL load to be performed lazily when the Tab is next shown.
*/
private LoadUrlParams mPendingLoadParams;
/**
* True while a page load is in progress.
*/
private boolean mIsLoading;
/**
* True while a restore page load is in progress.
*/
private boolean mIsBeingRestored;
/**
* Whether or not the Tab is currently visible to the user.
*/
private boolean mIsHidden = true;
/**
* Importance of the WebContents currently attached to this tab. Note the key difference from
* |mIsHidden| is that a tab is hidden when the application is hidden, but the importance is
* not affected by this signal.
*/
private @ChildProcessImportance int mImportance = ChildProcessImportance.NORMAL;
/** Whether the renderer is currently unresponsive. */
private boolean mIsRendererUnresponsive;
/**
* Whether didCommitProvisionalLoadForFrame() hasn't yet been called for the current native page
* (page A). To decrease latency, we show native pages in both loadUrl() and
* didCommitProvisionalLoadForFrame(). However, we mustn't show a new native page (page B) in
* loadUrl() if the current native page hasn't yet been committed. Otherwise, we'll show each
* page twice (A, B, A, B): the first two times in loadUrl(), the second two times in
* didCommitProvisionalLoadForFrame().
*/
private boolean mIsNativePageCommitPending;
private TabDelegateFactory mDelegateFactory;
/** Listens for views related to the tab to be attached or detached. */
private OnAttachStateChangeListener mAttachStateChangeListener;
/** Whether the tab can currently be interacted with. */
private boolean mInteractableState;
/** Whether or not the tab's active view is attached to the window. */
private boolean mIsViewAttachedToWindow;
private final UserDataHost mUserDataHost = new UserDataHost();
private boolean mIsDestroyed;
private ObservableSupplierImpl<Boolean> mIsTabSaveEnabledSupplier =
new ObservableSupplierImpl<>();
private final TabThemeColorHelper mThemeColorHelper;
private int mThemeColor;
private boolean mUsedCriticalPersistedTabData;
/**
* Creates an instance of a {@link TabImpl}.
*
* This constructor can be called before the native library has been loaded, so any additions
* must be vetted for library calls.
*
* Package-private. Use {@link TabBuilder} to create an instance.
*
* @param id The id this tab should be identified with.
* @param incognito Whether or not this tab is incognito.
* @param launchType Type indicating how this tab was launched.
* @param serializedCriticalPersistedTabData serialized {@link CriticalPersistedTabData}
*/
@SuppressLint("HandlerLeak")
TabImpl(int id, boolean incognito, @Nullable @TabLaunchType Integer launchType,
@Nullable ByteBuffer serializedCriticalPersistedTabData) {
mIsTabSaveEnabledSupplier.set(false);
mId = TabIdManager.getInstance().generateValidId(id);
mIncognito = incognito;
if (serializedCriticalPersistedTabData != null && useCriticalPersistedTabData()) {
CriticalPersistedTabData.build(this, serializedCriticalPersistedTabData, true);
mUsedCriticalPersistedTabData = true;
}
// Override the configuration for night mode to always stay in light mode until all UIs in
// Tab are inflated from activity context instead of application context. This is to
// avoid getting the wrong night mode state when application context inherits a system UI
// mode different from the UI mode we need.
// TODO(https://crbug.com/938641): Remove this once Tab UIs are all inflated from
// activity.
mThemedApplicationContext =
NightModeUtils.wrapContextWithNightModeConfig(ContextUtils.getApplicationContext(),
ActivityUtils.getThemeId(), false /*nightMode*/);
mLaunchType = launchType;
mAttachStateChangeListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
mIsViewAttachedToWindow = true;
updateInteractableState();
}
@Override
public void onViewDetachedFromWindow(View view) {
mIsViewAttachedToWindow = false;
updateInteractableState();
}
};
mTabViewManager = new TabViewManagerImpl(this);
mThemeColorHelper = new TabThemeColorHelper(this, this::updateThemeColor);
mThemeColor = TabState.UNSPECIFIED_THEME_COLOR;
}
@Override
public void addObserver(TabObserver observer) {
mObservers.addObserver(observer);
}
@Override
public void removeObserver(TabObserver observer) {
mObservers.removeObserver(observer);
}
@Override
public boolean hasObserver(TabObserver observer) {
return mObservers.hasObserver(observer);
}
@Override
public UserDataHost getUserDataHost() {
return mUserDataHost;
}
@Override
public WebContents getWebContents() {
return mWebContents;
}
@Override
public Context getContext() {
if (getWindowAndroid() == null) return mThemedApplicationContext;
Context context = getWindowAndroid().getContext().get();
return context == context.getApplicationContext() ? mThemedApplicationContext : context;
}
@Override
public WindowAndroid getWindowAndroid() {
return mWindowAndroid;
}
@Override
public void updateAttachment(
@Nullable WindowAndroid window, @Nullable TabDelegateFactory tabDelegateFactory) {
// Non-null delegate factory while being detached is not valid.
assert !(window == null && tabDelegateFactory != null);
if (window != null) {
updateWindowAndroid(window);
if (tabDelegateFactory != null) setDelegateFactory(tabDelegateFactory);
// Reload the NativePage (if any), since the old NativePage has a reference to the old
// activity.
if (isNativePage()) maybeShowNativePage(getUrl().getSpec(), true);
}
// Notify the event to observers only when we do the reparenting task, not when we simply
// switch window in which case a new window is non-null but delegate is null.
boolean notify = (window != null && tabDelegateFactory != null)
|| (window == null && tabDelegateFactory == null);
if (notify) {
for (TabObserver observer : mObservers) {
observer.onActivityAttachmentChanged(this, window);
}
}
updateInteractableState();
}
/**
* Sets a custom {@link View} for this {@link Tab} that replaces Content view.
*/
void setCustomView(@Nullable View view) {
mCustomView = view;
notifyContentChanged();
}
@Override
public ContentView getContentView() {
return mContentView;
}
@Override
public View getView() {
if (mCustomView != null) return mCustomView;
if (mNativePage != null && !mNativePage.isFrozen()) return mNativePage.getView();
return mContentView;
}
@Override
public TabViewManager getTabViewManager() {
return mTabViewManager;
}
@Override
@CalledByNative
public int getId() {
return mId;
}
@CalledByNative
@Override
// TODO(crbug.com/1113249) move getUrl() to CriticalPersistedTabData
public GURL getUrl() {
if (!isInitialized()) {
return GURL.emptyGURL();
}
GURL url = getWebContents() != null ? getWebContents().getVisibleUrl() : GURL.emptyGURL();
// If we have a ContentView, or a NativePage, or the url is not empty, we have a WebContents
// so cache the WebContent's url. If not use the cached version.
if (getWebContents() != null || isNativePage() || !url.getSpec().isEmpty()) {
CriticalPersistedTabData.from(this).setUrl(url);
}
return CriticalPersistedTabData.from(this).getUrl() != null
? CriticalPersistedTabData.from(this).getUrl()
: GURL.emptyGURL();
}
@Override
public GURL getOriginalUrl() {
return DomDistillerUrlUtils.getOriginalUrlFromDistillerUrl(getUrl());
}
@CalledByNative
@Override
// TODO(crbug.com/1113834) migrate getTitle() to CriticalPersistedTabData.from(tab).getTitle()
public String getTitle() {
if (CriticalPersistedTabData.from(this).getTitle() == null) updateTitle();
return CriticalPersistedTabData.from(this).getTitle();
}
Context getThemedApplicationContext() {
return mThemedApplicationContext;
}
@Override
public NativePage getNativePage() {
return mNativePage;
}
@Override
@CalledByNative
public boolean isNativePage() {
return mNativePage != null;
}
@Override
public boolean isShowingCustomView() {
return mCustomView != null;
}
@Override
public void freezeNativePage() {
if (mNativePage == null || mNativePage.isFrozen()
|| mNativePage.getView().getParent() != null) {
return;
}
mNativePage = FrozenNativePage.freeze(mNativePage);
updateInteractableState();
}
@Override
public @TabLaunchType int getLaunchType() {
return mLaunchType;
}
@Override
public int getThemeColor() {
return mThemeColor;
}
@Override
public boolean isThemingAllowed() {
// Do not apply the theme color if there are any security issues on the page.
int securityLevel = SecurityStateModel.getSecurityLevelForWebContents(getWebContents());
boolean hasSecurityIssue = securityLevel == ConnectionSecurityLevel.DANGEROUS
|| securityLevel == ConnectionSecurityLevel.SECURE_WITH_POLICY_INSTALLED_CERT;
// If chrome is showing an error page, allow theming so the system UI can match the page.
// This is considered acceptable since chrome is in control of the error page. Otherwise, if
// the page has a security issue, disable theming.
return isShowingErrorPage() || !hasSecurityIssue;
}
@Override
public boolean isIncognito() {
return mIncognito;
}
@Override
public boolean isShowingErrorPage() {
return mIsShowingErrorPage;
}
/**
* @return true iff the tab doesn't hold a live page. This happens before initialize() and when
* the tab holds frozen WebContents state that is yet to be inflated.
*/
@Override
public boolean isFrozen() {
return !isNativePage() && getWebContents() == null;
}
@CalledByNative
@Override
public boolean isUserInteractable() {
return mInteractableState;
}
@Override
public int loadUrl(LoadUrlParams params) {
try {
TraceEvent.begin("Tab.loadUrl");
// TODO(tedchoc): When showing the android NTP, delay the call to
// TabImplJni.get().loadUrl until the android view has entirely rendered.
if (!mIsNativePageCommitPending) {
mIsNativePageCommitPending = maybeShowNativePage(params.getUrl(), false);
}
if ("chrome://java-crash/".equals(params.getUrl())) {
return handleJavaCrash();
}
if (mNativeTabAndroid == 0) {
// if mNativeTabAndroid is null then we are going to crash anyways on the
// native side. Lets crash on the java side so that we can have a better stack
// trace.
throw new RuntimeException("Tab.loadUrl called when no native side exists");
}
// Request desktop sites for large screen tablets if necessary.
params.setOverrideUserAgent(calculateUserAgentOverrideOption());
@TabLoadStatus
int result = loadUrlInternal(params);
for (TabObserver observer : mObservers) {
observer.onLoadUrl(this, params, result);
}
return result;
} finally {
TraceEvent.end("Tab.loadUrl");
}
}
private @TabLoadStatus int loadUrlInternal(LoadUrlParams params) {
if (mWebContents == null) return TabLoadStatus.PAGE_LOAD_FAILED;
// TODO(https://crbug.com/783819): Don't fix up all URLs. Documentation on
// FixupURL explicitly says not to use it on URLs coming from untrustworthy
// sources, like other apps. Once migrations of Java code to GURL are complete
// and incoming URLs are converted to GURLs at their source, we can make
// decisions of whether or not to fix up GURLs on a case-by-case basis based
// on trustworthiness of the incoming URL.
GURL fixedUrl = UrlFormatter.fixupUrl(params.getUrl());
if (!fixedUrl.isValid()) return TabLoadStatus.PAGE_LOAD_FAILED;
// Record UMA "ShowHistory" here. That way it'll pick up both user
// typing chrome://history as well as selecting from the drop down menu.
if (fixedUrl.getSpec().equals(UrlConstants.HISTORY_URL)) {
RecordUserAction.record("ShowHistory");
}
if (TabImplJni.get().handleNonNavigationAboutURL(fixedUrl)) {
return TabLoadStatus.DEFAULT_PAGE_LOAD;
}
params.setUrl(fixedUrl.getSpec());
mWebContents.getNavigationController().loadUrl(params);
return TabLoadStatus.DEFAULT_PAGE_LOAD;
}
@Override
public boolean loadIfNeeded() {
if (getActivity() == null) {
Log.e(TAG, "Tab couldn't be loaded because Context was null.");
return false;
}
if (mPendingLoadParams != null) {
assert isFrozen();
WebContents webContents = WarmupManager.getInstance().takeSpareWebContents(
isIncognito(), isHidden(), isCustomTab());
if (webContents == null) {
Profile profile =
IncognitoUtils.getProfileFromWindowAndroid(mWindowAndroid, isIncognito());
webContents = WebContentsFactory.createWebContents(profile, isHidden());
}
initWebContents(webContents);
loadUrl(mPendingLoadParams);
mPendingLoadParams = null;
return true;
}
switchUserAgentIfNeeded();
restoreIfNeeded();
return true;
}
@Override
public void reload() {
// TODO(dtrainor): Should we try to rebuild the ContentView if it's frozen?
if (OfflinePageUtils.isOfflinePage(this)) {
// If current page is an offline page, reload it with custom behavior defined in extra
// header respected.
OfflinePageUtils.reload(getWebContents(),
/*loadUrlDelegate=*/new OfflinePageUtils.TabOfflinePageLoadUrlDelegate(this));
} else {
if (getWebContents() != null) {
switchUserAgentIfNeeded();
getWebContents().getNavigationController().reload(true);
}
}
}
@Override
public void reloadIgnoringCache() {
if (getWebContents() != null) {
switchUserAgentIfNeeded();
getWebContents().getNavigationController().reloadBypassingCache(true);
}
}
@Override
public void stopLoading() {
if (isLoading()) {
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) {
observers.next().onPageLoadFinished(this, getUrl());
}
}
if (getWebContents() != null) getWebContents().stop();
}
@Override
public boolean needsReload() {
return getWebContents() != null && getWebContents().getNavigationController().needsReload();
}
@Override
public boolean isLoading() {
return mIsLoading;
}
@Override
public boolean isBeingRestored() {
return mIsBeingRestored;
}
@Override
public float getProgress() {
return !isLoading() ? 1 : (int) mWebContents.getLoadProgress();
}
@Override
public boolean canGoBack() {
return getWebContents() != null && getWebContents().getNavigationController().canGoBack();
}
@Override
public boolean canGoForward() {
return getWebContents() != null
&& getWebContents().getNavigationController().canGoForward();
}
@Override
public void goBack() {
if (getWebContents() != null) getWebContents().getNavigationController().goBack();
}
@Override
public void goForward() {
if (getWebContents() != null) getWebContents().getNavigationController().goForward();
}
// TabLifecycle implementation.
@Override
public boolean isInitialized() {
return mNativeTabAndroid != 0;
}
@Override
public boolean isDestroyed() {
return mIsDestroyed;
}
@Override
public final void show(@TabSelectionType int type) {
try {
TraceEvent.begin("Tab.show");
if (!isHidden()) return;
// Keep unsetting mIsHidden above loadIfNeeded(), so that we pass correct visibility
// when spawning WebContents in loadIfNeeded().
mIsHidden = false;
updateInteractableState();
loadIfNeeded();
if (getWebContents() != null) getWebContents().onShow();
// If the NativePage was frozen while in the background (see NativePageAssassin),
// recreate the NativePage now.
NativePage nativePage = getNativePage();
if (nativePage != null && nativePage.isFrozen()) {
maybeShowNativePage(nativePage.getUrl(), true);
}
NativePageAssassin.getInstance().tabShown(this);
TabImportanceManager.tabShown(this);
// If the page is still loading, update the progress bar (otherwise it would not show
// until the renderer notifies of new progress being made).
if (getProgress() < 100) {
notifyLoadProgress(getProgress());
}
for (TabObserver observer : mObservers) observer.onShown(this, type);
// Updating the timestamp has to happen after the showInternal() call since subclasses
// may use it for logging.
CriticalPersistedTabData.from(this).setTimestampMillis(System.currentTimeMillis());
} finally {
TraceEvent.end("Tab.show");
}
}
@Override
public final void hide(@TabHidingType int type) {
try {
TraceEvent.begin("Tab.hide");
if (isHidden()) return;
mIsHidden = true;
updateInteractableState();
if (getWebContents() != null) getWebContents().onHide();
// Allow this tab's NativePage to be frozen if it stays hidden for a while.
NativePageAssassin.getInstance().tabHidden(this);
for (TabObserver observer : mObservers) observer.onHidden(this, type);
} finally {
TraceEvent.end("Tab.hide");
}
}
@Override
public boolean isClosing() {
return mIsClosing;
}
@Override
public void setClosing(boolean closing) {
mIsClosing = closing;
for (TabObserver observer : mObservers) observer.onClosingStateChanged(this, closing);
}
@CalledByNative
@Override
public boolean isHidden() {
return mIsHidden;
}
@Override
public void destroy() {
// Set at the start since destroying the WebContents can lead to calling back into
// this class.
mIsDestroyed = true;
// Update the title before destroying the tab. http://b/5783092
updateTitle();
for (TabObserver observer : mObservers) observer.onDestroyed(this);
mObservers.clear();
mUserDataHost.destroy();
mTabViewManager.destroy();
hideNativePage(false, null);
destroyWebContents(true);
TabImportanceManager.tabDestroyed(this);
// Destroys the native tab after destroying the ContentView but before destroying the
// InfoBarContainer. The native tab should be destroyed before the infobar container as
// destroying the native tab cleanups up any remaining infobars. The infobar container
// expects all infobars to be cleaned up before its own destruction.
if (mNativeTabAndroid != 0) {
TabImplJni.get().destroy(mNativeTabAndroid);
assert mNativeTabAndroid == 0;
}
}
/**
* WARNING: This method is deprecated. Consider other ways such as passing the dependencies
* to the constructor, rather than accessing ChromeActivity from Tab and using getters.
* @return {@link ChromeActivity} that currently contains this {@link Tab} in its
* {@link TabModel}.
*/
@Deprecated
ChromeActivity<?> getActivity() {
if (getWindowAndroid() == null) return null;
Activity activity = ContextUtils.activityFromContext(getWindowAndroid().getContext().get());
if (activity instanceof ChromeActivity) return (ChromeActivity<?>) activity;
return null;
}
/**
* @param tab {@link Tab} instance being checked.
* @return Whether the tab is detached from any Activity and its {@link WindowAndroid}.
* Certain functionalities will not work until it is attached to an activity
* with {@link ReparentingTask#finish}.
*/
static boolean isDetached(Tab tab) {
if (tab.getWebContents() == null) return true;
// Should get WindowAndroid from WebContents since the one from |getWindowAndroid()|
// is always non-null even when the tab is in detached state. See the comment in |detach()|.
WindowAndroid window = tab.getWebContents().getTopLevelNativeWindow();
if (window == null) return true;
Activity activity = ContextUtils.activityFromContext(window.getContext().get());
return !(activity instanceof ChromeActivity);
}
@Override
public void setIsTabSaveEnabled(boolean isTabSaveEnabled) {
mIsTabSaveEnabledSupplier.set(isTabSaveEnabled);
}
@VisibleForTesting
public ObservableSupplierImpl<Boolean> getIsTabSaveEnabledSupplierForTesting() {
return mIsTabSaveEnabledSupplier;
}
// TabObscuringHandler.Observer
@Override
public void updateObscured(boolean isObscured) {
// Update whether or not the current native tab and/or web contents are
// currently visible (from an accessibility perspective), or whether
// they're obscured by another view.
View view = getView();
if (view != null) {
int importantForAccessibility = isObscured
? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
: View.IMPORTANT_FOR_ACCESSIBILITY_YES;
if (view.getImportantForAccessibility() != importantForAccessibility) {
view.setImportantForAccessibility(importantForAccessibility);
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
}
WebContentsAccessibility wcax = getWebContentsAccessibility(getWebContents());
if (wcax != null) {
boolean isWebContentObscured = isObscured || isShowingCustomView();
wcax.setObscuredByAnotherView(isWebContentObscured);
}
}
/**
* Initializes {@link Tab} with {@code webContents}. If {@code webContents} is {@code null}
* a new {@link WebContents} will be created for this {@link Tab}.
* @param parent The tab that caused this tab to be opened.
* @param creationState State in which the tab is created.
* @param loadUrlParams Parameters used for a lazily loaded Tab.
* @param webContents A {@link WebContents} object or {@code null} if one should be created.
* @param delegateFactory The {@link TabDelegateFactory} to be used for delegate creation.
* @param initiallyHidden Only used if {@code webContents} is {@code null}. Determines
* whether or not the newly created {@link WebContents} will be hidden or not.
* @param tabState State containing information about this Tab, if it was persisted.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void initialize(Tab parent, @Nullable @TabCreationState Integer creationState,
LoadUrlParams loadUrlParams, WebContents webContents,
@Nullable TabDelegateFactory delegateFactory, boolean initiallyHidden,
TabState tabState) {
try {
TraceEvent.begin("Tab.initialize");
if (parent != null) {
CriticalPersistedTabData.from(this).setParentId(parent.getId());
mSourceTabId = parent.isIncognito() == mIncognito ? parent.getId() : INVALID_TAB_ID;
}
CriticalPersistedTabData.from(this).setLaunchTypeAtCreation(mLaunchType);
mCreationState = creationState;
mPendingLoadParams = loadUrlParams;
if (loadUrlParams != null) {
CriticalPersistedTabData.from(this).setUrl(new GURL(loadUrlParams.getUrl()));
}
// The {@link mDelegateFactory} needs to be set before calling
// {@link TabHelpers.initTabHelpers()}. This is because it creates a
// TabBrowserControlsConstraintsHelper, and
// {@link TabBrowserControlsConstraintsHelper#updateVisibilityDelegate()} will call the
// Tab#getDelegateFactory().createBrowserControlsVisibilityDelegate().
// See https://crbug.com/1179419.
mDelegateFactory = delegateFactory;
TabHelpers.initTabHelpers(this, parent);
if (tabState != null) {
restoreFieldsFromState(tabState);
}
initializeNative();
RevenueStats.getInstance().tabCreated(this);
// If there is a frozen WebContents state or a pending lazy load, don't create a new
// WebContents. Restoring will be done when showing the tab in the foreground.
if (CriticalPersistedTabData.from(this).getWebContentsState() != null
|| getPendingLoadParams() != null) {
return;
}
boolean creatingWebContents = webContents == null;
if (creatingWebContents) {
webContents = WarmupManager.getInstance().takeSpareWebContents(
isIncognito(), initiallyHidden, isCustomTab());
if (webContents == null) {
Profile profile = IncognitoUtils.getProfileFromWindowAndroid(
mWindowAndroid, isIncognito());
webContents = WebContentsFactory.createWebContents(profile, initiallyHidden);
}
}
initWebContents(webContents);
if (!creatingWebContents && webContents.shouldShowLoadingUI()) {
didStartPageLoad(webContents.getVisibleUrl());
}
} finally {
if (CriticalPersistedTabData.from(this).getTimestampMillis() == INVALID_TIMESTAMP) {
CriticalPersistedTabData.from(this).setTimestampMillis(System.currentTimeMillis());
}
registerTabSaving();
String appId = null;
Boolean hasThemeColor = null;
int themeColor = 0;
if (mUsedCriticalPersistedTabData) {
appId = CriticalPersistedTabData.from(this).getOpenerAppId();
themeColor = CriticalPersistedTabData.from(this).getThemeColor();
hasThemeColor = themeColor != TabState.UNSPECIFIED_THEME_COLOR
&& !ColorUtils.isThemeColorTooBright(themeColor);
} else if (tabState != null) {
appId = tabState.openerAppId;
themeColor = tabState.getThemeColor();
hasThemeColor = tabState.hasThemeColor();
}
if (hasThemeColor != null) {
updateThemeColor(hasThemeColor ? themeColor : TabState.UNSPECIFIED_THEME_COLOR);
}
for (TabObserver observer : mObservers) observer.onInitialized(this, appId);
TraceEvent.end("Tab.initialize");
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void registerTabSaving() {
CriticalPersistedTabData.from(this).registerIsTabSaveEnabledSupplier(
mIsTabSaveEnabledSupplier);
}
private boolean useCriticalPersistedTabData() {
return CachedFeatureFlags.isEnabled(ChromeFeatureList.CRITICAL_PERSISTED_TAB_DATA);
}
@Nullable
@TabCreationState
Integer getCreationState() {
return mCreationState;
}
/**
* Restores member fields from the given TabState.
* @param state TabState containing information about this Tab.
*/
void restoreFieldsFromState(TabState state) {
assert state != null;
CriticalPersistedTabData.from(this).setWebContentsState(state.contentsState);
CriticalPersistedTabData.from(this).setTimestampMillis(state.timestampMillis);
CriticalPersistedTabData.from(this).setUrl(
new GURL(state.contentsState.getVirtualUrlFromState()));
CriticalPersistedTabData.from(this).setTitle(
state.contentsState.getDisplayTitleFromState());
CriticalPersistedTabData.from(this).setLaunchTypeAtCreation(state.tabLaunchTypeAtCreation);
CriticalPersistedTabData.from(this).setRootId(
state.rootId == Tab.INVALID_TAB_ID ? mId : state.rootId);
CriticalPersistedTabData.from(this).setUserAgent(state.userAgent);
}
/**
* @return An {@link ObserverList.RewindableIterator} instance that points to all of
* the current {@link TabObserver}s on this class. Note that calling
* {@link java.util.Iterator#remove()} will throw an
* {@link UnsupportedOperationException}.
*/
ObserverList.RewindableIterator<TabObserver> getTabObservers() {
return mObservers.rewindableIterator();
}
final void setImportance(@ChildProcessImportance int importance) {
if (mImportance == importance) return;
mImportance = importance;
WebContents webContents = getWebContents();
if (webContents == null) return;
webContents.setImportance(mImportance);
}
/**
* Hides the current {@link NativePage}, if any, and shows the {@link WebContents}'s view.
*/
void showRenderedPage() {
updateTitle();
if (mNativePage != null) hideNativePage(true, null);
}
void updateWindowAndroid(WindowAndroid windowAndroid) {
// TODO(yusufo): mWindowAndroid can never be null until crbug.com/657007 is fixed.
assert windowAndroid != null;
mWindowAndroid = windowAndroid;
WebContents webContents = getWebContents();
if (webContents != null) webContents.setTopLevelNativeWindow(mWindowAndroid);
}
TabDelegateFactory getDelegateFactory() {
return mDelegateFactory;
}
@VisibleForTesting
TabWebContentsDelegateAndroidImpl getTabWebContentsDelegateAndroid() {
return mWebContentsDelegate;
}
// Forwarded from TabWebContentsDelegateAndroid.
/**
* Called when a navigation begins and no navigation was in progress
* @param toDifferentDocument Whether this navigation will transition between
* documents (i.e., not a fragment navigation or JS History API call).
*/
void onLoadStarted(boolean toDifferentDocument) {
if (toDifferentDocument) mIsLoading = true;
for (TabObserver observer : mObservers) observer.onLoadStarted(this, toDifferentDocument);
}
/**
* Called when a navigation completes and no other navigation is in progress.
*/
void onLoadStopped() {
// mIsLoading should only be false if this is a same-document navigation.
boolean toDifferentDocument = mIsLoading;
mIsLoading = false;
for (TabObserver observer : mObservers) observer.onLoadStopped(this, toDifferentDocument);
}
void handleRendererResponsiveStateChanged(boolean isResponsive) {
mIsRendererUnresponsive = !isResponsive;
for (TabObserver observer : mObservers) {
observer.onRendererResponsiveStateChanged(this, isResponsive);
}
}
// Forwarded from TabWebContentsObserver.
/**
* Called when a page has started loading.
* @param validatedUrl URL being loaded.
*/
void didStartPageLoad(GURL validatedUrl) {
updateTitle();
if (mIsRendererUnresponsive) handleRendererResponsiveStateChanged(true);
for (TabObserver observer : mObservers) {
observer.onPageLoadStarted(this, validatedUrl);
}
}
/**
* Called when a page has finished loading.
* @param url URL that was loaded.
*/
void didFinishPageLoad(GURL url) {
updateTitle();
for (TabObserver observer : mObservers) observer.onPageLoadFinished(this, url);
mIsBeingRestored = false;
}
/**
* Called when a page has failed loading.
* @param errorCode The error code causing the page to fail loading.
*/
void didFailPageLoad(int errorCode) {
for (TabObserver observer : mObservers) {
observer.onPageLoadFailed(this, errorCode);
}
mIsBeingRestored = false;
}
/**
* Update internal Tab state when provisional load gets committed.
* @param url The URL that was loaded.
* @param transitionType The transition type to the current URL.
*/
void handleDidFinishNavigation(GURL url, Integer transitionType) {
mIsNativePageCommitPending = false;
boolean isReload = (transitionType != null
&& (transitionType & PageTransition.CORE_MASK) == PageTransition.RELOAD);
if (!maybeShowNativePage(url.getSpec(), isReload)) {
showRenderedPage();
}
}
/**
* Notify the observers that the load progress has changed.
* @param progress The current percentage of progress.
*/
void notifyLoadProgress(float progress) {
for (TabObserver observer : mObservers) observer.onLoadProgressChanged(this, progress);
}
/**
* Add a new navigation entry for the current URL and page title.
*/
void pushNativePageStateToNavigationEntry() {
assert mNativeTabAndroid != 0 && getNativePage() != null;
TabImplJni.get().setActiveNavigationEntryTitleForUrl(
mNativeTabAndroid, getNativePage().getUrl(), getNativePage().getTitle());
}
/**
* Set whether the Tab needs to be reloaded.
*/
void setNeedsReload() {
assert getWebContents() != null;
getWebContents().getNavigationController().setNeedsReload();
}
/**
* Called when navigation entries were removed.
*/
void notifyNavigationEntriesDeleted() {
for (TabObserver observer : mObservers) observer.onNavigationEntriesDeleted(this);
}
//////////////
/**
* @return Whether the renderer is currently unresponsive.
*/
boolean isRendererUnresponsive() {
return mIsRendererUnresponsive;
}
/**
* Load the original image (uncompressed by spdy proxy) in this tab.
*/
void loadOriginalImage() {
if (mNativeTabAndroid != 0) {
TabImplJni.get().loadOriginalImage(mNativeTabAndroid);
}
}
/**
* Sets whether the tab is showing an error page. This is reset whenever the tab finishes a
* navigation.
* Note: This is kept here to keep the build green. Remove from interface as soon as
* the downstream patch lands.
* @param isShowingErrorPage Whether the tab shows an error page.
*/
void setIsShowingErrorPage(boolean isShowingErrorPage) {
mIsShowingErrorPage = isShowingErrorPage;
}
/**
* Shows a native page for url if it's a valid chrome-native URL. Otherwise, does nothing.
* @param url The url of the current navigation.
* @param forceReload If true, the current native page (if any) will not be reused, even if it
* matches the URL.
* @return True, if a native page was displayed for url.
*/
boolean maybeShowNativePage(String url, boolean forceReload) {
// While detached for reparenting we don't have an owning Activity, or TabModelSelector,
// so we can't create the native page. The native page will be created once reparenting is
// completed.
if (isDetached(this)) return false;
NativePage candidateForReuse = forceReload ? null : getNativePage();
NativePage nativePage = mDelegateFactory.createNativePage(url, candidateForReuse, this);
if (nativePage != null) {
showNativePage(nativePage);
notifyPageTitleChanged();
notifyFaviconChanged();
return true;
}
return false;
}
/**
* Calls onContentChanged on all TabObservers and updates accessibility visibility.
*/
void notifyContentChanged() {
for (TabObserver observer : mObservers) observer.onContentChanged(this);
}
void updateThemeColor(int themeColor) {
mThemeColor = themeColor;
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) observers.next().onDidChangeThemeColor(this, themeColor);
}
void updateTitle() {
if (isFrozen()) return;
// When restoring the tabs, the title will no longer be populated, so request it from the
// WebContents or NativePage (if present).
String title = "";
if (isNativePage()) {
title = mNativePage.getTitle();
} else if (getWebContents() != null) {
title = getWebContents().getTitle();
}
updateTitle(title);
}
/**
* Cache the title for the current page.
*
* {@link ContentViewClient#onUpdateTitle} is unreliable, particularly for navigating backwards
* and forwards in the history stack, so pull the correct title whenever the page changes.
* onUpdateTitle is only called when the title of a navigation entry changes. When the user goes
* back a page the navigation entry exists with the correct title, thus the title is not
* actually changed, and no notification is sent.
* @param title Title of the page.
*/
void updateTitle(String title) {
if (TextUtils.equals(CriticalPersistedTabData.from(this).getTitle(), title)) return;
CriticalPersistedTabData.from(this).setTitle(title);
notifyPageTitleChanged();
}
@Override
public LoadUrlParams getPendingLoadParams() {
return mPendingLoadParams;
}
/**
* Performs any subclass-specific tasks when the Tab crashes.
*/
void handleTabCrash() {
mIsLoading = false;
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) observers.next().onCrash(this);
mIsBeingRestored = false;
}
/**
* Called when the background color for the content changes.
* @param color The current for the background.
*/
void onBackgroundColorChanged(int color) {
for (TabObserver observer : mObservers) observer.onBackgroundColorChanged(this, color);
}
/**
* This is currently called when committing a pre-rendered page or activating a portal.
*/
@CalledByNative
void swapWebContents(WebContents webContents, boolean didStartLoad, boolean didFinishLoad) {
boolean hasWebContents = mContentView != null && mWebContents != null;
Rect original = hasWebContents
? new Rect(0, 0, mContentView.getWidth(), mContentView.getHeight())
: new Rect();
for (TabObserver observer : mObservers) observer.webContentsWillSwap(this);
if (hasWebContents) mWebContents.onHide();
Context appContext = ContextUtils.getApplicationContext();
Rect bounds = original.isEmpty() ? TabUtils.estimateContentSize(appContext) : null;
if (bounds != null) original.set(bounds);
mWebContents.setFocus(false);
destroyWebContents(false /* do not delete native web contents */);
hideNativePage(false, () -> {
// Size of the new content is zero at this point. Set the view size in advance
// so that next onShow() call won't send a resize message with zero size
// to the renderer process. This prevents the size fluttering that may confuse
// Blink and break rendered result (see http://crbug.com/340987).
webContents.setSize(original.width(), original.height());
if (bounds != null) {
assert mNativeTabAndroid != 0;
TabImplJni.get().onPhysicalBackingSizeChanged(
mNativeTabAndroid, webContents, bounds.right, bounds.bottom);
}
initWebContents(webContents);
webContents.onShow();
});
if (didStartLoad) {
// Simulate the PAGE_LOAD_STARTED notification that we did not get.
didStartPageLoad(getUrl());
// Simulate the PAGE_LOAD_FINISHED notification that we did not get.
if (didFinishLoad) didFinishPageLoad(getUrl());
}
for (TabObserver observer : mObservers) {
observer.onWebContentsSwapped(this, didStartLoad, didFinishLoad);
}
}
/**
* Builds the native counterpart to this class.
*/
private void initializeNative() {
if (mNativeTabAndroid == 0) TabImplJni.get().init(TabImpl.this);
assert mNativeTabAndroid != 0;
}
/**
* @return The native pointer representing the native side of this {@link TabImpl} object.
*/
@CalledByNative
private long getNativePtr() {
return mNativeTabAndroid;
}
@CalledByNative
private void clearNativePtr() {
assert mNativeTabAndroid != 0;
mNativeTabAndroid = 0;
}
@CalledByNative
private void setNativePtr(long nativePtr) {
assert nativePtr != 0;
assert mNativeTabAndroid == 0;
mNativeTabAndroid = nativePtr;
}
@CalledByNative
private static long[] getAllNativePtrs(Tab[] tabsArray) {
if (tabsArray == null) return null;
long[] tabsPtrArray = new long[tabsArray.length];
for (int i = 0; i < tabsArray.length; i++) {
tabsPtrArray[i] = ((TabImpl) tabsArray[i]).getNativePtr();
}
return tabsPtrArray;
}
/**
* Initializes the {@link WebContents}. Completes the browser content components initialization
* around a native WebContents pointer.
* <p>
* {@link #getNativePage()} will still return the {@link NativePage} if there is one.
* All initialization that needs to reoccur after a web contents swap should be added here.
* <p />
* NOTE: If you attempt to pass a native WebContents that does not have the same incognito
* state as this tab this call will fail.
*
* @param webContents The WebContents object that will initialize all the browser components.
*/
private void initWebContents(WebContents webContents) {
try {
TraceEvent.begin("ChromeTab.initWebContents");
WebContents oldWebContents = mWebContents;
mWebContents = webContents;
ContentView cv = ContentView.createContentView(
mThemedApplicationContext, null /* eventOffsetHandler */, webContents);
cv.setContentDescription(mThemedApplicationContext.getResources().getString(
R.string.accessibility_content_view));
mContentView = cv;
webContents.initialize(PRODUCT_VERSION, new TabViewAndroidDelegate(this, cv), cv,
getWindowAndroid(), WebContents.createDefaultInternalsHolder());
hideNativePage(false, null);
if (oldWebContents != null) {
oldWebContents.setImportance(ChildProcessImportance.NORMAL);
getWebContentsAccessibility(oldWebContents).setObscuredByAnotherView(false);
}
mWebContents.setImportance(mImportance);
ContentUtils.setUserAgentOverride(mWebContents,
calculateUserAgentOverrideOption() == UserAgentOverrideOption.TRUE);
mContentView.addOnAttachStateChangeListener(mAttachStateChangeListener);
updateInteractableState();
mWebContentsDelegate = createWebContentsDelegate();
assert mNativeTabAndroid != 0;
TabImplJni.get().initWebContents(mNativeTabAndroid, mIncognito, isDetached(this),
webContents, mSourceTabId, mWebContentsDelegate,
new TabContextMenuPopulatorFactory(
mDelegateFactory.createContextMenuPopulatorFactory(this), this));
mWebContents.notifyRendererPreferenceUpdate();
TabHelpers.initWebContentsHelpers(this);
notifyContentChanged();
} finally {
TraceEvent.end("ChromeTab.initWebContents");
}
}
private TabWebContentsDelegateAndroidImpl createWebContentsDelegate() {
TabWebContentsDelegateAndroid delegate = mDelegateFactory.createWebContentsDelegate(this);
return new TabWebContentsDelegateAndroidImpl(this, delegate);
}
/**
* Shows the given {@code nativePage} if it's not already showing.
* @param nativePage The {@link NativePage} to show.
*/
private void showNativePage(NativePage nativePage) {
assert nativePage != null;
if (mNativePage == nativePage) return;
hideNativePage(true, () -> {
mNativePage = nativePage;
if (!mNativePage.isFrozen()) {
mNativePage.getView().addOnAttachStateChangeListener(mAttachStateChangeListener);
}
pushNativePageStateToNavigationEntry();
updateThemeColor(TabState.UNSPECIFIED_THEME_COLOR);
});
}
/**
* Hide and destroy the native page if it was being shown.
* @param notify {@code true} to trigger {@link #onContentChanged} event.
* @param postHideTask {@link Runnable} task to run before actually destroying the
* native page. This is necessary to keep the tasks to perform in order.
*/
private void hideNativePage(boolean notify, Runnable postHideTask) {
NativePage previousNativePage = mNativePage;
if (mNativePage != null) {
if (!mNativePage.isFrozen()) {
mNativePage.getView().removeOnAttachStateChangeListener(mAttachStateChangeListener);
}
mNativePage = null;
}
if (postHideTask != null) postHideTask.run();
if (notify) notifyContentChanged();
destroyNativePageInternal(previousNativePage);
}
/**
* Set {@link TabDelegateFactory} instance and updates the references.
* @param factory TabDelegateFactory instance.
*/
private void setDelegateFactory(TabDelegateFactory factory) {
// Update the delegate factory, then recreate and propagate all delegates.
mDelegateFactory = factory;
mWebContentsDelegate = createWebContentsDelegate();
WebContents webContents = getWebContents();
if (webContents != null) {
TabImplJni.get().updateDelegates(mNativeTabAndroid, mWebContentsDelegate,
new TabContextMenuPopulatorFactory(
mDelegateFactory.createContextMenuPopulatorFactory(this), this));
webContents.notifyRendererPreferenceUpdate();
}
}
private void notifyPageTitleChanged() {
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) {
observers.next().onTitleUpdated(this);
}
}
private void notifyFaviconChanged() {
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) {
observers.next().onFaviconUpdated(this, null);
}
}
/**
* Update the interactable state of the tab. If the state has changed, it will call the
* {@link #onInteractableStateChanged(boolean)} method.
*/
private void updateInteractableState() {
boolean currentState = !mIsHidden && !isFrozen()
&& (mIsViewAttachedToWindow || VrModuleProvider.getDelegate().isInVr())
&& !isDetached(this);
if (currentState == mInteractableState) return;
mInteractableState = currentState;
for (TabObserver observer : mObservers) {
observer.onInteractabilityChanged(this, currentState);
}
}
/**
* Loads a tab that was already loaded but since then was lost. This happens either when we
* unfreeze the tab from serialized state or when we reload a tab that crashed. In both cases
* the load codepath is the same (run in loadIfNecessary()) and the same caching policies of
* history load are used.
*/
private final void restoreIfNeeded() {
// Attempts to display the Paint Preview representation of this Tab. Please note that this
// is behind an experimental flag (crbug.com/1008520).
if (isFrozen()) StartupPaintPreviewHelper.showPaintPreviewOnRestore(this);
try {
TraceEvent.begin("Tab.restoreIfNeeded");
// Restore is needed for a tab that is loaded for the first time. WebContents will
// be restored from a saved state.
if ((isFrozen() && CriticalPersistedTabData.from(this).getWebContentsState() != null
&& !unfreezeContents())
|| !needsReload()) {
return;
}
if (mWebContents != null) mWebContents.getNavigationController().loadIfNecessary();
mIsBeingRestored = true;
for (TabObserver observer : mObservers) observer.onRestoreStarted(this);
} finally {
TraceEvent.end("Tab.restoreIfNeeded");
}
}
/**
* Restores the WebContents from its saved state. This should only be called if the tab is
* frozen with a saved TabState, and NOT if it was frozen for a lazy load.
* @return Whether or not the restoration was successful.
*/
private boolean unfreezeContents() {
boolean restored = true;
try {
TraceEvent.begin("Tab.unfreezeContents");
WebContentsState webContentsState =
CriticalPersistedTabData.from(this).getWebContentsState();
assert webContentsState != null;
WebContents webContents = WebContentsStateBridge.restoreContentsFromByteBuffer(
webContentsState, isHidden());
if (webContents == null) {
// State restore failed, just create a new empty web contents as that is the best
// that can be done at this point. TODO(jcivelli) http://b/5910521 - we should show
// an error page instead of a blank page in that case (and the last loaded URL).
Profile profile =
IncognitoUtils.getProfileFromWindowAndroid(mWindowAndroid, isIncognito());
webContents = WebContentsFactory.createWebContents(profile, isHidden());
for (TabObserver observer : mObservers) observer.onRestoreFailed(this);
restored = false;
}
Supplier<CompositorViewHolder> compositorViewHolderSupplier =
getActivity().getCompositorViewHolderSupplier();
View compositorView = compositorViewHolderSupplier.get();
webContents.setSize(compositorView.getWidth(), compositorView.getHeight());
CriticalPersistedTabData.from(this).setWebContentsState(null);
initWebContents(webContents);
if (!restored) {
String url = CriticalPersistedTabData.from(this).getUrl().getSpec().isEmpty()
? UrlConstants.NTP_URL
: CriticalPersistedTabData.from(this).getUrl().getSpec();
loadUrl(new LoadUrlParams(url, PageTransition.GENERATED));
}
} finally {
TraceEvent.end("Tab.unfreezeContents");
}
return restored;
}
@CalledByNative
@Override
public boolean isCustomTab() {
ChromeActivity activity = getActivity();
return activity != null && activity.isCustomTab();
}
/**
* Throws a RuntimeException. Useful for testing crash reports with obfuscated Java stacktraces.
*/
private int handleJavaCrash() {
throw new RuntimeException("Intentional Java Crash");
}
/**
* Delete navigation entries from frozen state matching the predicate.
* @param predicate Handle for a deletion predicate interpreted by native code.
* Only valid during this call frame.
*/
@CalledByNative
private void deleteNavigationEntriesFromFrozenState(long predicate) {
WebContentsState webContentsState =
CriticalPersistedTabData.from(this).getWebContentsState();
if (webContentsState == null) return;
WebContentsState newState =
WebContentsStateBridge.deleteNavigationEntries(webContentsState, predicate);
if (newState != null) {
CriticalPersistedTabData.from(this).setWebContentsState(newState);
notifyNavigationEntriesDeleted();
}
}
private static WebContentsAccessibility getWebContentsAccessibility(WebContents webContents) {
return webContents != null ? WebContentsAccessibility.fromWebContents(webContents) : null;
}
private void destroyNativePageInternal(NativePage nativePage) {
if (nativePage == null) return;
assert nativePage != mNativePage : "Attempting to destroy active page.";
nativePage.destroy();
}
/**
* Destroys the current {@link WebContents}.
* @param deleteNativeWebContents Whether or not to delete the native WebContents pointer.
*/
private final void destroyWebContents(boolean deleteNativeWebContents) {
if (mWebContents == null) return;
mContentView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
mContentView = null;
updateInteractableState();
WebContents contentsToDestroy = mWebContents;
mWebContents = null;
mWebContentsDelegate = null;
assert mNativeTabAndroid != 0;
if (deleteNativeWebContents) {
// Destruction of the native WebContents will call back into Java to destroy the Java
// WebContents.
TabImplJni.get().destroyWebContents(mNativeTabAndroid);
} else {
// This branch is to not delete the WebContents, but just to release the WebContent from
// the Tab and clear the WebContents for two different cases a) The WebContents will be
// destroyed eventually, but from the native WebContents. b) The WebContents will be
// reused later. We need to clear the reference to the Tab from WebContentsObservers or
// the UserData. If the WebContents will be reused, we should set the necessary
// delegates again.
TabImplJni.get().releaseWebContents(mNativeTabAndroid);
// This call is just a workaround, Chrome should clean up the WebContentsObservers
// itself.
contentsToDestroy.clearJavaWebContentsObservers();
contentsToDestroy.initialize(PRODUCT_VERSION,
ViewAndroidDelegate.createBasicDelegate(/* containerView */ null),
/* accessDelegate */ null, /* windowAndroid */ null,
WebContents.createDefaultInternalsHolder());
}
}
private @UserAgentOverrideOption int calculateUserAgentOverrideOption() {
WebContents webContents = getWebContents();
boolean currentRequestDesktopSite = webContents == null
? false
: webContents.getNavigationController().getUseDesktopUserAgent();
@TabUserAgent
int tabUserAgent = CriticalPersistedTabData.from(this).getUserAgent();
// TabUserAgent.UNSET means this is a pre-existing tab from an earlier build. In this case
// we set the TabUserAgent bit based on last committed entry's user agent. If webContents is
// null, this method is triggered too early, and we cannot read the last committed entry's
// user agent yet. We will skip for now and let the following call set the TabUserAgent bit.
if (webContents != null && tabUserAgent == TabUserAgent.UNSET) {
if (currentRequestDesktopSite) {
tabUserAgent = TabUserAgent.DESKTOP;
} else {
tabUserAgent = TabUserAgent.DEFAULT;
}
CriticalPersistedTabData.from(this).setUserAgent(tabUserAgent);
}
// We only calculate the user agent when users did not manually choose one.
if (tabUserAgent == TabUserAgent.DEFAULT
&& ContentFeatureList.isEnabled(ContentFeatureList.REQUEST_DESKTOP_SITE_GLOBAL)) {
// We only do the following logic to choose the desktop/mobile user agent if:
// 1. User never manually made a choice in the app menu for requesting desktop site.
// 2. User-enabled request desktop site in site settings.
Profile profile =
IncognitoUtils.getProfileFromWindowAndroid(mWindowAndroid, isIncognito());
boolean shouldRequestDesktopSite;
if (ContentFeatureList.isEnabled(ContentFeatureList.REQUEST_DESKTOP_SITE_EXCEPTIONS)) {
shouldRequestDesktopSite = getWebContents() != null
&& TabUtils.isDesktopSiteEnabled(profile, getWebContents().getVisibleUrl());
} else {
shouldRequestDesktopSite = TabUtils.isDesktopSiteGlobalEnabled(profile);
}
if (shouldRequestDesktopSite != currentRequestDesktopSite) {
// TODO(crbug.com/1243758): Confirm if a new histogram should be used.
RecordHistogram.recordBooleanHistogram(
"Android.RequestDesktopSite.UseDesktopUserAgent", shouldRequestDesktopSite);
// The user is not forcing any mode and we determined that we need to
// change, therefore we are using TRUE or FALSE option. On Android TRUE mean
// override to Desktop user agent, while FALSE means go with Mobile version.
return shouldRequestDesktopSite ? UserAgentOverrideOption.TRUE
: UserAgentOverrideOption.FALSE;
}
}
RecordHistogram.recordBooleanHistogram(
"Android.RequestDesktopSite.UseDesktopUserAgent", currentRequestDesktopSite);
// INHERIT means use the same that was used last time.
return UserAgentOverrideOption.INHERIT;
}
private void switchUserAgentIfNeeded() {
if (calculateUserAgentOverrideOption() == UserAgentOverrideOption.INHERIT
|| getWebContents() == null) {
return;
}
boolean usingDesktopUserAgent =
getWebContents().getNavigationController().getUseDesktopUserAgent();
TabUtils.switchUserAgent(this, /* switchToDesktop */ !usingDesktopUserAgent,
/* forcedByUser */ false);
}
@NativeMethods
interface Natives {
TabImpl fromWebContents(WebContents webContents);
void init(TabImpl caller);
void destroy(long nativeTabAndroid);
void initWebContents(long nativeTabAndroid, boolean incognito, boolean isBackgroundTab,
WebContents webContents, int parentTabId,
TabWebContentsDelegateAndroidImpl delegate,
ContextMenuPopulatorFactory contextMenuPopulatorFactory);
void updateDelegates(long nativeTabAndroid, TabWebContentsDelegateAndroidImpl delegate,
ContextMenuPopulatorFactory contextMenuPopulatorFactory);
void destroyWebContents(long nativeTabAndroid);
void releaseWebContents(long nativeTabAndroid);
void onPhysicalBackingSizeChanged(
long nativeTabAndroid, WebContents webContents, int width, int height);
void setActiveNavigationEntryTitleForUrl(long nativeTabAndroid, String url, String title);
void loadOriginalImage(long nativeTabAndroid);
boolean handleNonNavigationAboutURL(GURL url);
}
}