1 // Copyright 2018 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;
6 
7 import androidx.annotation.CallSuper;
8 import androidx.annotation.VisibleForTesting;
9 
10 import org.chromium.base.ObserverList;
11 import org.chromium.base.ObserverList.RewindableIterator;
12 import org.chromium.base.supplier.Supplier;
13 import org.chromium.chrome.browser.compositor.layouts.Layout;
14 import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
15 import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
16 import org.chromium.chrome.browser.compositor.layouts.StaticLayout;
17 import org.chromium.chrome.browser.compositor.layouts.phone.SimpleAnimationLayout;
18 import org.chromium.chrome.browser.tab.EmptyTabObserver;
19 import org.chromium.chrome.browser.tab.Tab;
20 import org.chromium.chrome.browser.tab.TabSelectionType;
21 import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
22 import org.chromium.chrome.browser.tabmodel.TabModel;
23 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
24 import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
25 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
26 
27 /**
28  * A class that provides the current {@link Tab} for various states of the browser's activity.
29  */
30 public class ActivityTabProvider implements Supplier<Tab> {
31     /** An interface to track the visible tab for the activity. */
32     public interface ActivityTabObserver {
33         /**
34          * A notification that the activity's tab has changed. This will be triggered whenever a
35          * different tab is selected by the active {@link TabModel} and when that tab is
36          * interactive (i.e. not in a tab switching mode). When switching to toolbar swipe or tab
37          * switcher, this method will be called with {@code null} to indicate that there is no
38          * single activity tab (observers may or may not choose to ignore this event).
39          * @param tab The {@link Tab} that became visible or null if not in {@link StaticLayout}.
40          * @param hint Whether the change event is a hint that a tab change is likely. If true, the
41          *             provided tab may still be frozen and is not yet selected.
42          */
onActivityTabChanged(Tab tab, boolean hint)43         void onActivityTabChanged(Tab tab, boolean hint);
44     }
45 
46     /** An {@link ActivityTabObserver} that can be used to explicitly watch non-hint events. */
47     public abstract static class HintlessActivityTabObserver implements ActivityTabObserver {
48         @Override
onActivityTabChanged(Tab tab, boolean hint)49         public final void onActivityTabChanged(Tab tab, boolean hint) {
50             // Only pass the event through if it isn't a hint.
51             if (!hint) onActivityTabChanged(tab);
52         }
53 
54         /**
55          * A notification that the {@link Tab} in the {@link StaticLayout} has changed.
56          * @param tab The activity's tab.
57          */
onActivityTabChanged(Tab tab)58         public abstract void onActivityTabChanged(Tab tab);
59     }
60 
61     /**
62      * A utility class for observing the activity tab via {@link TabObserver}. When the activity
63      * tab changes, the observer is switched to that tab.
64      */
65     public static class ActivityTabTabObserver extends EmptyTabObserver {
66         /** A handle to the activity tab provider. */
67         private final ActivityTabProvider mTabProvider;
68 
69         /** An observer to watch for a changing activity tab and move this tab observer. */
70         private final ActivityTabObserver mActivityTabObserver;
71 
72         /** The current activity tab. */
73         private Tab mTab;
74 
75         /**
76          * Create a new {@link TabObserver} that only observes the activity tab. It doesn't trigger
77          * for the initial tab being attached to after creation.
78          * @param tabProvider An {@link ActivityTabProvider} to get the activity tab.
79          */
ActivityTabTabObserver(ActivityTabProvider tabProvider)80         public ActivityTabTabObserver(ActivityTabProvider tabProvider) {
81             this(tabProvider, false);
82         }
83 
84         /**
85          * Create a new {@link TabObserver} that only observes the activity tab. This constructor
86          * allows the option of triggering for the initial tab being attached to after creation.
87          * @param tabProvider An {@link ActivityTabProvider} to get the activity tab.
88          * @param shouldTrigger Whether the observer should be triggered for the initial tab after
89          * creation.
90          */
ActivityTabTabObserver(ActivityTabProvider tabProvider, boolean shouldTrigger)91         public ActivityTabTabObserver(ActivityTabProvider tabProvider, boolean shouldTrigger) {
92             mTabProvider = tabProvider;
93             mActivityTabObserver = (tab, hint) -> {
94                 updateObservedTab(tab);
95                 onObservingDifferentTab(tab, hint);
96             };
97             if (shouldTrigger) {
98                 mTabProvider.addObserverAndTrigger(mActivityTabObserver);
99             } else {
100                 mTabProvider.addObserver(mActivityTabObserver);
101             }
102             updateObservedTab(mTabProvider.get());
103         }
104 
105         /**
106          * Update the tab being observed.
107          * @param newTab The new tab to observe.
108          */
updateObservedTab(Tab newTab)109         private void updateObservedTab(Tab newTab) {
110             if (mTab != null) mTab.removeObserver(ActivityTabTabObserver.this);
111             mTab = newTab;
112             if (mTab != null) mTab.addObserver(ActivityTabTabObserver.this);
113         }
114 
115         /**
116          * A notification that the observer has switched to observing a different tab. This can be
117          * called a first time with the {@code hint} parameter set to true, indicating that a new
118          * tab is going to be selected.
119          * @param tab The tab that the observer is now observing. This can be null.
120          * @param hint Whether the change event is a hint that a tab change is likely. If true, the
121          *             provided tab may still be frozen and is not yet selected.
122          */
onObservingDifferentTab(Tab tab, boolean hint)123         protected void onObservingDifferentTab(Tab tab, boolean hint) {}
124 
125         /**
126          * Clean up any state held by this observer.
127          */
128         @CallSuper
destroy()129         public void destroy() {
130             if (mTab != null) {
131                 mTab.removeObserver(this);
132                 mTab = null;
133             }
134             mTabProvider.removeObserver(mActivityTabObserver);
135         }
136     }
137 
138     /** The list of observers to send events to. */
139     private final ObserverList<ActivityTabObserver> mObservers = new ObserverList<>();
140 
141     /**
142      * A single rewindable iterator bound to {@link #mObservers} to prevent constant allocation of
143      * new iterators.
144      */
145     private final RewindableIterator<ActivityTabObserver> mRewindableIterator;
146 
147     /** The {@link Tab} that is considered to be the activity's tab. */
148     private Tab mActivityTab;
149 
150     /** A handle to the {@link LayoutManagerImpl} to get the active layout. */
151     private LayoutManagerImpl mLayoutManager;
152 
153     /** The observer watching scene changes in the {@link LayoutManagerImpl}. */
154     private SceneChangeObserver mSceneChangeObserver;
155 
156     /** A handle to the {@link TabModelSelector}. */
157     private TabModelSelector mTabModelSelector;
158 
159     /** An observer for watching tab creation and switching events. */
160     private TabModelSelectorTabModelObserver mTabModelObserver;
161 
162     /** An observer for watching tab model switching event. */
163     private TabModelSelectorObserver mTabModelSelectorObserver;
164 
165     /** The last tab ID that was hinted. This is reset when the activity tab actually changes. */
166     private int mLastHintedTabId;
167 
168     /**
169      * Default constructor.
170      */
ActivityTabProvider()171     public ActivityTabProvider() {
172         mRewindableIterator = mObservers.rewindableIterator();
173         mSceneChangeObserver = new SceneChangeObserver() {
174             @Override
175             public void onTabSelectionHinted(int tabId) {
176                 if (mTabModelSelector == null || mLastHintedTabId == tabId) return;
177                 Tab tab = mTabModelSelector.getTabById(tabId);
178                 mLastHintedTabId = tabId;
179                 mRewindableIterator.rewind();
180                 while (mRewindableIterator.hasNext()) {
181                     mRewindableIterator.next().onActivityTabChanged(tab, true);
182                 }
183             }
184 
185             @Override
186             public void onSceneChange(Layout layout) {
187                 // The {@link SimpleAnimationLayout} is a special case, the intent is not to switch
188                 // tabs, but to merely run an animation. In this case, do nothing. If the animation
189                 // layout does result in a new tab {@link TabModelObserver#didSelectTab} will
190                 // trigger the event instead. If the tab does not change, the event will no
191                 if (layout instanceof SimpleAnimationLayout) return;
192 
193                 Tab tab = mTabModelSelector.getCurrentTab();
194                 if (!(layout instanceof StaticLayout)) tab = null;
195                 triggerActivityTabChangeEvent(tab);
196             }
197         };
198     }
199 
200     /**
201      * @return The activity's current tab.
202      */
203     @Override
get()204     public Tab get() {
205         return mActivityTab;
206     }
207 
208     /**
209      * @param selector A {@link TabModelSelector} for watching for changes in tabs.
210      */
setTabModelSelector(TabModelSelector selector)211     public void setTabModelSelector(TabModelSelector selector) {
212         assert mTabModelSelector == null;
213         mTabModelSelector = selector;
214         mTabModelObserver = new TabModelSelectorTabModelObserver(mTabModelSelector) {
215             @Override
216             public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
217                 triggerActivityTabChangeEvent(tab);
218             }
219 
220             @Override
221             public void willCloseTab(Tab tab, boolean animate) {
222                 // If this is the last tab to close, make sure a signal is sent to the observers.
223                 if (mTabModelSelector.getCurrentModel().getCount() <= 1) {
224                     triggerActivityTabChangeEvent(null);
225                 }
226             }
227         };
228 
229         mTabModelSelectorObserver = new EmptyTabModelSelectorObserver() {
230             @Override
231             public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
232                 // Send a signal with null tab if a new model has no tab. Other cases
233                 // are taken care of by TabModelSelectorTabModelObserver#didSelectTab.
234                 if (newModel.getCount() == 0) triggerActivityTabChangeEvent(null);
235             }
236         };
237         mTabModelSelector.addObserver(mTabModelSelectorObserver);
238     }
239 
240     /**
241      * @param layoutManager A {@link LayoutManagerImpl} for watching for scene changes.
242      */
setLayoutManager(LayoutManagerImpl layoutManager)243     public void setLayoutManager(LayoutManagerImpl layoutManager) {
244         assert mLayoutManager == null;
245         mLayoutManager = layoutManager;
246         mLayoutManager.addSceneChangeObserver(mSceneChangeObserver);
247     }
248 
249     /**
250      * Check if the interactive tab change event needs to be triggered based on the provided tab.
251      * @param tab The activity's tab.
252      */
triggerActivityTabChangeEvent(Tab tab)253     private void triggerActivityTabChangeEvent(Tab tab) {
254         // Allow the event to trigger before native is ready (before the layout manager is set).
255         if (mLayoutManager != null
256                 && !(mLayoutManager.getActiveLayout() instanceof StaticLayout
257                         || mLayoutManager.getActiveLayout() instanceof SimpleAnimationLayout)
258                 && tab != null) {
259             return;
260         }
261 
262         if (mActivityTab == tab) return;
263         mActivityTab = tab;
264         mLastHintedTabId = Tab.INVALID_TAB_ID;
265 
266         mRewindableIterator.rewind();
267         while (mRewindableIterator.hasNext()) {
268             mRewindableIterator.next().onActivityTabChanged(tab, false);
269         }
270     }
271 
272     /**
273      * Add an observer but do not immediately trigger the event. This should only be used in
274      * extremely specific cases where the observer would trigger an event from the constructor of
275      * the implementing class (see {@link ActivityTabTabObserver}).
276      * @param observer The observer to be added.
277      *
278      * TODO(fgorski): Find a different way to mock this in tests for {@link LoadProgressMediator}.
279      */
280     @VisibleForTesting
281     @Deprecated
addObserver(ActivityTabObserver observer)282     public void addObserver(ActivityTabObserver observer) {
283         mObservers.addObserver(observer);
284     }
285 
286     /**
287      * @param observer The {@link ActivityTabObserver} to add to the activity. This will trigger the
288      *                 {@link ActivityTabObserver#onActivityTabChanged(Tab, boolean)} event to be
289      *                 called on the added observer, providing access to the current tab.
290      */
addObserverAndTrigger(ActivityTabObserver observer)291     public void addObserverAndTrigger(ActivityTabObserver observer) {
292         mObservers.addObserver(observer);
293         observer.onActivityTabChanged(mActivityTab, false);
294     }
295 
296     /**
297      * @param observer The {@link ActivityTabObserver} to remove from the activity.
298      */
removeObserver(ActivityTabObserver observer)299     public void removeObserver(ActivityTabObserver observer) {
300         mObservers.removeObserver(observer);
301     }
302 
303     /** Clean up and detach any observers this object created. */
destroy()304     public void destroy() {
305         mObservers.clear();
306         if (mLayoutManager != null) mLayoutManager.removeSceneChangeObserver(mSceneChangeObserver);
307         mLayoutManager = null;
308         if (mTabModelObserver != null) mTabModelObserver.destroy();
309         if (mTabModelSelectorObserver != null) {
310             mTabModelSelector.removeObserver(mTabModelSelectorObserver);
311             mTabModelSelectorObserver = null;
312         }
313         mTabModelSelector = null;
314     }
315 }
316