// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.tabmodel; import android.os.Handler; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeTabbedActivity; import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior; import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType; import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType; import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabPersistentStoreObserver; import org.chromium.chrome.browser.util.FeatureUtilities; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.ui.base.WindowAndroid; import java.util.concurrent.atomic.AtomicBoolean; /** * This class manages all the ContentViews in the app. As it manipulates views, it must be * instantiated and used in the UI Thread. It acts as a TabModel which delegates all * TabModel methods to the active model that it contains. */ public class TabModelSelectorImpl extends TabModelSelectorBase implements TabModelDelegate { public static final int CUSTOM_TABS_SELECTOR_INDEX = -1; private final ChromeActivity mActivity; /** Flag set to false when the asynchronous loading of tabs is finished. */ private final AtomicBoolean mSessionRestoreInProgress = new AtomicBoolean(true); private final TabPersistentStore mTabSaver; // This flag signifies the object has gotten an onNativeReady callback and // has not been destroyed. private boolean mActiveState = false; private boolean mIsUndoSupported; private final TabModelOrderController mOrderController; private OverviewModeBehavior mOverviewModeBehavior; private TabContentManager mTabContentManager; private Tab mVisibleTab; private final TabModelSelectorUma mUma; private CloseAllTabsDelegate mCloseAllTabsDelegate; /** * Builds a {@link TabModelSelectorImpl} instance. * @param activity The {@link ChromeActivity} this model selector lives in. * @param windowAndroid The {@link WindowAndroid} associated with this model selector. * @param supportUndo Whether a tab closure can be undone. */ public TabModelSelectorImpl(ChromeActivity activity, TabPersistencePolicy persistencePolicy, WindowAndroid windowAndroid, boolean supportUndo) { super(); mActivity = activity; mUma = new TabModelSelectorUma(mActivity); final TabPersistentStoreObserver persistentStoreObserver = new TabPersistentStoreObserver() { @Override public void onStateLoaded() { markTabStateInitialized(); } @Override public void onStateMerged() { } @Override public void onDetailsRead(int index, int id, String url, boolean isStandardActiveIndex, boolean isIncognitoActiveIndex) { } @Override public void onInitialized(int tabCountAtStartup) { RecordHistogram.recordCountHistogram("Tabs.CountAtStartup", tabCountAtStartup); } @Override public void onMetadataSavedAsynchronously() { } }; mIsUndoSupported = supportUndo; // Merge tabs if this is the TabModelSelector for ChromeTabbedActivity and there are no // other instances running. This indicates that it is a complete cold start of // ChromeTabbedActivity. Tabs should only be merged during a cold start of // ChromeTabbedActivity and not other instances (e.g. ChromeTabbedActivity2). boolean mergeTabs = FeatureUtilities.isTabModelMergingEnabled() && mActivity.getClass().equals(ChromeTabbedActivity.class) && TabWindowManager.getInstance().getNumberOfAssignedTabModelSelectors() == 0; mTabSaver = new TabPersistentStore(persistencePolicy, this, mActivity, persistentStoreObserver, mergeTabs); mOrderController = new TabModelOrderController(this); } @Override public void markTabStateInitialized() { super.markTabStateInitialized(); if (!mSessionRestoreInProgress.getAndSet(false)) return; // This is the first time we set // |mSessionRestoreInProgress|, so we need to broadcast. TabModelImpl model = (TabModelImpl) getModel(false); if (model != null) { model.broadcastSessionRestoreComplete(); } else { assert false : "Normal tab model is null after tab state loaded."; } } private void handleOnPageLoadStopped(Tab tab) { if (tab != null) mTabSaver.addTabToSaveQueue(tab); } /** * * @param overviewModeBehavior The {@link OverviewModeBehavior} that should be used to determine * when the app is in overview mode or not. */ public void setOverviewModeBehavior(OverviewModeBehavior overviewModeBehavior) { assert overviewModeBehavior != null; mOverviewModeBehavior = overviewModeBehavior; } /** * Should be called when the app starts showing a view with multiple tabs. */ public void onTabsViewShown() { mUma.onTabsViewShown(); } /** * Should be called once the native library is loaded so that the actual internals of this * class can be initialized. * @param tabContentProvider A {@link TabContentManager} instance. */ public void onNativeLibraryReady(TabContentManager tabContentProvider) { assert !mActiveState : "onNativeLibraryReady called twice!"; mTabContentManager = tabContentProvider; ChromeTabCreator regularTabCreator = (ChromeTabCreator) mActivity.getTabCreator(false); ChromeTabCreator incognitoTabCreator = (ChromeTabCreator) mActivity.getTabCreator(true); TabModelImpl normalModel = new TabModelImpl(false, regularTabCreator, incognitoTabCreator, mUma, mOrderController, mTabContentManager, mTabSaver, this, mIsUndoSupported); TabModel incognitoModel = new IncognitoTabModel(new IncognitoTabModelImplCreator( regularTabCreator, incognitoTabCreator, mUma, mOrderController, mTabContentManager, mTabSaver, this)); initialize(isIncognitoSelected(), normalModel, incognitoModel); regularTabCreator.setTabModel(normalModel, mOrderController, mTabContentManager); incognitoTabCreator.setTabModel(incognitoModel, mOrderController, mTabContentManager); mTabSaver.setTabContentManager(mTabContentManager); addObserver(new EmptyTabModelSelectorObserver() { @Override public void onNewTabCreated(Tab tab) { // Only invalidate if the tab exists in the currently selected model. if (TabModelUtils.getTabById(getCurrentModel(), tab.getId()) != null) { mTabContentManager.invalidateIfChanged(tab.getId(), tab.getUrl()); } if (tab.hasPendingLoadParams()) mTabSaver.addTabToSaveQueue(tab); } }); mActiveState = true; new TabModelSelectorTabObserver(this) { @Override public void onUrlUpdated(Tab tab) { TabModel model = getModelForTabId(tab.getId()); if (model == getCurrentModel()) { mTabContentManager.invalidateIfChanged(tab.getId(), tab.getUrl()); } } @Override public void onLoadStopped(Tab tab, boolean toDifferentDocument) { handleOnPageLoadStopped(tab); } @Override public void onPageLoadStarted(Tab tab, String url) { String previousUrl = tab.getUrl(); mTabContentManager.invalidateTabThumbnail(tab.getId(), previousUrl); } @Override public void onPageLoadFinished(Tab tab) { mUma.onPageLoadFinished(tab.getId()); } @Override public void onPageLoadFailed(Tab tab, int errorCode) { mUma.onPageLoadFailed(tab.getId()); } @Override public void onCrash(Tab tab, boolean sadTabShown) { if (sadTabShown) { mTabContentManager.removeTabThumbnail(tab.getId()); } mUma.onTabCrashed(tab.getId()); } }; } /** * Exposed to allow tests to initialize the selector with different tab models. * @param normalModel The normal tab model. * @param incognitoModel The incognito tab model. */ @VisibleForTesting public void initializeForTesting(TabModel normalModel, TabModel incognitoModel) { initialize(isIncognitoSelected(), normalModel, incognitoModel); mActiveState = true; } @Override public void setCloseAllTabsDelegate(CloseAllTabsDelegate delegate) { mCloseAllTabsDelegate = delegate; } @Override public TabModel getModelAt(int index) { return mActiveState ? super.getModelAt(index) : EmptyTabModel.getInstance(); } @Override public void selectModel(boolean incognito) { TabModel oldModel = getCurrentModel(); super.selectModel(incognito); TabModel newModel = getCurrentModel(); if (oldModel != newModel) { TabModelUtils.setIndex(newModel, newModel.index()); // Make the call to notifyDataSetChanged() after any delayed events // have had a chance to fire. Otherwise, this may result in some // drawing to occur before animations have a chance to work. new Handler().post(new Runnable() { @Override public void run() { notifyChanged(); } }); } } /** * Commits all pending tab closures for all {@link TabModel}s in this {@link TabModelSelector}. */ @Override public void commitAllTabClosures() { for (int i = 0; i < getModels().size(); i++) { getModelAt(i).commitAllTabClosures(); } } @Override public boolean closeAllTabsRequest(boolean incognito) { return mCloseAllTabsDelegate.closeAllTabsRequest(incognito); } public void saveState() { commitAllTabClosures(); mTabSaver.saveState(); } /** * Load the saved tab state. This should be called before any new tabs are created. The saved * tabs shall not be restored until {@link #restoreTabs} is called. * @param ignoreIncognitoFiles Whether to skip loading incognito tabs. */ public void loadState(boolean ignoreIncognitoFiles) { mTabSaver.loadState(ignoreIncognitoFiles); } /** * Merges the tab states from two tab models. */ public void mergeState() { mTabSaver.mergeState(); } /** * Restore the saved tabs which were loaded by {@link #loadState}. * * @param setActiveTab If true, synchronously load saved active tab and set it as the current * active tab. */ public void restoreTabs(boolean setActiveTab) { mTabSaver.restoreTabs(setActiveTab); } /** * If there is an asynchronous session restore in-progress, try to synchronously restore * the state of a tab with the given url as a frozen tab. This method has no effect if * there isn't a tab being restored with this url, or the tab has already been restored. */ public void tryToRestoreTabStateForUrl(String url) { if (isSessionRestoreInProgress()) mTabSaver.restoreTabStateForUrl(url); } /** * If there is an asynchronous session restore in-progress, try to synchronously restore * the state of a tab with the given id as a frozen tab. This method has no effect if * there isn't a tab being restored with this id, or the tab has already been restored. */ public void tryToRestoreTabStateForId(int id) { if (isSessionRestoreInProgress()) mTabSaver.restoreTabStateForId(id); } public void clearState() { mTabSaver.clearState(); } @Override public void destroy() { mTabSaver.destroy(); mUma.destroy(); super.destroy(); mActiveState = false; } @Override public Tab openNewTab(LoadUrlParams loadUrlParams, TabLaunchType type, Tab parent, boolean incognito) { return mActivity.getTabCreator(incognito).createNewTab(loadUrlParams, type, parent); } /** * @return Number of restored tabs on cold startup. */ public int getRestoredTabCount() { return mTabSaver.getRestoredTabCount(); } @Override public void requestToShowTab(Tab tab, TabSelectionType type) { boolean isFromExternalApp = tab != null && tab.getLaunchType() == TabLaunchType.FROM_EXTERNAL_APP; if (mVisibleTab != tab && tab != null && !tab.isNativePage()) { TabModelImpl.startTabSwitchLatencyTiming(type); } if (mVisibleTab != null && mVisibleTab != tab && !mVisibleTab.needsReload()) { if (mVisibleTab.isInitialized() && !mVisibleTab.isDetachedForReparenting()) { // TODO(dtrainor): Once we figure out why we can't grab a snapshot from the current // tab when we have other tabs loading from external apps remove the checks for // FROM_EXTERNAL_APP/FROM_NEW. if (!mVisibleTab.isClosing() && (!isFromExternalApp || type != TabSelectionType.FROM_NEW)) { cacheTabBitmap(mVisibleTab); } mVisibleTab.hide(); mVisibleTab.setFullscreenManager(null); mTabSaver.addTabToSaveQueue(mVisibleTab); } mVisibleTab = null; } if (tab == null) { notifyChanged(); return; } // We hit this case when the user enters tab switcher and comes back to the current tab // without actual tab switch. if (mVisibleTab == tab && !mVisibleTab.isHidden()) { // The current tab might have been killed by the os while in tab switcher. tab.loadIfNeeded(); return; } tab.setFullscreenManager(mActivity.getFullscreenManager()); mVisibleTab = tab; // Don't execute the tab display part if Chrome has just been sent to background. This // avoids uneccessary work (tab restore) and prevents pollution of tab display metrics - see // http://crbug.com/316166. if (type != TabSelectionType.FROM_EXIT) { tab.show(type); mUma.onShowTab(tab.getId(), tab.isBeingRestored()); } } private void cacheTabBitmap(Tab tabToCache) { // Trigger a capture of this tab. if (tabToCache == null) return; mTabContentManager.cacheTabThumbnail(tabToCache); } @Override public boolean isInOverviewMode() { return mOverviewModeBehavior != null && mOverviewModeBehavior.overviewVisible(); } @Override public boolean isSessionRestoreInProgress() { return mSessionRestoreInProgress.get(); } // TODO(tedchoc): Remove the need for this to be exposed. @Override public void notifyChanged() { super.notifyChanged(); } @VisibleForTesting public TabPersistentStore getTabPersistentStoreForTesting() { return mTabSaver; } }