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;
13 import org.chromium.content_public.browser.WebContents;
15 import java.util.ArrayList;
16 import java.util.List;
19 * This is the default implementation of the {@link TabModel} interface.
21 public abstract class TabModelBase implements TabModel {
22 private static final String TAG = "TabModelBase";
25 * The main list of tabs. Note that when this changes, all pending closures must be committed
26 * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also
27 * {@link RewoundList#resetRewoundState()} must be called so that the full model will be up to
30 private final List<Tab> mTabs = new ArrayList<Tab>();
32 private final boolean mIsIncognito;
34 private final TabModelOrderController mOrderController;
36 protected final TabModelDelegate mModelDelegate;
38 private final ObserverList<TabModelObserver> mObservers;
40 // Undo State Tracking -------------------------------------------------------------------------
43 * A {@link TabList} that represents the complete list of {@link Tab}s. This is so that
44 * certain UI elements can call {@link TabModel#getComprehensiveModel()} to get a full list of
45 * {@link Tab}s that includes rewindable entries, as the typical {@link TabModel} does not
46 * return rewindable entries.
48 private final RewoundList mRewoundList = new RewoundList();
51 * This specifies the current {@link Tab} in {@link #mTabs}.
53 private int mIndex = INVALID_TAB_INDEX;
55 /** Native Tab pointer which will be set by nativeInit(). */
56 private long mNativeTabModelImpl = 0;
58 public TabModelBase(boolean incognito, TabModelOrderController orderController,
59 TabModelDelegate modelDelegate) {
60 mIsIncognito = incognito;
61 mNativeTabModelImpl = nativeInit(incognito);
62 mOrderController = orderController;
63 mModelDelegate = modelDelegate;
64 mObservers = new ObserverList<TabModelObserver>();
68 public Profile getProfile() {
69 return nativeGetProfileAndroid(mNativeTabModelImpl);
73 public boolean isIncognito() {
78 public void destroy() {
79 for (Tab tab : mTabs) {
80 if (tab.isInitialized()) tab.destroy();
83 mRewoundList.destroy();
85 if (mNativeTabModelImpl != 0) {
86 nativeDestroy(mNativeTabModelImpl);
87 mNativeTabModelImpl = 0;
95 public void addObserver(TabModelObserver observer) {
96 mObservers.addObserver(observer);
100 public void removeObserver(TabModelObserver observer) {
101 mObservers.removeObserver(observer);
105 * Initializes the newly created tab, adds it to controller, and dispatches creation
106 * step notifications.
109 public void addTab(Tab tab, int index, TabLaunchType type) {
112 for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
114 boolean selectTab = mOrderController.willOpenInForeground(type, mIsIncognito);
116 index = mOrderController.determineInsertionIndex(type, index, tab);
117 assert index <= mTabs.size();
119 assert tab.isIncognito() == mIsIncognito;
121 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
122 commitAllTabClosures();
124 if (index < 0 || index > mTabs.size()) {
127 mTabs.add(index, tab);
128 if (index <= mIndex) {
133 if (!isCurrentModel()) {
134 // When adding new tabs in the background, make sure we set a valid index when the
135 // first one is added. When in the foreground, calls to setIndex will take care of
137 mIndex = Math.max(mIndex, 0);
140 mRewoundList.resetRewoundState();
142 int newIndex = indexOf(tab);
143 mModelDelegate.didChange();
144 mModelDelegate.didCreateNewTab(tab);
146 if (mNativeTabModelImpl != 0) nativeTabAddedToModel(mNativeTabModelImpl, tab);
148 for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
151 mModelDelegate.selectModel(mIsIncognito);
152 setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
159 public void moveTab(int id, int newIndex) {
160 newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
162 int curIndex = TabModelUtils.getTabIndexById(this, id);
164 if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
168 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
169 commitAllTabClosures();
171 Tab tab = mTabs.remove(curIndex);
172 if (curIndex < newIndex) --newIndex;
174 mTabs.add(newIndex, tab);
176 if (curIndex == mIndex) {
178 } else if (curIndex < mIndex && newIndex >= mIndex) {
180 } else if (curIndex > mIndex && newIndex <= mIndex) {
184 mRewoundList.resetRewoundState();
186 mModelDelegate.didChange();
187 for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
192 public boolean closeTab(Tab tab) {
193 return closeTab(tab, true, false, false);
196 private Tab findTabInAllTabModels(int tabId) {
197 Tab tab = TabModelUtils.getTabById(mModelDelegate.getModel(mIsIncognito), tabId);
198 if (tab != null) return tab;
199 return TabModelUtils.getTabById(mModelDelegate.getModel(!mIsIncognito), tabId);
203 public Tab getNextTabIfClosed(int id) {
204 Tab tabToClose = TabModelUtils.getTabById(this, id);
205 Tab currentTab = TabModelUtils.getCurrentTab(this);
206 if (tabToClose == null) return currentTab;
208 int closingTabIndex = indexOf(tabToClose);
209 Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
210 Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
212 // Determine which tab to select next according to these rules:
213 // * If closing a background tab, keep the current tab selected.
214 // * Otherwise, if not in overview mode, select the parent tab if it exists.
215 // * Otherwise, select an adjacent tab if one exists.
216 // * Otherwise, if closing the last incognito tab, select the current normal tab.
217 // * Otherwise, select nothing.
219 if (tabToClose != currentTab && currentTab != null) {
220 nextTab = currentTab;
221 } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
223 } else if (adjacentTab != null) {
224 nextTab = adjacentTab;
225 } else if (mIsIncognito) {
226 nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
233 public boolean isClosurePending(int tabId) {
234 return mRewoundList.getPendingRewindTab(tabId) != null;
238 public boolean supportsPendingClosures() {
239 return !mIsIncognito;
243 public TabList getComprehensiveModel() {
244 if (!supportsPendingClosures()) return this;
249 public void cancelTabClosure(int tabId) {
250 Tab tab = mRewoundList.getPendingRewindTab(tabId);
251 if (tab == null) return;
253 tab.setClosing(false);
255 // Find a valid previous tab entry so we know what tab to insert after. With the following
256 // example, calling cancelTabClosure(4) would need to know to insert after 2. So we have to
257 // track across mRewoundTabs and mTabs and see what the last valid mTabs entry was (2) when
258 // we hit the 4 in the rewound list. An insertIndex of -1 represents the beginning of the
259 // list, as this is the index of tab to insert after.
261 // mRewoundTabs 0 1 2 3 4 5
263 final int stopIndex = mRewoundList.indexOf(tab);
264 for (int rewoundIndex = 0; rewoundIndex < stopIndex; rewoundIndex++) {
265 Tab rewoundTab = mRewoundList.getTabAt(rewoundIndex);
266 if (prevIndex == mTabs.size() - 1) break;
267 if (rewoundTab == mTabs.get(prevIndex + 1)) prevIndex++;
270 // Figure out where to insert the tab. Just add one to prevIndex, as -1 represents the
271 // beginning of the list, so we'll insert at 0.
272 int insertIndex = prevIndex + 1;
273 if (mIndex >= insertIndex) mIndex++;
274 mTabs.add(insertIndex, tab);
276 boolean activeModel = mModelDelegate.getCurrentModel() == this;
278 // If we're the active model call setIndex to actually select this tab, otherwise just set
279 // mIndex but don't kick off everything that happens when calling setIndex().
281 setIndex(insertIndex);
283 mIndex = insertIndex;
286 for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
290 public void commitTabClosure(int tabId) {
291 Tab tab = mRewoundList.getPendingRewindTab(tabId);
292 if (tab == null) return;
294 // We're committing the close, actually remove it from the lists and finalize the closing
296 mRewoundList.removeTab(tab);
297 finalizeTabClosure(tab);
298 for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
302 public void commitAllTabClosures() {
303 while (mRewoundList.getCount() > mTabs.size()) {
304 commitTabClosure(mRewoundList.getNextRewindableTab().getId());
307 assert !mRewoundList.hasPendingClosures();
311 public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
312 if (tabToClose == null) {
313 assert false : "Tab is null!";
317 if (!mTabs.contains(tabToClose)) {
318 assert false : "Tried to close a tab from another model!";
322 canUndo &= supportsPendingClosures();
325 for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
327 startTabClosure(tabToClose, animate, uponExit, canUndo);
328 if (!canUndo) finalizeTabClosure(tabToClose);
334 public void closeAllTabs() {
335 commitAllTabClosures();
337 while (getCount() > 0) {
338 TabModelUtils.closeTabByIndex(this, 0);
344 public Tab getTabAt(int index) {
345 // This will catch INVALID_TAB_INDEX and return null
346 if (index < 0 || index >= mTabs.size()) return null;
347 return mTabs.get(index);
350 // Index of the given tab in the order of the tab stack.
352 public int indexOf(Tab tab) {
353 return mTabs.indexOf(tab);
357 * @return true if this is the current model according to the model selector
359 private boolean isCurrentModel() {
360 return mModelDelegate.getCurrentModel() == this;
363 // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
364 private int getLastId(TabSelectionType type) {
365 if (type == TabSelectionType.FROM_CLOSE) return Tab.INVALID_TAB_ID;
367 // Get the current tab in the current tab model.
368 Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
369 return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
372 // This function is complex and its behavior depends on persisted state, including mIndex.
374 public void setIndex(int i, final TabSelectionType type) {
376 int lastId = getLastId(type);
378 if (!isCurrentModel()) {
379 mModelDelegate.selectModel(isIncognito());
382 if (mTabs.size() <= 0) {
383 mIndex = INVALID_TAB_INDEX;
385 mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
388 Tab tab = TabModelUtils.getCurrentTab(this);
390 mModelDelegate.requestToShowTab(tab, type);
393 for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
396 // notifyDataSetChanged() can call into
397 // ChromeViewHolderTablet.handleTabChangeExternal(), which will eventually move the
398 // ContentView onto the current view hierarchy (with addView()).
399 mModelDelegate.didChange();
405 * @param nativeWebContents
410 protected abstract Tab createTabWithNativeContents(boolean incognito, long nativeWebContents,
414 * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}.
415 * This does not actually destroy the {@link Tab} (see
416 * {@link #finalizeTabClosure(Tab)}.
418 * @param tab The {@link Tab} to remove from this {@link TabModel}.
419 * @param animate Whether or not to animate the closing.
420 * @param uponExit Whether or not this is closing while the Activity is exiting.
421 * @param canUndo Whether or not this operation can be undone. Note that if this is {@code true}
422 * and {@link #supportsPendingClosures()} is {@code true},
423 * {@link #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be
424 * called to actually delete and clean up {@code tab}.
426 private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
427 final int closingTabId = tab.getId();
428 final int closingTabIndex = indexOf(tab);
430 tab.setClosing(true);
432 for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
434 Tab currentTab = TabModelUtils.getCurrentTab(this);
435 Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
436 Tab nextTab = getNextTabIfClosed(closingTabId);
438 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
439 if (!canUndo) commitAllTabClosures();
441 // Cancel any media currently playing.
443 WebContents webContents = tab.getWebContents();
444 if (webContents != null) webContents.releaseMediaPlayers();
449 boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
450 int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
451 int nextTabIndex = nextTab == null ? INVALID_TAB_INDEX : TabModelUtils.getTabIndexById(
452 mModelDelegate.getModel(nextIsIncognito), nextTabId);
454 if (nextTab != currentTab) {
455 if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
457 TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
458 nextModel.setIndex(nextTabIndex,
459 uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
461 mIndex = nextTabIndex;
464 if (!canUndo) mRewoundList.resetRewoundState();
468 * Actually closes and cleans up {@code tab}.
469 * @param tab The {@link Tab} to close.
471 private void finalizeTabClosure(Tab tab) {
472 for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
476 private class RewoundList implements TabList {
478 * A list of {@link Tab}s that represents the completely rewound list (if all
479 * rewindable closes were undone). If there are no possible rewindable closes this list
480 * should match {@link #mTabs}.
482 private List<Tab> mRewoundTabs = new ArrayList<Tab>();
485 public boolean isIncognito() {
486 return TabModelBase.this.isIncognito();
490 * If {@link TabModel} has a valid selected tab, this will return that same tab in the
491 * context of the rewound list of tabs. If {@link TabModel} has no tabs but the rewound
492 * list is not empty, it will return 0, the first tab. Otherwise it will return
493 * {@link TabModel#INVALID_TAB_INDEX}.
494 * @return The selected index of the rewound list of tabs (includes all pending closures).
498 if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
499 return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
501 if (!mRewoundTabs.isEmpty()) return 0;
502 return INVALID_TAB_INDEX;
506 public int getCount() {
507 return mRewoundTabs.size();
511 public Tab getTabAt(int index) {
512 if (index < 0 || index >= mRewoundTabs.size()) return null;
513 return mRewoundTabs.get(index);
517 public int indexOf(Tab tab) {
518 return mRewoundTabs.indexOf(tab);
522 public boolean isClosurePending(int tabId) {
523 return TabModelBase.this.isClosurePending(tabId);
527 * Resets this list to match the original {@link TabModel}. Note that if the
528 * {@link TabModel} doesn't support pending closures this model will be empty. This should
529 * be called whenever {@link #mTabs} changes.
531 public void resetRewoundState() {
532 mRewoundTabs.clear();
534 if (TabModelBase.this.supportsPendingClosures()) {
535 for (int i = 0; i < TabModelBase.this.getCount(); i++) {
536 mRewoundTabs.add(TabModelBase.this.getTabAt(i));
542 * Finds the {@link Tab} specified by {@code tabId} and only returns it if it is
543 * actually a {@link Tab} that is in the middle of being closed (which means that it
544 * is present in this model but not in {@link #mTabs}.
546 * @param tabId The id of the {@link Tab} to search for.
547 * @return The {@link Tab} specified by {@code tabId} as long as that tab only exists
548 * in this model and not in {@link #mTabs}. {@code null} otherwise.
550 public Tab getPendingRewindTab(int tabId) {
551 if (!TabModelBase.this.supportsPendingClosures()) return null;
552 if (TabModelUtils.getTabById(TabModelBase.this, tabId) != null) return null;
553 return TabModelUtils.getTabById(this, tabId);
557 * A utility method for easily finding a {@link Tab} that can be closed.
558 * @return The next tab that is in the middle of being closed.
560 public Tab getNextRewindableTab() {
561 if (!hasPendingClosures()) return null;
563 for (int i = 0; i < mRewoundTabs.size(); i++) {
564 Tab tab = i < TabModelBase.this.getCount() ? TabModelBase.this.getTabAt(i) : null;
565 Tab rewoundTab = mRewoundTabs.get(i);
567 if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
574 * Removes a {@link Tab} from this internal list.
575 * @param tab The {@link Tab} to remove.
577 public void removeTab(Tab tab) {
578 mRewoundTabs.remove(tab);
582 * Destroy all tabs in this model. This will check to see if the tab is already destroyed
583 * before destroying it.
585 public void destroy() {
586 for (Tab tab : mRewoundTabs) {
587 if (tab.isInitialized()) tab.destroy();
591 public boolean hasPendingClosures() {
592 return TabModelBase.this.supportsPendingClosures()
593 && mRewoundTabs.size() > TabModelBase.this.getCount();
598 * Broadcast a notification (in native code) that all tabs are now loaded from storage.
600 public void broadcastSessionRestoreComplete() {
601 nativeBroadcastSessionRestoreComplete(mNativeTabModelImpl);
604 // JNI related methods -------------------------------------------------------------------------
608 public int getCount() {
618 @SuppressWarnings("unused")
620 private void setIndex(int index) {
621 TabModelUtils.setIndex(this, index);
625 * Used by Developer Tools to create a new tab with a given URL.
627 * @param url The URL to open.
628 * @return The new tab.
631 protected abstract Tab createNewTabForDevTools(String url);
634 private boolean isSessionRestoreInProgress() {
635 return mModelDelegate.isSessionRestoreInProgress();
638 private native long nativeInit(boolean isIncognito);
639 private native void nativeDestroy(long nativeTabModelBase);
640 private native void nativeBroadcastSessionRestoreComplete(long nativeTabModelBase);
641 private native Profile nativeGetProfileAndroid(long nativeTabModelBase);
642 private native void nativeTabAddedToModel(long nativeTabModelBase, Tab tab);