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