1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chrome.browser.tabmodel;
7 import org.chromium.base.ObserverList;
8 import org.chromium.base.TraceEvent;
9 import org.chromium.chrome.browser.Tab;
10 import org.chromium.chrome.browser.util.MathUtils;
11 import org.chromium.content_public.browser.WebContents;
13 import java.util.ArrayList;
14 import java.util.List;
17 * This is the default implementation of the {@link TabModel} interface.
19 public abstract class TabModelBase extends TabModelJniBridge {
20 private static final String TAG = "TabModelBase";
23 * The main list of tabs. Note that when this changes, all pending closures must be committed
24 * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also
25 * {@link RewoundList#resetRewoundState()} must be called so that the full model will be up to
28 private final List<Tab> mTabs = new ArrayList<Tab>();
30 private final TabModelOrderController mOrderController;
32 protected final TabModelDelegate mModelDelegate;
34 private final ObserverList<TabModelObserver> mObservers;
36 // Undo State Tracking -------------------------------------------------------------------------
39 * A {@link TabList} that represents the complete list of {@link Tab}s. This is so that
40 * certain UI elements can call {@link TabModel#getComprehensiveModel()} to get a full list of
41 * {@link Tab}s that includes rewindable entries, as the typical {@link TabModel} does not
42 * return rewindable entries.
44 private final RewoundList mRewoundList = new RewoundList();
47 * This specifies the current {@link Tab} in {@link #mTabs}.
49 private int mIndex = INVALID_TAB_INDEX;
51 public TabModelBase(boolean incognito, TabModelOrderController orderController,
52 TabModelDelegate modelDelegate) {
55 mOrderController = orderController;
56 mModelDelegate = modelDelegate;
57 mObservers = new ObserverList<TabModelObserver>();
61 public void destroy() {
62 for (Tab tab : mTabs) {
63 if (tab.isInitialized()) tab.destroy();
66 mRewoundList.destroy();
74 public void addObserver(TabModelObserver observer) {
75 mObservers.addObserver(observer);
79 public void removeObserver(TabModelObserver observer) {
80 mObservers.removeObserver(observer);
84 * Initializes the newly created tab, adds it to controller, and dispatches creation
88 public void addTab(Tab tab, int index, TabLaunchType type) {
91 for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
93 boolean selectTab = mOrderController.willOpenInForeground(type, isIncognito());
95 index = mOrderController.determineInsertionIndex(type, index, tab);
96 assert index <= mTabs.size();
98 assert tab.isIncognito() == isIncognito();
100 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
101 commitAllTabClosures();
103 if (index < 0 || index > mTabs.size()) {
106 mTabs.add(index, tab);
107 if (index <= mIndex) {
112 if (!isCurrentModel()) {
113 // When adding new tabs in the background, make sure we set a valid index when the
114 // first one is added. When in the foreground, calls to setIndex will take care of
116 mIndex = Math.max(mIndex, 0);
119 mRewoundList.resetRewoundState();
121 int newIndex = indexOf(tab);
122 mModelDelegate.didChange();
123 mModelDelegate.didCreateNewTab(tab);
125 tabAddedToModel(tab);
127 for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
130 mModelDelegate.selectModel(isIncognito());
131 setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
138 public void moveTab(int id, int newIndex) {
139 newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
141 int curIndex = TabModelUtils.getTabIndexById(this, id);
143 if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
147 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
148 commitAllTabClosures();
150 Tab tab = mTabs.remove(curIndex);
151 if (curIndex < newIndex) --newIndex;
153 mTabs.add(newIndex, tab);
155 if (curIndex == mIndex) {
157 } else if (curIndex < mIndex && newIndex >= mIndex) {
159 } else if (curIndex > mIndex && newIndex <= mIndex) {
163 mRewoundList.resetRewoundState();
165 mModelDelegate.didChange();
166 for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
170 public boolean closeTab(Tab tab) {
171 return closeTab(tab, true, false, false);
174 private Tab findTabInAllTabModels(int tabId) {
175 Tab tab = TabModelUtils.getTabById(mModelDelegate.getModel(isIncognito()), tabId);
176 if (tab != null) return tab;
177 return TabModelUtils.getTabById(mModelDelegate.getModel(!isIncognito()), tabId);
181 public Tab getNextTabIfClosed(int id) {
182 Tab tabToClose = TabModelUtils.getTabById(this, id);
183 Tab currentTab = TabModelUtils.getCurrentTab(this);
184 if (tabToClose == null) return currentTab;
186 int closingTabIndex = indexOf(tabToClose);
187 Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
188 Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
190 // Determine which tab to select next according to these rules:
191 // * If closing a background tab, keep the current tab selected.
192 // * Otherwise, if not in overview mode, select the parent tab if it exists.
193 // * Otherwise, select an adjacent tab if one exists.
194 // * Otherwise, if closing the last incognito tab, select the current normal tab.
195 // * Otherwise, select nothing.
197 if (tabToClose != currentTab && currentTab != null) {
198 nextTab = currentTab;
199 } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
201 } else if (adjacentTab != null) {
202 nextTab = adjacentTab;
203 } else if (isIncognito()) {
204 nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
211 public boolean isClosurePending(int tabId) {
212 return mRewoundList.getPendingRewindTab(tabId) != null;
216 public boolean supportsPendingClosures() {
217 return !isIncognito();
221 public TabList getComprehensiveModel() {
222 if (!supportsPendingClosures()) return this;
227 public void cancelTabClosure(int tabId) {
228 Tab tab = mRewoundList.getPendingRewindTab(tabId);
229 if (tab == null) return;
231 tab.setClosing(false);
233 // Find a valid previous tab entry so we know what tab to insert after. With the following
234 // example, calling cancelTabClosure(4) would need to know to insert after 2. So we have to
235 // track across mRewoundTabs and mTabs and see what the last valid mTabs entry was (2) when
236 // we hit the 4 in the rewound list. An insertIndex of -1 represents the beginning of the
237 // list, as this is the index of tab to insert after.
239 // mRewoundTabs 0 1 2 3 4 5
241 final int stopIndex = mRewoundList.indexOf(tab);
242 for (int rewoundIndex = 0; rewoundIndex < stopIndex; rewoundIndex++) {
243 Tab rewoundTab = mRewoundList.getTabAt(rewoundIndex);
244 if (prevIndex == mTabs.size() - 1) break;
245 if (rewoundTab == mTabs.get(prevIndex + 1)) prevIndex++;
248 // Figure out where to insert the tab. Just add one to prevIndex, as -1 represents the
249 // beginning of the list, so we'll insert at 0.
250 int insertIndex = prevIndex + 1;
251 if (mIndex >= insertIndex) mIndex++;
252 mTabs.add(insertIndex, tab);
254 boolean activeModel = mModelDelegate.getCurrentModel() == this;
256 // If we're the active model call setIndex to actually select this tab, otherwise just set
257 // mIndex but don't kick off everything that happens when calling setIndex().
259 TabModelUtils.setIndex(this, insertIndex);
261 mIndex = insertIndex;
264 for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
268 public void commitTabClosure(int tabId) {
269 Tab tab = mRewoundList.getPendingRewindTab(tabId);
270 if (tab == null) return;
272 // We're committing the close, actually remove it from the lists and finalize the closing
274 mRewoundList.removeTab(tab);
275 finalizeTabClosure(tab);
276 for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
280 public void commitAllTabClosures() {
281 while (mRewoundList.getCount() > mTabs.size()) {
282 commitTabClosure(mRewoundList.getNextRewindableTab().getId());
285 assert !mRewoundList.hasPendingClosures();
289 public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
290 return closeTab(tabToClose, animate, uponExit, canUndo, canUndo);
294 * See TabModel.java documentation for description of other parameters.
295 * @param notify Whether or not to notify observers about the pending closure. If this is
296 * {@code true}, {@link #supportsPendingClosures()} is {@code true},
297 * and canUndo is {@code true}, observers will be notified of the pending
298 * closure. Observers will still be notified of a committed/cancelled closure
299 * even if they are not notified of a pending closure to start with.
301 private boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit,
302 boolean canUndo, boolean notify) {
303 if (tabToClose == null) {
304 assert false : "Tab is null!";
308 if (!mTabs.contains(tabToClose)) {
309 assert false : "Tried to close a tab from another model!";
313 canUndo &= supportsPendingClosures();
315 if (notify && canUndo) {
316 for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
318 startTabClosure(tabToClose, animate, uponExit, canUndo);
319 if (!canUndo) finalizeTabClosure(tabToClose);
325 public void closeAllTabs() {
326 closeAllTabs(true, false);
330 public void closeAllTabs(boolean allowDelegation, boolean uponExit) {
331 commitAllTabClosures();
333 while (getCount() > 0) {
334 TabModelUtils.closeTabByIndex(this, 0);
339 * Close all tabs on this model without notifying observers about pending tab closures.
341 * @param animate true iff the closing animation should be displayed
342 * @param uponExit true iff the tabs are being closed upon application exit (after user presses
343 * the system back button)
344 * @param canUndo Whether or not this action can be undone. If this is {@code true} and
345 * {@link #supportsPendingClosures()} is {@code true}, these {@link Tab}s
346 * will not actually be closed until {@link #commitTabClosure(int)} or
347 * {@link #commitAllTabClosures()} is called, but they will be effectively
348 * removed from this list.
349 * @return a list containing the ids of tabs that have been closed
351 public ArrayList<Integer> closeAllTabs(boolean animate, boolean uponExit, boolean canUndo) {
352 ArrayList<Integer> closedTabs = new ArrayList<Integer>();
353 while (getCount() > 0) {
354 Tab tab = getTabAt(0);
355 closedTabs.add(tab.getId());
356 closeTab(tab, animate, uponExit, canUndo, false);
362 public Tab getTabAt(int index) {
363 // This will catch INVALID_TAB_INDEX and return null
364 if (index < 0 || index >= mTabs.size()) return null;
365 return mTabs.get(index);
368 // Index of the given tab in the order of the tab stack.
370 public int indexOf(Tab tab) {
371 return mTabs.indexOf(tab);
375 * @return true if this is the current model according to the model selector
377 private boolean isCurrentModel() {
378 return mModelDelegate.getCurrentModel() == this;
381 // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
382 private int getLastId(TabSelectionType type) {
383 if (type == TabSelectionType.FROM_CLOSE) return Tab.INVALID_TAB_ID;
385 // Get the current tab in the current tab model.
386 Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
387 return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
390 // This function is complex and its behavior depends on persisted state, including mIndex.
392 public void setIndex(int i, final TabSelectionType type) {
394 int lastId = getLastId(type);
396 if (!isCurrentModel()) {
397 mModelDelegate.selectModel(isIncognito());
400 if (mTabs.size() <= 0) {
401 mIndex = INVALID_TAB_INDEX;
403 mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
406 Tab tab = TabModelUtils.getCurrentTab(this);
408 mModelDelegate.requestToShowTab(tab, type);
411 for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
414 // notifyDataSetChanged() can call into
415 // ChromeViewHolderTablet.handleTabChangeExternal(), which will eventually move the
416 // ContentView onto the current view hierarchy (with addView()).
417 mModelDelegate.didChange();
422 * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}.
423 * This does not actually destroy the {@link Tab} (see
424 * {@link #finalizeTabClosure(Tab)}.
426 * @param tab The {@link Tab} to remove from this {@link TabModel}.
427 * @param animate Whether or not to animate the closing.
428 * @param uponExit Whether or not this is closing while the Activity is exiting.
429 * @param canUndo Whether or not this operation can be undone. Note that if this is {@code true}
430 * and {@link #supportsPendingClosures()} is {@code true},
431 * {@link #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be
432 * called to actually delete and clean up {@code tab}.
434 private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
435 final int closingTabId = tab.getId();
436 final int closingTabIndex = indexOf(tab);
438 tab.setClosing(true);
440 for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
442 Tab currentTab = TabModelUtils.getCurrentTab(this);
443 Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
444 Tab nextTab = getNextTabIfClosed(closingTabId);
446 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
447 if (!canUndo) commitAllTabClosures();
449 // Cancel any media currently playing.
451 WebContents webContents = tab.getWebContents();
452 if (webContents != null) webContents.releaseMediaPlayers();
457 boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
458 int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
459 int nextTabIndex = nextTab == null ? INVALID_TAB_INDEX : TabModelUtils.getTabIndexById(
460 mModelDelegate.getModel(nextIsIncognito), nextTabId);
462 if (nextTab != currentTab) {
463 if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
465 TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
466 nextModel.setIndex(nextTabIndex,
467 uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
469 mIndex = nextTabIndex;
472 if (!canUndo) mRewoundList.resetRewoundState();
476 * Actually closes and cleans up {@code tab}.
477 * @param tab The {@link Tab} to close.
479 private void finalizeTabClosure(Tab tab) {
480 for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
484 private class RewoundList implements TabList {
486 * A list of {@link Tab}s that represents the completely rewound list (if all
487 * rewindable closes were undone). If there are no possible rewindable closes this list
488 * should match {@link #mTabs}.
490 private final List<Tab> mRewoundTabs = new ArrayList<Tab>();
493 public boolean isIncognito() {
494 return TabModelBase.this.isIncognito();
498 * If {@link TabModel} has a valid selected tab, this will return that same tab in the
499 * context of the rewound list of tabs. If {@link TabModel} has no tabs but the rewound
500 * list is not empty, it will return 0, the first tab. Otherwise it will return
501 * {@link TabModel#INVALID_TAB_INDEX}.
502 * @return The selected index of the rewound list of tabs (includes all pending closures).
506 if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
507 return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
509 if (!mRewoundTabs.isEmpty()) return 0;
510 return INVALID_TAB_INDEX;
514 public int getCount() {
515 return mRewoundTabs.size();
519 public Tab getTabAt(int index) {
520 if (index < 0 || index >= mRewoundTabs.size()) return null;
521 return mRewoundTabs.get(index);
525 public int indexOf(Tab tab) {
526 return mRewoundTabs.indexOf(tab);
530 public boolean isClosurePending(int tabId) {
531 return TabModelBase.this.isClosurePending(tabId);
535 * Resets this list to match the original {@link TabModel}. Note that if the
536 * {@link TabModel} doesn't support pending closures this model will be empty. This should
537 * be called whenever {@link #mTabs} changes.
539 public void resetRewoundState() {
540 mRewoundTabs.clear();
542 if (TabModelBase.this.supportsPendingClosures()) {
543 for (int i = 0; i < TabModelBase.this.getCount(); i++) {
544 mRewoundTabs.add(TabModelBase.this.getTabAt(i));
550 * Finds the {@link Tab} specified by {@code tabId} and only returns it if it is
551 * actually a {@link Tab} that is in the middle of being closed (which means that it
552 * is present in this model but not in {@link #mTabs}.
554 * @param tabId The id of the {@link Tab} to search for.
555 * @return The {@link Tab} specified by {@code tabId} as long as that tab only exists
556 * in this model and not in {@link #mTabs}. {@code null} otherwise.
558 public Tab getPendingRewindTab(int tabId) {
559 if (!TabModelBase.this.supportsPendingClosures()) return null;
560 if (TabModelUtils.getTabById(TabModelBase.this, tabId) != null) return null;
561 return TabModelUtils.getTabById(this, tabId);
565 * A utility method for easily finding a {@link Tab} that can be closed.
566 * @return The next tab that is in the middle of being closed.
568 public Tab getNextRewindableTab() {
569 if (!hasPendingClosures()) return null;
571 for (int i = 0; i < mRewoundTabs.size(); i++) {
572 Tab tab = i < TabModelBase.this.getCount() ? TabModelBase.this.getTabAt(i) : null;
573 Tab rewoundTab = mRewoundTabs.get(i);
575 if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
582 * Removes a {@link Tab} from this internal list.
583 * @param tab The {@link Tab} to remove.
585 public void removeTab(Tab tab) {
586 mRewoundTabs.remove(tab);
590 * Destroy all tabs in this model. This will check to see if the tab is already destroyed
591 * before destroying it.
593 public void destroy() {
594 for (Tab tab : mRewoundTabs) {
595 if (tab.isInitialized()) tab.destroy();
599 public boolean hasPendingClosures() {
600 return TabModelBase.this.supportsPendingClosures()
601 && mRewoundTabs.size() > TabModelBase.this.getCount();
606 protected boolean closeTabAt(int index) {
607 return closeTab(getTabAt(index));
611 public int getCount() {
621 protected boolean isSessionRestoreInProgress() {
622 return mModelDelegate.isSessionRestoreInProgress();