Upstream version 11.40.277.0
[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.ObserverList;
8 import org.chromium.base.TraceEvent;
9 import org.chromium.chrome.browser.Tab;
10 import org.chromium.chrome.browser.util.MathUtils;
11 import org.chromium.content_public.browser.WebContents;
12
13 import java.util.ArrayList;
14 import java.util.List;
15
16 /**
17  * This is the default implementation of the {@link TabModel} interface.
18  */
19 public abstract class TabModelBase extends TabModelJniBridge {
20     private static final String TAG = "TabModelBase";
21
22     /**
23      * The main list of tabs.  Note that when this changes, all pending closures must be committed
24      * via {@link #commitAllTabClosures()} as the indices are no longer valid. Also
25      * {@link RewoundList#resetRewoundState()} must be called so that the full model will be up to
26      * date.
27      */
28     private final List<Tab> mTabs = new ArrayList<Tab>();
29
30     private final TabModelOrderController mOrderController;
31
32     protected final TabModelDelegate mModelDelegate;
33
34     private final ObserverList<TabModelObserver> mObservers;
35
36     // Undo State Tracking -------------------------------------------------------------------------
37
38     /**
39      * A {@link TabList} that represents the complete list of {@link Tab}s. This is so that
40      * certain UI elements can call {@link TabModel#getComprehensiveModel()} to get a full list of
41      * {@link Tab}s that includes rewindable entries, as the typical {@link TabModel} does not
42      * return rewindable entries.
43      */
44     private final RewoundList mRewoundList = new RewoundList();
45
46     /**
47      * This specifies the current {@link Tab} in {@link #mTabs}.
48      */
49     private int mIndex = INVALID_TAB_INDEX;
50
51     public TabModelBase(boolean incognito, TabModelOrderController orderController,
52             TabModelDelegate modelDelegate) {
53         super(incognito);
54         initializeNative();
55         mOrderController = orderController;
56         mModelDelegate = modelDelegate;
57         mObservers = new ObserverList<TabModelObserver>();
58     }
59
60     @Override
61     public void destroy() {
62         for (Tab tab : mTabs) {
63             if (tab.isInitialized()) tab.destroy();
64         }
65
66         mRewoundList.destroy();
67         mTabs.clear();
68         mObservers.clear();
69
70         super.destroy();
71     }
72
73     @Override
74     public void addObserver(TabModelObserver observer) {
75         mObservers.addObserver(observer);
76     }
77
78     @Override
79     public void removeObserver(TabModelObserver observer) {
80         mObservers.removeObserver(observer);
81     }
82
83     /**
84      * Initializes the newly created tab, adds it to controller, and dispatches creation
85      * step notifications.
86      */
87     @Override
88     public void addTab(Tab tab, int index, TabLaunchType type) {
89         TraceEvent.begin();
90
91         for (TabModelObserver obs : mObservers) obs.willAddTab(tab, type);
92
93         boolean selectTab = mOrderController.willOpenInForeground(type, isIncognito());
94
95         index = mOrderController.determineInsertionIndex(type, index, tab);
96         assert index <= mTabs.size();
97
98         assert tab.isIncognito() == isIncognito();
99
100         // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
101         commitAllTabClosures();
102
103         if (index < 0 || index > mTabs.size()) {
104             mTabs.add(tab);
105         } else {
106             mTabs.add(index, tab);
107             if (index <= mIndex) {
108                 mIndex++;
109             }
110         }
111
112         if (!isCurrentModel()) {
113             // When adding new tabs in the background, make sure we set a valid index when the
114             // first one is added.  When in the foreground, calls to setIndex will take care of
115             // this.
116             mIndex = Math.max(mIndex, 0);
117         }
118
119         mRewoundList.resetRewoundState();
120
121         int newIndex = indexOf(tab);
122         mModelDelegate.didChange();
123         mModelDelegate.didCreateNewTab(tab);
124
125         tabAddedToModel(tab);
126
127         for (TabModelObserver obs : mObservers) obs.didAddTab(tab, type);
128
129         if (selectTab) {
130             mModelDelegate.selectModel(isIncognito());
131             setIndex(newIndex, TabModel.TabSelectionType.FROM_NEW);
132         }
133
134         TraceEvent.end();
135     }
136
137     @Override
138     public void moveTab(int id, int newIndex) {
139         newIndex = MathUtils.clamp(newIndex, 0, mTabs.size());
140
141         int curIndex = TabModelUtils.getTabIndexById(this, id);
142
143         if (curIndex == INVALID_TAB_INDEX || curIndex == newIndex || curIndex + 1 == newIndex) {
144             return;
145         }
146
147         // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
148         commitAllTabClosures();
149
150         Tab tab = mTabs.remove(curIndex);
151         if (curIndex < newIndex) --newIndex;
152
153         mTabs.add(newIndex, tab);
154
155         if (curIndex == mIndex) {
156             mIndex = newIndex;
157         } else if (curIndex < mIndex && newIndex >= mIndex) {
158             --mIndex;
159         } else if (curIndex > mIndex && newIndex <= mIndex) {
160             ++mIndex;
161         }
162
163         mRewoundList.resetRewoundState();
164
165         mModelDelegate.didChange();
166         for (TabModelObserver obs : mObservers) obs.didMoveTab(tab, newIndex, curIndex);
167     }
168
169     @Override
170     public boolean closeTab(Tab tab) {
171         return closeTab(tab, true, false, false);
172     }
173
174     private Tab findTabInAllTabModels(int tabId) {
175         Tab tab = TabModelUtils.getTabById(mModelDelegate.getModel(isIncognito()), tabId);
176         if (tab != null) return tab;
177         return TabModelUtils.getTabById(mModelDelegate.getModel(!isIncognito()), tabId);
178     }
179
180     @Override
181     public Tab getNextTabIfClosed(int id) {
182         Tab tabToClose = TabModelUtils.getTabById(this, id);
183         Tab currentTab = TabModelUtils.getCurrentTab(this);
184         if (tabToClose == null) return currentTab;
185
186         int closingTabIndex = indexOf(tabToClose);
187         Tab adjacentTab = getTabAt((closingTabIndex == 0) ? 1 : closingTabIndex - 1);
188         Tab parentTab = findTabInAllTabModels(tabToClose.getParentId());
189
190         // Determine which tab to select next according to these rules:
191         //   * If closing a background tab, keep the current tab selected.
192         //   * Otherwise, if not in overview mode, select the parent tab if it exists.
193         //   * Otherwise, select an adjacent tab if one exists.
194         //   * Otherwise, if closing the last incognito tab, select the current normal tab.
195         //   * Otherwise, select nothing.
196         Tab nextTab = null;
197         if (tabToClose != currentTab && currentTab != null) {
198             nextTab = currentTab;
199         } else if (parentTab != null && !mModelDelegate.isInOverviewMode()) {
200             nextTab = parentTab;
201         } else if (adjacentTab != null) {
202             nextTab = adjacentTab;
203         } else if (isIncognito()) {
204             nextTab = TabModelUtils.getCurrentTab(mModelDelegate.getModel(false));
205         }
206
207         return nextTab;
208     }
209
210     @Override
211     public boolean isClosurePending(int tabId) {
212         return mRewoundList.getPendingRewindTab(tabId) != null;
213     }
214
215     @Override
216     public boolean supportsPendingClosures() {
217         return !isIncognito();
218     }
219
220     @Override
221     public TabList getComprehensiveModel() {
222         if (!supportsPendingClosures()) return this;
223         return mRewoundList;
224     }
225
226     @Override
227     public void cancelTabClosure(int tabId) {
228         Tab tab = mRewoundList.getPendingRewindTab(tabId);
229         if (tab == null) return;
230
231         tab.setClosing(false);
232
233         // Find a valid previous tab entry so we know what tab to insert after.  With the following
234         // example, calling cancelTabClosure(4) would need to know to insert after 2.  So we have to
235         // track across mRewoundTabs and mTabs and see what the last valid mTabs entry was (2) when
236         // we hit the 4 in the rewound list.  An insertIndex of -1 represents the beginning of the
237         // list, as this is the index of tab to insert after.
238         // mTabs:       0   2     5
239         // mRewoundTabs 0 1 2 3 4 5
240         int prevIndex = -1;
241         final int stopIndex = mRewoundList.indexOf(tab);
242         for (int rewoundIndex = 0; rewoundIndex < stopIndex; rewoundIndex++) {
243             Tab rewoundTab = mRewoundList.getTabAt(rewoundIndex);
244             if (prevIndex == mTabs.size() - 1) break;
245             if (rewoundTab == mTabs.get(prevIndex + 1)) prevIndex++;
246         }
247
248         // Figure out where to insert the tab.  Just add one to prevIndex, as -1 represents the
249         // beginning of the list, so we'll insert at 0.
250         int insertIndex = prevIndex + 1;
251         if (mIndex >= insertIndex) mIndex++;
252         mTabs.add(insertIndex, tab);
253
254         boolean activeModel = mModelDelegate.getCurrentModel() == this;
255
256         // If we're the active model call setIndex to actually select this tab, otherwise just set
257         // mIndex but don't kick off everything that happens when calling setIndex().
258         if (activeModel) {
259             TabModelUtils.setIndex(this, insertIndex);
260         } else {
261             mIndex = insertIndex;
262         }
263
264         for (TabModelObserver obs : mObservers) obs.tabClosureUndone(tab);
265     }
266
267     @Override
268     public void commitTabClosure(int tabId) {
269         Tab tab = mRewoundList.getPendingRewindTab(tabId);
270         if (tab == null) return;
271
272         // We're committing the close, actually remove it from the lists and finalize the closing
273         // operation.
274         mRewoundList.removeTab(tab);
275         finalizeTabClosure(tab);
276         for (TabModelObserver obs : mObservers) obs.tabClosureCommitted(tab);
277     }
278
279     @Override
280     public void commitAllTabClosures() {
281         while (mRewoundList.getCount() > mTabs.size()) {
282             commitTabClosure(mRewoundList.getNextRewindableTab().getId());
283         }
284
285         assert !mRewoundList.hasPendingClosures();
286     }
287
288     @Override
289     public boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit, boolean canUndo) {
290         return closeTab(tabToClose, animate, uponExit, canUndo, canUndo);
291     }
292
293     /**
294      * See TabModel.java documentation for description of other parameters.
295      * @param notify Whether or not to notify observers about the pending closure. If this is
296      *               {@code true}, {@link #supportsPendingClosures()} is {@code true},
297      *               and canUndo is {@code true}, observers will be notified of the pending
298      *               closure. Observers will still be notified of a committed/cancelled closure
299      *               even if they are not notified of a pending closure to start with.
300      */
301     private boolean closeTab(Tab tabToClose, boolean animate, boolean uponExit,
302             boolean canUndo, boolean notify) {
303         if (tabToClose == null) {
304             assert false : "Tab is null!";
305             return false;
306         }
307
308         if (!mTabs.contains(tabToClose)) {
309             assert false : "Tried to close a tab from another model!";
310             return false;
311         }
312
313         canUndo &= supportsPendingClosures();
314
315         if (notify && canUndo) {
316             for (TabModelObserver obs : mObservers) obs.tabPendingClosure(tabToClose);
317         }
318         startTabClosure(tabToClose, animate, uponExit, canUndo);
319         if (!canUndo) finalizeTabClosure(tabToClose);
320
321         return true;
322     }
323
324     @Override
325     public void closeAllTabs() {
326         closeAllTabs(true, false);
327     }
328
329     @Override
330     public void closeAllTabs(boolean allowDelegation, boolean uponExit) {
331         commitAllTabClosures();
332
333         while (getCount() > 0) {
334             TabModelUtils.closeTabByIndex(this, 0);
335         }
336     }
337
338     /**
339      * Close all tabs on this model without notifying observers about pending tab closures.
340      *
341      * @param animate true iff the closing animation should be displayed
342      * @param uponExit true iff the tabs are being closed upon application exit (after user presses
343      *                 the system back button)
344      * @param canUndo Whether or not this action can be undone. If this is {@code true} and
345      *                {@link #supportsPendingClosures()} is {@code true}, these {@link Tab}s
346      *                will not actually be closed until {@link #commitTabClosure(int)} or
347      *                {@link #commitAllTabClosures()} is called, but they will be effectively
348      *                removed from this list.
349      * @return a list containing the ids of tabs that have been closed
350      */
351     public ArrayList<Integer> closeAllTabs(boolean animate, boolean uponExit, boolean canUndo) {
352         ArrayList<Integer> closedTabs = new ArrayList<Integer>();
353         while (getCount() > 0) {
354             Tab tab = getTabAt(0);
355             closedTabs.add(tab.getId());
356             closeTab(tab, animate, uponExit, canUndo, false);
357         }
358         return closedTabs;
359     }
360
361     @Override
362     public Tab getTabAt(int index) {
363         // This will catch INVALID_TAB_INDEX and return null
364         if (index < 0 || index >= mTabs.size()) return null;
365         return mTabs.get(index);
366     }
367
368     // Index of the given tab in the order of the tab stack.
369     @Override
370     public int indexOf(Tab tab) {
371         return mTabs.indexOf(tab);
372     }
373
374     /**
375      * @return true if this is the current model according to the model selector
376      */
377     private boolean isCurrentModel() {
378         return mModelDelegate.getCurrentModel() == this;
379     }
380
381     // TODO(aurimas): Move this method to TabModelSelector when notifications move there.
382     private int getLastId(TabSelectionType type) {
383         if (type == TabSelectionType.FROM_CLOSE) return Tab.INVALID_TAB_ID;
384
385         // Get the current tab in the current tab model.
386         Tab currentTab = TabModelUtils.getCurrentTab(mModelDelegate.getCurrentModel());
387         return currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
388     }
389
390     // This function is complex and its behavior depends on persisted state, including mIndex.
391     @Override
392     public void setIndex(int i, final TabSelectionType type) {
393         TraceEvent.begin();
394         int lastId = getLastId(type);
395
396         if (!isCurrentModel()) {
397             mModelDelegate.selectModel(isIncognito());
398         }
399
400         if (mTabs.size() <= 0) {
401             mIndex = INVALID_TAB_INDEX;
402         } else {
403             mIndex = MathUtils.clamp(i, 0, mTabs.size() - 1);
404         }
405
406         Tab tab = TabModelUtils.getCurrentTab(this);
407
408         mModelDelegate.requestToShowTab(tab, type);
409
410         if (tab != null) {
411             for (TabModelObserver obs : mObservers) obs.didSelectTab(tab, type, lastId);
412         }
413
414         // notifyDataSetChanged() can call into
415         // ChromeViewHolderTablet.handleTabChangeExternal(), which will eventually move the
416         // ContentView onto the current view hierarchy (with addView()).
417         mModelDelegate.didChange();
418         TraceEvent.end();
419     }
420
421     /**
422      * Performs the necessary actions to remove this {@link Tab} from this {@link TabModel}.
423      * This does not actually destroy the {@link Tab} (see
424      * {@link #finalizeTabClosure(Tab)}.
425      *
426      * @param tab The {@link Tab} to remove from this {@link TabModel}.
427      * @param animate Whether or not to animate the closing.
428      * @param uponExit Whether or not this is closing while the Activity is exiting.
429      * @param canUndo Whether or not this operation can be undone. Note that if this is {@code true}
430      *                and {@link #supportsPendingClosures()} is {@code true},
431      *                {@link #commitTabClosure(int)} or {@link #commitAllTabClosures()} needs to be
432      *                called to actually delete and clean up {@code tab}.
433      */
434     private void startTabClosure(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
435         final int closingTabId = tab.getId();
436         final int closingTabIndex = indexOf(tab);
437
438         tab.setClosing(true);
439
440         for (TabModelObserver obs : mObservers) obs.willCloseTab(tab, animate);
441
442         Tab currentTab = TabModelUtils.getCurrentTab(this);
443         Tab adjacentTab = getTabAt(closingTabIndex == 0 ? 1 : closingTabIndex - 1);
444         Tab nextTab = getNextTabIfClosed(closingTabId);
445
446         // TODO(dtrainor): Update the list of undoable tabs instead of committing it.
447         if (!canUndo) commitAllTabClosures();
448
449         // Cancel any media currently playing.
450         if (canUndo) {
451             WebContents webContents = tab.getWebContents();
452             if (webContents != null) webContents.releaseMediaPlayers();
453         }
454
455         mTabs.remove(tab);
456
457         boolean nextIsIncognito = nextTab == null ? false : nextTab.isIncognito();
458         int nextTabId = nextTab == null ? Tab.INVALID_TAB_ID : nextTab.getId();
459         int nextTabIndex = nextTab == null ? INVALID_TAB_INDEX : TabModelUtils.getTabIndexById(
460                 mModelDelegate.getModel(nextIsIncognito), nextTabId);
461
462         if (nextTab != currentTab) {
463             if (nextIsIncognito != isIncognito()) mIndex = indexOf(adjacentTab);
464
465             TabModel nextModel = mModelDelegate.getModel(nextIsIncognito);
466             nextModel.setIndex(nextTabIndex,
467                     uponExit ? TabSelectionType.FROM_EXIT : TabSelectionType.FROM_CLOSE);
468         } else {
469             mIndex = nextTabIndex;
470         }
471
472         if (!canUndo) mRewoundList.resetRewoundState();
473     }
474
475     /**
476      * Actually closes and cleans up {@code tab}.
477      * @param tab The {@link Tab} to close.
478      */
479     private void finalizeTabClosure(Tab tab) {
480         for (TabModelObserver obs : mObservers) obs.didCloseTab(tab);
481         tab.destroy();
482     }
483
484     private class RewoundList implements TabList {
485         /**
486          * A list of {@link Tab}s that represents the completely rewound list (if all
487          * rewindable closes were undone). If there are no possible rewindable closes this list
488          * should match {@link #mTabs}.
489          */
490         private final List<Tab> mRewoundTabs = new ArrayList<Tab>();
491
492         @Override
493         public boolean isIncognito() {
494             return TabModelBase.this.isIncognito();
495         }
496
497         /**
498          * If {@link TabModel} has a valid selected tab, this will return that same tab in the
499          * context of the rewound list of tabs.  If {@link TabModel} has no tabs but the rewound
500          * list is not empty, it will return 0, the first tab.  Otherwise it will return
501          * {@link TabModel#INVALID_TAB_INDEX}.
502          * @return The selected index of the rewound list of tabs (includes all pending closures).
503          */
504         @Override
505         public int index() {
506             if (TabModelBase.this.index() != INVALID_TAB_INDEX) {
507                 return mRewoundTabs.indexOf(TabModelUtils.getCurrentTab(TabModelBase.this));
508             }
509             if (!mRewoundTabs.isEmpty()) return 0;
510             return INVALID_TAB_INDEX;
511         }
512
513         @Override
514         public int getCount() {
515             return mRewoundTabs.size();
516         }
517
518         @Override
519         public Tab getTabAt(int index) {
520             if (index < 0 || index >= mRewoundTabs.size()) return null;
521             return mRewoundTabs.get(index);
522         }
523
524         @Override
525         public int indexOf(Tab tab) {
526             return mRewoundTabs.indexOf(tab);
527         }
528
529         @Override
530         public boolean isClosurePending(int tabId) {
531             return TabModelBase.this.isClosurePending(tabId);
532         }
533
534         /**
535          * Resets this list to match the original {@link TabModel}.  Note that if the
536          * {@link TabModel} doesn't support pending closures this model will be empty.  This should
537          * be called whenever {@link #mTabs} changes.
538          */
539         public void resetRewoundState() {
540             mRewoundTabs.clear();
541
542             if (TabModelBase.this.supportsPendingClosures()) {
543                 for (int i = 0; i < TabModelBase.this.getCount(); i++) {
544                     mRewoundTabs.add(TabModelBase.this.getTabAt(i));
545                 }
546             }
547         }
548
549         /**
550          * Finds the {@link Tab} specified by {@code tabId} and only returns it if it is
551          * actually a {@link Tab} that is in the middle of being closed (which means that it
552          * is present in this model but not in {@link #mTabs}.
553          *
554          * @param tabId The id of the {@link Tab} to search for.
555          * @return The {@link Tab} specified by {@code tabId} as long as that tab only exists
556          *         in this model and not in {@link #mTabs}. {@code null} otherwise.
557          */
558         public Tab getPendingRewindTab(int tabId) {
559             if (!TabModelBase.this.supportsPendingClosures()) return null;
560             if (TabModelUtils.getTabById(TabModelBase.this, tabId) != null) return null;
561             return TabModelUtils.getTabById(this, tabId);
562         }
563
564         /**
565          * A utility method for easily finding a {@link Tab} that can be closed.
566          * @return The next tab that is in the middle of being closed.
567          */
568         public Tab getNextRewindableTab() {
569             if (!hasPendingClosures()) return null;
570
571             for (int i = 0; i < mRewoundTabs.size(); i++) {
572                 Tab tab = i < TabModelBase.this.getCount() ? TabModelBase.this.getTabAt(i) : null;
573                 Tab rewoundTab = mRewoundTabs.get(i);
574
575                 if (tab == null || rewoundTab.getId() != tab.getId()) return rewoundTab;
576             }
577
578             return null;
579         }
580
581         /**
582          * Removes a {@link Tab} from this internal list.
583          * @param tab The {@link Tab} to remove.
584          */
585         public void removeTab(Tab tab) {
586             mRewoundTabs.remove(tab);
587         }
588
589         /**
590          * Destroy all tabs in this model.  This will check to see if the tab is already destroyed
591          * before destroying it.
592          */
593         public void destroy() {
594             for (Tab tab : mRewoundTabs) {
595                 if (tab.isInitialized()) tab.destroy();
596             }
597         }
598
599         public boolean hasPendingClosures() {
600             return TabModelBase.this.supportsPendingClosures()
601                     && mRewoundTabs.size() > TabModelBase.this.getCount();
602         }
603     }
604
605     @Override
606     protected boolean closeTabAt(int index) {
607         return closeTab(getTabAt(index));
608     }
609
610     @Override
611     public int getCount() {
612         return mTabs.size();
613     }
614
615     @Override
616     public int index() {
617         return mIndex;
618     }
619
620     @Override
621     protected boolean isSessionRestoreInProgress() {
622         return mModelDelegate.isSessionRestoreInProgress();
623     }
624 }