blob: 2b4dc0eca058106f137b6b36116d8378d139f867 [file] [log] [blame]
// Copyright 2018 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.customtabs.content;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.provider.Browser;
import android.text.TextUtils;
import android.view.Window;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsSessionToken;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.HintlessActivityTabObserver;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.ServiceTabLauncher;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.WebContentsFactory;
import org.chromium.chrome.browser.compositor.CompositorViewHolder;
import org.chromium.chrome.browser.customtabs.CustomTabDelegateFactory;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.LaunchSourceType;
import org.chromium.chrome.browser.customtabs.CustomTabNavigationEventObserver;
import org.chromium.chrome.browser.customtabs.CustomTabObserver;
import org.chromium.chrome.browser.customtabs.CustomTabTabPersistencePolicy;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.FirstMeaningfulPaintObserver;
import org.chromium.chrome.browser.customtabs.PageLoadMetricsObserver;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.InflationObserver;
import org.chromium.chrome.browser.lifecycle.NativeInitObserver;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabAssociatedApp;
import org.chromium.chrome.browser.tab.TabObserverRegistrar;
import org.chromium.chrome.browser.tab.TabRedirectHandler;
import org.chromium.chrome.browser.tabmodel.AsyncTabParams;
import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
import org.chromium.chrome.browser.tabmodel.TabReparentingParams;
import org.chromium.chrome.browser.translate.TranslateBridge;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.content_public.browser.WebContents;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Inject;
import dagger.Lazy;
/**
* Creates a new Tab or retrieves an existing Tab for the CustomTabActivity, and initializes it.
*/
@ActivityScope
public class CustomTabActivityTabController implements InflationObserver, NativeInitObserver {
// For CustomTabs.WebContentsStateOnLaunch, see histograms.xml. Append only.
@IntDef({WebContentsState.NO_WEBCONTENTS, WebContentsState.PRERENDERED_WEBCONTENTS,
WebContentsState.SPARE_WEBCONTENTS, WebContentsState.TRANSFERRED_WEBCONTENTS})
@Retention(RetentionPolicy.SOURCE)
private @interface WebContentsState {
int NO_WEBCONTENTS = 0;
int PRERENDERED_WEBCONTENTS = 1;
int SPARE_WEBCONTENTS = 2;
int TRANSFERRED_WEBCONTENTS = 3;
int NUM_ENTRIES = 4;
}
private final Lazy<CustomTabDelegateFactory> mCustomTabDelegateFactory;
private final ChromeActivity mActivity;
private final CustomTabsConnection mConnection;
private final CustomTabIntentDataProvider mIntentDataProvider;
private final TabObserverRegistrar mTabObserverRegistrar;
private final Lazy<CompositorViewHolder> mCompositorViewHolder;
private final WarmupManager mWarmupManager;
private final CustomTabTabPersistencePolicy mTabPersistencePolicy;
private final CustomTabActivityTabFactory mTabFactory;
private final Lazy<CustomTabObserver> mCustomTabObserver;
private final WebContentsFactory mWebContentsFactory;
private final CustomTabNavigationEventObserver mTabNavigationEventObserver;
private final ActivityTabProvider mActivityTabProvider;
private final CustomTabActivityTabProvider mTabProvider;
@Nullable
private final CustomTabsSessionToken mSession;
private final Intent mIntent;
@Nullable
private HintlessActivityTabObserver mTabSwapObserver = new HintlessActivityTabObserver() {
@Override
public void onActivityTabChanged(@Nullable Tab tab) {
mTabProvider.swapTab(tab);
}
};
@Inject
public CustomTabActivityTabController(ChromeActivity activity,
Lazy<CustomTabDelegateFactory> customTabDelegateFactory,
CustomTabsConnection connection, CustomTabIntentDataProvider intentDataProvider,
ActivityTabProvider activityTabProvider, TabObserverRegistrar tabObserverRegistrar,
Lazy<CompositorViewHolder> compositorViewHolder,
ActivityLifecycleDispatcher lifecycleDispatcher, WarmupManager warmupManager,
CustomTabTabPersistencePolicy persistencePolicy, CustomTabActivityTabFactory tabFactory,
Lazy<CustomTabObserver> customTabObserver, WebContentsFactory webContentsFactory,
CustomTabNavigationEventObserver tabNavigationEventObserver,
CustomTabActivityTabProvider tabProvider) {
mCustomTabDelegateFactory = customTabDelegateFactory;
mActivity = activity;
mConnection = connection;
mIntentDataProvider = intentDataProvider;
mTabObserverRegistrar = tabObserverRegistrar;
mCompositorViewHolder = compositorViewHolder;
mWarmupManager = warmupManager;
mTabPersistencePolicy = persistencePolicy;
mTabFactory = tabFactory;
mCustomTabObserver = customTabObserver;
mWebContentsFactory = webContentsFactory;
mTabNavigationEventObserver = tabNavigationEventObserver;
mActivityTabProvider = activityTabProvider;
mTabProvider = tabProvider;
mSession = mIntentDataProvider.getSession();
mIntent = mIntentDataProvider.getIntent();
// Save speculated url, because it will be erased later with mConnection.takeHiddenTab().
mTabProvider.setSpeculatedUrl(mConnection.getSpeculatedUrl(mSession));
lifecycleDispatcher.register(this);
}
/** @return whether allocating a child connection is needed during native initialization. */
public boolean shouldAllocateChildConnection() {
boolean hasSpeculated = !TextUtils.isEmpty(mConnection.getSpeculatedUrl(mSession));
int mode = mTabProvider.getInitialTabCreationMode();
return mode != TabCreationMode.EARLY && mode != TabCreationMode.HIDDEN
&& !hasSpeculated && !mWarmupManager.hasSpareWebContents();
}
/**
* Detaches the tab and starts reparenting into the browser using given {@param intent} and
* {@param startActivityOptions}.
*/
void detachAndStartReparenting(Intent intent, Bundle startActivityOptions,
Runnable finishCallback) {
Tab tab = mTabProvider.getTab();
if (tab == null) {
assert false;
return;
}
mTabProvider.removeTab();
tab.detachAndStartReparenting(intent, startActivityOptions, finishCallback);
}
/**
* Closes the current tab. This doesn't necessarily lead to closing the entire activity, in
* case links with target="_blank" were followed. See the comment to
* {@link CustomTabActivityTabProvider.Observer#onAllTabsClosed}.
*/
public void closeTab() {
mTabFactory.getTabModelSelector().getCurrentModel().closeTab(mTabProvider.getTab(),
false, false, false);
}
/** Closes the tab and deletes related metadata. */
void closeAndForgetTab() {
mTabFactory.getTabModelSelector().closeAllTabs(true);
mTabPersistencePolicy.deleteMetadataStateFileAsync();
}
/** Save the current state of the tab. */
void saveState() {
mTabFactory.getTabModelSelector().saveState();
}
/** Returns {@link TabModelSelector}. Should be called after postInflationStartup. */
public TabModelSelector getTabModelSelector() {
return mTabFactory.getTabModelSelector();
}
@Override
public void onPreInflationStartup() {
// This must be requested before adding content.
mActivity.supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY);
if (mActivity.getSavedInstanceState() == null && mConnection.hasWarmUpBeenFinished()) {
mTabFactory.initializeTabModels();
Tab tab = getHiddenTab();
if (tab == null) {
tab = createTab();
mTabProvider.setInitialTab(tab, TabCreationMode.EARLY);
} else {
mTabProvider.setInitialTab(tab, TabCreationMode.HIDDEN);
}
}
}
@Override
public void onPostInflationStartup() {}
@Override
public void onFinishNativeInitialization() {
// If extra headers have been passed, cancel any current speculation, as
// speculation doesn't support extra headers.
if (IntentHandler.getExtraHeadersFromIntent(mIntent) != null) {
mConnection.cancelSpeculation(mSession);
}
TabModelSelectorImpl tabModelSelector = mTabFactory.getTabModelSelector();
TabModel tabModel = tabModelSelector.getModel(mIntentDataProvider.isIncognito());
tabModel.addObserver(mTabObserverRegistrar);
finalizeCreatingTab(tabModelSelector, tabModel);
Tab tab = mTabProvider.getTab();
assert tab != null;
assert mTabProvider.getInitialTabCreationMode() != TabCreationMode.NONE;
// Put Sync in the correct state by calling tab state initialized. crbug.com/581811.
tabModelSelector.markTabStateInitialized();
// Notify ServiceTabLauncher if this is an asynchronous tab launch.
if (mIntent.hasExtra(ServiceTabLauncher.LAUNCH_REQUEST_ID_EXTRA)) {
ServiceTabLauncher.onWebContentsForRequestAvailable(
mIntent.getIntExtra(ServiceTabLauncher.LAUNCH_REQUEST_ID_EXTRA, 0),
tab.getWebContents());
}
}
// Creates the tab on native init, if it hasn't been created yet, and does all the additional
// initialization steps necessary at this stage.
private void finalizeCreatingTab(TabModelSelectorImpl tabModelSelector, TabModel tabModel) {
Tab earlyCreatedTab = mTabProvider.getTab();
Tab tab = earlyCreatedTab;
@TabCreationMode int mode = mTabProvider.getInitialTabCreationMode();
Tab restoredTab = tryRestoringTab(tabModelSelector);
if (restoredTab != null) {
assert earlyCreatedTab == null :
"Shouldn't create a new tab when there's one to restore";
tab = restoredTab;
mode = TabCreationMode.RESTORED;
}
if (tab == null) {
// No tab was restored or created early, creating a new tab.
tab = createTab();
mode = TabCreationMode.DEFAULT;
}
assert tab != null;
if (mode != TabCreationMode.RESTORED) {
tabModel.addTab(tab, 0, tab.getLaunchType());
}
// This cannot be done before because we want to do the reparenting only
// when we have compositor related controllers.
if (mode == TabCreationMode.HIDDEN) {
TabReparentingParams params =
(TabReparentingParams) AsyncTabParamsManager.remove(tab.getId());
tab.attachAndFinishReparenting(mActivity, mCustomTabDelegateFactory.get(),
(params == null ? null : params.getFinalizeCallback()));
}
if (tab != earlyCreatedTab) {
mTabProvider.setInitialTab(tab, mode);
} // else we've already set the initial tab.
// Listen to tab swapping and closing.
mActivityTabProvider.addObserverAndTrigger(mTabSwapObserver);
}
@Nullable
private Tab tryRestoringTab(TabModelSelectorImpl tabModelSelector) {
if (mActivity.getSavedInstanceState() == null) return null;
tabModelSelector.loadState(true);
tabModelSelector.restoreTabs(true);
Tab tab = tabModelSelector.getCurrentTab();
if (tab != null) {
initializeTab(tab);
}
return tab;
}
/** Encapsulates CustomTabsConnection#takeHiddenTab() with additional initialization logic. */
@Nullable
private Tab getHiddenTab() {
String url = mIntentDataProvider.getUrlToLoad();
String referrerUrl = mConnection.getReferrer(mSession, mIntent);
Tab tab = mConnection.takeHiddenTab(mSession, url, referrerUrl);
if (tab == null) return null;
RecordHistogram.recordEnumeratedHistogram("CustomTabs.WebContentsStateOnLaunch",
WebContentsState.PRERENDERED_WEBCONTENTS, WebContentsState.NUM_ENTRIES);
TabAssociatedApp.from(tab).setAppId(mConnection.getClientPackageNameForSession(mSession));
if (mIntentDataProvider.shouldEnableEmbeddedMediaExperience()) {
// Configures web preferences for viewing downloaded media.
if (tab.getWebContents() != null) tab.getWebContents().notifyRendererPreferenceUpdate();
}
initializeTab(tab);
return tab;
}
private Tab createTab() {
WebContents webContents = takeWebContents();
Tab tab = mTabFactory.createTab();
int launchSource = mIntent.getIntExtra(
CustomTabIntentDataProvider.EXTRA_BROWSER_LAUNCH_SOURCE, LaunchSourceType.OTHER);
if (launchSource == LaunchSourceType.WEBAPK) {
String webapkPackageName = mIntent.getStringExtra(Browser.EXTRA_APPLICATION_ID);
TabAssociatedApp.from(tab).setAppId(webapkPackageName);
} else {
TabAssociatedApp.from(tab).setAppId(
mConnection.getClientPackageNameForSession(mSession));
}
tab.initialize(webContents, mCustomTabDelegateFactory.get(), false /*initiallyHidden*/,
null, false /*unfreeze*/);
if (mIntentDataProvider.shouldEnableEmbeddedMediaExperience()) {
if (tab.getWebContents() != null) tab.getWebContents().notifyRendererPreferenceUpdate();
}
initializeTab(tab);
if (mIntentDataProvider.getTranslateLanguage() != null) {
TranslateBridge.setPredefinedTargetLanguage(
tab, mIntentDataProvider.getTranslateLanguage());
}
return tab;
}
private WebContents takeWebContents() {
int webContentsStateOnLaunch;
WebContents webContents = takeAsyncWebContents();
if (webContents != null) {
webContentsStateOnLaunch = WebContentsState.TRANSFERRED_WEBCONTENTS;
webContents.resumeLoadingCreatedWebContents();
} else {
webContents = mWarmupManager.takeSpareWebContents(mIntentDataProvider.isIncognito(),
false /*initiallyHidden*/, WarmupManager.FOR_CCT);
if (webContents != null) {
webContentsStateOnLaunch = WebContentsState.SPARE_WEBCONTENTS;
} else {
webContents = mWebContentsFactory.createWebContentsWithWarmRenderer(
mIntentDataProvider.isIncognito(), false);
webContentsStateOnLaunch = WebContentsState.NO_WEBCONTENTS;
}
}
RecordHistogram.recordEnumeratedHistogram("CustomTabs.WebContentsStateOnLaunch",
webContentsStateOnLaunch, WebContentsState.NUM_ENTRIES);
return webContents;
}
@Nullable
private WebContents takeAsyncWebContents() {
int assignedTabId = IntentUtils.safeGetIntExtra(
mIntent, IntentHandler.EXTRA_TAB_ID, Tab.INVALID_TAB_ID);
AsyncTabParams asyncParams = AsyncTabParamsManager.remove(assignedTabId);
if (asyncParams == null) return null;
return asyncParams.getWebContents();
}
private void initializeTab(Tab tab) {
TabRedirectHandler.from(tab).updateIntent(mIntent);
tab.getView().requestFocus();
// TODO(pshmakov): invert these dependencies.
// Please don't register new observers here. Instead, inject TabObserverRegistrar in classes
// dedicated to your feature, and register there.
mTabObserverRegistrar.registerTabObserver(mCustomTabObserver.get());
mTabObserverRegistrar.registerTabObserver(mTabNavigationEventObserver);
mTabObserverRegistrar.registerPageLoadMetricsObserver(
new PageLoadMetricsObserver(mConnection, mSession, tab));
mTabObserverRegistrar.registerPageLoadMetricsObserver(
new FirstMeaningfulPaintObserver(mCustomTabObserver.get(), tab));
// Immediately add the observer to PageLoadMetrics to catch early events that may
// be generated in the middle of tab initialization.
mTabObserverRegistrar.addObserversForTab(tab);
prepareTabBackground(tab);
}
/** Sets the initial background color for the Tab, shown before the page content is ready. */
private void prepareTabBackground(final Tab tab) {
if (!IntentHandler.notSecureIsIntentChromeOrFirstParty(mIntent)) return;
int backgroundColor = mIntentDataProvider.getInitialBackgroundColor();
if (backgroundColor == Color.TRANSPARENT) return;
// Set the background color.
tab.getView().setBackgroundColor(backgroundColor);
// Unset the background when the page has rendered.
EmptyTabObserver mediaObserver = new EmptyTabObserver() {
@Override
public void didFirstVisuallyNonEmptyPaint(final Tab tab) {
tab.removeObserver(this);
// Blink has rendered the page by this point, but we need to wait for the compositor
// frame swap to avoid flash of white content.
mCompositorViewHolder.get().getCompositorView().surfaceRedrawNeededAsync(() -> {
if (!tab.isInitialized() || mActivity.isActivityFinishingOrDestroyed()) return;
tab.getView().setBackgroundResource(0);
});
}
};
tab.addObserver(mediaObserver);
}
}