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.CalledByNative;
8 import org.chromium.base.ObserverList;
9 import org.chromium.base.TraceEvent;
10 import org.chromium.chrome.browser.Tab;
11 import org.chromium.chrome.browser.profiles.Profile;
12 import org.chromium.chrome.browser.util.MathUtils;
14 import java.util.ArrayList;
15 import java.util.List;
18 * This is the default implementation of the {@link TabModel} interface.
20 public abstract class TabModelBase implements TabModel {
21 private static final String TAG = "TabModelBase";
24 * The main list of tabs. Note that when this changes, all pending closures must be committed
25 * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also
26 * {@link RewoundList#resetRewoundState()} must be called so that the full model will be up to
29 private final List<Tab> mTabs = new ArrayList<Tab>();
31 private final boolean mIsIncognito;
33 private final TabModelOrderController mOrderController;
35 protected final TabModelDelegate mModelDelegate;
37 private final ObserverList<TabModelObserver> mObservers;
39 // Undo State Tracking -------------------------------------------------------------------------
42 * A {@link TabList} that represents the complete list of {@link Tab}s. This is so that
43 * certain UI elements can call {@link TabModel#getComprehensiveModel()} to get a full list of
44 * {@link Tab}s that includes rewindable entries, as the typical {@link TabModel} does not
45 * return rewindable entries.
47 private final RewoundList mRewoundList = new RewoundList();
50 * This specifies the current {@link Tab} in {@link #mTabs}.
52 private int mIndex = INVALID_TAB_INDEX;
54 /** Native Tab pointer which will be set by nativeInit(). */
55 private long mNativeTabModelImpl = 0;
57 public TabModelBase(boolean incognito, TabModelOrderController orderController,
58 TabModelDelegate modelDelegate) {
59 mIsIncognito = incognito;
60 mNativeTabModelImpl = nativeInit(incognito);
61 mOrderController = orderController;
62 mModelDelegate = modelDelegate;
63 mObservers = new ObserverList<TabModelObserver>();
67 public Profile getProfile() {
68 return nativeGetProfileAndroid(mNativeTabModelImpl);
72 public boolean isIncognito() {
77 public void destroy() {
78 for (Tab tab : mTabs) {
79 if (tab.isInitialized()) tab.destroy();
82 mRewoundList.destroy();
84 if (mNativeTabModelImpl != 0) {
85 nativeDestroy(mNativeTabModelImpl);
86 mNativeTabModelImpl = 0;
91 public void addObserver(TabModelObserver observer) {
92 mObservers.addObserver(observer);
96 public void removeObserver(TabModelObserver observer) {
97 mObservers.removeObserver(observer);
101 * Initializes the newly created tab, adds it to controller, and dispatches creation
102 * step notifications.
105 public void addTab(Tab tab, int index, TabLaunchType type) {
108 for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
110 boolean selectTab = mOrderController.willOpenInForeground(type, mIsIncognito);
112 index = mOrderController.determineInsertionIndex(type, index, tab);
113 assert index <= mTabs.size();
115 assert tab.isIncognito() == mIsIncognito;
117 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
118 commitAllTabClosures();
120 if (index < 0 || index > mTabs.size()) {
123 mTabs.add(index, tab);
124 if (index <= mIndex) {
129 if (!isCurrentModel()) {
130 // When adding new tabs in the background, make sure we set a valid index when the
131 // first one is added. When in the foreground, calls to setIndex will take care of
133 mIndex = Math.max(mIndex, 0);
136 mRewoundList.resetRewoundState();
138 int newIndex = indexOf(tab);
139 mModelDelegate.didChange();
140 mModelDelegate.didCreateNewTab(tab);
142 if (mNativeTabModelImpl != 0) nativeTabAddedToModel(mNativeTabModelImpl, tab);
144 for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
147 mModelDelegate.selectModel(mIsIncognito);
148 setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
155 public void moveTab(int id, int newIndex) {
156 newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
158 int curIndex = TabModelUtils.getTabIndexById(this, id);
160 if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
164 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
165 commitAllTabClosures();
167 Tab tab = mTabs.remove(curIndex);
168 if (curIndex < newIndex) --newIndex;
170 mTabs.add(newIndex, tab);
172 if (curIndex == mIndex) {
174 } else if (curIndex < mIndex && newIndex >= mIndex) {
176 } else if (curIndex > mIndex && newIndex <= mIndex) {
180 mRewoundList.resetRewoundState();
182 mModelDelegate.didChange();
183 for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
188 public boolean closeTab(Tab tab) {
189 return closeTab(tab, true, false, false);
192 private Tab findTabInAllTabModels(int tabId) {
193 Tab tab = TabModelUtils.getTabById(mModelDelegate.getModel(mIsIncognito), tabId);
194 if (tab != null) return tab;
195 return TabModelUtils.getTabById(mModelDelegate.getModel(!mIsIncognito), tabId);
199 public Tab getNextTabIfClosed(int id) {
200 Tab tabToClose = TabModelUtils.getTabById(this, id);
201 Tab currentTab = TabModelUtils.getCurrentTab(this);
202 if (tabToClose == null) return currentTab;
204 int closingTabIndex = indexOf(tabToClose);
205 Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
206 Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
208 // Determine which tab to select next according to these rules:
209 // * If closing a background tab, keep the current tab selected.
210 // * Otherwise, if not in overview mode, select the parent tab if it exists.
211 // * Otherwise, select an adjacent tab if one exists.
212 // * Otherwise, if closing the last incognito tab, select the current normal tab.
213 // * Otherwise, select nothing.
215 if (tabToClose != currentTab && currentTab != null) {
216 nextTab = currentTab;
217 } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
219 } else if (adjacentTab != null) {
220 nextTab = adjacentTab;
221 } else if (mIsIncognito) {
222 nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
229 public boolean isClosurePending(int tabId) {
230 return mRewoundList.getPendingRewindTab(tabId) != null;
234 public boolean supportsPendingClosures() {
235 return !mIsIncognito;
239 public TabList getComprehensiveModel() {
240 if (!supportsPendingClosures()) return this;
245 public void cancelTabClosure(int tabId) {
246 Tab tab = mRewoundList.getPendingRewindTab(tabId);
247 if (tab == null) return;
249 tab.setClosing(false);
251 // Find a valid previous tab entry so we know what tab to insert after. With the following
252 // example, calling cancelTabClosure(4) would need to know to insert after 2. So we have to
253 // track across mRewoundTabs and mTabs and see what the last valid mTabs entry was (2) when
254 // we hit the 4 in the rewound list. An insertIndex of -1 represents the beginning of the
255 // list, as this is the index of tab to insert after.
257 // mRewoundTabs 0 1 2 3 4 5
259 final int stopIndex = mRewoundList.indexOf(tab);
260 for (int rewoundIndex = 0; rewoundIndex < stopIndex; rewoundIndex++) {
261 Tab rewoundTab = mRewoundList.getTabAt(rewoundIndex);
262 if (prevIndex == mTabs.size() - 1) break;
263 if (rewoundTab == mTabs.get(prevIndex + 1)) prevIndex++;
266 // Figure out where to insert the tab. Just add one to prevIndex, as -1 represents the
267 // beginning of the list, so we'll insert at 0.
268 int insertIndex = prevIndex + 1;
269 if (mIndex >= insertIndex) mIndex++;
270 mTabs.add(insertIndex, tab);
272 boolean activeModel = mModelDelegate.getCurrentModel() == this;
274 // If we're the active model call setIndex to actually select this tab, otherwise just set
275 // mIndex but don't kick off everything that happens when calling setIndex().
277 setIndex(insertIndex);
279 mIndex = insertIndex;
282 for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
286 public void commitTabClosure(int tabId) {
287 Tab tab = mRewoundList.getPendingRewindTab(tabId);
288 if (tab == null) return;
290 // We're committing the close, actually remove it from the lists and finalize the closing
292 mRewoundList.removeTab(tab);
293 finalizeTabClosure(tab);
294 for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
298 public void commitAllTabClosures() {
299 while (mRewoundList.getCount() > mTabs.size()) {
300 commitTabClosure(mRewoundList.getNextRewindableTab().getId());
303 assert !mRewoundList.hasPendingClosures();
307 public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
308 if (tabToClose == null) {
309 assert false : "Tab is null!";
313 if (!mTabs.contains(tabToClose)) {
314 assert false : "Tried to close a tab from another model!";
318 canUndo &= supportsPendingClosures();
321 for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
323 startTabClosure(tabToClose, animate, uponExit, canUndo);
324 if (!canUndo) finalizeTabClosure(tabToClose);
330 public void closeAllTabs() {
331 commitAllTabClosures();
333 while (getCount() > 0) {
334 TabModelUtils.closeTabByIndex(this, 0);
340 public Tab getTabAt(int index) {
341 // This will catch INVALID_TAB_INDEX and return null
342 if (index < 0 || index >= mTabs.size()) return null;
343 return mTabs.get(index);
346 // Index of the given tab in the order of the tab stack.
348 public int indexOf(Tab tab) {
349 return mTabs.indexOf(tab);
353 * @return true if this is the current model according to the model selector
355 private boolean isCurrentModel() {
356 return mModelDelegate.getCurrentModel() == this;
359 // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
360 private int getLastId(TabSelectionType type) {
361 if (type == TabSelectionType.FROM_CLOSE) return Tab.INVALID_TAB_ID;
363 // Get the current tab in the current tab model.
364 Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
365 return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
368 // This function is complex and its behavior depends on persisted state, including mIndex.
370 public void setIndex(int i, final TabSelectionType type) {
372 int lastId = getLastId(type);
374 if (!isCurrentModel()) {
375 mModelDelegate.selectModel(isIncognito());
378 if (mTabs.size() <= 0) {
379 mIndex = INVALID_TAB_INDEX;
381 mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
384 Tab tab = TabModelUtils.getCurrentTab(this);
386 mModelDelegate.requestToShowTab(tab, type);
389 for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
392 // notifyDataSetChanged() can call into
393 // ChromeViewHolderTablet.handleTabChangeExternal(), which will eventually move the
394 // ContentView onto the current view hierarchy (with addView()).
395 mModelDelegate.didChange();
401 * @param nativeWebContents
406 protected abstract Tab createTabWithNativeContents(boolean incognito, long nativeWebContents,
410 * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}.
411 * This does not actually destroy the {@link Tab} (see
412 * {@link #finalizeTabClosure(Tab)}.
414 * @param tab The {@link Tab} to remove from this {@link TabModel}.
415 * @param animate Whether or not to animate the closing.
416 * @param uponExit Whether or not this is closing while the Activity is exiting.
417 * @param canUndo Whether or not this operation can be undone. Note that if this is {@code true}
418 * and {@link #supportsPendingClosures()} is {@code true},
419 * {@link #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be
420 * called to actually delete and clean up {@code tab}.
422 private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
423 final int closingTabId = tab.getId();
424 final int closingTabIndex = indexOf(tab);
426 tab.setClosing(true);
428 for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
430 Tab currentTab = TabModelUtils.getCurrentTab(this);
431 Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
432 Tab nextTab = getNextTabIfClosed(closingTabId);
434 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
435 if (!canUndo) commitAllTabClosures();
438 boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
439 int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
440 int nextTabIndex = nextTab == null ? INVALID_TAB_INDEX : TabModelUtils.getTabIndexById(
441 mModelDelegate.getModel(nextIsIncognito), nextTabId);
443 if (nextTab != currentTab) {
444 if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
446 TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
447 nextModel.setIndex(nextTabIndex,
448 uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
450 mIndex = nextTabIndex;
453 if (!canUndo) mRewoundList.resetRewoundState();
457 * Actually closes and cleans up {@code tab}.
458 * @param tab The {@link Tab} to close.
460 private void finalizeTabClosure(Tab tab) {
461 for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
465 private class RewoundList implements TabList {
467 * A list of {@link Tab}s that represents the completely rewound list (if all
468 * rewindable closes were undone). If there are no possible rewindable closes this list
469 * should match {@link #mTabs}.
471 private List<Tab> mRewoundTabs = new ArrayList<Tab>();
474 public boolean isIncognito() {
475 return TabModelBase.this.isIncognito();
479 * If {@link TabModel} has a valid selected tab, this will return that same tab in the
480 * context of the rewound list of tabs. If {@link TabModel} has no tabs but the rewound
481 * list is not empty, it will return 0, the first tab. Otherwise it will return
482 * {@link TabModel#INVALID_TAB_INDEX}.
483 * @return The selected index of the rewound list of tabs (includes all pending closures).
487 if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
488 return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
490 if (!mRewoundTabs.isEmpty()) return 0;
491 return INVALID_TAB_INDEX;
495 public int getCount() {
496 return mRewoundTabs.size();
500 public Tab getTabAt(int index) {
501 if (index < 0 || index >= mRewoundTabs.size()) return null;
502 return mRewoundTabs.get(index);
506 public int indexOf(Tab tab) {
507 return mRewoundTabs.indexOf(tab);
511 public boolean isClosurePending(int tabId) {
512 return TabModelBase.this.isClosurePending(tabId);
516 * Resets this list to match the original {@link TabModel}. Note that if the
517 * {@link TabModel} doesn't support pending closures this model will be empty. This should
518 * be called whenever {@link #mTabs} changes.
520 public void resetRewoundState() {
521 mRewoundTabs.clear();
523 if (TabModelBase.this.supportsPendingClosures()) {
524 for (int i = 0; i < TabModelBase.this.getCount(); i++) {
525 mRewoundTabs.add(TabModelBase.this.getTabAt(i));
531 * Finds the {@link Tab} specified by {@code tabId} and only returns it if it is
532 * actually a {@link Tab} that is in the middle of being closed (which means that it
533 * is present in this model but not in {@link #mTabs}.
535 * @param tabId The id of the {@link Tab} to search for.
536 * @return The {@link Tab} specified by {@code tabId} as long as that tab only exists
537 * in this model and not in {@link #mTabs}. {@code null} otherwise.
539 public Tab getPendingRewindTab(int tabId) {
540 if (!TabModelBase.this.supportsPendingClosures()) return null;
541 if (TabModelUtils.getTabById(TabModelBase.this, tabId) != null) return null;
542 return TabModelUtils.getTabById(this, tabId);
546 * A utility method for easily finding a {@link Tab} that can be closed.
547 * @return The next tab that is in the middle of being closed.
549 public Tab getNextRewindableTab() {
550 if (!hasPendingClosures()) return null;
552 for (int i = 0; i < mRewoundTabs.size(); i++) {
553 Tab tab = i < TabModelBase.this.getCount() ? TabModelBase.this.getTabAt(i) : null;
554 Tab rewoundTab = mRewoundTabs.get(i);
556 if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
563 * Removes a {@link Tab} from this internal list.
564 * @param tab The {@link Tab} to remove.
566 public void removeTab(Tab tab) {
567 mRewoundTabs.remove(tab);
571 * Destroy all tabs in this model. This will check to see if the tab is already destroyed
572 * before destroying it.
574 public void destroy() {
575 for (Tab tab : mRewoundTabs) {
576 if (tab.isInitialized()) tab.destroy();
580 public boolean hasPendingClosures() {
581 return TabModelBase.this.supportsPendingClosures()
582 && mRewoundTabs.size() > TabModelBase.this.getCount();
587 * Broadcast a notification (in native code) that all tabs are now loaded from storage.
589 public void broadcastSessionRestoreComplete() {
590 nativeBroadcastSessionRestoreComplete(mNativeTabModelImpl);
593 // JNI related methods -------------------------------------------------------------------------
597 public int getCount() {
607 @SuppressWarnings("unused")
609 private void setIndex(int index) {
610 TabModelUtils.setIndex(this, index);
614 * Used by Developer Tools to create a new tab with a given URL.
616 * @param url The URL to open.
617 * @return The new tab.
620 protected abstract Tab createNewTabForDevTools(String url);
623 private boolean isSessionRestoreInProgress() {
624 return mModelDelegate.isSessionRestoreInProgress();
627 private native long nativeInit(boolean isIncognito);
628 private native void nativeDestroy(long nativeTabModelBase);
629 private native void nativeBroadcastSessionRestoreComplete(long nativeTabModelBase);
630 private native Profile nativeGetProfileAndroid(long nativeTabModelBase);
631 private native void nativeTabAddedToModel(long nativeTabModelBase, Tab tab);