Commit e318bb92 authored by Matt Jones's avatar Matt Jones Committed by Commit Bot

Split out a public and private interface for BottomSheetController

This patch splits the BottomSheetController into 3 pieces:
  - BottomSheetController: This interface is intended to be the public
    interface for anyone trying to use the bottom sheet and should
    freely be passed around in features.
  - BottomSheetControllerInternal: This interface specifies methods
    that are necessary for "glue" code to effectively manage the
    sheet. When the sheet has its own component, the only code allowed
    to depend on it will be glue.
  - BottomSheetControllerImpl: This is the implementation of the above
    interfaces and will eventually be private (or just internal to the
    bottom sheet widget).

This patch also replaces most of the test-exclusive methods using
various alternatives in tests outside of the bottom sheet's package.

Bug: 1002277
Change-Id: Ic84ae52fab3554ef38d84ee32734340c224d8872
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2225845
Commit-Queue: Matthew Jones <mdjones@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#775278}
parent 826af0ee
......@@ -1782,8 +1782,11 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/widget/ScrimView.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheet.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetController.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetControllerImpl.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetControllerInternal.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetObserver.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetSwipeDetector.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetTestSupport.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/EmptyBottomSheetObserver.java",
"java/src/org/chromium/chrome/browser/widget/bottomsheet/TouchRestrictingFrameLayout.java",
]
......@@ -180,13 +180,11 @@ public class AssistantOnboardingCoordinatorTest {
coordinator.disableAnimationForTesting();
showOnboardingAndWait(coordinator, mCallback);
TextView termsView = mBottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.onboarding_subtitle);
TextView termsView = mActivity.findViewById(R.id.onboarding_subtitle);
assertEquals(
mActivity.getResources().getText(R.string.autofill_assistant_init_message_short),
termsView.getText());
TextView titleView = mBottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.onboarding_try_assistant);
TextView titleView = mActivity.findViewById(R.id.onboarding_try_assistant);
assertEquals(
mActivity.getResources().getText(R.string.autofill_assistant_init_message_rent_car),
titleView.getText());
......@@ -205,13 +203,11 @@ public class AssistantOnboardingCoordinatorTest {
coordinator.disableAnimationForTesting();
showOnboardingAndWait(coordinator, mCallback);
TextView termsView = mBottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.onboarding_subtitle);
TextView termsView = mActivity.findViewById(R.id.onboarding_subtitle);
assertEquals(
mActivity.getResources().getText(R.string.autofill_assistant_init_message_short),
termsView.getText());
TextView titleView = mBottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.onboarding_try_assistant);
TextView titleView = mActivity.findViewById(R.id.onboarding_try_assistant);
assertEquals(mActivity.getResources().getText(
R.string.autofill_assistant_init_message_buy_movie_tickets),
titleView.getText());
......@@ -229,13 +225,11 @@ public class AssistantOnboardingCoordinatorTest {
coordinator.disableAnimationForTesting();
showOnboardingAndWait(coordinator, mCallback);
TextView termsView = mBottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.onboarding_subtitle);
TextView termsView = mActivity.findViewById(R.id.onboarding_subtitle);
assertEquals(View.VISIBLE, termsView.getVisibility());
assertEquals(mActivity.getResources().getText(R.string.autofill_assistant_init_message),
termsView.getText());
TextView titleView = mBottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.onboarding_try_assistant);
TextView titleView = mActivity.findViewById(R.id.onboarding_try_assistant);
assertEquals(mActivity.getResources().getText(R.string.autofill_assistant_init_title),
titleView.getText());
}
......
......@@ -132,9 +132,10 @@ public class AutofillAssistantUiTest {
/* bottomSheetDelegate= */ null));
// Bottom sheet is shown in the BottomSheet when creating the AssistantCoordinator.
ViewGroup bottomSheetContent =
bottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.autofill_assistant);
View contentView = AutofillAssistantUiTestUtil.getBottomSheetController(getActivity())
.getCurrentSheetContent()
.getContentView();
ViewGroup bottomSheetContent = contentView.findViewById(R.id.autofill_assistant);
Assert.assertNotNull(bottomSheetContent);
// Disable bottom sheet content animations. This is a workaround for http://crbug/943483.
......@@ -258,9 +259,10 @@ public class AutofillAssistantUiTest {
/* bottomSheetDelegate= */ null));
// Bottom sheet is shown in the BottomSheet when creating the AssistantCoordinator.
ViewGroup bottomSheetContent =
bottomSheetController.getBottomSheetViewForTesting().findViewById(
R.id.autofill_assistant);
View contentView = AutofillAssistantUiTestUtil.getBottomSheetController(getActivity())
.getCurrentSheetContent()
.getContentView();
ViewGroup bottomSheetContent = contentView.findViewById(R.id.autofill_assistant);
Assert.assertNotNull(bottomSheetContent);
// Disable bottom sheet content animations. This is a workaround for http://crbug/943483.
......
......@@ -1825,7 +1825,8 @@ public class ChromeTabbedActivity extends ChromeActivity<ChromeActivityComponent
return true;
}
if (getBottomSheetController().handleBackPress()) return true;
// TODO(1091411): Find a better mechanism for back-press handling for features.
if (mRootUiCoordinator.getBottomSheetController().handleBackPress()) return true;
if (mTabModalHandler.handleBackPress()) return true;
......
......@@ -23,7 +23,6 @@ import org.chromium.chrome.R;
import org.chromium.chrome.browser.thinwebview.ThinWebView;
import org.chromium.chrome.browser.thinwebview.ThinWebViewConstraints;
import org.chromium.chrome.browser.thinwebview.ThinWebViewFactory;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.widget.FadingShadow;
import org.chromium.components.browser_ui.widget.FadingShadowView;
......@@ -39,6 +38,12 @@ import org.chromium.url.GURL;
* Represents ephemeral tab content and the toolbar, which can be included inside the bottom sheet.
*/
public class EphemeralTabSheetContent implements BottomSheetContent {
/**
* The base duration of the settling animation of the sheet. 218 ms is a spec for material
* design (this is the minimum time a user is guaranteed to pay attention to something).
*/
private static final int BASE_ANIMATION_DURATION_MS = 218;
private static final float PEEK_TOOLBAR_HEIGHT_MULTIPLE = 2.f;
private final Context mContext;
......@@ -159,7 +164,7 @@ public class EphemeralTabSheetContent implements BottomSheetContent {
TransitionDrawable transitionDrawable = ApiCompatibilityUtils.createTransitionDrawable(
new Drawable[] {mCurrentFavicon, favicon});
transitionDrawable.setCrossFadeEnabled(true);
transitionDrawable.startTransition(BottomSheetController.BASE_ANIMATION_DURATION_MS);
transitionDrawable.startTransition(BASE_ANIMATION_DURATION_MS);
presentedDrawable = transitionDrawable;
}
......
......@@ -17,6 +17,7 @@ import org.chromium.chrome.browser.ui.TabObscuringHandler;
import org.chromium.chrome.browser.widget.ScrimView;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController.SheetState;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetControllerImpl;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.content_public.browser.NavigationController;
......@@ -137,9 +138,12 @@ import org.chromium.ui.util.TokenHolder;
ChromeActivity activity = ChromeActivity.fromWebContents(mWebContentsRef);
assert activity != null;
ScrimView.ScrimParams params = activity.getBottomSheetController().createScrimParams(
new ScrimView.EmptyScrimObserver());
ScrimView scrim = activity.getScrim();
// TODO(1002277): Use the proper scrim API when available.
BottomSheetControllerImpl controller =
(BottomSheetControllerImpl) activity.getBottomSheetController();
ScrimView.ScrimParams params =
controller.createScrimParams(new ScrimView.EmptyScrimObserver());
ScrimView scrim = ChromeActivity.fromWebContents(mWebContentsRef).getScrim();
scrim.showScrim(params);
scrim.setViewAlpha(0);
......
......@@ -65,6 +65,8 @@ import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.chrome.browser.widget.ScrimView;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController.SheetState;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetControllerImpl;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetControllerInternal;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetObserver;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
......@@ -124,7 +126,7 @@ public class RootUiCoordinator
private VrModeObserver mVrModeObserver;
private BottomSheetManager mBottomSheetManager;
private BottomSheetController mBottomSheetController;
private BottomSheetControllerImpl mBottomSheetController;
private SnackbarManager mBottomSheetSnackbarManager;
private ScrimView mScrimView;
......@@ -629,12 +631,10 @@ public class RootUiCoordinator
Supplier<OverlayPanelManager> panelManagerSupplier = ()
-> mActivity.getCompositorViewHolder().getLayoutManager().getOverlayPanelManager();
mBottomSheetController =
new BottomSheetController(mActivity.getLifecycleDispatcher(), mActivityTabProvider,
mBottomSheetController = new BottomSheetControllerImpl(mActivityTabProvider,
() -> mScrimCoordinator, sheetViewSupplier, panelManagerSupplier,
mActivity.getFullscreenManager(), mActivity.getWindow(),
mActivity.getWindowAndroid().getKeyboardDelegate(),
mOmniboxFocusStateSupplier);
mActivity.getWindowAndroid().getKeyboardDelegate(), mOmniboxFocusStateSupplier);
mBottomSheetManager = new BottomSheetManager(mBottomSheetController, mActivityTabProvider,
mActivity::getModalDialogManager, this::getBottomSheetSnackbarManager,
......@@ -650,7 +650,7 @@ public class RootUiCoordinator
}
/** @return The {@link BottomSheetController} for this activity. */
public BottomSheetController getBottomSheetController() {
public BottomSheetControllerInternal getBottomSheetController() {
return mBottomSheetController;
}
......
......@@ -45,6 +45,12 @@ import org.chromium.ui.KeyboardVisibilityDelegate;
*/
class BottomSheet extends FrameLayout
implements BottomSheetSwipeDetector.SwipeableBottomSheet, View.OnLayoutChangeListener {
/**
* The base duration of the settling animation of the sheet. 218 ms is a spec for material
* design (this is the minimum time a user is guaranteed to pay attention to something).
*/
private static final int BASE_ANIMATION_DURATION_MS = 218;
/**
* The fraction of the way to the next state the sheet must be swiped to animate there when
* released. This is the value used when there are 3 active states. A smaller value here means
......@@ -559,7 +565,7 @@ class BottomSheet extends FrameLayout
mTargetState = targetState;
mSettleAnimator =
ValueAnimator.ofFloat(getCurrentOffsetPx(), getSheetHeightForState(targetState));
mSettleAnimator.setDuration(BottomSheetController.BASE_ANIMATION_DURATION_MS);
mSettleAnimator.setDuration(BASE_ANIMATION_DURATION_MS);
mSettleAnimator.setInterpolator(mInterpolator);
// When the animation is canceled or ends, reset the handle to null.
......@@ -663,16 +669,6 @@ class BottomSheet extends FrameLayout
}
}
/**
* This is the same as {@link #setSheetOffsetFromBottom(float, int)} but exclusively for
* testing.
* @param offset The offset to set the sheet to.
*/
@VisibleForTesting
public void setSheetOffsetFromBottomForTesting(float offset) {
setSheetOffsetFromBottom(offset, StateChangeReason.NONE);
}
/**
* @return The ratio of the height of the screen that the hidden state is.
*/
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.widget.bottomsheet;
import android.view.View;
import android.view.Window;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.HintlessActivityTabObserver;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
import org.chromium.chrome.browser.fullscreen.BrowserControlsStateProvider;
import org.chromium.chrome.browser.fullscreen.BrowserControlsStateProvider.Observer;
import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager;
import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.Destroyable;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.chrome.browser.widget.ScrimView.ScrimObserver;
import org.chromium.chrome.browser.widget.ScrimView.ScrimParams;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.components.browser_ui.widget.scrim.ScrimProperties;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.vr.VrModeObserver;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
/**
* This class is responsible for managing the content shown by the {@link BottomSheet}. Features
* wishing to show content in the {@link BottomSheet} UI must implement {@link BottomSheetContent}
* and call {@link #requestShowContent(BottomSheetContent, boolean)} which will return true if the
* content was actually shown (see full doc on method).
*/
public class BottomSheetController implements Destroyable {
/**
* The base duration of the settling animation of the sheet. 218 ms is a spec for material
* design (this is the minimum time a user is guaranteed to pay attention to something).
* The public interface for the bottom sheet's controller. Features wishing to show content in the
* sheet UI must implement {@link BottomSheetContent} and call
* {@link #requestShowContent(BottomSheetContent, boolean)} which will return true if the content
* was actually shown (see full doc on method).
*/
public static final int BASE_ANIMATION_DURATION_MS = 218;
public interface BottomSheetController {
/** The different states that the bottom sheet can have. */
@IntDef({SheetState.NONE, SheetState.HIDDEN, SheetState.PEEK, SheetState.HALF, SheetState.FULL,
SheetState.SCROLLING})
@Retention(RetentionPolicy.SOURCE)
public @interface SheetState {
@interface SheetState {
/**
* NONE is for internal use only and indicates the sheet is not currently
* transitioning between states.
......@@ -89,7 +51,7 @@ public class BottomSheetController implements Destroyable {
StateChangeReason.COMPOSITED_UI, StateChangeReason.VR, StateChangeReason.PROMOTE_TAB,
StateChangeReason.OMNIBOX_FOCUS, StateChangeReason.MAX_VALUE})
@Retention(RetentionPolicy.SOURCE)
public @interface StateChangeReason {
@interface StateChangeReason {
int NONE = 0;
int SWIPE = 1;
int BACK_PRESS = 2;
......@@ -102,513 +64,6 @@ public class BottomSheetController implements Destroyable {
int MAX_VALUE = OMNIBOX_FOCUS;
}
/** The initial capacity for the priority queue handling pending content show requests. */
private static final int INITIAL_QUEUE_CAPACITY = 1;
/** A {@link VrModeObserver} that observers events of entering and exiting VR mode. */
private final VrModeObserver mVrModeObserver;
/** A listener for browser controls offset changes. */
private final BrowserControlsStateProvider.Observer mBrowserControlsObserver;
/** A listener for fullscreen events. */
private final ChromeFullscreenManager.FullscreenListener mFullscreenListener;
/** A means of accessing the focus state of the omibox. */
private final ObservableSupplier<Boolean> mOmniboxFocusStateSupplier;
/** An observer of the omnibox that suppresses the sheet when the omnibox is focused. */
private final Callback<Boolean> mOmniboxFocusObserver;
/** The height of the shadow that sits above the toolbar. */
private int mToolbarShadowHeight;
/** The offset of the toolbar shadow from the top that remains empty. */
private int mShadowTopOffset;
/** A handle to the {@link BottomSheet} that this class controls. */
private BottomSheet mBottomSheet;
/** A queue for content that is waiting to be shown in the {@link BottomSheet}. */
private PriorityQueue<BottomSheetContent> mContentQueue;
/** Whether the controller is already processing a hide request for the tab. */
private boolean mIsProcessingHideRequest;
/** Whether the bottom sheet is temporarily suppressed. */
private boolean mIsSuppressed;
/** The manager for overlay panels to attach listeners to. */
private Supplier<OverlayPanelManager> mOverlayPanelManager;
/** A means for getting the activity's current tab and observing change events. */
private ActivityTabProvider mTabProvider;
/** A browser controls manager for polling browser controls offsets. */
private BrowserControlsStateProvider mBrowserControlsStateProvider;
/** A fullscreen manager for listening to fullscreen events. */
private ChromeFullscreenManager mFullscreenManager;
/** The last known activity tab, if available. */
private Tab mLastActivityTab;
/** A runnable that initializes the bottom sheet when necessary. */
private Runnable mSheetInitializer;
/**
* A list of observers maintained by this controller until the bottom sheet is created, at which
* point they will be added to the bottom sheet.
*/
private List<BottomSheetObserver> mPendingSheetObservers;
/** The state of the sheet so it can be returned to what it was prior to suppression. */
@SheetState
private int mSheetStateBeforeSuppress;
/** The content being shown prior to the sheet being suppressed. */
private BottomSheetContent mContentWhenSuppressed;
/**
* Build a new controller of the bottom sheet.
* @param lifecycleDispatcher The {@link ActivityLifecycleDispatcher} for the {@code activity}.
* @param activityTabProvider The provider of the activity's current tab.
* @param scrim A supplier of the scrim that shows when the bottom sheet is opened.
* @param bottomSheetViewSupplier A mechanism for creating a {@link BottomSheet}.
* @param overlayManager A supplier of the manager for overlay panels to attach listeners to.
* This is a supplier to get around wating for native to be initialized.
* @param fullscreenManager A fullscreen manager for access to browser controls offsets.
* @param omniboxFocusStateSupplier A means of accessing the focused state of the omnibox.
*/
public BottomSheetController(final ActivityLifecycleDispatcher lifecycleDispatcher,
final ActivityTabProvider activityTabProvider, final Supplier<ScrimCoordinator> scrim,
Supplier<View> bottomSheetViewSupplier, Supplier<OverlayPanelManager> overlayManager,
ChromeFullscreenManager fullscreenManager, Window window,
KeyboardVisibilityDelegate keyboardDelegate,
ObservableSupplier<Boolean> omniboxFocusStateSupplier) {
mTabProvider = activityTabProvider;
mOverlayPanelManager = overlayManager;
mBrowserControlsStateProvider = fullscreenManager;
mFullscreenManager = fullscreenManager;
mPendingSheetObservers = new ArrayList<>();
mPendingSheetObservers.add(new EmptyBottomSheetObserver() {
/** The token used to enable browser controls persistence. */
private int mPersistentControlsToken;
@Override
public void onSheetOpened(int reason) {
if (mFullscreenManager.getBrowserVisibilityDelegate() == null) return;
// Browser controls should stay visible until the sheet is closed.
mPersistentControlsToken =
mFullscreenManager.getBrowserVisibilityDelegate().showControlsPersistent();
}
@Override
public void onSheetClosed(int reason) {
if (mFullscreenManager.getBrowserVisibilityDelegate() == null) return;
// Update the browser controls since they are permanently shown while the sheet is
// open.
mFullscreenManager.getBrowserVisibilityDelegate().releasePersistentShowingToken(
mPersistentControlsToken);
}
});
mVrModeObserver = new VrModeObserver() {
@Override
public void onEnterVr() {
suppressSheet(StateChangeReason.VR);
}
@Override
public void onExitVr() {
unsuppressSheet();
}
};
mBrowserControlsObserver = new Observer() {
@Override
public void onControlsOffsetChanged(int topOffset, int topControlsMinHeightOffset,
int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {
if (mBottomSheet != null) {
mBottomSheet.setBrowserControlsHiddenRatio(
mBrowserControlsStateProvider.getBrowserControlHiddenRatio());
}
}
};
mBrowserControlsStateProvider.addObserver(mBrowserControlsObserver);
mFullscreenListener = new ChromeFullscreenManager.FullscreenListener() {
@Override
public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
if (mBottomSheet == null || mTabProvider.get() != tab) return;
suppressSheet(StateChangeReason.COMPOSITED_UI);
}
@Override
public void onExitFullscreen(Tab tab) {
if (mBottomSheet == null || mTabProvider.get() != tab) return;
unsuppressSheet();
}
};
mFullscreenManager.addListener(mFullscreenListener);
mOmniboxFocusStateSupplier = omniboxFocusStateSupplier;
mOmniboxFocusObserver = (focused) -> {
if (focused) {
suppressSheet(StateChangeReason.NONE);
} else {
unsuppressSheet();
}
};
mSheetInitializer = () -> {
initializeSheet(
lifecycleDispatcher, scrim, bottomSheetViewSupplier, window, keyboardDelegate);
};
}
/**
* Do the actual initialization of the bottom sheet.
* @param lifecycleDispatcher A means of binding to the activity's lifecycle.
* @param scrim The scrim to show behind the sheet.
* @param bottomSheetViewSupplier A means of creating the bottom sheet.
*/
private void initializeSheet(final ActivityLifecycleDispatcher lifecycleDispatcher,
final Supplier<ScrimCoordinator> scrim, Supplier<View> bottomSheetViewSupplier,
Window window, KeyboardVisibilityDelegate keyboardDelegate) {
mBottomSheet = (BottomSheet) bottomSheetViewSupplier.get();
mBottomSheet.init(window, keyboardDelegate);
mToolbarShadowHeight = mBottomSheet.getResources().getDimensionPixelOffset(
BottomSheet.getTopShadowResourceId());
mShadowTopOffset = mBottomSheet.getResources().getDimensionPixelOffset(
BottomSheet.getShadowTopOffsetResourceId());
mOmniboxFocusStateSupplier.addObserver(mOmniboxFocusObserver);
// Initialize the queue with a comparator that checks content priority.
mContentQueue = new PriorityQueue<>(INITIAL_QUEUE_CAPACITY,
(content1, content2) -> content1.getPriority() - content2.getPriority());
lifecycleDispatcher.register(this);
VrModuleProvider.registerVrModeObserver(mVrModeObserver);
final TabObserver tabObserver = new EmptyTabObserver() {
@Override
public void onPageLoadStarted(Tab tab, String url) {
clearRequestsAndHide();
}
@Override
public void onCrash(Tab tab) {
clearRequestsAndHide();
}
@Override
public void onDestroyed(Tab tab) {
if (mLastActivityTab != tab) return;
mLastActivityTab = null;
// Remove the suppressed sheet if its lifecycle is tied to the tab being
// destroyed.
clearRequestsAndHide();
}
};
mTabProvider.addObserverAndTrigger(new HintlessActivityTabObserver() {
@Override
public void onActivityTabChanged(Tab tab) {
// Temporarily suppress the sheet if entering a state where there is no activity
// tab.
if (tab == null) {
suppressSheet(StateChangeReason.COMPOSITED_UI);
return;
}
// If refocusing the same tab, simply unsuppress the sheet.
if (mLastActivityTab == tab) {
unsuppressSheet();
return;
}
// Move the observer to the new activity tab and clear the sheet.
if (mLastActivityTab != null) mLastActivityTab.removeObserver(tabObserver);
mLastActivityTab = tab;
mLastActivityTab.addObserver(tabObserver);
clearRequestsAndHide();
}
});
PropertyModel scrimProperties =
new PropertyModel.Builder(ScrimProperties.REQUIRED_KEYS)
.with(ScrimProperties.TOP_MARGIN, 0)
.with(ScrimProperties.AFFECTS_STATUS_BAR, true)
.with(ScrimProperties.ANCHOR_VIEW, mBottomSheet)
.with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
.with(ScrimProperties.CLICK_DELEGATE,
() -> {
if (!mBottomSheet.isSheetOpen()) return;
mBottomSheet.setSheetState(
mBottomSheet.getMinSwipableSheetState(), true,
StateChangeReason.TAP_SCRIM);
})
.build();
mBottomSheet.addObserver(new EmptyBottomSheetObserver() {
/**
* Whether the scrim was shown for the last content.
* TODO(mdjones): We should try to make sure the content in the sheet is not nulled
* prior to the close event occurring; sheets that don't have a peek
* state make this difficult since the sheet needs to be hidden before it
* is closed.
*/
private boolean mScrimShown;
@Override
public void onSheetOpened(@StateChangeReason int reason) {
if (mBottomSheet.getCurrentSheetContent() != null
&& mBottomSheet.getCurrentSheetContent().hasCustomScrimLifecycle()) {
return;
}
scrim.get().showScrim(scrimProperties);
mScrimShown = true;
}
@Override
public void onSheetClosed(@StateChangeReason int reason) {
// Hide the scrim if the current content doesn't have a custom scrim lifecycle.
if (mScrimShown) {
scrim.get().hideScrim(true);
mScrimShown = false;
}
// Try to swap contents unless the sheet's content has a custom lifecycle.
if (mBottomSheet.getCurrentSheetContent() != null
&& !mBottomSheet.getCurrentSheetContent().hasCustomLifecycle()) {
// If the sheet is closed, it is an opportunity for another content to try to
// take its place if it is a higher priority.
BottomSheetContent content = mBottomSheet.getCurrentSheetContent();
BottomSheetContent nextContent = mContentQueue.peek();
if (content != null && nextContent != null
&& nextContent.getPriority() < content.getPriority()) {
mContentQueue.add(content);
mBottomSheet.setSheetState(SheetState.HIDDEN, true);
}
}
}
@Override
public void onSheetStateChanged(@SheetState int state) {
// If hiding request is in progress, destroy the current sheet content being hidden
// even when it is in suppressed state. See https://crbug.com/1057966.
if (state != SheetState.HIDDEN || (!mIsProcessingHideRequest && mIsSuppressed)) {
return;
}
if (mBottomSheet.getCurrentSheetContent() != null) {
mBottomSheet.getCurrentSheetContent().destroy();
}
mIsProcessingHideRequest = false;
showNextContent(true);
}
});
// Add any of the pending observers that were added prior to the sheet being created.
for (int i = 0; i < mPendingSheetObservers.size(); i++) {
mBottomSheet.addObserver(mPendingSheetObservers.get(i));
}
mPendingSheetObservers.clear();
mSheetInitializer = null;
}
/**
* Create a ScrimParams anchoring on the bottom-sheet view.
* @param scrimObserver The scrimObserver to set for the ScrimParams.
*/
public ScrimParams createScrimParams(ScrimObserver scrimObserver) {
return new ScrimParams(/*anchorView=*/mBottomSheet, /*showInFrontOfAnchorView=*/false,
/*affectsStatusBar=*/true, /*topMargin=*/0, /*observer*/ scrimObserver);
}
// Destroyable implementation.
@Override
public void destroy() {
VrModuleProvider.unregisterVrModeObserver(mVrModeObserver);
mFullscreenManager.removeListener(mFullscreenListener);
mBrowserControlsStateProvider.removeObserver(mBrowserControlsObserver);
mOmniboxFocusStateSupplier.removeObserver(mOmniboxFocusObserver);
if (mBottomSheet != null) mBottomSheet.destroy();
}
/**
* Handle a back press event. By default this will return the bottom sheet to it's minimum /
* peeking state if it is open. However, the sheet's content has the opportunity to intercept
* this event and block the default behavior {@see BottomSheetContent#handleBackPress()}.
* @return {@code true} if the sheet or content handled the back press.
*/
public boolean handleBackPress() {
// If suppressed (therefore invisible), users are likely to expect for Chrome
// browser, not the bottom sheet, to react. Do not consume the event.
if (mBottomSheet == null || mIsSuppressed) return false;
// Give the sheet the opportunity to handle the back press itself before falling to the
// default "close" behavior.
if (getCurrentSheetContent() != null && getCurrentSheetContent().handleBackPress()) {
return true;
}
if (!mBottomSheet.isSheetOpen()) return false;
int sheetState = mBottomSheet.getMinSwipableSheetState();
mBottomSheet.setSheetState(sheetState, true, StateChangeReason.BACK_PRESS);
return true;
}
/** @return The content currently showing in the bottom sheet. */
public BottomSheetContent getCurrentSheetContent() {
return mBottomSheet == null ? null : mBottomSheet.getCurrentSheetContent();
}
/** @return The current state of the bottom sheet. */
@SheetState
public int getSheetState() {
return mBottomSheet == null ? SheetState.HIDDEN : mBottomSheet.getSheetState();
}
/** @return The target state of the bottom sheet (usually during animations). */
@SheetState
public int getTargetSheetState() {
return mBottomSheet == null ? SheetState.NONE : mBottomSheet.getTargetSheetState();
}
/** @return Whether the bottom sheet is currently open (expanded beyond peek state). */
public boolean isSheetOpen() {
return mBottomSheet != null && mBottomSheet.isSheetOpen();
}
/** @return Whether the bottom sheet is in the process of hiding. */
public boolean isSheetHiding() {
return mBottomSheet == null ? false : mBottomSheet.isHiding();
}
/** @return The current offset from the bottom of the screen that the sheet is in px. */
public int getCurrentOffset() {
return mBottomSheet == null ? 0 : (int) mBottomSheet.getCurrentOffsetPx();
}
/**
* @return The height of the bottom sheet's container in px. This will return 0 if the sheet has
* not been initialized (content has not been requested).
*/
public int getContainerHeight() {
return mBottomSheet != null ? (int) mBottomSheet.getSheetContainerHeight() : 0;
}
/** @return The height of the shadow above the bottom sheet in px. */
public int getTopShadowHeight() {
return mToolbarShadowHeight + mShadowTopOffset;
}
/**
* @param observer The observer to add.
*/
public void addObserver(BottomSheetObserver observer) {
if (mBottomSheet == null) {
mPendingSheetObservers.add(observer);
return;
}
mBottomSheet.addObserver(observer);
}
/**
* @param observer The observer to remove.
*/
public void removeObserver(BottomSheetObserver observer) {
if (mBottomSheet != null) {
mBottomSheet.removeObserver(observer);
} else {
mPendingSheetObservers.remove(observer);
}
}
/**
* Temporarily suppress the bottom sheet while other UI is showing. This will not itself change
* the content displayed by the sheet.
* @param reason The reason the sheet was suppressed.
*/
@VisibleForTesting
void suppressSheet(@StateChangeReason int reason) {
mSheetStateBeforeSuppress = getSheetState();
mContentWhenSuppressed = getCurrentSheetContent();
mIsSuppressed = true;
mBottomSheet.setSheetState(SheetState.HIDDEN, false, reason);
}
/**
* Unsuppress the bottom sheet. This may or may not affect the sheet depending on the state of
* the browser (i.e. the tab switcher may be showing).
*/
@VisibleForTesting
void unsuppressSheet() {
if (!mIsSuppressed || mTabProvider.get() == null || VrModuleProvider.getDelegate().isInVr()
|| mOmniboxFocusStateSupplier.get()) {
return;
}
mIsSuppressed = false;
if (mBottomSheet.getCurrentSheetContent() != null) {
@SheetState
int openState = mContentWhenSuppressed == getCurrentSheetContent()
? mSheetStateBeforeSuppress
: mBottomSheet.getOpeningState();
mBottomSheet.setSheetState(openState, true);
} else {
// In the event the previous content was hidden, try to show the next one.
showNextContent(true);
}
mContentWhenSuppressed = null;
mSheetStateBeforeSuppress = SheetState.NONE;
}
@VisibleForTesting
public void setSheetStateForTesting(@SheetState int state, boolean animate) {
mBottomSheet.setSheetState(state, animate);
}
@VisibleForTesting
public View getBottomSheetViewForTesting() {
return mBottomSheet;
}
/**
* This is the same as {@link BottomSheet#setSheetOffsetFromBottom(float, int)} but exclusively
* for testing.
* @param offset The offset to set the sheet to.
*/
@VisibleForTesting
public void setSheetOffsetFromBottomForTesting(float offset) {
mBottomSheet.setSheetOffsetFromBottom(offset, StateChangeReason.NONE);
}
/**
* WARNING: This destroys the internal sheet state. Only use in tests and only use once!
*
* To simulate scrolling, this method puts the sheet in a permanent scrolling state.
* @return The target state of the bottom sheet (to check thresholds).
*/
@VisibleForTesting
@SheetState
int forceScrollingForTesting(float sheetHeight, float yVelocity) {
return mBottomSheet.forceScrollingStateForTesting(sheetHeight, yVelocity);
}
@VisibleForTesting
public void endAnimationsForTesting() {
mBottomSheet.endAnimations();
}
/**
* Request that some content be shown in the bottom sheet.
* @param content The content to be shown in the bottom sheet.
......@@ -617,33 +72,7 @@ public class BottomSheetController implements Destroyable {
* higher priority content is in the sheet, the sheet is expanded beyond the peeking
* state, or the browser is in a mode that does not support showing the sheet.
*/
public boolean requestShowContent(BottomSheetContent content, boolean animate) {
if (mBottomSheet == null) mSheetInitializer.run();
// If already showing the requested content, do nothing.
if (content == mBottomSheet.getCurrentSheetContent()) return true;
// Showing the sheet requires a tab.
if (mTabProvider.get() == null) return false;
boolean shouldSuppressExistingContent = mBottomSheet.getCurrentSheetContent() != null
&& content.getPriority() < mBottomSheet.getCurrentSheetContent().getPriority()
&& canBottomSheetSwitchContent();
// Always add the content to the queue, it will be handled after the sheet closes if
// necessary. If already hidden, |showNextContent| will handle the request.
mContentQueue.add(content);
if (mBottomSheet.getCurrentSheetContent() == null) {
showNextContent(animate);
return true;
} else if (shouldSuppressExistingContent) {
mContentQueue.add(mBottomSheet.getCurrentSheetContent());
mBottomSheet.setSheetState(SheetState.HIDDEN, animate);
return true;
}
return false;
}
boolean requestShowContent(BottomSheetContent content, boolean animate);
/**
* Hide content shown in the bottom sheet. If the content is not showing, this call retracts the
......@@ -652,54 +81,21 @@ public class BottomSheetController implements Destroyable {
* @param animate Whether the sheet should animate when hiding.
* @param hideReason The reason that the content is being hidden.
*/
public void hideContent(
BottomSheetContent content, boolean animate, @StateChangeReason int hideReason) {
if (mBottomSheet == null) return;
if (content != mBottomSheet.getCurrentSheetContent()) {
mContentQueue.remove(content);
return;
}
void hideContent(
BottomSheetContent content, boolean animate, @StateChangeReason int hideReason);
if (mIsProcessingHideRequest) return;
void hideContent(BottomSheetContent content, boolean animate);
// Handle showing the next content if it exists.
if (mBottomSheet.getSheetState() == SheetState.HIDDEN) {
// If the sheet is already hidden, simply show the next content.
showNextContent(animate);
} else {
mIsProcessingHideRequest = true;
mBottomSheet.setSheetState(SheetState.HIDDEN, animate, hideReason);
}
}
/** @param observer The observer to add. */
void addObserver(BottomSheetObserver observer);
/**
* Hide content shown in the bottom sheet. If the content is not showing, this call retracts the
* request to show it.
* Supply a close reason if possible: hideContent(BottomSheetContent, boolean, int).
* @param content The content to be hidden.
* @param animate Whether the sheet should animate when hiding.
* @see #hideContent(BottomSheetContent, boolean, int) method.
*/
public void hideContent(BottomSheetContent content, boolean animate) {
hideContent(content, animate, StateChangeReason.NONE);
}
/** @param observer The observer to remove. */
void removeObserver(BottomSheetObserver observer);
/**
* Expand the {@link BottomSheet}. If there is no content in the sheet, this is a noop.
* Expand the sheet. If there is no content in the sheet, this is a noop.
*/
public void expandSheet() {
if (mBottomSheet == null || mIsSuppressed) return;
if (mBottomSheet.getCurrentSheetContent() == null) return;
mBottomSheet.setSheetState(SheetState.HALF, true);
if (mOverlayPanelManager.get().getActivePanel() != null) {
// TODO(mdjones): This should only apply to contextual search, but contextual search is
// the only implementation. Fix this to only apply to contextual search.
mOverlayPanelManager.get().getActivePanel().closePanel(
OverlayPanel.StateChangeReason.UNKNOWN, true);
}
}
void expandSheet();
/**
* Collapse the current sheet to peek state. Sheet may not change the state if the state
......@@ -707,66 +103,34 @@ public class BottomSheetController implements Destroyable {
* @param animate {@code true} for animation effect.
* @return {@code true} if the sheet could go to the peek state.
*/
public boolean collapseSheet(boolean animate) {
if (mBottomSheet == null || mIsSuppressed) return false;
if (mBottomSheet.isSheetOpen() && mBottomSheet.isPeekStateEnabled()) {
mBottomSheet.setSheetState(SheetState.PEEK, animate);
return true;
}
return false;
}
boolean collapseSheet(boolean animate);
/**
* Show the next {@link BottomSheetContent} if it is available and peek the sheet. If no content
* is available the sheet's content is set to null.
* @param animate Whether the sheet should animate opened.
*/
private void showNextContent(boolean animate) {
if (mContentQueue.isEmpty()) {
mBottomSheet.showContent(null);
return;
}
/** @return The content currently showing in the bottom sheet. */
BottomSheetContent getCurrentSheetContent();
BottomSheetContent nextContent = mContentQueue.poll();
mBottomSheet.showContent(nextContent);
mBottomSheet.setSheetState(mBottomSheet.getOpeningState(), animate);
}
/** @return The current state of the bottom sheet. */
@SheetState
int getSheetState();
/**
* For all contents that don't have a custom lifecycle, we remove them from show requests or
* hide it if it is currently shown.
*/
private void clearRequestsAndHide() {
clearRequests(mContentQueue.iterator());
/** @return The target state of the bottom sheet (usually during animations). */
@SheetState
int getTargetSheetState();
BottomSheetContent currentContent = mBottomSheet.getCurrentSheetContent();
if (currentContent == null || !currentContent.hasCustomLifecycle()) {
if (mContentQueue.isEmpty()) mIsSuppressed = false;
/** @return Whether the bottom sheet is currently open (expanded beyond peek state). */
boolean isSheetOpen();
hideContent(currentContent, /* animate= */ true);
}
mContentWhenSuppressed = null;
mSheetStateBeforeSuppress = SheetState.NONE;
}
/** @return Whether the bottom sheet is in the process of hiding. */
boolean isSheetHiding();
/**
* Remove all contents from {@code iterator} that don't have a custom lifecycle.
* @param iterator The iterator whose items must be removed.
*/
private void clearRequests(Iterator<BottomSheetContent> iterator) {
while (iterator.hasNext()) {
if (!iterator.next().hasCustomLifecycle()) {
iterator.remove();
}
}
}
/** @return The current offset from the bottom of the screen that the sheet is in px. */
int getCurrentOffset();
/**
* The bottom sheet cannot change content while it is open. If the user has the bottom sheet
* open, they are currently engaged in a task and shouldn't be interrupted.
* @return Whether the sheet currently supports switching its content.
* @return The height of the bottom sheet's container in px. This will return 0 if the sheet has
* not been initialized (content has not been requested).
*/
private boolean canBottomSheetSwitchContent() {
return !mBottomSheet.isSheetOpen();
}
int getContainerHeight();
/** @return The height of the shadow above the bottom sheet in px. */
int getTopShadowHeight();
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.widget.bottomsheet;
import android.view.View;
import android.view.Window;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.HintlessActivityTabObserver;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
import org.chromium.chrome.browser.fullscreen.BrowserControlsStateProvider;
import org.chromium.chrome.browser.fullscreen.BrowserControlsStateProvider.Observer;
import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager;
import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.chrome.browser.widget.ScrimView.ScrimObserver;
import org.chromium.chrome.browser.widget.ScrimView.ScrimParams;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.components.browser_ui.widget.scrim.ScrimProperties;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.vr.VrModeObserver;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
/**
* This class is responsible for managing the content shown by the {@link BottomSheet}. Features
* wishing to show content in the {@link BottomSheet} UI must implement {@link BottomSheetContent}
* and call {@link #requestShowContent(BottomSheetContent, boolean)} which will return true if the
* content was actually shown (see full doc on method).
*/
public class BottomSheetControllerImpl implements BottomSheetControllerInternal {
/** The initial capacity for the priority queue handling pending content show requests. */
private static final int INITIAL_QUEUE_CAPACITY = 1;
/** A {@link VrModeObserver} that observers events of entering and exiting VR mode. */
private final VrModeObserver mVrModeObserver;
/** A listener for browser controls offset changes. */
private final BrowserControlsStateProvider.Observer mBrowserControlsObserver;
/** A listener for fullscreen events. */
private final ChromeFullscreenManager.FullscreenListener mFullscreenListener;
/** A means of accessing the focus state of the omibox. */
private final ObservableSupplier<Boolean> mOmniboxFocusStateSupplier;
/** An observer of the omnibox that suppresses the sheet when the omnibox is focused. */
private final Callback<Boolean> mOmniboxFocusObserver;
/** The height of the shadow that sits above the toolbar. */
private int mToolbarShadowHeight;
/** The offset of the toolbar shadow from the top that remains empty. */
private int mShadowTopOffset;
/** A handle to the {@link BottomSheet} that this class controls. */
private BottomSheet mBottomSheet;
/** A queue for content that is waiting to be shown in the {@link BottomSheet}. */
private PriorityQueue<BottomSheetContent> mContentQueue;
/** Whether the controller is already processing a hide request for the tab. */
private boolean mIsProcessingHideRequest;
/** Whether the bottom sheet is temporarily suppressed. */
private boolean mIsSuppressed;
/** The manager for overlay panels to attach listeners to. */
private Supplier<OverlayPanelManager> mOverlayPanelManager;
/** A means for getting the activity's current tab and observing change events. */
private ActivityTabProvider mTabProvider;
/** A browser controls manager for polling browser controls offsets. */
private BrowserControlsStateProvider mBrowserControlsStateProvider;
/** A fullscreen manager for listening to fullscreen events. */
private ChromeFullscreenManager mFullscreenManager;
/** The last known activity tab, if available. */
private Tab mLastActivityTab;
/** A runnable that initializes the bottom sheet when necessary. */
private Runnable mSheetInitializer;
/**
* A list of observers maintained by this controller until the bottom sheet is created, at which
* point they will be added to the bottom sheet.
*/
private List<BottomSheetObserver> mPendingSheetObservers;
/** The state of the sheet so it can be returned to what it was prior to suppression. */
@SheetState
private int mSheetStateBeforeSuppress;
/** The content being shown prior to the sheet being suppressed. */
private BottomSheetContent mContentWhenSuppressed;
/**
* Build a new controller of the bottom sheet.
* @param activityTabProvider The provider of the activity's current tab.
* @param scrim A supplier of the scrim that shows when the bottom sheet is opened.
* @param bottomSheetViewSupplier A mechanism for creating a {@link BottomSheet}.
* @param overlayManager A supplier of the manager for overlay panels to attach listeners to.
* This is a supplier to get around waiting for native to be initialized.
* @param fullscreenManager A fullscreen manager for access to browser controls offsets.
* @param omniboxFocusStateSupplier A means of accessing the focused state of the omnibox.
*/
public BottomSheetControllerImpl(final ActivityTabProvider activityTabProvider,
final Supplier<ScrimCoordinator> scrim, Supplier<View> bottomSheetViewSupplier,
Supplier<OverlayPanelManager> overlayManager, ChromeFullscreenManager fullscreenManager,
Window window, KeyboardVisibilityDelegate keyboardDelegate,
ObservableSupplier<Boolean> omniboxFocusStateSupplier) {
mTabProvider = activityTabProvider;
mOverlayPanelManager = overlayManager;
mBrowserControlsStateProvider = fullscreenManager;
mFullscreenManager = fullscreenManager;
mPendingSheetObservers = new ArrayList<>();
mPendingSheetObservers.add(new EmptyBottomSheetObserver() {
/** The token used to enable browser controls persistence. */
private int mPersistentControlsToken;
@Override
public void onSheetOpened(int reason) {
if (mFullscreenManager.getBrowserVisibilityDelegate() == null) return;
// Browser controls should stay visible until the sheet is closed.
mPersistentControlsToken =
mFullscreenManager.getBrowserVisibilityDelegate().showControlsPersistent();
}
@Override
public void onSheetClosed(int reason) {
if (mFullscreenManager.getBrowserVisibilityDelegate() == null) return;
// Update the browser controls since they are permanently shown while the sheet is
// open.
mFullscreenManager.getBrowserVisibilityDelegate().releasePersistentShowingToken(
mPersistentControlsToken);
}
});
mVrModeObserver = new VrModeObserver() {
@Override
public void onEnterVr() {
suppressSheet(StateChangeReason.VR);
}
@Override
public void onExitVr() {
unsuppressSheet();
}
};
mBrowserControlsObserver = new Observer() {
@Override
public void onControlsOffsetChanged(int topOffset, int topControlsMinHeightOffset,
int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {
if (mBottomSheet != null) {
mBottomSheet.setBrowserControlsHiddenRatio(
mBrowserControlsStateProvider.getBrowserControlHiddenRatio());
}
}
};
mBrowserControlsStateProvider.addObserver(mBrowserControlsObserver);
mFullscreenListener = new ChromeFullscreenManager.FullscreenListener() {
@Override
public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
if (mBottomSheet == null || mTabProvider.get() != tab) return;
suppressSheet(StateChangeReason.COMPOSITED_UI);
}
@Override
public void onExitFullscreen(Tab tab) {
if (mBottomSheet == null || mTabProvider.get() != tab) return;
unsuppressSheet();
}
};
mFullscreenManager.addListener(mFullscreenListener);
mOmniboxFocusStateSupplier = omniboxFocusStateSupplier;
mOmniboxFocusObserver = (focused) -> {
if (focused) {
suppressSheet(StateChangeReason.NONE);
} else {
unsuppressSheet();
}
};
mSheetInitializer = () -> {
initializeSheet(scrim, bottomSheetViewSupplier, window, keyboardDelegate);
};
}
/**
* Do the actual initialization of the bottom sheet.
* @param scrim The scrim to show behind the sheet.
* @param bottomSheetViewSupplier A means of creating the bottom sheet.
*/
private void initializeSheet(final Supplier<ScrimCoordinator> scrim,
Supplier<View> bottomSheetViewSupplier, Window window,
KeyboardVisibilityDelegate keyboardDelegate) {
mBottomSheet = (BottomSheet) bottomSheetViewSupplier.get();
mBottomSheet.init(window, keyboardDelegate);
mToolbarShadowHeight = mBottomSheet.getResources().getDimensionPixelOffset(
BottomSheet.getTopShadowResourceId());
mShadowTopOffset = mBottomSheet.getResources().getDimensionPixelOffset(
BottomSheet.getShadowTopOffsetResourceId());
mOmniboxFocusStateSupplier.addObserver(mOmniboxFocusObserver);
// Initialize the queue with a comparator that checks content priority.
mContentQueue = new PriorityQueue<>(INITIAL_QUEUE_CAPACITY,
(content1, content2) -> content1.getPriority() - content2.getPriority());
VrModuleProvider.registerVrModeObserver(mVrModeObserver);
final TabObserver tabObserver = new EmptyTabObserver() {
@Override
public void onPageLoadStarted(Tab tab, String url) {
clearRequestsAndHide();
}
@Override
public void onCrash(Tab tab) {
clearRequestsAndHide();
}
@Override
public void onDestroyed(Tab tab) {
if (mLastActivityTab != tab) return;
mLastActivityTab = null;
// Remove the suppressed sheet if its lifecycle is tied to the tab being
// destroyed.
clearRequestsAndHide();
}
};
mTabProvider.addObserverAndTrigger(new HintlessActivityTabObserver() {
@Override
public void onActivityTabChanged(Tab tab) {
// Temporarily suppress the sheet if entering a state where there is no activity
// tab.
if (tab == null) {
suppressSheet(StateChangeReason.COMPOSITED_UI);
return;
}
// If refocusing the same tab, simply unsuppress the sheet.
if (mLastActivityTab == tab) {
unsuppressSheet();
return;
}
// Move the observer to the new activity tab and clear the sheet.
if (mLastActivityTab != null) mLastActivityTab.removeObserver(tabObserver);
mLastActivityTab = tab;
mLastActivityTab.addObserver(tabObserver);
clearRequestsAndHide();
}
});
PropertyModel scrimProperties =
new PropertyModel.Builder(ScrimProperties.REQUIRED_KEYS)
.with(ScrimProperties.TOP_MARGIN, 0)
.with(ScrimProperties.AFFECTS_STATUS_BAR, true)
.with(ScrimProperties.ANCHOR_VIEW, mBottomSheet)
.with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
.with(ScrimProperties.CLICK_DELEGATE,
() -> {
if (!mBottomSheet.isSheetOpen()) return;
mBottomSheet.setSheetState(
mBottomSheet.getMinSwipableSheetState(), true,
StateChangeReason.TAP_SCRIM);
})
.build();
mBottomSheet.addObserver(new EmptyBottomSheetObserver() {
/**
* Whether the scrim was shown for the last content.
* TODO(mdjones): We should try to make sure the content in the sheet is not nulled
* prior to the close event occurring; sheets that don't have a peek
* state make this difficult since the sheet needs to be hidden before it
* is closed.
*/
private boolean mScrimShown;
@Override
public void onSheetOpened(@StateChangeReason int reason) {
if (mBottomSheet.getCurrentSheetContent() != null
&& mBottomSheet.getCurrentSheetContent().hasCustomScrimLifecycle()) {
return;
}
scrim.get().showScrim(scrimProperties);
mScrimShown = true;
}
@Override
public void onSheetClosed(@StateChangeReason int reason) {
// Hide the scrim if the current content doesn't have a custom scrim lifecycle.
if (mScrimShown) {
scrim.get().hideScrim(true);
mScrimShown = false;
}
// Try to swap contents unless the sheet's content has a custom lifecycle.
if (mBottomSheet.getCurrentSheetContent() != null
&& !mBottomSheet.getCurrentSheetContent().hasCustomLifecycle()) {
// If the sheet is closed, it is an opportunity for another content to try to
// take its place if it is a higher priority.
BottomSheetContent content = mBottomSheet.getCurrentSheetContent();
BottomSheetContent nextContent = mContentQueue.peek();
if (content != null && nextContent != null
&& nextContent.getPriority() < content.getPriority()) {
mContentQueue.add(content);
mBottomSheet.setSheetState(SheetState.HIDDEN, true);
}
}
}
@Override
public void onSheetStateChanged(@SheetState int state) {
// If hiding request is in progress, destroy the current sheet content being hidden
// even when it is in suppressed state. See https://crbug.com/1057966.
if (state != SheetState.HIDDEN || (!mIsProcessingHideRequest && mIsSuppressed)) {
return;
}
if (mBottomSheet.getCurrentSheetContent() != null) {
mBottomSheet.getCurrentSheetContent().destroy();
}
mIsProcessingHideRequest = false;
showNextContent(true);
}
});
// Add any of the pending observers that were added prior to the sheet being created.
for (int i = 0; i < mPendingSheetObservers.size(); i++) {
mBottomSheet.addObserver(mPendingSheetObservers.get(i));
}
mPendingSheetObservers.clear();
mSheetInitializer = null;
}
/**
* Create a ScrimParams anchoring on the bottom-sheet view.
* @param scrimObserver The scrimObserver to set for the ScrimParams.
*/
public ScrimParams createScrimParams(ScrimObserver scrimObserver) {
return new ScrimParams(/*anchorView=*/mBottomSheet, /*showInFrontOfAnchorView=*/false,
/*affectsStatusBar=*/true, /*topMargin=*/0, /*observer*/ scrimObserver);
}
// Destroyable implementation.
@Override
public void destroy() {
VrModuleProvider.unregisterVrModeObserver(mVrModeObserver);
mFullscreenManager.removeListener(mFullscreenListener);
mBrowserControlsStateProvider.removeObserver(mBrowserControlsObserver);
mOmniboxFocusStateSupplier.removeObserver(mOmniboxFocusObserver);
if (mBottomSheet != null) mBottomSheet.destroy();
}
@Override
public boolean handleBackPress() {
// If suppressed (therefore invisible), users are likely to expect for Chrome
// browser, not the bottom sheet, to react. Do not consume the event.
if (mBottomSheet == null || mIsSuppressed) return false;
// Give the sheet the opportunity to handle the back press itself before falling to the
// default "close" behavior.
if (getCurrentSheetContent() != null && getCurrentSheetContent().handleBackPress()) {
return true;
}
if (!mBottomSheet.isSheetOpen()) return false;
int sheetState = mBottomSheet.getMinSwipableSheetState();
mBottomSheet.setSheetState(sheetState, true, StateChangeReason.BACK_PRESS);
return true;
}
@Override
public BottomSheetContent getCurrentSheetContent() {
return mBottomSheet == null ? null : mBottomSheet.getCurrentSheetContent();
}
@Override
@SheetState
public int getSheetState() {
return mBottomSheet == null ? SheetState.HIDDEN : mBottomSheet.getSheetState();
}
@Override
@SheetState
public int getTargetSheetState() {
return mBottomSheet == null ? SheetState.NONE : mBottomSheet.getTargetSheetState();
}
@Override
public boolean isSheetOpen() {
return mBottomSheet != null && mBottomSheet.isSheetOpen();
}
@Override
public boolean isSheetHiding() {
return mBottomSheet == null ? false : mBottomSheet.isHiding();
}
@Override
public int getCurrentOffset() {
return mBottomSheet == null ? 0 : (int) mBottomSheet.getCurrentOffsetPx();
}
@Override
public int getContainerHeight() {
return mBottomSheet != null ? (int) mBottomSheet.getSheetContainerHeight() : 0;
}
@Override
public int getTopShadowHeight() {
return mToolbarShadowHeight + mShadowTopOffset;
}
@Override
public void addObserver(BottomSheetObserver observer) {
if (mBottomSheet == null) {
mPendingSheetObservers.add(observer);
return;
}
mBottomSheet.addObserver(observer);
}
@Override
public void removeObserver(BottomSheetObserver observer) {
if (mBottomSheet != null) {
mBottomSheet.removeObserver(observer);
} else {
mPendingSheetObservers.remove(observer);
}
}
@Override
public void suppressSheet(@StateChangeReason int reason) {
mSheetStateBeforeSuppress = getSheetState();
mContentWhenSuppressed = getCurrentSheetContent();
mIsSuppressed = true;
mBottomSheet.setSheetState(SheetState.HIDDEN, false, reason);
}
@Override
public void unsuppressSheet() {
if (!mIsSuppressed || mTabProvider.get() == null || VrModuleProvider.getDelegate().isInVr()
|| mOmniboxFocusStateSupplier.get()) {
return;
}
mIsSuppressed = false;
if (mBottomSheet.getCurrentSheetContent() != null) {
@SheetState
int openState = mContentWhenSuppressed == getCurrentSheetContent()
? mSheetStateBeforeSuppress
: mBottomSheet.getOpeningState();
mBottomSheet.setSheetState(openState, true);
} else {
// In the event the previous content was hidden, try to show the next one.
showNextContent(true);
}
mContentWhenSuppressed = null;
mSheetStateBeforeSuppress = SheetState.NONE;
}
@VisibleForTesting
void setSheetStateForTesting(@SheetState int state, boolean animate) {
mBottomSheet.setSheetState(state, animate);
}
@VisibleForTesting
View getBottomSheetViewForTesting() {
return mBottomSheet;
}
/**
* WARNING: This destroys the internal sheet state. Only use in tests and only use once!
*
* To simulate scrolling, this method puts the sheet in a permanent scrolling state.
* @return The target state of the bottom sheet (to check thresholds).
*/
@VisibleForTesting
@SheetState
int forceScrollingForTesting(float sheetHeight, float yVelocity) {
return mBottomSheet.forceScrollingStateForTesting(sheetHeight, yVelocity);
}
@VisibleForTesting
public void endAnimationsForTesting() {
mBottomSheet.endAnimations();
}
@Override
public boolean requestShowContent(BottomSheetContent content, boolean animate) {
if (mBottomSheet == null) mSheetInitializer.run();
// If already showing the requested content, do nothing.
if (content == mBottomSheet.getCurrentSheetContent()) return true;
// Showing the sheet requires a tab.
if (mTabProvider.get() == null) return false;
boolean shouldSuppressExistingContent = mBottomSheet.getCurrentSheetContent() != null
&& content.getPriority() < mBottomSheet.getCurrentSheetContent().getPriority()
&& canBottomSheetSwitchContent();
// Always add the content to the queue, it will be handled after the sheet closes if
// necessary. If already hidden, |showNextContent| will handle the request.
mContentQueue.add(content);
if (mBottomSheet.getCurrentSheetContent() == null) {
showNextContent(animate);
return true;
} else if (shouldSuppressExistingContent) {
mContentQueue.add(mBottomSheet.getCurrentSheetContent());
mBottomSheet.setSheetState(SheetState.HIDDEN, animate);
return true;
}
return false;
}
@Override
public void hideContent(
BottomSheetContent content, boolean animate, @StateChangeReason int hideReason) {
if (mBottomSheet == null) return;
if (content != mBottomSheet.getCurrentSheetContent()) {
mContentQueue.remove(content);
return;
}
if (mIsProcessingHideRequest) return;
// Handle showing the next content if it exists.
if (mBottomSheet.getSheetState() == SheetState.HIDDEN) {
// If the sheet is already hidden, simply show the next content.
showNextContent(animate);
} else {
mIsProcessingHideRequest = true;
mBottomSheet.setSheetState(SheetState.HIDDEN, animate, hideReason);
}
}
@Override
public void hideContent(BottomSheetContent content, boolean animate) {
hideContent(content, animate, StateChangeReason.NONE);
}
@Override
public void expandSheet() {
if (mBottomSheet == null || mIsSuppressed) return;
if (mBottomSheet.getCurrentSheetContent() == null) return;
mBottomSheet.setSheetState(SheetState.HALF, true);
if (mOverlayPanelManager.get().getActivePanel() != null) {
// TODO(mdjones): This should only apply to contextual search, but contextual search is
// the only implementation. Fix this to only apply to contextual search.
mOverlayPanelManager.get().getActivePanel().closePanel(
OverlayPanel.StateChangeReason.UNKNOWN, true);
}
}
@Override
public boolean collapseSheet(boolean animate) {
if (mBottomSheet == null || mIsSuppressed) return false;
if (mBottomSheet.isSheetOpen() && mBottomSheet.isPeekStateEnabled()) {
mBottomSheet.setSheetState(SheetState.PEEK, animate);
return true;
}
return false;
}
/**
* Show the next {@link BottomSheetContent} if it is available and peek the sheet. If no content
* is available the sheet's content is set to null.
* @param animate Whether the sheet should animate opened.
*/
private void showNextContent(boolean animate) {
if (mContentQueue.isEmpty()) {
mBottomSheet.showContent(null);
return;
}
BottomSheetContent nextContent = mContentQueue.poll();
mBottomSheet.showContent(nextContent);
mBottomSheet.setSheetState(mBottomSheet.getOpeningState(), animate);
}
@Override
public void clearRequestsAndHide() {
clearRequests(mContentQueue.iterator());
BottomSheetContent currentContent = mBottomSheet.getCurrentSheetContent();
if (currentContent == null || !currentContent.hasCustomLifecycle()) {
if (mContentQueue.isEmpty()) mIsSuppressed = false;
hideContent(currentContent, /* animate= */ true);
}
mContentWhenSuppressed = null;
mSheetStateBeforeSuppress = SheetState.NONE;
}
/**
* Remove all contents from {@code iterator} that don't have a custom lifecycle.
* @param iterator The iterator whose items must be removed.
*/
private void clearRequests(Iterator<BottomSheetContent> iterator) {
while (iterator.hasNext()) {
if (!iterator.next().hasCustomLifecycle()) {
iterator.remove();
}
}
}
/**
* The bottom sheet cannot change content while it is open. If the user has the bottom sheet
* open, they are currently engaged in a task and shouldn't be interrupted.
* @return Whether the sheet currently supports switching its content.
*/
private boolean canBottomSheetSwitchContent() {
return !mBottomSheet.isSheetOpen();
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.widget.bottomsheet;
/**
* An interface for the owning object to manage interaction between the bottom sheet and the rest
* of the system.
*/
public interface BottomSheetControllerInternal extends BottomSheetController {
/**
* Temporarily suppress the bottom sheet while other UI is showing. This will not itself change
* the content displayed by the sheet.
* @param reason The reason the sheet was suppressed.
*/
void suppressSheet(@StateChangeReason int reason);
/**
* Unsuppress the bottom sheet. This may or may not affect the sheet depending on the state of
* the browser (i.e. the tab switcher may be showing).
*/
void unsuppressSheet();
/**
* For all contents that don't have a custom lifecycle, we remove them from show requests or
* hide it if it is currently shown.
*/
void clearRequestsAndHide();
/**
* Handle a back press event. By default this will return the bottom sheet to it's minimum /
* peeking state if it is open. However, the sheet's content has the opportunity to intercept
* this event and block the default behavior {@see BottomSheetContent#handleBackPress()}.
* @return {@code true} if the sheet or content handled the back press.
*/
boolean handleBackPress();
/** Clean up any state maintained by the controller. */
void destroy();
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.widget.bottomsheet;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController.SheetState;
/** Utilities to support testing with the {@link BottomSheetController}. */
public class BottomSheetTestSupport {
/** A handle to the actual implementation class of the {@link BottomSheetController}. */
BottomSheetControllerImpl mController;
/**
* @param controller A handle to the public {@link BottomSheetController}.
*/
public BottomSheetTestSupport(BottomSheetController controller) {
mController = (BottomSheetControllerImpl) controller;
}
/** End all animations on the sheet for testing purposes. */
public void endAllAnimations() {
mController.endAnimationsForTesting();
}
/**
* Force the sheet's state for testing.
* @param state The state the sheet should be in.
* @param animate Whether the sheet should animate to the specified state.
*/
public void setSheetState(@SheetState int state, boolean animate) {
mController.setSheetStateForTesting(state, animate);
}
}
......@@ -22,6 +22,7 @@ import org.chromium.chrome.browser.firstrun.DisableFirstRun;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabbed_mode.TabbedRootUiCoordinator;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetTestSupport;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
......@@ -56,6 +57,7 @@ public class PreviewTabTest {
private static final String NEAR_BOTTOM_DOM_ID = "nearBottom";
private EphemeralTabCoordinator mEphemeralTabCoordinator;
private BottomSheetTestSupport mSheetTestSupport;
@Before
public void setUp() {
......@@ -67,13 +69,13 @@ public class PreviewTabTest {
mEphemeralTabCoordinator =
tabbedRootUiCoordinator.getEphemeralTabCoordinatorForTesting();
});
mSheetTestSupport = new BottomSheetTestSupport(mActivityTestRule.getActivity()
.getRootUiCoordinatorForTesting()
.getBottomSheetController());
}
private void endAnimations() {
TestThreadUtils.runOnUiThreadBlocking(
mActivityTestRule.getActivity()
.getRootUiCoordinatorForTesting()
.getBottomSheetController()::endAnimationsForTesting);
TestThreadUtils.runOnUiThreadBlocking(mSheetTestSupport::endAllAnimations);
}
private void closePreviewTab() {
......
......@@ -58,7 +58,7 @@ public class BottomSheetControllerTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
private BottomSheetController mSheetController;
private BottomSheetControllerImpl mSheetController;
private TestBottomSheetContent mLowPriorityContent;
private TestBottomSheetContent mHighPriorityContent;
private TestBottomSheetContent mPeekableContent;
......@@ -80,7 +80,8 @@ public class BottomSheetControllerTest {
.getScrimCoordinatorForTesting();
mScrimCoordinator.disableAnimationForTesting(true);
mSheetController = activity.getBottomSheetController();
mSheetController = (BottomSheetControllerImpl) activity.getRootUiCoordinatorForTesting()
.getBottomSheetController();
mLowPriorityContent = new TestBottomSheetContent(
mActivityTestRule.getActivity(), BottomSheetContent.ContentPriority.LOW, false);
......
......@@ -101,15 +101,16 @@ public class BottomSheetObserverTest {
public ChromeTabbedActivityTestRule mTestRule = new ChromeTabbedActivityTestRule();
private TestSheetObserver mObserver;
private TestBottomSheetContent mSheetContent;
private BottomSheetController mBottomSheetController;
private BottomSheetControllerImpl mBottomSheetController;
private BottomSheet mSheetView;
@Before
public void setUp() throws Exception {
BottomSheet.setSmallScreenForTesting(false);
mTestRule.startMainActivityOnBlankPage();
mBottomSheetController =
mTestRule.getActivity().getRootUiCoordinatorForTesting().getBottomSheetController();
mBottomSheetController = (BottomSheetControllerImpl) mTestRule.getActivity()
.getRootUiCoordinatorForTesting()
.getBottomSheetController();
ThreadUtils.runOnUiThreadBlocking(() -> {
mSheetContent = new TestBottomSheetContent(
mTestRule.getActivity(), BottomSheetContent.ContentPriority.HIGH, false);
......
......@@ -48,14 +48,15 @@ public class BottomSheetTest {
public ChromeTabbedActivityTestRule mTestRule = new ChromeTabbedActivityTestRule();
private TestBottomSheetContent mLowPriorityContent;
private TestBottomSheetContent mHighPriorityContent;
private BottomSheetController mSheetController;
private BottomSheetControllerImpl mSheetController;
@Before
public void setUp() throws Exception {
BottomSheet.setSmallScreenForTesting(false);
mTestRule.startMainActivityOnBlankPage();
mSheetController =
mTestRule.getActivity().getRootUiCoordinatorForTesting().getBottomSheetController();
mSheetController = (BottomSheetControllerImpl) mTestRule.getActivity()
.getRootUiCoordinatorForTesting()
.getBottomSheetController();
runOnUiThreadBlocking(() -> {
mLowPriorityContent = new TestBottomSheetContent(
mTestRule.getActivity(), BottomSheetContent.ContentPriority.LOW, false);
......
......@@ -50,6 +50,7 @@ import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderPro
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController.SheetState;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetTestSupport;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
......@@ -306,12 +307,12 @@ public class TouchToFillViewTest {
mModel.set(VISIBLE, true);
});
pollUiThread(() -> getBottomSheetState() == BottomSheetController.SheetState.HALF);
BottomSheetTestSupport sheetSupport = new BottomSheetTestSupport(
getActivity().getRootUiCoordinatorForTesting().getBottomSheetController());
TestThreadUtils.runOnUiThreadBlocking(() -> {
getActivity().getBottomSheetController().setSheetStateForTesting(
SheetState.FULL, false);
});
pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
// Swipe the sheet up to it's full state.
TestThreadUtils.runOnUiThreadBlocking(
() -> { sheetSupport.setSheetState(SheetState.FULL, false); });
TextView manageButton = mTouchToFillView.getContentView().findViewById(
R.id.touch_to_fill_sheet_manage_passwords);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment