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