Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4985)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelBase.java

Issue 270883002: Upstream base implementations of TabModel, TabModelSelector, and TabModelObserver. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: sync Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelBase.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelBase.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e2b0a263e87921c077a72207b8c656c6d1035c3
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelBase.java
@@ -0,0 +1,638 @@
+// 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.CalledByNative;
+import org.chromium.base.ObserverList;
+import org.chromium.base.TraceEvent;
+import org.chromium.chrome.browser.Tab;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.util.MathUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is the default implementation of the {@link TabModel} interface.
+ */
+public abstract class TabModelBase implements TabModel {
+ private static final String TAG = "TabModelBase";
+
+ /**
+ * The main list of tabs. Note that when this changes, all pending closures must be committed
+ * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also
+ * {@link RewoundList#resetRewoundState()} must be called so that the full model will be up to
+ * date.
+ */
+ private final List<Tab> mTabs = new ArrayList<Tab>();
+
+ private final boolean mIsIncognito;
+
+ private final TabModelOrderController mOrderController;
+
+ protected final TabModelDelegate mModelDelegate;
+
+ private final ObserverList<TabModelObserver> mObservers;
+
+ // Undo State Tracking -------------------------------------------------------------------------
+
+ /**
+ * A {@link TabList} that represents the complete list of {@link Tab}s. This is so that
+ * certain UI elements can call {@link TabModel#getComprehensiveModel()} to get a full list of
+ * {@link Tab}s that includes rewindable entries, as the typical {@link TabModel} does not
+ * return rewindable entries.
+ */
+ private final RewoundList mRewoundList = new RewoundList();
+
+ /**
+ * This specifies the current {@link Tab} in {@link #mTabs}.
+ */
+ private int mIndex = INVALID_TAB_INDEX;
+
+ /** Native Tab pointer which will be set by nativeInit(). */
+ private long mNativeTabModelImpl = 0;
+
+ public TabModelBase(boolean incognito, TabModelOrderController orderController,
+ TabModelDelegate modelDelegate) {
+ mIsIncognito = incognito;
+ mNativeTabModelImpl = nativeInit(incognito);
+ mOrderController = orderController;
+ mModelDelegate = modelDelegate;
+ mObservers = new ObserverList<TabModelObserver>();
+ }
+
+ @Override
+ public Profile getProfile() {
+ return nativeGetProfileAndroid(mNativeTabModelImpl);
+ }
+
+ @Override
+ public boolean isIncognito() {
+ return mIsIncognito;
+ }
+
+ @Override
+ public void destroy() {
+ for (Tab tab : mTabs) {
+ if (tab.isInitialized()) tab.destroy();
+ }
+
+ mRewoundList.destroy();
+
+ if (mNativeTabModelImpl != 0) {
+ nativeDestroy(mNativeTabModelImpl);
+ mNativeTabModelImpl = 0;
+ }
+ }
+
+ @Override
+ public void addObserver(TabModelObserver observer) {
+ mObservers.addObserver(observer);
+ }
+
+ @Override
+ public void removeObserver(TabModelObserver observer) {
+ mObservers.removeObserver(observer);
+ }
+
+ /**
+ * Initializes the newly created tab, adds it to controller, and dispatches creation
+ * step notifications.
+ */
+ @Override
+ public void addTab(Tab tab, int index, TabLaunchType type) {
+ TraceEvent.begin();
+
+ for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
+
+ boolean selectTab = mOrderController.willOpenInForeground(type, mIsIncognito);
+
+ index = mOrderController.determineInsertionIndex(type, index, tab);
+ assert index <= mTabs.size();
+
+ assert tab.isIncognito() == mIsIncognito;
+
+ // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
+ commitAllTabClosures();
+
+ if (index < 0 || index > mTabs.size()) {
+ mTabs.add(tab);
+ } else {
+ mTabs.add(index, tab);
+ if (index <= mIndex) {
+ mIndex++;
+ }
+ }
+
+ if (!isCurrentModel()) {
+ // When adding new tabs in the background, make sure we set a valid index when the
+ // first one is added. When in the foreground, calls to setIndex will take care of
+ // this.
+ mIndex = Math.max(mIndex, 0);
+ }
+
+ mRewoundList.resetRewoundState();
+
+ int newIndex = indexOf(tab);
+ mModelDelegate.didChange();
+ mModelDelegate.didCreateNewTab(tab);
+
+ if (mNativeTabModelImpl != 0) nativeTabAddedToModel(mNativeTabModelImpl, tab);
+
+ for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
+
+ if (selectTab) {
+ mModelDelegate.selectModel(mIsIncognito);
+ setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
+ }
+
+ TraceEvent.end();
+ }
+
+ @Override
+ public void moveTab(int id, int newIndex) {
+ newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
+
+ int curIndex = TabModelUtils.getTabIndexById(this, id);
+
+ if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
+ return;
+ }
+
+ // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
+ commitAllTabClosures();
+
+ Tab tab = mTabs.remove(curIndex);
+ if (curIndex < newIndex) --newIndex;
+
+ mTabs.add(newIndex, tab);
+
+ if (curIndex == mIndex) {
+ mIndex = newIndex;
+ } else if (curIndex < mIndex && newIndex >= mIndex) {
+ --mIndex;
+ } else if (curIndex > mIndex && newIndex <= mIndex) {
+ ++mIndex;
+ }
+
+ mRewoundList.resetRewoundState();
+
+ mModelDelegate.didChange();
+ for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
+ }
+
+ @Override
+ @CalledByNative
+ public boolean closeTab(Tab tab) {
+ return closeTab(tab, true, false, false);
+ }
+
+ private Tab findTabInAllTabModels(int tabId) {
+ Tab tab = TabModelUtils.getTabById(mModelDelegate.getModel(mIsIncognito), tabId);
+ if (tab != null) return tab;
+ return TabModelUtils.getTabById(mModelDelegate.getModel(!mIsIncognito), tabId);
+ }
+
+ @Override
+ public Tab getNextTabIfClosed(int id) {
+ Tab tabToClose = TabModelUtils.getTabById(this, id);
+ Tab currentTab = TabModelUtils.getCurrentTab(this);
+ if (tabToClose == null) return currentTab;
+
+ int closingTabIndex = indexOf(tabToClose);
+ Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
+ Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
+
+ // Determine which tab to select next according to these rules:
+ // * If closing a background tab, keep the current tab selected.
+ // * Otherwise, if not in overview mode, select the parent tab if it exists.
+ // * Otherwise, select an adjacent tab if one exists.
+ // * Otherwise, if closing the last incognito tab, select the current normal tab.
+ // * Otherwise, select nothing.
+ Tab nextTab = null;
+ if (tabToClose != currentTab && currentTab != null) {
+ nextTab = currentTab;
+ } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
+ nextTab = parentTab;
+ } else if (adjacentTab != null) {
+ nextTab = adjacentTab;
+ } else if (mIsIncognito) {
+ nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
+ }
+
+ return nextTab;
+ }
+
+ @Override
+ public boolean isClosurePending(int tabId) {
+ return mRewoundList.getPendingRewindTab(tabId) != null;
+ }
+
+ @Override
+ public boolean supportsPendingClosures() {
+ return !mIsIncognito;
+ }
+
+ @Override
+ public TabList getComprehensiveModel() {
+ if (!supportsPendingClosures()) return this;
+ return mRewoundList;
+ }
+
+ @Override
+ public void cancelTabClosure(int tabId) {
+ Tab tab = mRewoundList.getPendingRewindTab(tabId);
+ if (tab == null) return;
+
+ tab.setClosing(false);
+
+ // Find a valid previous tab entry so we know what tab to insert after. With the following
+ // example, calling cancelTabClosure(4) would need to know to insert after 2. So we have to
+ // track across mRewoundTabs and mTabs and see what the last valid mTabs entry was (2) when
+ // we hit the 4 in the rewound list. An insertIndex of -1 represents the beginning of the
+ // list, as this is the index of tab to insert after.
+ // mTabs: 0 2 5
+ // mRewoundTabs 0 1 2 3 4 5
+ int prevIndex = -1;
+ final int stopIndex = mRewoundList.indexOf(tab);
+ for (int rewoundIndex = 0; rewoundIndex < stopIndex; rewoundIndex++) {
+ Tab rewoundTab = mRewoundList.getTabAt(rewoundIndex);
+ if (prevIndex == mTabs.size() - 1) break;
+ if (rewoundTab == mTabs.get(prevIndex + 1)) prevIndex++;
+ }
+
+ // Figure out where to insert the tab. Just add one to prevIndex, as -1 represents the
+ // beginning of the list, so we'll insert at 0.
+ int insertIndex = prevIndex + 1;
+ if (mIndex >= insertIndex) mIndex++;
+ mTabs.add(insertIndex, tab);
+
+ boolean activeModel = mModelDelegate.getCurrentModel() == this;
+
+ // If we're the active model call setIndex to actually select this tab, otherwise just set
+ // mIndex but don't kick off everything that happens when calling setIndex().
+ if (activeModel) {
+ setIndex(insertIndex);
+ } else {
+ mIndex = insertIndex;
+ }
+
+ for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
+ }
+
+ @Override
+ public void commitTabClosure(int tabId) {
+ Tab tab = mRewoundList.getPendingRewindTab(tabId);
+ if (tab == null) return;
+
+ // We're committing the close, actually remove it from the lists and finalize the closing
+ // operation.
+ mRewoundList.removeTab(tab);
+ finalizeTabClosure(tab);
+ for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
+ }
+
+ @Override
+ public void commitAllTabClosures() {
+ while (mRewoundList.getCount() > mTabs.size()) {
+ commitTabClosure(mRewoundList.getNextRewindableTab().getId());
+ }
+
+ assert !mRewoundList.hasPendingClosures();
+ }
+
+ @Override
+ public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
+ if (tabToClose == null) {
+ assert false : "Tab is null!";
+ return false;
+ }
+
+ if (!mTabs.contains(tabToClose)) {
+ assert false : "Tried to close a tab from another model!";
+ return false;
+ }
+
+ canUndo &= supportsPendingClosures();
+
+ if (canUndo) {
+ for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
+ }
+ startTabClosure(tabToClose, animate, uponExit, canUndo);
+ if (!canUndo) finalizeTabClosure(tabToClose);
+
+ return true;
+ }
+
+ @Override
+ public void closeAllTabs() {
+ commitAllTabClosures();
+
+ while (getCount() > 0) {
+ TabModelUtils.closeTabByIndex(this, 0);
+ }
+ }
+
+ @Override
+ @CalledByNative
+ public Tab getTabAt(int index) {
+ // This will catch INVALID_TAB_INDEX and return null
+ if (index < 0 || index >= mTabs.size()) return null;
+ return mTabs.get(index);
+ }
+
+ // Index of the given tab in the order of the tab stack.
+ @Override
+ public int indexOf(Tab tab) {
+ return mTabs.indexOf(tab);
+ }
+
+ /**
+ * @return true if this is the current model according to the model selector
+ */
+ private boolean isCurrentModel() {
+ return mModelDelegate.getCurrentModel() == this;
+ }
+
+ // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
+ private int getLastId(TabSelectionType type) {
+ if (type == TabSelectionType.FROM_CLOSE) return Tab.INVALID_TAB_ID;
+
+ // Get the current tab in the current tab model.
+ Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
+ return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
+ }
+
+ // This function is complex and its behavior depends on persisted state, including mIndex.
+ @Override
+ public void setIndex(int i, final TabSelectionType type) {
+ TraceEvent.begin();
+ int lastId = getLastId(type);
+
+ if (!isCurrentModel()) {
+ mModelDelegate.selectModel(isIncognito());
+ }
+
+ if (mTabs.size() <= 0) {
+ mIndex = INVALID_TAB_INDEX;
+ } else {
+ mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
+ }
+
+ Tab tab = TabModelUtils.getCurrentTab(this);
+
+ mModelDelegate.requestToShowTab(tab, type);
+
+ if (tab != null) {
+ for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
+ }
+
+ // notifyDataSetChanged() can call into
+ // ChromeViewHolderTablet.handleTabChangeExternal(), which will eventually move the
+ // ContentView onto the current view hierarchy (with addView()).
+ mModelDelegate.didChange();
+ TraceEvent.end();
+ }
+
+ /**
+ * @param incognito
+ * @param nativeWebContents
+ * @param parentId
+ * @return
+ */
+ @CalledByNative
+ protected abstract Tab createTabWithNativeContents(boolean incognito, long nativeWebContents,
+ int parentId);
+
+ /**
+ * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}.
+ * This does not actually destroy the {@link Tab} (see
+ * {@link #finalizeTabClosure(Tab)}.
+ *
+ * @param tab The {@link Tab} to remove from this {@link TabModel}.
+ * @param animate Whether or not to animate the closing.
+ * @param uponExit Whether or not this is closing while the Activity is exiting.
+ * @param canUndo Whether or not this operation can be undone. Note that if this is {@code true}
+ * and {@link #supportsPendingClosures()} is {@code true},
+ * {@link #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be
+ * called to actually delete and clean up {@code tab}.
+ */
+ private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
+ final int closingTabId = tab.getId();
+ final int closingTabIndex = indexOf(tab);
+
+ tab.setClosing(true);
+
+ for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
+
+ Tab currentTab = TabModelUtils.getCurrentTab(this);
+ Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
+ Tab nextTab = getNextTabIfClosed(closingTabId);
+
+ // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
+ if (!canUndo) commitAllTabClosures();
+ mTabs.remove(tab);
+
+ boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
+ int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
+ int nextTabIndex = nextTab == null ? INVALID_TAB_INDEX : TabModelUtils.getTabIndexById(
+ mModelDelegate.getModel(nextIsIncognito), nextTabId);
+
+ if (nextTab != currentTab) {
+ if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
+
+ TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
+ nextModel.setIndex(nextTabIndex,
+ uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
+ } else {
+ mIndex = nextTabIndex;
+ }
+
+ if (!canUndo) mRewoundList.resetRewoundState();
+ }
+
+ /**
+ * Actually closes and cleans up {@code tab}.
+ * @param tab The {@link Tab} to close.
+ */
+ private void finalizeTabClosure(Tab tab) {
+ for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
+ tab.destroy();
+ }
+
+ private class RewoundList implements TabList {
+ /**
+ * A list of {@link Tab}s that represents the completely rewound list (if all
+ * rewindable closes were undone). If there are no possible rewindable closes this list
+ * should match {@link #mTabs}.
+ */
+ private List<Tab> mRewoundTabs = new ArrayList<Tab>();
+
+ @Override
+ public boolean isIncognito() {
+ return TabModelBase.this.isIncognito();
+ }
+
+ /**
+ * If {@link TabModel} has a valid selected tab, this will return that same tab in the
+ * context of the rewound list of tabs. If {@link TabModel} has no tabs but the rewound
+ * list is not empty, it will return 0, the first tab. Otherwise it will return
+ * {@link TabModel#INVALID_TAB_INDEX}.
+ * @return The selected index of the rewound list of tabs (includes all pending closures).
+ */
+ @Override
+ public int index() {
+ if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
+ return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
+ }
+ if (!mRewoundTabs.isEmpty()) return 0;
+ return INVALID_TAB_INDEX;
+ }
+
+ @Override
+ public int getCount() {
+ return mRewoundTabs.size();
+ }
+
+ @Override
+ public Tab getTabAt(int index) {
+ if (index < 0 || index >= mRewoundTabs.size()) return null;
+ return mRewoundTabs.get(index);
+ }
+
+ @Override
+ public int indexOf(Tab tab) {
+ return mRewoundTabs.indexOf(tab);
+ }
+
+ @Override
+ public boolean isClosurePending(int tabId) {
+ return TabModelBase.this.isClosurePending(tabId);
+ }
+
+ /**
+ * Resets this list to match the original {@link TabModel}. Note that if the
+ * {@link TabModel} doesn't support pending closures this model will be empty. This should
+ * be called whenever {@link #mTabs} changes.
+ */
+ public void resetRewoundState() {
+ mRewoundTabs.clear();
+
+ if (TabModelBase.this.supportsPendingClosures()) {
+ for (int i = 0; i < TabModelBase.this.getCount(); i++) {
+ mRewoundTabs.add(TabModelBase.this.getTabAt(i));
+ }
+ }
+ }
+
+ /**
+ * Finds the {@link Tab} specified by {@code tabId} and only returns it if it is
+ * actually a {@link Tab} that is in the middle of being closed (which means that it
+ * is present in this model but not in {@link #mTabs}.
+ *
+ * @param tabId The id of the {@link Tab} to search for.
+ * @return The {@link Tab} specified by {@code tabId} as long as that tab only exists
+ * in this model and not in {@link #mTabs}. {@code null} otherwise.
+ */
+ public Tab getPendingRewindTab(int tabId) {
+ if (!TabModelBase.this.supportsPendingClosures()) return null;
+ if (TabModelUtils.getTabById(TabModelBase.this, tabId) != null) return null;
+ return TabModelUtils.getTabById(this, tabId);
+ }
+
+ /**
+ * A utility method for easily finding a {@link Tab} that can be closed.
+ * @return The next tab that is in the middle of being closed.
+ */
+ public Tab getNextRewindableTab() {
+ if (!hasPendingClosures()) return null;
+
+ for (int i = 0; i < mRewoundTabs.size(); i++) {
+ Tab tab = i < TabModelBase.this.getCount() ? TabModelBase.this.getTabAt(i) : null;
+ Tab rewoundTab = mRewoundTabs.get(i);
+
+ if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes a {@link Tab} from this internal list.
+ * @param tab The {@link Tab} to remove.
+ */
+ public void removeTab(Tab tab) {
+ mRewoundTabs.remove(tab);
+ }
+
+ /**
+ * Destroy all tabs in this model. This will check to see if the tab is already destroyed
+ * before destroying it.
+ */
+ public void destroy() {
+ for (Tab tab : mRewoundTabs) {
+ if (tab.isInitialized()) tab.destroy();
+ }
+ }
+
+ public boolean hasPendingClosures() {
+ return TabModelBase.this.supportsPendingClosures()
+ && mRewoundTabs.size() > TabModelBase.this.getCount();
+ }
+ }
+
+ /**
+ * Broadcast a notification (in native code) that all tabs are now loaded from storage.
+ */
+ public void broadcastSessionRestoreComplete() {
+ nativeBroadcastSessionRestoreComplete(mNativeTabModelImpl);
+ }
+
+ // JNI related methods -------------------------------------------------------------------------
+
+ @Override
+ @CalledByNative
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ @CalledByNative
+ public int index() {
+ return mIndex;
+ }
+
+ @SuppressWarnings("unused")
+ @CalledByNative
+ private void setIndex(int index) {
+ TabModelUtils.setIndex(this, index);
+ }
+
+ /**
+ * Used by Developer Tools to create a new tab with a given URL.
+ *
+ * @param url The URL to open.
+ * @return The new tab.
+ */
+ @CalledByNative
+ protected abstract Tab createNewTabForDevTools(String url);
+
+ /**
+ * Opens the Clear Browsing Data dialog.
+ */
+ @CalledByNative
+ protected abstract void openClearBrowsingData();
+
+ @CalledByNative
+ private boolean isSessionRestoreInProgress() {
+ return mModelDelegate.isSessionRestoreInProgress();
+ }
+
+ private native long nativeInit(boolean isIncognito);
+ private native void nativeDestroy(long nativeTabModelBase);
+ private native void nativeBroadcastSessionRestoreComplete(long nativeTabModelBase);
+ private native Profile nativeGetProfileAndroid(long nativeTabModelBase);
+ private native void nativeTabAddedToModel(long nativeTabModelBase, Tab tab);
+}

Powered by Google App Engine
This is Rietveld 408576698