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