50dca1d9cd185fe31c01d853fa678077463bb8b5
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / tabmodel / TabModelBase.java
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.
4
5 package org.chromium.chrome.browser.tabmodel;
6
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
14 import java.util.ArrayList;
15 import java.util.List;
16
17 /**
18  * This is the default implementation of the {@link TabModel} interface.
19  */
20 public abstract class TabModelBase implements TabModel {
21     private static final String TAG = "TabModelBase";
22
23     /**
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
27      * date.
28      */
29     private final List<Tab> mTabs = new ArrayList<Tab>();
30
31     private final boolean mIsIncognito;
32
33     private final TabModelOrderController mOrderController;
34
35     protected final TabModelDelegate mModelDelegate;
36
37     private final ObserverList<TabModelObserver> mObservers;
38
39     // Undo State Tracking -------------------------------------------------------------------------
40
41     /**
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.
46      */
47     private final RewoundList mRewoundList = new RewoundList();
48
49     /**
50      * This specifies the current {@link Tab} in {@link #mTabs}.
51      */
52     private int mIndex = INVALID_TAB_INDEX;
53
54     /** Native Tab pointer which will be set by nativeInit(). */
55     private long mNativeTabModelImpl = 0;
56
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>();
64     }
65
66     @Override
67     public Profile getProfile() {
68         return nativeGetProfileAndroid(mNativeTabModelImpl);
69     }
70
71     @Override
72     public boolean isIncognito() {
73         return mIsIncognito;
74     }
75
76     @Override
77     public void destroy() {
78         for (Tab tab : mTabs) {
79             if (tab.isInitialized()) tab.destroy();
80         }
81
82         mRewoundList.destroy();
83
84         if (mNativeTabModelImpl != 0) {
85             nativeDestroy(mNativeTabModelImpl);
86             mNativeTabModelImpl = 0;
87         }
88     }
89
90     @Override
91     public void addObserver(TabModelObserver observer) {
92         mObservers.addObserver(observer);
93     }
94
95     @Override
96     public void removeObserver(TabModelObserver observer) {
97         mObservers.removeObserver(observer);
98     }
99
100     /**
101      * Initializes the newly created tab, adds it to controller, and dispatches creation
102      * step notifications.
103      */
104     @Override
105     public void addTab(Tab tab, int index, TabLaunchType type) {
106         TraceEvent.begin();
107
108         for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
109
110         boolean selectTab = mOrderController.willOpenInForeground(type, mIsIncognito);
111
112         index = mOrderController.determineInsertionIndex(type, index, tab);
113         assert index <= mTabs.size();
114
115         assert tab.isIncognito() == mIsIncognito;
116
117         // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
118         commitAllTabClosures();
119
120         if (index < 0 || index > mTabs.size()) {
121             mTabs.add(tab);
122         } else {
123             mTabs.add(index, tab);
124             if (index <= mIndex) {
125                 mIndex++;
126             }
127         }
128
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
132             // this.
133             mIndex = Math.max(mIndex, 0);
134         }
135
136         mRewoundList.resetRewoundState();
137
138         int newIndex = indexOf(tab);
139         mModelDelegate.didChange();
140         mModelDelegate.didCreateNewTab(tab);
141
142         if (mNativeTabModelImpl != 0) nativeTabAddedToModel(mNativeTabModelImpl, tab);
143
144         for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
145
146         if (selectTab) {
147             mModelDelegate.selectModel(mIsIncognito);
148             setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
149         }
150
151         TraceEvent.end();
152     }
153
154     @Override
155     public void moveTab(int id, int newIndex) {
156         newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
157
158         int curIndex = TabModelUtils.getTabIndexById(this, id);
159
160         if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
161             return;
162         }
163
164         // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
165         commitAllTabClosures();
166
167         Tab tab = mTabs.remove(curIndex);
168         if (curIndex < newIndex) --newIndex;
169
170         mTabs.add(newIndex, tab);
171
172         if (curIndex == mIndex) {
173             mIndex = newIndex;
174         } else if (curIndex < mIndex && newIndex >= mIndex) {
175             --mIndex;
176         } else if (curIndex > mIndex && newIndex <= mIndex) {
177             ++mIndex;
178         }
179
180         mRewoundList.resetRewoundState();
181
182         mModelDelegate.didChange();
183         for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
184     }
185
186     @Override
187     @CalledByNative
188     public boolean closeTab(Tab tab) {
189         return closeTab(tab, true, false, false);
190     }
191
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);
196     }
197
198     @Override
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;
203
204         int closingTabIndex = indexOf(tabToClose);
205         Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
206         Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
207
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.
214         Tab nextTab = null;
215         if (tabToClose != currentTab && currentTab != null) {
216             nextTab = currentTab;
217         } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
218             nextTab = parentTab;
219         } else if (adjacentTab != null) {
220             nextTab = adjacentTab;
221         } else if (mIsIncognito) {
222             nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
223         }
224
225         return nextTab;
226     }
227
228     @Override
229     public boolean isClosurePending(int tabId) {
230         return mRewoundList.getPendingRewindTab(tabId) != null;
231     }
232
233     @Override
234     public boolean supportsPendingClosures() {
235         return !mIsIncognito;
236     }
237
238     @Override
239     public TabList getComprehensiveModel() {
240         if (!supportsPendingClosures()) return this;
241         return mRewoundList;
242     }
243
244     @Override
245     public void cancelTabClosure(int tabId) {
246         Tab tab = mRewoundList.getPendingRewindTab(tabId);
247         if (tab == null) return;
248
249         tab.setClosing(false);
250
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.
256         // mTabs:       0   2     5
257         // mRewoundTabs 0 1 2 3 4 5
258         int prevIndex = -1;
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++;
264         }
265
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);
271
272         boolean activeModel = mModelDelegate.getCurrentModel() == this;
273
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().
276         if (activeModel) {
277             setIndex(insertIndex);
278         } else {
279             mIndex = insertIndex;
280         }
281
282         for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
283     }
284
285     @Override
286     public void commitTabClosure(int tabId) {
287         Tab tab = mRewoundList.getPendingRewindTab(tabId);
288         if (tab == null) return;
289
290         // We're committing the close, actually remove it from the lists and finalize the closing
291         // operation.
292         mRewoundList.removeTab(tab);
293         finalizeTabClosure(tab);
294         for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
295     }
296
297     @Override
298     public void commitAllTabClosures() {
299         while (mRewoundList.getCount() > mTabs.size()) {
300             commitTabClosure(mRewoundList.getNextRewindableTab().getId());
301         }
302
303         assert !mRewoundList.hasPendingClosures();
304     }
305
306     @Override
307     public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
308         if (tabToClose == null) {
309             assert false : "Tab is null!";
310             return false;
311         }
312
313         if (!mTabs.contains(tabToClose)) {
314             assert false : "Tried to close a tab from another model!";
315             return false;
316         }
317
318         canUndo &= supportsPendingClosures();
319
320         if (canUndo) {
321             for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
322         }
323         startTabClosure(tabToClose, animate, uponExit, canUndo);
324         if (!canUndo) finalizeTabClosure(tabToClose);
325
326         return true;
327     }
328
329     @Override
330     public void closeAllTabs() {
331         commitAllTabClosures();
332
333         while (getCount() > 0) {
334             TabModelUtils.closeTabByIndex(this, 0);
335         }
336     }
337
338     @Override
339     @CalledByNative
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);
344     }
345
346     // Index of the given tab in the order of the tab stack.
347     @Override
348     public int indexOf(Tab tab) {
349         return mTabs.indexOf(tab);
350     }
351
352     /**
353      * @return true if this is the current model according to the model selector
354      */
355     private boolean isCurrentModel() {
356         return mModelDelegate.getCurrentModel() == this;
357     }
358
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;
362
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;
366     }
367
368     // This function is complex and its behavior depends on persisted state, including mIndex.
369     @Override
370     public void setIndex(int i, final TabSelectionType type) {
371         TraceEvent.begin();
372         int lastId = getLastId(type);
373
374         if (!isCurrentModel()) {
375             mModelDelegate.selectModel(isIncognito());
376         }
377
378         if (mTabs.size() <= 0) {
379             mIndex = INVALID_TAB_INDEX;
380         } else {
381             mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
382         }
383
384         Tab tab = TabModelUtils.getCurrentTab(this);
385
386         mModelDelegate.requestToShowTab(tab, type);
387
388         if (tab != null) {
389             for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
390         }
391
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();
396         TraceEvent.end();
397     }
398
399     /**
400      * @param incognito
401      * @param nativeWebContents
402      * @param parentId
403      * @return
404      */
405     @CalledByNative
406     protected abstract Tab createTabWithNativeContents(boolean incognito, long nativeWebContents,
407             int parentId);
408
409     /**
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)}.
413      *
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}.
421      */
422     private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
423         final int closingTabId = tab.getId();
424         final int closingTabIndex = indexOf(tab);
425
426         tab.setClosing(true);
427
428         for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
429
430         Tab currentTab = TabModelUtils.getCurrentTab(this);
431         Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
432         Tab nextTab = getNextTabIfClosed(closingTabId);
433
434         // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
435         if (!canUndo) commitAllTabClosures();
436         mTabs.remove(tab);
437
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);
442
443         if (nextTab != currentTab) {
444             if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
445
446             TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
447             nextModel.setIndex(nextTabIndex,
448                     uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
449         } else {
450             mIndex = nextTabIndex;
451         }
452
453         if (!canUndo) mRewoundList.resetRewoundState();
454     }
455
456     /**
457      * Actually closes and cleans up {@code tab}.
458      * @param tab The {@link Tab} to close.
459      */
460     private void finalizeTabClosure(Tab tab) {
461         for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
462         tab.destroy();
463     }
464
465     private class RewoundList implements TabList {
466         /**
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}.
470          */
471         private List<Tab> mRewoundTabs = new ArrayList<Tab>();
472
473         @Override
474         public boolean isIncognito() {
475             return TabModelBase.this.isIncognito();
476         }
477
478         /**
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).
484          */
485         @Override
486         public int index() {
487             if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
488                 return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
489             }
490             if (!mRewoundTabs.isEmpty()) return 0;
491             return INVALID_TAB_INDEX;
492         }
493
494         @Override
495         public int getCount() {
496             return mRewoundTabs.size();
497         }
498
499         @Override
500         public Tab getTabAt(int index) {
501             if (index < 0 || index >= mRewoundTabs.size()) return null;
502             return mRewoundTabs.get(index);
503         }
504
505         @Override
506         public int indexOf(Tab tab) {
507             return mRewoundTabs.indexOf(tab);
508         }
509
510         @Override
511         public boolean isClosurePending(int tabId) {
512             return TabModelBase.this.isClosurePending(tabId);
513         }
514
515         /**
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.
519          */
520         public void resetRewoundState() {
521             mRewoundTabs.clear();
522
523             if (TabModelBase.this.supportsPendingClosures()) {
524                 for (int i = 0; i < TabModelBase.this.getCount(); i++) {
525                     mRewoundTabs.add(TabModelBase.this.getTabAt(i));
526                 }
527             }
528         }
529
530         /**
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}.
534          *
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.
538          */
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);
543         }
544
545         /**
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.
548          */
549         public Tab getNextRewindableTab() {
550             if (!hasPendingClosures()) return null;
551
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);
555
556                 if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
557             }
558
559             return null;
560         }
561
562         /**
563          * Removes a {@link Tab} from this internal list.
564          * @param tab The {@link Tab} to remove.
565          */
566         public void removeTab(Tab tab) {
567             mRewoundTabs.remove(tab);
568         }
569
570         /**
571          * Destroy all tabs in this model.  This will check to see if the tab is already destroyed
572          * before destroying it.
573          */
574         public void destroy() {
575             for (Tab tab : mRewoundTabs) {
576                 if (tab.isInitialized()) tab.destroy();
577             }
578         }
579
580         public boolean hasPendingClosures() {
581             return TabModelBase.this.supportsPendingClosures()
582                     && mRewoundTabs.size() > TabModelBase.this.getCount();
583         }
584     }
585
586     /**
587      * Broadcast a notification (in native code) that all tabs are now loaded from storage.
588      */
589     public void broadcastSessionRestoreComplete() {
590         nativeBroadcastSessionRestoreComplete(mNativeTabModelImpl);
591     }
592
593     // JNI related methods -------------------------------------------------------------------------
594
595     @Override
596     @CalledByNative
597     public int getCount() {
598         return mTabs.size();
599     }
600
601     @Override
602     @CalledByNative
603     public int index() {
604         return mIndex;
605     }
606
607     @SuppressWarnings("unused")
608     @CalledByNative
609     private void setIndex(int index) {
610         TabModelUtils.setIndex(this, index);
611     }
612
613     /**
614      * Used by Developer Tools to create a new tab with a given URL.
615      *
616      * @param url The URL to open.
617      * @return The new tab.
618      */
619     @CalledByNative
620     protected abstract Tab createNewTabForDevTools(String url);
621
622     @CalledByNative
623     private boolean isSessionRestoreInProgress() {
624         return mModelDelegate.isSessionRestoreInProgress();
625     }
626
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);
632 }