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