// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.tabmodel; import org.chromium.base.ObserverList; import org.chromium.base.ThreadUtils; import org.chromium.chrome.browser.incognito.IncognitoNotificationManager; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.tab.Tab; /** * A TabModel implementation that handles off the record tabs. * * <p> * This is not thread safe and must only be operated on the UI thread. * * <p> * The lifetime of this object is not tied to that of the native TabModel. This ensures the * native TabModel is present when at least one incognito Tab has been created and added. When * no Tabs remain, the native model will be destroyed and only rebuilt when a new incognito Tab * is created. */ public class IncognitoTabModel implements TabModel { /** Creates TabModels for use in IncognitoModel. */ public interface IncognitoTabModelDelegate { /** Creates a fully working TabModel to delegate calls to. */ TabModel createTabModel(); /** @return Whether Incognito Tabs exist. */ boolean doIncognitoTabsExist(); } private final IncognitoTabModelDelegate mDelegate; private final ObserverList<TabModelObserver> mObservers = new ObserverList<TabModelObserver>(); private TabModel mDelegateModel; private boolean mIsAddingTab; /** * Constructor for IncognitoTabModel. */ public IncognitoTabModel(IncognitoTabModelDelegate tabModelCreator) { mDelegate = tabModelCreator; mDelegateModel = EmptyTabModel.getInstance(); } /** * Ensures that the real TabModel has been created. */ protected void ensureTabModelImpl() { ThreadUtils.assertOnUiThread(); if (!(mDelegateModel instanceof EmptyTabModel)) return; IncognitoNotificationManager.showIncognitoNotification(); mDelegateModel = mDelegate.createTabModel(); for (TabModelObserver observer : mObservers) { mDelegateModel.addObserver(observer); } } /** * @return The TabModel that this {@link IncognitoTabModel} is delegating calls to. */ protected TabModel getDelegateModel() { return mDelegateModel; } /** * Destroys the Incognito profile when all Incognito tabs have been closed. Also resets the * delegate TabModel to be a stub EmptyTabModel. */ protected void destroyIncognitoIfNecessary() { ThreadUtils.assertOnUiThread(); if (!isEmpty() || mDelegateModel instanceof EmptyTabModel || mIsAddingTab) { return; } Profile profile = getProfile(); mDelegateModel.destroy(); // Only delete the incognito profile if there are no incognito tabs open in any tab // model selector as the profile is shared between them. if (profile != null && !mDelegate.doIncognitoTabsExist()) { IncognitoNotificationManager.dismissIncognitoNotification(); profile.destroyWhenAppropriate(); } mDelegateModel = EmptyTabModel.getInstance(); } private boolean isEmpty() { return getComprehensiveModel().getCount() == 0; } @Override public Profile getProfile() { if (mDelegateModel instanceof TabModelJniBridge) { TabModelJniBridge tabModel = (TabModelJniBridge) mDelegateModel; return tabModel.isNativeInitialized() ? tabModel.getProfile() : null; } return mDelegateModel.getProfile(); } @Override public boolean isIncognito() { return true; } @Override public boolean closeTab(Tab tab) { boolean retVal = mDelegateModel.closeTab(tab); destroyIncognitoIfNecessary(); return retVal; } @Override public boolean closeTab(Tab tab, boolean animate, boolean uponExit, boolean canUndo) { boolean retVal = mDelegateModel.closeTab(tab, animate, uponExit, canUndo); destroyIncognitoIfNecessary(); return retVal; } @Override public Tab getNextTabIfClosed(int id) { return mDelegateModel.getNextTabIfClosed(id); } @Override public void closeAllTabs() { mDelegateModel.closeAllTabs(); destroyIncognitoIfNecessary(); } @Override public void closeAllTabs(boolean allowDelegation, boolean uponExit) { mDelegateModel.closeAllTabs(allowDelegation, uponExit); destroyIncognitoIfNecessary(); } @Override public int getCount() { return mDelegateModel.getCount(); } @Override public Tab getTabAt(int index) { return mDelegateModel.getTabAt(index); } @Override public int indexOf(Tab tab) { return mDelegateModel.indexOf(tab); } @Override public int index() { return mDelegateModel.index(); } @Override public void setIndex(int i, TabSelectionType type) { mDelegateModel.setIndex(i, type); } @Override public void moveTab(int id, int newIndex) { mDelegateModel.moveTab(id, newIndex); } @Override public void destroy() { mDelegateModel.destroy(); } @Override public boolean isClosurePending(int tabId) { return mDelegateModel.isClosurePending(tabId); } @Override public boolean supportsPendingClosures() { return mDelegateModel.supportsPendingClosures(); } @Override public TabList getComprehensiveModel() { return mDelegateModel.getComprehensiveModel(); } @Override public void commitAllTabClosures() { // Return early if no tabs are open. In particular, we don't want to destroy the incognito // tab model, in case we are about to add a tab to it. if (isEmpty()) return; mDelegateModel.commitAllTabClosures(); destroyIncognitoIfNecessary(); } @Override public void commitTabClosure(int tabId) { mDelegateModel.commitTabClosure(tabId); destroyIncognitoIfNecessary(); } @Override public void cancelTabClosure(int tabId) { mDelegateModel.cancelTabClosure(tabId); } @Override public void addTab(Tab tab, int index, TabLaunchType type) { mIsAddingTab = true; ensureTabModelImpl(); mDelegateModel.addTab(tab, index, type); mIsAddingTab = false; } @Override public void addObserver(TabModelObserver observer) { mObservers.addObserver(observer); mDelegateModel.addObserver(observer); } @Override public void removeObserver(TabModelObserver observer) { mObservers.removeObserver(observer); mDelegateModel.removeObserver(observer); } @Override public void removeTab(Tab tab) { mDelegateModel.removeTab(tab); // Call destroyIncognitoIfNecessary() in case the last incognito tab in this model is // reparented to a different activity. See crbug.com/611806. destroyIncognitoIfNecessary(); } @Override public void openMostRecentlyClosedTab() { } }