1 // Copyright 2019 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.ui;
6 
7 import androidx.annotation.Nullable;
8 
9 import org.chromium.base.Callback;
10 import org.chromium.base.CallbackController;
11 import org.chromium.base.supplier.ObservableSupplier;
12 import org.chromium.base.supplier.OneshotSupplier;
13 import org.chromium.base.supplier.Supplier;
14 import org.chromium.chrome.browser.ActivityTabProvider;
15 import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabObserver;
16 import org.chromium.chrome.browser.ActivityTabProvider.HintlessActivityTabObserver;
17 import org.chromium.chrome.browser.browser_controls.BrowserControlsVisibilityManager;
18 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
19 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
20 import org.chromium.chrome.browser.fullscreen.FullscreenManager;
21 import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
22 import org.chromium.chrome.browser.lifecycle.Destroyable;
23 import org.chromium.chrome.browser.tab.EmptyTabObserver;
24 import org.chromium.chrome.browser.tab.Tab;
25 import org.chromium.chrome.browser.tab.TabObserver;
26 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
27 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
28 import org.chromium.chrome.browser.vr.VrModuleProvider;
29 import org.chromium.chrome.features.start_surface.StartSurface;
30 import org.chromium.chrome.features.start_surface.StartSurface.StateObserver;
31 import org.chromium.chrome.features.start_surface.StartSurfaceState;
32 import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
33 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
34 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
35 import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
36 import org.chromium.components.browser_ui.bottomsheet.ManagedBottomSheetController;
37 import org.chromium.content_public.browser.SelectionPopupController;
38 import org.chromium.content_public.browser.WebContents;
39 import org.chromium.ui.modaldialog.ModalDialogManager;
40 import org.chromium.ui.util.TokenHolder;
41 import org.chromium.ui.vr.VrModeObserver;
42 
43 /**
44  * A class that manages activity-specific interactions with the BottomSheet component that it
45  * otherwise shouldn't know about.
46  */
47 class BottomSheetManager extends EmptyBottomSheetObserver implements Destroyable {
48     /** A means of accessing the focus state of the omibox. */
49     private final ObservableSupplier<Boolean> mOmniboxFocusStateSupplier;
50 
51     /** An observer of the omnibox that suppresses the sheet when the omnibox is focused. */
52     private final Callback<Boolean> mOmniboxFocusObserver;
53 
54     /** A {@link VrModeObserver} that observers events of entering and exiting VR mode. */
55     private final VrModeObserver mVrModeObserver;
56 
57     /** A listener for fullscreen state changes. */
58     private final FullscreenManager.Observer mFullscreenObserver;
59 
60     /** A listener for browser controls offset changes. */
61     private final BrowserControlsVisibilityManager.Observer mBrowserControlsObserver;
62 
63     /** An observer for the tab provider. */
64     private final ActivityTabObserver mActivityTabObserver;
65 
66     /** A tab observer that is only attached to the active tab. */
67     private final TabObserver mTabObserver;
68 
69     private final CallbackController mCallbackController;
70 
71     /** The supplier of {@link StartSurface} instance. */
72     private final OneshotSupplier<StartSurface> mStartSurfaceSupplier;
73     private StateObserver mStartSurfaceStateObserver;
74 
75     /** A browser controls manager for polling browser controls offsets. */
76     private BrowserControlsVisibilityManager mBrowserControlsVisibilityManager;
77 
78     /** A fullscreen manager for listening to fullscreen events. */
79     private FullscreenManager mFullscreenManager;
80 
81     /** A token for suppressing app modal dialogs. */
82     private int mAppModalToken = TokenHolder.INVALID_TOKEN;
83 
84     /** A token for suppressing tab modal dialogs. */
85     private int mTabModalToken = TokenHolder.INVALID_TOKEN;
86 
87     /**
88      * A handle to the {@link ManagedBottomSheetController} this class manages interactions with.
89      */
90     private ManagedBottomSheetController mSheetController;
91 
92     /** A mechanism for accessing the currently active tab. */
93     private ActivityTabProvider mTabProvider;
94 
95     /** A supplier of the activity's dialog manager. */
96     private Supplier<ModalDialogManager> mDialogManager;
97 
98     /** A supplier of a snackbar manager for the bottom sheet. */
99     private Supplier<SnackbarManager> mSnackbarManager;
100 
101     /** A delegate that provides the functionality of obscuring all tabs. */
102     private TabObscuringHandler mTabObscuringHandler;
103 
104     /** A token held while the bottom sheet is obscuring all visible tabs. */
105     private int mTabObscuringToken;
106 
107     /** The manager for overlay panels to attach listeners to. */
108     private Supplier<OverlayPanelManager> mOverlayPanelManager;
109 
110     /** The last known activity tab, if available. */
111     private Tab mLastActivityTab;
112 
113     /**
114      * Used to track whether the active content has a custom scrim lifecycle. This is kept here
115      * because there are some instances where the active content is changed prior to the close event
116      * being called.
117      */
118     private boolean mContentHasCustomScrimLifecycle;
119 
120     /** The token used to enable browser controls persistence. */
121     private int mPersistentControlsToken;
122 
123     /** A token used to suppress the bottom sheet in Tab switcher. */
124     private int mTabSwitcherToken;
125 
BottomSheetManager(ManagedBottomSheetController controller, ActivityTabProvider tabProvider, BrowserControlsVisibilityManager controlsVisibilityManager, FullscreenManager fullscreenManager, Supplier<ModalDialogManager> dialogManager, Supplier<SnackbarManager> snackbarManagerSupplier, TabObscuringHandler obscuringDelegate, ObservableSupplier<Boolean> omniboxFocusStateSupplier, Supplier<OverlayPanelManager> overlayManager, OneshotSupplier<StartSurface> startSurfaceSupplier)126     public BottomSheetManager(ManagedBottomSheetController controller,
127             ActivityTabProvider tabProvider,
128             BrowserControlsVisibilityManager controlsVisibilityManager,
129             FullscreenManager fullscreenManager, Supplier<ModalDialogManager> dialogManager,
130             Supplier<SnackbarManager> snackbarManagerSupplier,
131             TabObscuringHandler obscuringDelegate,
132             ObservableSupplier<Boolean> omniboxFocusStateSupplier,
133             Supplier<OverlayPanelManager> overlayManager,
134             OneshotSupplier<StartSurface> startSurfaceSupplier) {
135         mSheetController = controller;
136         mTabProvider = tabProvider;
137         mBrowserControlsVisibilityManager = controlsVisibilityManager;
138         mFullscreenManager = fullscreenManager;
139         mDialogManager = dialogManager;
140         mSnackbarManager = snackbarManagerSupplier;
141         mTabObscuringHandler = obscuringDelegate;
142         mTabObscuringToken = TokenHolder.INVALID_TOKEN;
143         mOmniboxFocusStateSupplier = omniboxFocusStateSupplier;
144         mOverlayPanelManager = overlayManager;
145         mStartSurfaceSupplier = startSurfaceSupplier;
146         mCallbackController = new CallbackController();
147         mStartSurfaceSupplier.onAvailable(
148                 mCallbackController.makeCancelable(this::addStartSurfaceStateObserver));
149 
150         mSheetController.addObserver(this);
151         mSheetController.setAccssibilityUtil(ChromeAccessibilityUtil.get());
152 
153         // TODO(1092686): We should wait to instantiate all of these observers until the bottom
154         //                sheet is actually used.
155         mTabObserver = new EmptyTabObserver() {
156             @Override
157             public void onPageLoadStarted(Tab tab, String url) {
158                 controller.clearRequestsAndHide();
159             }
160 
161             @Override
162             public void onCrash(Tab tab) {
163                 controller.clearRequestsAndHide();
164             }
165 
166             @Override
167             public void onDestroyed(Tab tab) {
168                 if (mLastActivityTab != tab) return;
169                 mLastActivityTab = null;
170 
171                 // Remove the suppressed sheet if its lifecycle is tied to the tab being destroyed.
172                 controller.clearRequestsAndHide();
173             }
174         };
175 
176         mActivityTabObserver = new HintlessActivityTabObserver() {
177             @Override
178             public void onActivityTabChanged(Tab tab) {
179                 // Temporarily suppress the sheet if entering a state where there is no activity
180                 // tab and the Start surface homepage isn't showing.
181                 updateSuppressionForTabSwitcher(tab,
182                         mStartSurfaceSupplier.get() == null ? null
183                                                             : mStartSurfaceSupplier.get()
184                                                                       .getController()
185                                                                       .getStartSurfaceState());
186 
187                 if (tab == null) return;
188 
189                 // If refocusing the same tab, simply unsuppress the sheet.
190                 if (mLastActivityTab == tab) return;
191 
192                 // Move the observer to the new activity tab and clear the sheet.
193                 if (mLastActivityTab != null) mLastActivityTab.removeObserver(mTabObserver);
194                 mLastActivityTab = tab;
195                 mLastActivityTab.addObserver(mTabObserver);
196                 controller.clearRequestsAndHide();
197             }
198         };
199         mTabProvider.addObserverAndTrigger(mActivityTabObserver);
200 
201         mVrModeObserver = new VrModeObserver() {
202             /** A token held while this object is suppressing the bottom sheet. */
203             private int mToken;
204 
205             @Override
206             public void onEnterVr() {
207                 mToken = controller.suppressSheet(StateChangeReason.VR);
208             }
209 
210             @Override
211             public void onExitVr() {
212                 controller.unsuppressSheet(mToken);
213             }
214         };
215         VrModuleProvider.registerVrModeObserver(mVrModeObserver);
216 
217         mBrowserControlsObserver = new BrowserControlsVisibilityManager.Observer() {
218             @Override
219             public void onControlsOffsetChanged(int topOffset, int topControlsMinHeightOffset,
220                     int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {
221                 controller.setBrowserControlsHiddenRatio(
222                         mBrowserControlsVisibilityManager.getBrowserControlHiddenRatio());
223             }
224         };
225         mBrowserControlsVisibilityManager.addObserver(mBrowserControlsObserver);
226 
227         mFullscreenObserver = new FullscreenManager.Observer() {
228             /** A token held while this object is suppressing the bottom sheet. */
229             private int mToken;
230 
231             @Override
232             public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
233                 if (mOverlayPanelManager.get() != null
234                         && mOverlayPanelManager.get().getActivePanel() != null) {
235                     // TODO(mdjones): This should only apply to contextual search, but contextual
236                     //                search is the only implementation. Fix this to only apply to
237                     //                contextual search.
238                     mOverlayPanelManager.get().getActivePanel().closePanel(
239                             OverlayPanel.StateChangeReason.UNKNOWN, true);
240                 }
241 
242                 if (mTabProvider.get() != tab) return;
243                 mToken = controller.suppressSheet(StateChangeReason.COMPOSITED_UI);
244             }
245 
246             @Override
247             public void onExitFullscreen(Tab tab) {
248                 if (mTabProvider.get() != tab) return;
249                 controller.unsuppressSheet(mToken);
250             }
251         };
252         mFullscreenManager.addObserver(mFullscreenObserver);
253 
254         mOmniboxFocusObserver = new Callback<Boolean>() {
255             /** A token held while this object is suppressing the bottom sheet. */
256             private int mToken;
257 
258             @Override
259             public void onResult(Boolean focused) {
260                 if (focused) {
261                     mToken = controller.suppressSheet(BottomSheetController.StateChangeReason.NONE);
262                 } else {
263                     controller.unsuppressSheet(mToken);
264                 }
265             }
266         };
267         mOmniboxFocusStateSupplier.addObserver(mOmniboxFocusObserver);
268     }
269 
270     /**
271      * Called by both {@link StateObserver} and {@link HintlessActivityTabObserver} to update the
272      * suppression of the bottom sheet for Tab switcher.
273      * @param tab The current tab. It might be null when the Start surface or the Tab switcher is
274      *            showing.
275      * @param startSurfaceState The current state surface state when the Start surface is enabled,
276      *                          null otherwise.
277      */
updateSuppressionForTabSwitcher( @ullable Tab tab, @Nullable @StartSurfaceState Integer startSurfaceState)278     private void updateSuppressionForTabSwitcher(
279             @Nullable Tab tab, @Nullable @StartSurfaceState Integer startSurfaceState) {
280         if (shouldSuppressForTabSwitcher(tab, startSurfaceState)) {
281             if (mTabSwitcherToken == 0) {
282                 mTabSwitcherToken = mSheetController.suppressSheet(StateChangeReason.COMPOSITED_UI);
283             }
284         } else {
285             mSheetController.unsuppressSheet(mTabSwitcherToken);
286             /**
287              * Reset the token after unsuppression. Without resetting the token, the bottom sheet
288              * won't be suppress again the next time entering Tab switcher. This is because the
289              * bottom sheet is only suppressed in Tab switcher if {@link mTabSwitcherToken} is 0 by
290              * the first observer who notices the event.
291              */
292             mTabSwitcherToken = 0;
293         }
294     }
295 
shouldSuppressForTabSwitcher( Tab tab, @StartSurfaceState Integer startSurfaceState)296     private boolean shouldSuppressForTabSwitcher(
297             Tab tab, @StartSurfaceState Integer startSurfaceState) {
298         StartSurface startSurface = mStartSurfaceSupplier.get();
299         if (tab == null && startSurface == null) return true;
300 
301         /** When the Start surface is enabled, the {@link startSurfaceState} isn't null. */
302         if (startSurfaceState != null) {
303             if (startSurfaceState == StartSurfaceState.SHOWING_HOMEPAGE
304                     || startSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE) {
305                 return false;
306             } else if (startSurfaceState != StartSurfaceState.NOT_SHOWN
307                     && startSurfaceState != StartSurfaceState.DISABLED) {
308                 return true;
309             }
310         }
311 
312         return tab == null;
313     }
314 
addStartSurfaceStateObserver(StartSurface startSurface)315     private void addStartSurfaceStateObserver(StartSurface startSurface) {
316         mStartSurfaceStateObserver = new StateObserver() {
317             private int mStartSurfaceState;
318             @Override
319             public void onStateChanged(
320                     int startSurfaceState, boolean shouldShowTabSwitcherToolbar) {
321                 if (mStartSurfaceState == startSurfaceState) return;
322 
323                 assert startSurfaceState == startSurface.getController().getStartSurfaceState();
324                 mStartSurfaceState = startSurfaceState;
325                 updateSuppressionForTabSwitcher(mTabProvider.get(), startSurfaceState);
326 
327                 if (startSurfaceState == StartSurfaceState.SHOWN_HOMEPAGE) {
328                     mSheetController.clearRequestsAndHide();
329                 }
330             }
331         };
332 
333         startSurface.addStateChangeObserver(mStartSurfaceStateObserver);
334     }
335 
336     @Override
onSheetOpened(int reason)337     public void onSheetOpened(int reason) {
338         if (mBrowserControlsVisibilityManager.getBrowserVisibilityDelegate() != null) {
339             // Browser controls should stay visible until the sheet is closed.
340             mPersistentControlsToken =
341                     mBrowserControlsVisibilityManager.getBrowserVisibilityDelegate()
342                             .showControlsPersistent();
343         }
344 
345         Tab activeTab = mTabProvider.get();
346         if (activeTab != null) {
347             WebContents webContents = activeTab.getWebContents();
348             if (webContents != null) {
349                 SelectionPopupController.fromWebContents(webContents).clearSelection();
350             }
351         }
352 
353         BottomSheetContent content = mSheetController.getCurrentSheetContent();
354         // Content with a custom scrim lifecycle should not obscure the tab. The feature
355         // is responsible for adding itself to the list of obscuring views when applicable.
356         if (content != null && content.hasCustomScrimLifecycle()) {
357             mContentHasCustomScrimLifecycle = true;
358             return;
359         }
360 
361         setIsObscuringAllTabs(true);
362 
363         assert mAppModalToken == TokenHolder.INVALID_TOKEN;
364         assert mTabModalToken == TokenHolder.INVALID_TOKEN;
365         if (mDialogManager.get() != null) {
366             mAppModalToken =
367                     mDialogManager.get().suspendType(ModalDialogManager.ModalDialogType.APP);
368             mTabModalToken =
369                     mDialogManager.get().suspendType(ModalDialogManager.ModalDialogType.TAB);
370         }
371     }
372 
373     @Override
onSheetClosed(int reason)374     public void onSheetClosed(int reason) {
375         if (mBrowserControlsVisibilityManager.getBrowserVisibilityDelegate() != null) {
376             // Update the browser controls since they are permanently shown while the sheet is
377             // open.
378             mBrowserControlsVisibilityManager.getBrowserVisibilityDelegate()
379                     .releasePersistentShowingToken(mPersistentControlsToken);
380         }
381 
382         BottomSheetContent content = mSheetController.getCurrentSheetContent();
383         // If the content has a custom scrim, it wasn't obscuring tabs.
384         if (mContentHasCustomScrimLifecycle) {
385             mContentHasCustomScrimLifecycle = false;
386             return;
387         }
388 
389         setIsObscuringAllTabs(false);
390 
391         // Tokens can be invalid if the sheet has a custom lifecycle.
392         if (mDialogManager.get() != null
393                 && (mAppModalToken != TokenHolder.INVALID_TOKEN
394                         || mTabModalToken != TokenHolder.INVALID_TOKEN)) {
395             // If one modal dialog token is set, the other should be as well.
396             assert mAppModalToken != TokenHolder.INVALID_TOKEN
397                     && mTabModalToken != TokenHolder.INVALID_TOKEN;
398             mDialogManager.get().resumeType(ModalDialogManager.ModalDialogType.APP, mAppModalToken);
399             mDialogManager.get().resumeType(ModalDialogManager.ModalDialogType.TAB, mTabModalToken);
400         }
401         mAppModalToken = TokenHolder.INVALID_TOKEN;
402         mTabModalToken = TokenHolder.INVALID_TOKEN;
403     }
404 
405     /**
406      * Set whether the bottom sheet is obscuring all tabs.
407      * @param isObscuring Whether the bottom sheet is considered to be obscuring.
408      */
setIsObscuringAllTabs(boolean isObscuring)409     private void setIsObscuringAllTabs(boolean isObscuring) {
410         if (isObscuring) {
411             assert mTabObscuringToken == TokenHolder.INVALID_TOKEN;
412             mTabObscuringToken = mTabObscuringHandler.obscureAllTabs();
413         } else {
414             mTabObscuringHandler.unobscureAllTabs(mTabObscuringToken);
415             mTabObscuringToken = TokenHolder.INVALID_TOKEN;
416         }
417     }
418 
419     @Override
onSheetOffsetChanged(float heightFraction, float offsetPx)420     public void onSheetOffsetChanged(float heightFraction, float offsetPx) {
421         if (mSnackbarManager.get() == null) return;
422         mSnackbarManager.get().dismissAllSnackbars();
423     }
424 
425     @Override
destroy()426     public void destroy() {
427         mCallbackController.destroy();
428         if (mLastActivityTab != null) mLastActivityTab.removeObserver(mTabObserver);
429         mTabProvider.removeObserver(mActivityTabObserver);
430         mSheetController.removeObserver(this);
431         mFullscreenManager.removeObserver(mFullscreenObserver);
432         mBrowserControlsVisibilityManager.removeObserver(mBrowserControlsObserver);
433         mOmniboxFocusStateSupplier.removeObserver(mOmniboxFocusObserver);
434         VrModuleProvider.unregisterVrModeObserver(mVrModeObserver);
435         if (mStartSurfaceSupplier.get() != null) {
436             mStartSurfaceSupplier.get().removeStateChangeObserver(mStartSurfaceStateObserver);
437         }
438     }
439 }
440