// Copyright 2014 The Chromium Authors
// 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.net.Uri;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewStructure;
import android.view.accessibility.AccessibilityEvent;
import android.view.autofill.AutofillValue;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;
import org.chromium.base.CommandLine;
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.ThreadUtils;
import org.chromium.base.Token;
import org.chromium.base.TraceEvent;
import org.chromium.base.UserDataHost;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.version_info.VersionInfo;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ActivityUtils;
import org.chromium.chrome.browser.WarmupManager;
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.content.WebContentsFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
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.pdf.PdfInfo;
import org.chromium.chrome.browser.pdf.PdfUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.rlz.RevenueStats;
import org.chromium.chrome.browser.tab.TabUtils.UseDesktopUserAgentCaller;
import org.chromium.chrome.browser.ui.native_page.FrozenNativePage;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.ui.native_page.NativePage.SmoothTransitionDelegate;
import org.chromium.components.autofill.AutofillFeatures;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillSelectionActionMenuDelegate;
import org.chromium.components.autofill.AutofillSelectionMenuItemHelper;
import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
import org.chromium.components.embedder_support.contextmenu.ContextMenuPopulatorFactory;
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.content_public.browser.ChildProcessImportance;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.content_public.browser.back_forward_transition.AnimationStage;
import org.chromium.content_public.browser.navigation_controller.UserAgentOverrideOption;
import org.chromium.content_public.common.Referrer;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Objects;
/**
* Implementation of the interface {@link Tab}. Contains and manages a {@link ContentView}. This
* class is not intended to be extended.
*/
class TabImpl implements Tab {
/** Used for logging. */
private static final String TAG = "Tab";
private static final String BACKGROUND_COLOR_CHANGE_PRE_OPTIMIZATION_HISTOGRAM =
"Android.Tab.BackgroundColorChange.PreOptimization";
private static final String BACKGROUND_COLOR_CHANGE_HISTOGRAM =
"Android.Tab.BackgroundColorChange";
private static final String PRODUCT_VERSION = VersionInfo.getProductVersion();
private long mNativeTabAndroid;
/** Unique id of this tab (within its container). */
private final int mId;
/** The Profile associated with this tab. */
private final Profile mProfile;
/**
* 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;
/**
* True after a native page has been hidden, before a new background color has been explicitly
* set. This is useful when the implicit background color (previously set before showing the
* native page) is no longer necessarily relevant.
*/
private boolean mWaitingOnBgColorAfterHidingNativePage;
/** {@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;
private @ColorInt Integer mCustomViewBackgroundColor;
AutofillProvider mAutofillProvider;
/**
* 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;
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 @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 the tab is currently detached for reparenting. */
private boolean mIsDetached;
/** 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 int mThemeColor;
private int mWebContentBackgroundColor;
private int mTabBackgroundColor;
private boolean mIsWebContentObscured;
private long mTimestampMillis = INVALID_TIMESTAMP;
private int mParentId = INVALID_TAB_ID;
private int mRootId;
private @Nullable Token mTabGroupId;
private @TabUserAgent int mUserAgent = TabUserAgent.DEFAULT;
/**
* Navigation state of the WebContents as returned by nativeGetContentsStateAsByteBuffer(),
* stored to be inflated on demand using unfreezeContents(). If this is not null, there is no
* WebContents around. Upon tab switch WebContents will be unfrozen and the variable will be set
* to null.
*/
private WebContentsState mWebContentsState;
/** Title of the ContentViews webpage. */
private String mTitle;
/** URL of the page currently loading. Used as a fall-back in case tab restore fails. */
private GURL mUrl;
private long mLastNavigationCommittedTimestampMillis = INVALID_TIMESTAMP;
/**
* Saves how this tab was initially launched so that we can record metrics on how the
* tab was created. This is different than {@link Tab#getLaunchType()}, since {@link
* Tab#getLaunchType()} will be overridden to "FROM_RESTORE" during tab restoration.
*/
private @Nullable @TabLaunchType Integer mTabLaunchTypeAtCreation;
/**
* Variables used to track native page creation prior to mNativePage assignment. Avoids the case
* where native pages can unintentionally re-create themselves by calling {@link
* NativePage#onStateChange} during the creation process.
*/
private boolean mIsAlreadyCreatingNativePage;
private String mPendingNativePageHost;
private SmoothTransitionDelegate mNativePageSmoothTransitionDelegate;
/** Tracks the origin of a background color change. */
@IntDef({
BackgroundColorChangeOrigin.WEB_BACKGROUND_COLOR_CHANGE,
BackgroundColorChangeOrigin.CUSTOM_VIEW_SET,
BackgroundColorChangeOrigin.NATIVE_PAGE_SHOWN,
BackgroundColorChangeOrigin.BG_COLOR_UPDATE_AFTER_HIDING_NATIVE_PAGE
})
@Retention(RetentionPolicy.SOURCE)
public @interface BackgroundColorChangeOrigin {
int WEB_BACKGROUND_COLOR_CHANGE = 0;
int CUSTOM_VIEW_SET = 1;
int NATIVE_PAGE_SHOWN = 2;
int BG_COLOR_UPDATE_AFTER_HIDING_NATIVE_PAGE = 3;
int NUM_ENTRIES = 4;
}
/**
* Creates an instance of a {@link TabImpl}. Package-private. Use {@link TabBuilder} to create
* an instance.
*
* @param id The id this tab should be identified with.
* @param profile The profile associated with this Tab.
* @param launchType Type indicating how this tab was launched.
*/
@SuppressLint("HandlerLeak")
TabImpl(int id, @NonNull Profile profile, @Nullable @TabLaunchType Integer launchType) {
mId = TabIdManager.getInstance().generateValidId(id);
mProfile = profile;
assert mProfile != null;
mRootId = mId;
// 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(crbug.com/41445155): Remove this once Tab UIs are all inflated from
// activity.
mThemedApplicationContext =
NightModeUtils.wrapContextWithNightModeConfig(
ContextUtils.getApplicationContext(),
ActivityUtils.getThemeId(),
/* nightMode= */ false);
mLaunchType = launchType;
mAttachStateChangeListener =
new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
mIsViewAttachedToWindow = true;
updateInteractableState();
}
@Override
public void onViewDetachedFromWindow(View view) {
if (isNativePage() && getNativePage().getView() == view) {
if (mNativePageSmoothTransitionDelegate != null) {
mNativePageSmoothTransitionDelegate.cancel();
mNativePageSmoothTransitionDelegate = null;
} else {
// reset ntp view state.
getView().setAlpha(1f);
}
}
mIsViewAttachedToWindow = false;
updateInteractableState();
}
};
mTabViewManager = new TabViewManagerImpl(this);
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 Profile getProfile() {
return mProfile;
}
@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, PdfUtils.getPdfInfo(getNativePage()));
}
} else {
updateIsDetached(window);
}
// 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();
}
public void didChangeCloseSignalInterceptStatus() {
for (TabObserver observer : mObservers) {
observer.onDidChangeCloseSignalInterceptStatus();
}
}
/** Sets a custom {@link View} for this {@link Tab} that replaces Content view. */
void setCustomView(@Nullable View view, @Nullable Integer backgroundColor) {
mCustomView = view;
mCustomViewBackgroundColor = backgroundColor;
notifyContentChanged();
onBackgroundColorChanged(BackgroundColorChangeOrigin.CUSTOM_VIEW_SET);
}
@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
public int getId() {
return mId;
}
@CalledByNative
@Override
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()) {
mUrl = url;
}
return mUrl != null ? mUrl : GURL.emptyGURL();
}
@Override
public GURL getOriginalUrl() {
return DomDistillerUrlUtils.getOriginalUrlFromDistillerUrl(getUrl());
}
@CalledByNative
@Override
public String getTitle() {
if (TextUtils.isEmpty(mTitle)) updateTitle();
return mTitle;
}
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
@CalledByNative
public @TabLaunchType int getLaunchType() {
return mLaunchType;
}
@Override
public int getThemeColor() {
return mThemeColor;
}
@Override
public int getBackgroundColor() {
if (ChromeFeatureList.sNavBarColorMatchesTabBackground.isEnabled()) {
if (mCustomView != null && mCustomViewBackgroundColor != null) {
return mCustomViewBackgroundColor;
}
if (mNativePage != null) {
return mNativePage.getBackgroundColor();
}
}
return mWebContentBackgroundColor;
}
@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;
}
@CalledByNative
@Deprecated
@Override
public boolean isIncognito() {
return mProfile.isOffTheRecord();
}
@Override
public boolean isOffTheRecord() {
return mProfile.isOffTheRecord();
}
@Override
public boolean isIncognitoBranded() {
return mProfile.isIncognitoBranded();
}
@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 boolean isDetached() {
assert !checkAttached() == mIsDetached
: "Activity/Window attachment does not match Tab.mIsDetached == " + mIsDetached;
return mIsDetached;
}
private void updateIsDetached(WindowAndroid window) {
// HiddenTabHolder relies on isDetached() being true to determine whether the tab is
// a background tab during initWebContents() before invoking ReparentingTask#detach().
// In this scenario, the tab owns its own WindowAndroid and has no activity attachment.
// We must check this as an additional condition to detachment for this case to continue
// to work. See https://crbug.com/1501849.
mIsDetached = window == null || !windowHasActivity(window);
}
private boolean checkAttached() {
// getWindowAndroid() cannot be null (see updateWindowAndroid()) so this is effectively
// checking to ensure whether the WebContents has a window and the tab is attached to an
// activity.
boolean hasActivity = getWindowAndroid() != null && windowHasActivity(getWindowAndroid());
WebContents webContents = getWebContents();
return webContents == null
? !mIsDetached && hasActivity
: (webContents.getTopLevelNativeWindow() != null && hasActivity);
}
private boolean windowHasActivity(WindowAndroid window) {
return ContextUtils.activityFromContext(window.getContext().get()) != null;
}
/**
* The parent tab for the current tab is set and the DelegateFactory is updated if it is not set
* already. This happens only if the tab has been detached and the parent has not been set yet,
* for example, for the spare tab before loading url.
* @param parent The tab that caused this tab to be opened.
*/
@Override
public void reparentTab(Tab parent) {
// When parent is null, no action is taken since it is the same as the default setting (no
// parent).
if (parent != null) {
mParentId = parent.getId();
// Update the DelegateFactory if it is not already set, since it is associated with the
// parent tab.
if (mDelegateFactory == null) {
mDelegateFactory = ((TabImpl) parent).getDelegateFactory();
setDelegateFactory(mDelegateFactory);
}
}
}
@Override
public LoadUrlResult 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) {
boolean isPdf =
PdfUtils.shouldOpenPdfInline(isIncognito())
&& PdfUtils.isPdfNavigation(params.getUrl(), params);
mIsNativePageCommitPending =
maybeShowNativePage(params.getUrl(), false, isPdf ? new PdfInfo() : null);
if (isPdf) {
params.setIsPdf(true);
}
}
if ("chrome://java-crash/".equals(params.getUrl())) {
return handleJavaCrash();
}
if (isDestroyed()) {
// This will crash below, but we want to know if the tab was destroyed or just never
// initialize.
throw new RuntimeException("Tab.loadUrl called on a destroyed tab");
}
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");
}
// TODO(crbug.com/40549331): 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());
// Request desktop sites if necessary.
if (fixedUrl.isValid()) {
params.setOverrideUserAgent(calculateUserAgentOverrideOption(fixedUrl));
} else {
// Fall back to the Url in webContents for site level setting.
params.setOverrideUserAgent(calculateUserAgentOverrideOption(null));
}
LoadUrlResult result = loadUrlInternal(params, fixedUrl);
for (TabObserver observer : mObservers) {
observer.onLoadUrl(this, params, result);
}
return result;
} finally {
TraceEvent.end("Tab.loadUrl");
}
}
private LoadUrlResult loadUrlInternal(LoadUrlParams params, GURL fixedUrl) {
if (mWebContents == null) return new LoadUrlResult(TabLoadStatus.PAGE_LOAD_FAILED, null);
if (!fixedUrl.isValid()) return new LoadUrlResult(TabLoadStatus.PAGE_LOAD_FAILED, null);
// 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 new LoadUrlResult(TabLoadStatus.DEFAULT_PAGE_LOAD, null);
}
params.setUrl(fixedUrl.getSpec());
NavigationHandle handle = mWebContents.getNavigationController().loadUrl(params);
return new LoadUrlResult(TabLoadStatus.DEFAULT_PAGE_LOAD, handle);
}
@Override
public void freezeAndAppendPendingNavigation(LoadUrlParams params, @Nullable String title) {
assert isHidden() : "Should only freeze and apprend a navigation to a tab that is hidden.";
// If the native page is not already torn down make sure we remove it so it isn't visible if
// this tab is foregrounded again in the current session.
hideNativePage(/* notify= */ false, /* postHideTask= */ null);
WebContentsState oldWebContentsState = TabStateExtractor.getWebContentsState(this);
WebContents oldWebContents = mWebContents;
destroyWebContents(false);
mWebContents = null;
RewindableIterator<TabObserver> observers = getTabObservers();
if (oldWebContents != null) {
while (observers.hasNext()) {
observers.next().onContentChanged(this);
}
observers.rewind();
oldWebContents.destroy();
}
Referrer referrer = params.getReferrer();
mWebContentsState =
WebContentsStateBridge.appendPendingNavigation(
oldWebContentsState,
title,
params.getUrl(),
referrer != null ? referrer.getUrl() : null,
// Policy will be ignored for null referrer url, 0 is just a placeholder.
referrer != null ? referrer.getPolicy() : 0,
params.getInitiatorOrigin(),
isOffTheRecord());
// The only reason this should still be null is if we failed to allocate a byte buffer,
// which probably means we are close to an OOM.
boolean success = mWebContentsState != null;
RecordHistogram.recordBooleanHistogram(
"Tabs.FreezeAndAppendPendingNavigationResult", success);
if (success) {
mUrl = new GURL(mWebContentsState.getVirtualUrlFromState());
} else {
// Since we are not allowed to auto-navigate the only remaining fallback is to clobber
// all navigation state and treat the tab as if it is in a pending load state. All the
// previous state was already cleaned up so we just need to set the params here.
mPendingLoadParams = params;
mUrl = new GURL(params.getUrl());
}
while (observers.hasNext()) {
observers.next().onUrlUpdated(this);
}
observers.rewind();
notifyFaviconChanged();
updateTitle(title);
while (observers.hasNext()) {
observers.next().onNavigationEntriesAppended(this);
}
}
@Override
public boolean loadIfNeeded(@TabLoadIfNeededCaller int caller) {
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());
if (webContents == null) {
webContents = WebContentsFactory.createWebContents(mProfile, isHidden(), false);
}
initWebContents(webContents);
loadUrl(mPendingLoadParams);
mPendingLoadParams = null;
return true;
}
restoreIfNeeded(caller);
return true;
}
@Override
public void reload() {
NativePage nativePage = getNativePage();
if (nativePage != null) {
nativePage.reload();
return;
}
// 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));
return;
}
if (getWebContents() == null) return;
switchUserAgentIfNeeded(UseDesktopUserAgentCaller.RELOAD);
getWebContents().getNavigationController().reload(true);
}
@Override
public void reloadIgnoringCache() {
if (getWebContents() != null) {
switchUserAgentIfNeeded(UseDesktopUserAgentCaller.RELOAD_IGNORING_CACHE);
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, @TabLoadIfNeededCaller int caller) {
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(caller);
// TODO(crbug.com/40199376): We should provide a timestamp that apporoximates the input
// event timestamp. When presenting a Tablet UI, StripLayoutTab.handleClick does
// receive a timestamp. When presenting a Phone UI
// TabGridViewBinder.bindClosableTabProperties is called by Android.View.performClick,
// and does not receive the event timestamp. This currently triggers an animation in
// TabSwitcherLayout.startHidingImpl which lasts around 300ms.
// TabSwitcherLayout.doneHiding runs after the animation, actually triggering this tab
// change.
//
// We should also consider merging the TabImpl and WebContents onShow into a single Jni
// call.
TabImplJni.get().onShow(mNativeTabAndroid);
if (getWebContents() != null) getWebContents().onShow();
// If the NativePage was frozen while in the background (see NativePageAssassin),
// recreate the NativePage now.
NativePage nativePage = getNativePage();
PdfUtils.recordIsPdfFrozen(nativePage);
if (nativePage != null && nativePage.isFrozen()) {
maybeShowNativePage(nativePage.getUrl(), true, PdfUtils.getPdfInfo(nativePage));
}
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.
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) {
if (mIsClosing == closing) return;
mIsClosing = closing;
for (TabObserver observer : mObservers) observer.onClosingStateChanged(this, closing);
}
@CalledByNative
@Override
public boolean isHidden() {
return mIsHidden;
}
@Override
public void destroy() {
ThreadUtils.assertOnUiThread();
// 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;
}
protected void updateWebContentObscured(boolean obscureWebContent) {
// 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 =
obscureWebContent
? 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) {
if (mIsWebContentObscured == obscureWebContent) return;
wcax.setObscuredByAnotherView(obscureWebContent);
mIsWebContentObscured = obscureWebContent;
}
}
/**
* 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 or null if we initialize a tab
* without an URL.
* @param pendingTitle The title used for a lazily load Tab. Ignored if {@code loadUrlParams} is
* {@code null}.
* @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.
* @param initializeRenderer Determines whether or not we initialize renderer with {@link
* WebContents} creation.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
void initialize(
Tab parent,
@Nullable @TabCreationState Integer creationState,
@Nullable LoadUrlParams loadUrlParams,
@Nullable String pendingTitle,
WebContents webContents,
@Nullable TabDelegateFactory delegateFactory,
boolean initiallyHidden,
TabState tabState,
boolean initializeRenderer) {
try {
TraceEvent.begin("Tab.initialize");
if (parent != null) {
mParentId = parent.getId();
}
mTabLaunchTypeAtCreation = mLaunchType;
mCreationState = creationState;
// If applicable set up for a lazy background tab load.
mPendingLoadParams = loadUrlParams;
if (loadUrlParams != null) {
mUrl = new GURL(loadUrlParams.getUrl());
if (pendingTitle != null) {
setTitle(pendingTitle);
}
}
// 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 (mWebContentsState != null || getPendingLoadParams() != null) {
return;
}
boolean creatingWebContents = webContents == null;
if (creatingWebContents) {
webContents =
WarmupManager.getInstance()
.takeSpareWebContents(isIncognito(), initiallyHidden);
if (webContents == null) {
webContents =
WebContentsFactory.createWebContents(
mProfile, initiallyHidden, initializeRenderer);
}
}
initWebContents(webContents);
// Avoid an empty title by updating the title here. This could happen if restoring from
// a WebContents that has no renderer and didn't force a reload. This happens on
// background tab creation from Recent Tabs (TabRestoreService).
updateTitle();
if (!creatingWebContents && webContents.shouldShowLoadingUI()) {
didStartPageLoad(webContents.getVisibleUrl());
}
} finally {
if (mTimestampMillis == INVALID_TIMESTAMP) {
setTimestampMillis(System.currentTimeMillis());
}
String appId = null;
Boolean hasThemeColor = null;
int themeColor = 0;
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");
}
}
@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;
mWebContentsState = state.contentsState;
setTimestampMillis(state.timestampMillis);
setLastNavigationCommittedTimestampMillis(state.lastNavigationCommittedTimestampMillis);
mUrl = new GURL(state.contentsState.getVirtualUrlFromState());
setTitle(state.contentsState.getDisplayTitleFromState());
mTabLaunchTypeAtCreation = state.tabLaunchTypeAtCreation;
setRootId(state.rootId == Tab.INVALID_TAB_ID ? mId : state.rootId);
setTabGroupId(state.tabGroupId);
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() {
// During title update, we prioritize titles in NativePage instead of those from
// WebContents. Thus we should remove the obsolete NativePage before title update.
if (mNativePage != null) hideNativePage(true, null);
updateTitle();
}
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);
updateIsDetached(windowAndroid);
}
TabDelegateFactory getDelegateFactory() {
return mDelegateFactory;
}
@VisibleForTesting
TabWebContentsDelegateAndroidImpl getTabWebContentsDelegateAndroid() {
return mWebContentsDelegate;
}
// Forwarded from TabViewAndroidDelegate.
/**
* Implementation of the {@link View#onProvideAutofillVirtualStructure(ViewStructure, int)}
* method for this tab. Noop if {@link AutofillProvider} isn't used on this tab.
*
* @see View#onProvideAutofillVirtualStructure(ViewStructure structure, int flags)
* @see ViewAndroidDelegate#onProvideAutofillVirtualStructure(ViewStructure structure, int
* flags)
*/
void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
if (mAutofillProvider != null) {
mAutofillProvider.onProvideAutoFillVirtualStructure(structure, flags);
}
}
/**
* Implementation of the {@link View#autofill(SparseArray))} method for this tab. Noop if {@link
* AutofillProvider} isn't used on this tab.
*
* @see View#autofill(SparseArray)
* @see ViewAndroidDelegate#autofill(SparseArray)
*/
void autofill(final SparseArray<AutofillValue> values) {
if (mAutofillProvider != null) {
mAutofillProvider.autofill(values);
}
}
/**
* Check whether the platform can request a ViewStructure.
*
* @return iff the AutofillProvider should provide a ViewStructure when prompted.
*/
boolean providesAutofillStructure() {
// TODO(b/326231439): Check pref and AutofillService!
return ChromeFeatureList.isEnabled(
AutofillFeatures.AUTOFILL_VIRTUAL_VIEW_STRUCTURE_ANDROID);
}
// 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);
}
}
void handleBackForwardTransitionUiChanged() {
for (TabObserver observer : mObservers) {
observer.didBackForwardTransitionAnimationChange();
}
// Start the cross-fade animation after the invoking animation is done.
switch (getWebContents().getCurrentBackForwardTransitionStage()) {
case AnimationStage.NONE:
// Native animator is destroy before animation is done.
// Non-null nativePageSmoothTransitionDelegate means the page is transiting to
// a native page; otherwise, it possibly means transiting from a native page to
// another page.
if (mNativePageSmoothTransitionDelegate != null) {
mNativePageSmoothTransitionDelegate.cancel();
mNativePageSmoothTransitionDelegate = null;
} else if (isNativePage()) {
// This means the ntp is fully showing now. Reset back to 1f.
getView().setAlpha(1f);
}
return;
case AnimationStage.OTHER:
if (mNativePageSmoothTransitionDelegate != null) {
mNativePageSmoothTransitionDelegate.start(
() -> {
getWebContents().onContentForNavigationEntryShown();
notifyContentChanged();
});
mNativePageSmoothTransitionDelegate = null;
} else if (isNativePage()) {
// Do a hidden transition for NTP view.
getView().setAlpha(0.f);
}
return;
case AnimationStage.INVOKE_ANIMATION:
// invoking animation is in-progress. Wait for it to be finished.
return;
}
}
// 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.
* @param isPdf Whether the navigation is for PDF content.
*/
void handleDidFinishNavigation(GURL url, int transitionType, boolean isPdf) {
mIsNativePageCommitPending = false;
boolean isReload = (transitionType & PageTransition.CORE_MASK) == PageTransition.RELOAD;
// Set isPdf param based on the url. This is because the isPdf param in NavigationHandle is
// not set in some cases (e.g. Chrome restart or navigate backward to pdf page). When the
// pdf file is downloaded to media store, we should set isPdf param and open pdf page
// immediately, because no re-download is expected.
isPdf |=
PdfUtils.shouldOpenPdfInline(isIncognito())
&& PdfUtils.isDownloadedPdf(url.getSpec());
if (!maybeShowNativePage(url.getSpec(), isReload, isPdf ? new PdfInfo() : null)) {
String downloadUrl = PdfUtils.decodePdfPageUrl(url.getSpec());
if (downloadUrl != null) {
// When the download url is not null, we are on a pdf native page which requires
// re-download. Load the download url to trigger the re-download.
loadUrl(new LoadUrlParams(downloadUrl));
} else {
showRenderedPage();
}
}
setLastNavigationCommittedTimestampMillis(System.currentTimeMillis());
}
/**
* 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.
* @param pdfInfo Information of the pdf, or null if there is no associated pdf download.
* @return True, if a native page was displayed for url.
*/
boolean maybeShowNativePage(String url, boolean forceReload, PdfInfo pdfInfo) {
// 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()) return false;
// TODO(crbug.com/40943608): Remove the assert after determining why WebContents can be
// null.
WebContents webContents = getWebContents();
assert webContents != null;
if (webContents == null) return false;
// If the given url is null, there's no work to do.
if (url == null) return false;
// We might be in the middle of loading a native page, in that case we should bail to avoid
// recreating another instance.
String nativePageHost = Uri.parse(url).getHost();
if (mIsAlreadyCreatingNativePage
&& TextUtils.equals(mPendingNativePageHost, nativePageHost)) {
return true;
}
mPendingNativePageHost = nativePageHost;
mIsAlreadyCreatingNativePage = true;
NativePage candidateForReuse = forceReload ? null : getNativePage();
NativePage nativePage =
mDelegateFactory.createNativePage(url, candidateForReuse, this, pdfInfo);
mIsAlreadyCreatingNativePage = false;
mPendingNativePageHost = null;
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) {
if (mThemeColor == themeColor) return;
mThemeColor = themeColor;
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) observers.next().onDidChangeThemeColor(this, themeColor);
}
/** Update the title for the current page if changed. */
@Override
public 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(mTitle, title)) return;
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();
// When the renderer crashes for a hidden spare tab, we can skip notifying the observers to
// crash the underlying tab. This is because it is safe to keep the spare tab around without
// a renderer process, and since the tab is hidden, we don't need to show a sad tab. When
// the spare tab is used for navigation it will create a new renderer process.
// TODO(crbug.com/40268909): Make this logic more robust for all hidden tab cases.
if (!WarmupManager.getInstance().isSpareTab(this)) {
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 changeWebContentBackgroundColor(int color) {
mWebContentBackgroundColor = color;
onBackgroundColorChanged(BackgroundColorChangeOrigin.WEB_BACKGROUND_COLOR_CHANGE);
mWaitingOnBgColorAfterHidingNativePage = false;
}
/** Called to notify when the page had painted something non-empty. */
void notifyDidFirstVisuallyNonEmptyPaint() {
if (ChromeFeatureList.sNavBarColorMatchesTabBackground.isEnabled()
&& mWaitingOnBgColorAfterHidingNativePage) {
onBackgroundColorChanged(
BackgroundColorChangeOrigin.BG_COLOR_UPDATE_AFTER_HIDING_NATIVE_PAGE);
}
mWaitingOnBgColorAfterHidingNativePage = false;
}
/**
* @param backgroundColorChangeOrigin The origin of the background color change update. This is
* used to track the number of color changes and the potential performance impact those
* entail.
*/
private void onBackgroundColorChanged(
@BackgroundColorChangeOrigin int backgroundColorChangeOrigin) {
RecordHistogram.recordEnumeratedHistogram(
BACKGROUND_COLOR_CHANGE_PRE_OPTIMIZATION_HISTOGRAM,
backgroundColorChangeOrigin,
BackgroundColorChangeOrigin.NUM_ENTRIES);
int newBackgroundColor = getBackgroundColor();
// Avoid notifying the observers if the background color hasn't actually changed.
if (mTabBackgroundColor == newBackgroundColor
&& ChromeFeatureList.sNavBarColorMatchesTabBackground.isEnabled()) return;
mTabBackgroundColor = newBackgroundColor;
RecordHistogram.recordEnumeratedHistogram(
BACKGROUND_COLOR_CHANGE_HISTOGRAM,
backgroundColorChangeOrigin,
BackgroundColorChangeOrigin.NUM_ENTRIES);
for (TabObserver observer : mObservers) {
observer.onBackgroundColorChanged(this, mTabBackgroundColor);
}
}
/** This is currently used when restoring tabs, and by DOMDistiller */
@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, mProfile, mId);
}
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 long getLastShownTimestamp() {
return mTimestampMillis;
}
@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;
}
@CalledByNative
private ByteBuffer getWebContentsStateByteBuffer() {
// Return a temp byte buffer if the state is null.
if (mWebContentsState == null) {
return ByteBuffer.allocateDirect(0);
}
assert mWebContentsState.buffer().isDirect();
return mWebContentsState.buffer();
}
@CalledByNative
private int getWebContentsStateSavedStateVersion() {
// Return an invalid saved state version if the state is null.
return mWebContentsState == null ? -1 : mWebContentsState.version();
}
/**
* 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(@NonNull WebContents webContents) {
try {
TraceEvent.begin("ChromeTab.initWebContents");
WebContents oldWebContents = mWebContents;
mWebContents = webContents;
ContentView cv = ContentView.createContentView(mThemedApplicationContext, webContents);
cv.setContentDescription(
mThemedApplicationContext
.getResources()
.getString(R.string.accessibility_content_view));
mContentView = cv;
webContents.setDelegates(
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(null) == UserAgentOverrideOption.TRUE);
mContentView.addOnAttachStateChangeListener(mAttachStateChangeListener);
updateInteractableState();
mWebContentsDelegate = createWebContentsDelegate();
// TODO(crbug.com/40942165): Find a better way of indicating this is a background tab
// (or
// move the logic elsewhere).
boolean isBackgroundTab = isDetached();
assert mNativeTabAndroid != 0;
TabImplJni.get()
.initWebContents(
mNativeTabAndroid,
isOffTheRecord(),
isBackgroundTab,
webContents,
mWebContentsDelegate,
new TabContextMenuPopulatorFactory(
mDelegateFactory.createContextMenuPopulatorFactory(this),
this));
mWebContents.notifyRendererPreferenceUpdate();
mContentView.setImportantForAutofill(
prepareAutofillProvider(webContents)
? View.IMPORTANT_FOR_AUTOFILL_YES
: View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
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);
}
if (isDisplayingBackForwardAnimation()) {
assert ChromeFeatureList.isEnabled(
ChromeFeatureList.BACK_FORWARD_TRANSITIONS)
: "Must not draw bf screenshot if back forward transition is"
+ " disabled";
mNativePageSmoothTransitionDelegate = mNativePage.enableSmoothTransition();
mNativePageSmoothTransitionDelegate.prepare();
}
pushNativePageStateToNavigationEntry();
if (ChromeFeatureList.sNavBarColorMatchesTabBackground.isEnabled()) {
onBackgroundColorChanged(BackgroundColorChangeOrigin.NATIVE_PAGE_SHOWN);
}
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) {
if (mNativePageSmoothTransitionDelegate != null) {
mNativePageSmoothTransitionDelegate.cancel();
mNativePageSmoothTransitionDelegate = null;
} else if (isNativePage() && getView() != null) {
getView().setAlpha(1.f);
}
NativePage previousNativePage = mNativePage;
if (mNativePage != null) {
if (!mNativePage.isFrozen()) {
mNativePage.getView().removeOnAttachStateChangeListener(mAttachStateChangeListener);
}
mNativePage = null;
mWaitingOnBgColorAfterHidingNativePage = true;
}
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, 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 && !isDetached();
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 void restoreIfNeeded(@TabLoadIfNeededCaller int caller) {
// Attempts to display the Paint Preview representation of this Tab.
if (isFrozen()) StartupPaintPreviewHelper.showPaintPreviewOnRestore(this);
try {
TraceEvent.begin("Tab.restoreIfNeeded");
assert !isFrozen() || mWebContentsState != null
: "crbug/1393848: A frozen tab must have WebContentsState to restore from.";
// Restore is needed for a tab that is loaded for the first time. WebContents will
// be restored from a saved state.
if ((isFrozen() && mWebContentsState != null && !unfreezeContents())
|| !needsReload()) {
return;
}
if (mWebContents != null) {
// Invoke switchUserAgentIfNeeded() from restoreIfNeeded() instead of loadIfNeeded()
// to avoid reload without explicit user intent.
switchUserAgentIfNeeded(UseDesktopUserAgentCaller.LOAD_IF_NEEDED + caller);
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");
assert mWebContentsState != null;
WebContents webContents =
WebContentsStateBridge.restoreContentsFromByteBuffer(
mWebContentsState, 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).
webContents = WebContentsFactory.createWebContents(mProfile, isHidden(), false);
for (TabObserver observer : mObservers) observer.onRestoreFailed(this);
restored = false;
}
Supplier<CompositorViewHolder> compositorViewHolderSupplier =
getActivity().getCompositorViewHolderSupplier();
View compositorView = compositorViewHolderSupplier.get();
webContents.setSize(compositorView.getWidth(), compositorView.getHeight());
mWebContentsState = null;
initWebContents(webContents);
if (!restored) {
String url = mUrl.getSpec().isEmpty() ? UrlConstants.NTP_URL : mUrl.getSpec();
loadUrl(new LoadUrlParams(url, PageTransition.GENERATED));
}
} finally {
TraceEvent.end("Tab.unfreezeContents");
}
return restored;
}
/**
* Initializes the {@link AutofillProvider} so that it can provide a ViewStructure for the given
* WebContents. If the provider existed already, it's only assigned the new WebContents.
*
* @param newWebContents The webcontents to prepare the provider for.
* @return true if the the provider is available for the given WebContents.
*/
private boolean prepareAutofillProvider(WebContents newWebContents) {
assert isInitialized();
if (!providesAutofillStructure()) {
mAutofillProvider = null;
return false; // Autofill provider can't be prepared.
}
if (mAutofillProvider != null) {
// Provider already existed. Swapping contents suffices.
mAutofillProvider.setWebContents(newWebContents);
} else {
mAutofillProvider =
new AutofillProvider(
getContext(),
mContentView,
newWebContents,
getContext().getString(R.string.app_name));
TabImplJni.get().initializeAutofillIfNecessary(mNativeTabAndroid);
}
addAutofillItemsToSelectionActionMenu(newWebContents);
return true;
}
private void addAutofillItemsToSelectionActionMenu(WebContents webContents) {
assert webContents != null;
assert mAutofillProvider != null;
SelectionPopupController controller = SelectionPopupController.fromWebContents(webContents);
if (controller == null) {
return;
}
AutofillSelectionActionMenuDelegate selectionActionMenuDelegate =
new AutofillSelectionActionMenuDelegate();
selectionActionMenuDelegate.setAutofillSelectionMenuItemHelper(
new AutofillSelectionMenuItemHelper(
ContextUtils.getApplicationContext(), mAutofillProvider));
controller.setSelectionActionMenuDelegate(selectionActionMenuDelegate);
}
@CalledByNative
@Override
public boolean isCustomTab() {
ChromeActivity activity = getActivity();
return activity != null && activity.isCustomTab();
}
@Override
public long getTimestampMillis() {
return mTimestampMillis;
}
private void setTimestampMillis(long timestampMillis) {
mTimestampMillis = timestampMillis;
for (TabObserver tabObserver : mObservers) {
tabObserver.onTimestampChanged(this, timestampMillis);
}
}
/**
* @return parent identifier for the {@link Tab}
*/
@Override
public int getParentId() {
return mParentId;
}
@Override
public void setParentId(int parentId) {
mParentId = parentId;
}
@Override
public int getRootId() {
return mRootId;
}
@Override
public void setRootId(int rootId) {
if (mRootId == rootId || isDestroyed()) return;
mRootId = rootId;
for (TabObserver observer : mObservers) {
observer.onRootIdChanged(this, rootId);
}
}
@Override
public @Nullable Token getTabGroupId() {
return mTabGroupId;
}
@Override
public void setTabGroupId(@Nullable Token tabGroupId) {
assert tabGroupId == null || !tabGroupId.isZero() : "A TabGroupId token must be non-zero.";
if (Objects.equals(mTabGroupId, tabGroupId) || isDestroyed()) return;
mTabGroupId = tabGroupId;
for (TabObserver observer : mObservers) {
observer.onTabGroupIdChanged(this, tabGroupId);
}
}
@Override
@CalledByNative
public @TabUserAgent int getUserAgent() {
return mUserAgent;
}
@Override
public void setUserAgent(@TabUserAgent int userAgent) {
mUserAgent = userAgent;
}
@Override
public WebContentsState getWebContentsState() {
return mWebContentsState;
}
@VisibleForTesting
void setWebContentsState(WebContentsState webContentsState) {
mWebContentsState = webContentsState;
}
@VisibleForTesting
void setAutofillProvider(AutofillProvider autofillProvider) {
mAutofillProvider = autofillProvider;
}
@VisibleForTesting
protected void setTitle(String title) {
mTitle = title;
}
public void setTimestampMillisForTesting(long timestamp) {
mTimestampMillis = timestamp;
}
@Override
public long getLastNavigationCommittedTimestampMillis() {
return mLastNavigationCommittedTimestampMillis;
}
/**
* Set the last hidden timestamp.
*
* @param lastNavigationCommittedTimestampMillis The timestamp when the tab was last interacted.
*/
@VisibleForTesting
public void setLastNavigationCommittedTimestampMillis(
long lastNavigationCommittedTimestampMillis) {
mLastNavigationCommittedTimestampMillis = lastNavigationCommittedTimestampMillis;
}
@Override
public @Nullable @TabLaunchType Integer getTabLaunchTypeAtCreation() {
return mTabLaunchTypeAtCreation;
}
/**
* Throws a RuntimeException. Useful for testing crash reports with obfuscated Java stacktraces.
*/
private LoadUrlResult 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) {
if (mWebContentsState == null) return;
WebContentsState newState =
WebContentsStateBridge.deleteNavigationEntries(mWebContentsState, predicate);
if (newState != null) {
mWebContentsState = 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;
if (mAutofillProvider != null) {
mAutofillProvider.destroy();
mAutofillProvider = null;
}
mContentView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
mContentView = null;
updateInteractableState();
WebContents contentsToDestroy = mWebContents;
if (contentsToDestroy.getViewAndroidDelegate() != null
&& contentsToDestroy.getViewAndroidDelegate() instanceof TabViewAndroidDelegate) {
((TabViewAndroidDelegate) contentsToDestroy.getViewAndroidDelegate()).destroy();
}
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.setDelegates(
PRODUCT_VERSION,
ViewAndroidDelegate.createBasicDelegate(/* containerView= */ null),
/* accessDelegate= */ null,
/* windowAndroid= */ null,
WebContents.createDefaultInternalsHolder());
}
}
private @UserAgentOverrideOption int calculateUserAgentOverrideOption(@Nullable GURL url) {
WebContents webContents = getWebContents();
boolean currentRequestDesktopSite = TabUtils.isUsingDesktopUserAgent(webContents);
@TabUserAgent int tabUserAgent = TabUtils.getTabUserAgent(this);
// INHERIT means use the same UA that was used last time.
@UserAgentOverrideOption int userAgentOverrideOption = UserAgentOverrideOption.INHERIT;
if (url == null && webContents != null) {
url = webContents.getVisibleUrl();
}
// Do not override UA if there is a tab level setting.
if (tabUserAgent != TabUserAgent.DEFAULT) {
recordHistogramUseDesktopUserAgent(currentRequestDesktopSite);
RequestDesktopUtils.maybeUpgradeTabLevelDesktopSiteSetting(
this, mProfile, tabUserAgent, url);
return userAgentOverrideOption;
}
CommandLine commandLine = CommandLine.getInstance();
// For --request-desktop-sites, always override the user agent.
boolean alwaysRequestDesktopSite =
commandLine.hasSwitch(ChromeSwitches.REQUEST_DESKTOP_SITES);
boolean shouldRequestDesktopSite =
alwaysRequestDesktopSite
|| (TabUtils.readRequestDesktopSiteContentSettings(mProfile, url)
&& !RequestDesktopUtils.shouldApplyWindowSetting(
mProfile, url, getContext()));
if (shouldRequestDesktopSite != currentRequestDesktopSite) {
// 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.
userAgentOverrideOption =
shouldRequestDesktopSite
? UserAgentOverrideOption.TRUE
: UserAgentOverrideOption.FALSE;
}
recordHistogramUseDesktopUserAgent(shouldRequestDesktopSite);
return userAgentOverrideOption;
}
// TODO(crbug.com/40195571): Confirm if a new histogram should be used.
private void recordHistogramUseDesktopUserAgent(boolean value) {
RecordHistogram.recordBooleanHistogram(
"Android.RequestDesktopSite.UseDesktopUserAgent", value);
}
private void switchUserAgentIfNeeded(int caller) {
if (calculateUserAgentOverrideOption(null) == UserAgentOverrideOption.INHERIT
|| getWebContents() == null) {
return;
}
boolean usingDesktopUserAgent =
getWebContents().getNavigationController().getUseDesktopUserAgent();
TabUtils.switchUserAgent(this, /* switchToDesktop= */ !usingDesktopUserAgent, caller);
}
/** Sets the TabLaunchType for tabs launched with an unset launch type. */
@Override
public void setTabLaunchType(@TabLaunchType int launchType) {
assert mLaunchType == TabLaunchType.UNSET;
mLaunchType = launchType;
}
@Override
public boolean isDisplayingBackForwardAnimation() {
if (getWebContents() == null) return false;
return getWebContents().getCurrentBackForwardTransitionStage() != AnimationStage.NONE;
}
/**
* Forces a resize of the web contents view to accommodate for browser controls immediately.
*
* <p>This is used to force the resize to happen at the same time as the controls are requested
* to show (potentially animate) so that web content can be adapted to the controls sooner.
*/
public void willShowBrowserControls() {
assert mWebContents != null;
boolean hasViewTransitionOptIn = mWebContents.hasViewTransitionOptIn();
for (TabObserver observer : mObservers) {
observer.onWillShowBrowserControls(this, hasViewTransitionOptIn);
}
}
@CalledByNative
@Override
public boolean isTrustedWebActivity() {
if (getWebContents() == null) return false;
return mWebContentsDelegate.isTrustedWebActivity(getWebContents());
}
@NativeMethods
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public interface Natives {
TabImpl fromWebContents(WebContents webContents);
void init(TabImpl caller, @JniType("Profile*") Profile profile, int id);
void destroy(long nativeTabAndroid);
void initWebContents(
long nativeTabAndroid,
boolean isOffTheRecord,
boolean isBackgroundTab,
WebContents webContents,
TabWebContentsDelegateAndroidImpl delegate,
ContextMenuPopulatorFactory contextMenuPopulatorFactory);
void initializeAutofillIfNecessary(long nativeTabAndroid);
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);
void onShow(long nativeTabAndroid);
}
}