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 android.os.SystemClock;
9 import org.chromium.base.CalledByNative;
10 import org.chromium.base.ObserverList;
11 import org.chromium.base.TraceEvent;
12 import org.chromium.chrome.browser.Tab;
13 import org.chromium.chrome.browser.profiles.Profile;
14 import org.chromium.chrome.browser.util.MathUtils;
15 import org.chromium.content_public.browser.WebContents;
17 import java.util.ArrayList;
18 import java.util.List;
21 * This is the default implementation of the {@link TabModel} interface.
23 public abstract class TabModelBase implements TabModel {
24 private static final String TAG = "TabModelBase";
26 // TODO(dtrainor, simonb): Make these non-static so we don't break if we have multiple instances
27 // of chrome running. Also investigate how this affects document mode.
28 private static long sTabSwitchStartTime;
29 private static TabSelectionType sTabSelectionType;
30 private static boolean sTabSwitchLatencyMetricRequired;
31 private static boolean sPerceivedTabSwitchLatencyMetricLogged;
34 * The main list of tabs. Note that when this changes, all pending closures must be committed
35 * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also
36 * {@link RewoundList#resetRewoundState()} must be called so that the full model will be up to
39 private final List<Tab> mTabs = new ArrayList<Tab>();
41 private final boolean mIsIncognito;
43 private final TabModelOrderController mOrderController;
45 protected final TabModelDelegate mModelDelegate;
47 private final ObserverList<TabModelObserver> mObservers;
49 // Undo State Tracking -------------------------------------------------------------------------
52 * A {@link TabList} that represents the complete list of {@link Tab}s. This is so that
53 * certain UI elements can call {@link TabModel#getComprehensiveModel()} to get a full list of
54 * {@link Tab}s that includes rewindable entries, as the typical {@link TabModel} does not
55 * return rewindable entries.
57 private final RewoundList mRewoundList = new RewoundList();
60 * This specifies the current {@link Tab} in {@link #mTabs}.
62 private int mIndex = INVALID_TAB_INDEX;
64 /** Native Tab pointer which will be set by nativeInit(). */
65 private long mNativeTabModelImpl = 0;
67 public TabModelBase(boolean incognito, TabModelOrderController orderController,
68 TabModelDelegate modelDelegate) {
69 mIsIncognito = incognito;
70 mNativeTabModelImpl = nativeInit(incognito);
71 mOrderController = orderController;
72 mModelDelegate = modelDelegate;
73 mObservers = new ObserverList<TabModelObserver>();
77 public Profile getProfile() {
78 return nativeGetProfileAndroid(mNativeTabModelImpl);
82 public boolean isIncognito() {
87 public void destroy() {
88 for (Tab tab : mTabs) {
89 if (tab.isInitialized()) tab.destroy();
92 mRewoundList.destroy();
94 if (mNativeTabModelImpl != 0) {
95 nativeDestroy(mNativeTabModelImpl);
96 mNativeTabModelImpl = 0;
104 public void addObserver(TabModelObserver observer) {
105 mObservers.addObserver(observer);
109 public void removeObserver(TabModelObserver observer) {
110 mObservers.removeObserver(observer);
114 * Initializes the newly created tab, adds it to controller, and dispatches creation
115 * step notifications.
118 public void addTab(Tab tab, int index, TabLaunchType type) {
121 for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
123 boolean selectTab = mOrderController.willOpenInForeground(type, mIsIncognito);
125 index = mOrderController.determineInsertionIndex(type, index, tab);
126 assert index <= mTabs.size();
128 assert tab.isIncognito() == mIsIncognito;
130 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
131 commitAllTabClosures();
133 if (index < 0 || index > mTabs.size()) {
136 mTabs.add(index, tab);
137 if (index <= mIndex) {
142 if (!isCurrentModel()) {
143 // When adding new tabs in the background, make sure we set a valid index when the
144 // first one is added. When in the foreground, calls to setIndex will take care of
146 mIndex = Math.max(mIndex, 0);
149 mRewoundList.resetRewoundState();
151 int newIndex = indexOf(tab);
152 mModelDelegate.didChange();
153 mModelDelegate.didCreateNewTab(tab);
155 if (mNativeTabModelImpl != 0) nativeTabAddedToModel(mNativeTabModelImpl, tab);
157 for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
160 mModelDelegate.selectModel(mIsIncognito);
161 setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
168 public void moveTab(int id, int newIndex) {
169 newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
171 int curIndex = TabModelUtils.getTabIndexById(this, id);
173 if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
177 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
178 commitAllTabClosures();
180 Tab tab = mTabs.remove(curIndex);
181 if (curIndex < newIndex) --newIndex;
183 mTabs.add(newIndex, tab);
185 if (curIndex == mIndex) {
187 } else if (curIndex < mIndex && newIndex >= mIndex) {
189 } else if (curIndex > mIndex && newIndex <= mIndex) {
193 mRewoundList.resetRewoundState();
195 mModelDelegate.didChange();
196 for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
201 public boolean closeTab(Tab tab) {
202 return closeTab(tab, true, false, false);
205 private Tab findTabInAllTabModels(int tabId) {
206 Tab tab = TabModelUtils.getTabById(mModelDelegate.getModel(mIsIncognito), tabId);
207 if (tab != null) return tab;
208 return TabModelUtils.getTabById(mModelDelegate.getModel(!mIsIncognito), tabId);
212 public Tab getNextTabIfClosed(int id) {
213 Tab tabToClose = TabModelUtils.getTabById(this, id);
214 Tab currentTab = TabModelUtils.getCurrentTab(this);
215 if (tabToClose == null) return currentTab;
217 int closingTabIndex = indexOf(tabToClose);
218 Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
219 Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
221 // Determine which tab to select next according to these rules:
222 // * If closing a background tab, keep the current tab selected.
223 // * Otherwise, if not in overview mode, select the parent tab if it exists.
224 // * Otherwise, select an adjacent tab if one exists.
225 // * Otherwise, if closing the last incognito tab, select the current normal tab.
226 // * Otherwise, select nothing.
228 if (tabToClose != currentTab && currentTab != null) {
229 nextTab = currentTab;
230 } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
232 } else if (adjacentTab != null) {
233 nextTab = adjacentTab;
234 } else if (mIsIncognito) {
235 nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
242 public boolean isClosurePending(int tabId) {
243 return mRewoundList.getPendingRewindTab(tabId) != null;
247 public boolean supportsPendingClosures() {
248 return !mIsIncognito;
252 public TabList getComprehensiveModel() {
253 if (!supportsPendingClosures()) return this;
258 public void cancelTabClosure(int tabId) {
259 Tab tab = mRewoundList.getPendingRewindTab(tabId);
260 if (tab == null) return;
262 tab.setClosing(false);
264 // Find a valid previous tab entry so we know what tab to insert after. With the following
265 // example, calling cancelTabClosure(4) would need to know to insert after 2. So we have to
266 // track across mRewoundTabs and mTabs and see what the last valid mTabs entry was (2) when
267 // we hit the 4 in the rewound list. An insertIndex of -1 represents the beginning of the
268 // list, as this is the index of tab to insert after.
270 // mRewoundTabs 0 1 2 3 4 5
272 final int stopIndex = mRewoundList.indexOf(tab);
273 for (int rewoundIndex = 0; rewoundIndex < stopIndex; rewoundIndex++) {
274 Tab rewoundTab = mRewoundList.getTabAt(rewoundIndex);
275 if (prevIndex == mTabs.size() - 1) break;
276 if (rewoundTab == mTabs.get(prevIndex + 1)) prevIndex++;
279 // Figure out where to insert the tab. Just add one to prevIndex, as -1 represents the
280 // beginning of the list, so we'll insert at 0.
281 int insertIndex = prevIndex + 1;
282 if (mIndex >= insertIndex) mIndex++;
283 mTabs.add(insertIndex, tab);
285 boolean activeModel = mModelDelegate.getCurrentModel() == this;
287 // If we're the active model call setIndex to actually select this tab, otherwise just set
288 // mIndex but don't kick off everything that happens when calling setIndex().
290 setIndex(insertIndex);
292 mIndex = insertIndex;
295 for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
299 public void commitTabClosure(int tabId) {
300 Tab tab = mRewoundList.getPendingRewindTab(tabId);
301 if (tab == null) return;
303 // We're committing the close, actually remove it from the lists and finalize the closing
305 mRewoundList.removeTab(tab);
306 finalizeTabClosure(tab);
307 for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
311 public void commitAllTabClosures() {
312 while (mRewoundList.getCount() > mTabs.size()) {
313 commitTabClosure(mRewoundList.getNextRewindableTab().getId());
316 assert !mRewoundList.hasPendingClosures();
320 public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
321 if (tabToClose == null) {
322 assert false : "Tab is null!";
326 if (!mTabs.contains(tabToClose)) {
327 assert false : "Tried to close a tab from another model!";
331 canUndo &= supportsPendingClosures();
334 for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
336 startTabClosure(tabToClose, animate, uponExit, canUndo);
337 if (!canUndo) finalizeTabClosure(tabToClose);
343 public void closeAllTabs() {
344 commitAllTabClosures();
346 while (getCount() > 0) {
347 TabModelUtils.closeTabByIndex(this, 0);
353 public Tab getTabAt(int index) {
354 // This will catch INVALID_TAB_INDEX and return null
355 if (index < 0 || index >= mTabs.size()) return null;
356 return mTabs.get(index);
359 // Index of the given tab in the order of the tab stack.
361 public int indexOf(Tab tab) {
362 return mTabs.indexOf(tab);
366 * @return true if this is the current model according to the model selector
368 private boolean isCurrentModel() {
369 return mModelDelegate.getCurrentModel() == this;
372 // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
373 private int getLastId(TabSelectionType type) {
374 if (type == TabSelectionType.FROM_CLOSE) return Tab.INVALID_TAB_ID;
376 // Get the current tab in the current tab model.
377 Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
378 return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
381 // This function is complex and its behavior depends on persisted state, including mIndex.
383 public void setIndex(int i, final TabSelectionType type) {
385 int lastId = getLastId(type);
387 if (!isCurrentModel()) {
388 mModelDelegate.selectModel(isIncognito());
391 if (mTabs.size() <= 0) {
392 mIndex = INVALID_TAB_INDEX;
394 mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
397 Tab tab = TabModelUtils.getCurrentTab(this);
399 mModelDelegate.requestToShowTab(tab, type);
402 for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
405 // notifyDataSetChanged() can call into
406 // ChromeViewHolderTablet.handleTabChangeExternal(), which will eventually move the
407 // ContentView onto the current view hierarchy (with addView()).
408 mModelDelegate.didChange();
414 * @param nativeWebContents
419 protected abstract Tab createTabWithNativeContents(boolean incognito, long nativeWebContents,
423 * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}.
424 * This does not actually destroy the {@link Tab} (see
425 * {@link #finalizeTabClosure(Tab)}.
427 * @param tab The {@link Tab} to remove from this {@link TabModel}.
428 * @param animate Whether or not to animate the closing.
429 * @param uponExit Whether or not this is closing while the Activity is exiting.
430 * @param canUndo Whether or not this operation can be undone. Note that if this is {@code true}
431 * and {@link #supportsPendingClosures()} is {@code true},
432 * {@link #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be
433 * called to actually delete and clean up {@code tab}.
435 private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
436 final int closingTabId = tab.getId();
437 final int closingTabIndex = indexOf(tab);
439 tab.setClosing(true);
441 for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
443 Tab currentTab = TabModelUtils.getCurrentTab(this);
444 Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
445 Tab nextTab = getNextTabIfClosed(closingTabId);
447 // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
448 if (!canUndo) commitAllTabClosures();
450 // Cancel any media currently playing.
452 WebContents webContents = tab.getWebContents();
453 if (webContents != null) webContents.releaseMediaPlayers();
458 boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
459 int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
460 int nextTabIndex = nextTab == null ? INVALID_TAB_INDEX : TabModelUtils.getTabIndexById(
461 mModelDelegate.getModel(nextIsIncognito), nextTabId);
463 if (nextTab != currentTab) {
464 if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
466 TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
467 nextModel.setIndex(nextTabIndex,
468 uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
470 mIndex = nextTabIndex;
473 if (!canUndo) mRewoundList.resetRewoundState();
477 * Actually closes and cleans up {@code tab}.
478 * @param tab The {@link Tab} to close.
480 private void finalizeTabClosure(Tab tab) {
481 for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
485 private class RewoundList implements TabList {
487 * A list of {@link Tab}s that represents the completely rewound list (if all
488 * rewindable closes were undone). If there are no possible rewindable closes this list
489 * should match {@link #mTabs}.
491 private List<Tab> mRewoundTabs = new ArrayList<Tab>();
494 public boolean isIncognito() {
495 return TabModelBase.this.isIncognito();
499 * If {@link TabModel} has a valid selected tab, this will return that same tab in the
500 * context of the rewound list of tabs. If {@link TabModel} has no tabs but the rewound
501 * list is not empty, it will return 0, the first tab. Otherwise it will return
502 * {@link TabModel#INVALID_TAB_INDEX}.
503 * @return The selected index of the rewound list of tabs (includes all pending closures).
507 if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
508 return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
510 if (!mRewoundTabs.isEmpty()) return 0;
511 return INVALID_TAB_INDEX;
515 public int getCount() {
516 return mRewoundTabs.size();
520 public Tab getTabAt(int index) {
521 if (index < 0 || index >= mRewoundTabs.size()) return null;
522 return mRewoundTabs.get(index);
526 public int indexOf(Tab tab) {
527 return mRewoundTabs.indexOf(tab);
531 public boolean isClosurePending(int tabId) {
532 return TabModelBase.this.isClosurePending(tabId);
536 * Resets this list to match the original {@link TabModel}. Note that if the
537 * {@link TabModel} doesn't support pending closures this model will be empty. This should
538 * be called whenever {@link #mTabs} changes.
540 public void resetRewoundState() {
541 mRewoundTabs.clear();
543 if (TabModelBase.this.supportsPendingClosures()) {
544 for (int i = 0; i < TabModelBase.this.getCount(); i++) {
545 mRewoundTabs.add(TabModelBase.this.getTabAt(i));
551 * Finds the {@link Tab} specified by {@code tabId} and only returns it if it is
552 * actually a {@link Tab} that is in the middle of being closed (which means that it
553 * is present in this model but not in {@link #mTabs}.
555 * @param tabId The id of the {@link Tab} to search for.
556 * @return The {@link Tab} specified by {@code tabId} as long as that tab only exists
557 * in this model and not in {@link #mTabs}. {@code null} otherwise.
559 public Tab getPendingRewindTab(int tabId) {
560 if (!TabModelBase.this.supportsPendingClosures()) return null;
561 if (TabModelUtils.getTabById(TabModelBase.this, tabId) != null) return null;
562 return TabModelUtils.getTabById(this, tabId);
566 * A utility method for easily finding a {@link Tab} that can be closed.
567 * @return The next tab that is in the middle of being closed.
569 public Tab getNextRewindableTab() {
570 if (!hasPendingClosures()) return null;
572 for (int i = 0; i < mRewoundTabs.size(); i++) {
573 Tab tab = i < TabModelBase.this.getCount() ? TabModelBase.this.getTabAt(i) : null;
574 Tab rewoundTab = mRewoundTabs.get(i);
576 if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
583 * Removes a {@link Tab} from this internal list.
584 * @param tab The {@link Tab} to remove.
586 public void removeTab(Tab tab) {
587 mRewoundTabs.remove(tab);
591 * Destroy all tabs in this model. This will check to see if the tab is already destroyed
592 * before destroying it.
594 public void destroy() {
595 for (Tab tab : mRewoundTabs) {
596 if (tab.isInitialized()) tab.destroy();
600 public boolean hasPendingClosures() {
601 return TabModelBase.this.supportsPendingClosures()
602 && mRewoundTabs.size() > TabModelBase.this.getCount();
607 * Broadcast a notification (in native code) that all tabs are now loaded from storage.
609 public void broadcastSessionRestoreComplete() {
610 nativeBroadcastSessionRestoreComplete(mNativeTabModelImpl);
613 // JNI related methods -------------------------------------------------------------------------
617 public int getCount() {
627 @SuppressWarnings("unused")
629 private void setIndex(int index) {
630 TabModelUtils.setIndex(this, index);
634 * Used by Developer Tools to create a new tab with a given URL.
636 * @param url The URL to open.
637 * @return The new tab.
640 protected abstract Tab createNewTabForDevTools(String url);
643 private boolean isSessionRestoreInProgress() {
644 return mModelDelegate.isSessionRestoreInProgress();
648 * Register the start of tab switch latency timing. Called when setIndex() indicates a tab
650 * @param type The type of action that triggered the tab selection.
652 public static void startTabSwitchLatencyTiming(final TabSelectionType type) {
653 sTabSwitchStartTime = SystemClock.uptimeMillis();
654 sTabSelectionType = type;
655 sTabSwitchLatencyMetricRequired = false;
656 sPerceivedTabSwitchLatencyMetricLogged = false;
660 * Should be called a visible {@link ChromeTab} gets a frame to render in the browser process.
661 * If we don't get this call, we ignore requests to
662 * {@link #flushActualTabSwitchLatencyMetric()}.
664 public static void setActualTabSwitchLatencyMetricRequired() {
665 if (sTabSwitchStartTime <= 0) return;
666 sTabSwitchLatencyMetricRequired = true;
670 * Logs the perceived tab switching latency metric. This will automatically be logged if
671 * the actual metric is set and flushed.
673 public static void logPerceivedTabSwitchLatencyMetric() {
674 if (sTabSwitchStartTime <= 0 || sPerceivedTabSwitchLatencyMetricLogged) return;
676 flushTabSwitchLatencyMetric(true);
677 sPerceivedTabSwitchLatencyMetricLogged = true;
681 * Flush the latency metric if called after the indication that a frame is ready.
683 public static void flushActualTabSwitchLatencyMetric() {
684 if (sTabSwitchStartTime <= 0 || !sTabSwitchLatencyMetricRequired) return;
685 logPerceivedTabSwitchLatencyMetric();
686 flushTabSwitchLatencyMetric(false);
688 sTabSwitchStartTime = 0;
689 sTabSwitchLatencyMetricRequired = false;
692 private static void flushTabSwitchLatencyMetric(boolean perceived) {
693 if (sTabSwitchStartTime <= 0) return;
694 final long ms = SystemClock.uptimeMillis() - sTabSwitchStartTime;
695 switch (sTabSelectionType) {
697 nativeLogFromCloseMetric(ms, perceived);
700 nativeLogFromExitMetric(ms, perceived);
703 nativeLogFromNewMetric(ms, perceived);
706 nativeLogFromUserMetric(ms, perceived);
711 private native long nativeInit(boolean isIncognito);
712 private native void nativeDestroy(long nativeTabModelBase);
713 private native void nativeBroadcastSessionRestoreComplete(long nativeTabModelBase);
714 private native Profile nativeGetProfileAndroid(long nativeTabModelBase);
715 private native void nativeTabAddedToModel(long nativeTabModelBase, Tab tab);
716 // Native methods for tab switch latency metrics.
717 private static native void nativeLogFromCloseMetric(long ms, boolean perceived);
718 private static native void nativeLogFromExitMetric(long ms, boolean perceived);
719 private static native void nativeLogFromNewMetric(long ms, boolean perceived);
720 private static native void nativeLogFromUserMetric(long ms, boolean perceived);