// Copyright 2015 The Chromium Authors
// 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.toolbar.top;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewStub;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
import org.chromium.base.Callback;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tasks.tab_management.TabUiThemeUtil;
import org.chromium.chrome.browser.toolbar.ConstraintsChecker;
import org.chromium.chrome.browser.toolbar.ControlContainer;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.ToolbarCaptureType;
import org.chromium.chrome.browser.toolbar.ToolbarFeatures;
import org.chromium.chrome.browser.toolbar.ToolbarProgressBar;
import org.chromium.chrome.browser.toolbar.top.CaptureReadinessResult.TopToolbarBlockCaptureReason;
import org.chromium.chrome.browser.ui.desktop_windowing.AppHeaderState;
import org.chromium.chrome.browser.ui.desktop_windowing.DesktopWindowStateProvider;
import org.chromium.components.browser_ui.widget.ClipDrawableProgressBar.DrawingInfo;
import org.chromium.components.browser_ui.widget.ViewResourceFrameLayout;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;
import org.chromium.ui.util.TokenHolder;
import org.chromium.ui.widget.OptimizedFrameLayout;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.BooleanSupplier;
/** Layout for the browser controls (omnibox, menu, tab strip, etc..). */
public class ToolbarControlContainer extends OptimizedFrameLayout
implements ControlContainer, DesktopWindowStateProvider.AppHeaderObserver {
private boolean mIncognito;
private boolean mMidVisibilityToggle;
private boolean mIsCompositorInitialized;
private @Nullable AppHeaderState mAppHeaderState;
private Toolbar mToolbar;
private ToolbarViewResourceFrameLayout mToolbarContainer;
private SwipeGestureListener mSwipeGestureListener;
private OnDragListener mToolbarContainerDragListener;
private boolean mIsAppInUnfocusedDesktopWindow;
/**
* Constructs a new control container.
*
* <p>This constructor is used when inflating from XML.
*
* @param context The context used to build this view.
* @param attrs The attributes used to determine how to construct this view.
*/
public ToolbarControlContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public ViewResourceAdapter getToolbarResourceAdapter() {
return mToolbarContainer.getResourceAdapter();
}
@Override
public View getView() {
return this;
}
@Override
public void getProgressBarDrawingInfo(DrawingInfo drawingInfoOut) {
if (mToolbar == null) return;
// TODO(yusufo): Avoid casting to the layout without making the interface bigger.
ToolbarProgressBar progressBar = mToolbar.getProgressBar();
if (progressBar != null) progressBar.getDrawingInfo(drawingInfoOut);
}
@Override
public int getToolbarBackgroundColor() {
if (mToolbar == null) return 0;
return mToolbar.getPrimaryColor();
}
@Override
public void setSwipeHandler(SwipeHandler handler) {
mSwipeGestureListener = new SwipeGestureListenerImpl(getContext(), handler);
}
@Override
public void initWithToolbar(int toolbarLayoutId) {
try (TraceEvent te = TraceEvent.scoped("ToolbarControlContainer.initWithToolbar")) {
mToolbarContainer =
(ToolbarViewResourceFrameLayout) findViewById(R.id.toolbar_container);
ViewStub toolbarStub = findViewById(R.id.toolbar_stub);
toolbarStub.setLayoutResource(toolbarLayoutId);
toolbarStub.inflate();
}
}
@Override
public void onTabOrModelChanged(boolean incognito) {
if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(getContext())
|| getBackground() == null) {
return;
}
if (mIncognito != incognito) {
maybeUpdateTempTabStripDrawableBackground(incognito, mAppHeaderState);
mIncognito = incognito;
}
}
public void onPageLoadStopped() {
((ToolbarViewResourceAdapter) getToolbarResourceAdapter()).onPageLoadStopped();
}
@Override
public void setCompositorBackgroundInitialized() {
mIsCompositorInitialized = true;
setBackgroundResource(0);
}
@Override
public void destroy() {
((ToolbarViewResourceAdapter) getToolbarResourceAdapter()).destroy();
if (mToolbarContainerDragListener != null) {
mToolbarContainer.setOnDragListener(null);
mToolbarContainerDragListener = null;
}
}
@Override
public void setVisibility(int visibility) {
mMidVisibilityToggle = true;
super.setVisibility(visibility);
mMidVisibilityToggle = false;
}
@Override
public void onAppHeaderStateChanged(AppHeaderState newState) {
maybeUpdateTempTabStripDrawableBackground(mIncognito, newState);
mAppHeaderState = newState;
}
private void maybeUpdateTempTabStripDrawableBackground(
boolean incognito, @Nullable AppHeaderState appHeaderState) {
// If compositor is initialized, we don't want to set the background drawable again since
// it'll block the real tab strip in the compositor.
if (mIsCompositorInitialized) return;
Drawable backgroundColor =
new ColorDrawable(
TabUiThemeUtil.getTabStripBackgroundColorForActivityState(
getContext(), mIncognito, !mIsAppInUnfocusedDesktopWindow));
Drawable backgroundTabImage =
ResourcesCompat.getDrawable(
getContext().getResources(),
TabUiThemeUtil.getTabResource(),
getContext().getTheme());
backgroundTabImage.setTint(
TabUiThemeUtil.getTabStripContainerColor(
getContext(), incognito, true, false, false, false));
LayerDrawable backgroundDrawable =
new LayerDrawable(new Drawable[] {backgroundColor, backgroundTabImage});
final int backgroundTabImageIndex = 1;
// Set image size to match tab size.
backgroundDrawable.setPadding(0, 0, 0, 0);
backgroundDrawable.setLayerSize(
backgroundTabImageIndex,
ViewUtils.dpToPx(getContext(), TabUiThemeUtil.getMaxTabStripTabWidthDp()),
// TODO(crbug.com/335660381): We should use the tab strip height from resource
// and add a top insets.
mToolbar.getTabStripHeight());
// Tab should show up at start of layer based on layout.
backgroundDrawable.setLayerGravity(backgroundTabImageIndex, Gravity.START);
// When app header state available, set the state accordingly.
if (appHeaderState != null && appHeaderState.isInDesktopWindow()) {
backgroundDrawable.setLayerInset(
backgroundTabImageIndex,
appHeaderState.getLeftPadding(),
0,
appHeaderState.getRightPadding(),
0);
}
setBackground(backgroundDrawable);
}
/**
* @param toolbar The toolbar contained inside this control container. Should be called after
* inflation is complete.
* @param isIncognito Whether the toolbar should be initialized with incognito colors.
* @param constraintsSupplier Used to access current constraints of the browser controls.
* @param tabSupplier Used to access the current tab state.
* @param compositorInMotionSupplier Whether there is an ongoing touch or gesture.
* @param browserStateBrowserControlsVisibilityDelegate Used to keep controls locked when
* captures are stale and not able to be taken.
* @param layoutStateProviderSupplier Used to check the current layout type.
* @param fullscreenManager Used to check whether in fullscreen.
*/
public void setPostInitializationDependencies(
Toolbar toolbar,
boolean isIncognito,
ObservableSupplier<Integer> constraintsSupplier,
Supplier<Tab> tabSupplier,
ObservableSupplier<Boolean> compositorInMotionSupplier,
BrowserStateBrowserControlsVisibilityDelegate
browserStateBrowserControlsVisibilityDelegate,
OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier,
FullscreenManager fullscreenManager) {
mToolbar = toolbar;
mIncognito = isIncognito;
BooleanSupplier isVisible = () -> this.getVisibility() == View.VISIBLE;
mToolbarContainer.setPostInitializationDependencies(
mToolbar,
constraintsSupplier,
tabSupplier,
compositorInMotionSupplier,
browserStateBrowserControlsVisibilityDelegate,
isVisible,
layoutStateProviderSupplier,
fullscreenManager,
() -> mMidVisibilityToggle);
View toolbarView = findViewById(R.id.toolbar);
assert toolbarView != null;
if (toolbarView instanceof ToolbarTablet) {
// On tablet, draw a fake tab strip and toolbar until the compositor is
// ready to draw the real tab strip. (On phone, the toolbar is made entirely
// of Android views, which are already initialized.)
maybeUpdateTempTabStripDrawableBackground(isIncognito, mAppHeaderState);
// Manually setting the top margin of the toolbar hairline. On high density tablets,
// the rounding for dp -> px conversion can cause off-by-one error for the toolbar
// hairline top margin, result in a sequence of top UI misalignment.
// See https://crbug.com/40941027.
final int toolbarLayoutHeight =
getResources().getDimensionPixelSize(R.dimen.toolbar_height_no_shadow);
View toolbarHairline = mToolbarContainer.findViewById(R.id.toolbar_hairline);
var lp = (MarginLayoutParams) toolbarHairline.getLayoutParams();
lp.topMargin = mToolbar.getTabStripHeight() + toolbarLayoutHeight;
toolbarHairline.setLayoutParams(lp);
}
}
@Override
// TODO(crbug.com/40779510): work out why this is causing a lint error
@SuppressWarnings("Override")
public boolean gatherTransparentRegion(Region region) {
// Reset the translation on the control container before attempting to compute the
// transparent region.
float translateY = getTranslationY();
setTranslationY(0);
ViewUtils.gatherTransparentRegionsForOpaqueView(this, region);
setTranslationY(translateY);
return true;
}
/** Invalidate the entire capturing bitmap region. */
public void invalidateBitmap() {
((ToolbarViewResourceAdapter) getToolbarResourceAdapter()).forceInvalidate();
}
/**
* Update whether the control container is ready to have the bitmap representation of
* itself be captured.
*/
public void setReadyForBitmapCapture(boolean ready) {
mToolbarContainer.mReadyForBitmapCapture = ready;
}
/**
* Sets whether the current activity is starting in an unfocused desktop window. This state is
* set exactly once at startup and is not updated thereafter.
*
* @param isAppInUnfocusedDesktopWindow Whether the current activity is in an unfocused desktop
* window.
*/
public void setAppInUnfocusedDesktopWindow(boolean isAppInUnfocusedDesktopWindow) {
// TODO (crbug/337132433): Observe window focus state changes to update this state.
mIsAppInUnfocusedDesktopWindow = isAppInUnfocusedDesktopWindow;
}
/**
* Sets drag listener for toolbar container.
*
* @param toolbarContainerDragListener Listener to set.
*/
public void setToolbarContainerDragListener(OnDragListener toolbarContainerDragListener) {
mToolbarContainerDragListener = toolbarContainerDragListener;
mToolbarContainer.setOnDragListener(mToolbarContainerDragListener);
}
/** The layout that handles generating the toolbar view resource. */
// Only publicly visible due to lint warnings.
public static class ToolbarViewResourceFrameLayout extends ViewResourceFrameLayout {
@Nullable private BooleanSupplier mIsMidVisibilityToggle;
private boolean mReadyForBitmapCapture;
public ToolbarViewResourceFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected ViewResourceAdapter createResourceAdapter() {
return new ToolbarViewResourceAdapter(this);
}
/**
* @see ToolbarViewResourceAdapter#setPostInitializationDependencies.
*/
public void setPostInitializationDependencies(
Toolbar toolbar,
ObservableSupplier<Integer> constraintsSupplier,
Supplier<Tab> tabSupplier,
ObservableSupplier<Boolean> compositorInMotionSupplier,
BrowserStateBrowserControlsVisibilityDelegate
browserStateBrowserControlsVisibilityDelegate,
BooleanSupplier isVisible,
OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier,
FullscreenManager fullscreenManager,
BooleanSupplier isMidVisibilityToggle) {
mIsMidVisibilityToggle = isMidVisibilityToggle;
ToolbarViewResourceAdapter adapter =
((ToolbarViewResourceAdapter) getResourceAdapter());
adapter.setPostInitializationDependencies(
toolbar,
constraintsSupplier,
tabSupplier,
compositorInMotionSupplier,
browserStateBrowserControlsVisibilityDelegate,
isVisible,
layoutStateProviderSupplier,
fullscreenManager);
}
@Override
protected boolean isReadyForCapture() {
// This method is checked when invalidateChildInParent happens. Returning false will
// prevent the dirty bit from being set in ViewResourceAdapter. This is what we want
// when the visibility of this view is being toggled. Many of our children report
// material changes that propagate back up. But we don't care about any of this for
// capturing as the captures occur below this frame layout.
return mReadyForBitmapCapture
&& getVisibility() == VISIBLE
&& !mIsMidVisibilityToggle.getAsBoolean();
}
}
@VisibleForTesting
protected static class ToolbarViewResourceAdapter extends ViewResourceAdapter {
/**
* Emitted at various points during the in motion observer method. Note that it is not the
* toolbar that is in motion, but the toolbar's handling of the compositor being in motion.
* Treat this list as append only and keep it in sync with ToolbarInMotionStage in
* enums.xml.
*/
@IntDef({
ToolbarInMotionStage.SUPPRESSION_ENABLED,
ToolbarInMotionStage.READINESS_CHECKED,
ToolbarInMotionStage.NUM_ENTRIES
})
@Retention(RetentionPolicy.SOURCE)
@interface ToolbarInMotionStage {
int SUPPRESSION_ENABLED = 0;
int READINESS_CHECKED = 1;
int NUM_ENTRIES = 2;
}
private final int[] mTempPosition = new int[2];
private final Rect mLocationBarRect = new Rect();
private final Rect mToolbarRect = new Rect();
private final View mToolbarContainer;
private final View mToolbarHairline;
private final Callback<Boolean> mOnCompositorInMotionChange =
this::onCompositorInMotionChange;
@Nullable private Toolbar mToolbar;
private int mTabStripHeightPx;
@Nullable private ConstraintsChecker mConstraintsObserver;
@Nullable private Supplier<Tab> mTabSupplier;
@Nullable private ObservableSupplier<Boolean> mCompositorInMotionSupplier;
@Nullable
private BrowserStateBrowserControlsVisibilityDelegate
mBrowserStateBrowserControlsVisibilityDelegate;
@Nullable private BooleanSupplier mControlContainerIsVisibleSupplier;
@Nullable private LayoutStateProvider mLayoutStateProvider;
@Nullable private FullscreenManager mFullscreenManager;
private int mControlsToken = TokenHolder.INVALID_TOKEN;
private boolean mNeedCaptureAfterPageLoad;
/** Builds the resource adapter for the toolbar. */
public ToolbarViewResourceAdapter(View toolbarContainer) {
super(toolbarContainer);
mToolbarContainer = toolbarContainer;
mToolbarHairline = mToolbarContainer.findViewById(R.id.toolbar_hairline);
}
/**
* Set the toolbar after it has been dynamically inflated.
*
* @param toolbar The browser's toolbar.
* @param constraintsSupplier Used to access current constraints of the browser controls.
* @param tabSupplier Used to access the current tab state.
* @param compositorInMotionSupplier Whether there is an ongoing touch or gesture.
* @param browserStateBrowserControlsVisibilityDelegate Used to keep controls locked when
* captures are stale and not able to be taken.
* @param controlContainerIsVisibleSupplier Whether the toolbar is visible.
* @param layoutStateProviderSupplier Used to check the current layout type.
* @param fullscreenManager Used to check whether in fullscreen.
*/
public void setPostInitializationDependencies(
Toolbar toolbar,
ObservableSupplier<Integer> constraintsSupplier,
Supplier<Tab> tabSupplier,
ObservableSupplier<Boolean> compositorInMotionSupplier,
BrowserStateBrowserControlsVisibilityDelegate
browserStateBrowserControlsVisibilityDelegate,
BooleanSupplier controlContainerIsVisibleSupplier,
OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier,
FullscreenManager fullscreenManager) {
assert mToolbar == null;
mToolbar = toolbar;
mTabStripHeightPx = mToolbar.getTabStripHeight();
// These dependencies only matter when ChromeFeatureList.SUPPRESS_TOOLBAR_CAPTURES is
// enabled. Unfortunately this method is often called before native is initialized,
// and so we do not know if we'll need them yet. Store all of them, and then
// conditionally use them when captures are requested.
mConstraintsObserver =
new ConstraintsChecker(this, constraintsSupplier, Looper.getMainLooper());
mTabSupplier = tabSupplier;
mCompositorInMotionSupplier = compositorInMotionSupplier;
mCompositorInMotionSupplier.addObserver(mOnCompositorInMotionChange);
mBrowserStateBrowserControlsVisibilityDelegate =
browserStateBrowserControlsVisibilityDelegate;
mControlContainerIsVisibleSupplier = controlContainerIsVisibleSupplier;
layoutStateProviderSupplier.onAvailable(
(layoutStateProvider) -> mLayoutStateProvider = layoutStateProvider);
mFullscreenManager = fullscreenManager;
}
/**
* Force this resource to be recaptured in full, ignoring the checks
* {@link #invalidate(Rect)} does.
*/
public void forceInvalidate() {
super.invalidate(null);
}
@Override
public boolean isDirty() {
if (!super.isDirty()) {
CaptureReadinessResult.logCaptureReasonFromResult(
CaptureReadinessResult.notReady(
TopToolbarBlockCaptureReason.VIEW_NOT_DIRTY));
return false;
}
if (ToolbarFeatures.shouldSuppressCaptures()) {
if (ChromeFeatureList.sShouldBlockCapturesForFullscreenParam.getValue()
&& mFullscreenManager.getPersistentFullscreenMode()) {
// The toolbar is never shown during fullscreen, so no point in capturing. The
// dimensions are likely wrong and will only be restored after fullscreen is
// exited.
CaptureReadinessResult.logCaptureReasonFromResult(
CaptureReadinessResult.notReady(
TopToolbarBlockCaptureReason.FULLSCREEN));
return false;
}
final @LayoutType int layoutType = getCurrentLayoutType();
if (layoutType != LayoutType.TOOLBAR_SWIPE) {
// With BCIV enabled, we need a capture after page load before the controls are
// unlocked. So, only go into this section that potentially blocks the capture
// if we didn't just load a page.
if (!mNeedCaptureAfterPageLoad
&& mConstraintsObserver != null
&& mTabSupplier != null) {
Tab tab = mTabSupplier.get();
// TODO(crbug.com/40859837): Understand and fix this for native
// pages. It seems capturing is required for some part of theme observers to
// work correctly, but it shouldn't be.
boolean isNativePage = tab == null || tab.isNativePage();
if (!isNativePage && mConstraintsObserver.areControlsLocked()) {
mConstraintsObserver.scheduleRequestResourceOnUnlock();
CaptureReadinessResult.logCaptureReasonFromResult(
CaptureReadinessResult.notReady(
TopToolbarBlockCaptureReason.BROWSER_CONTROLS_LOCKED));
return false;
}
}
// The heavy lifting is done by #onCompositorInMotionChange and the above
// browser controls state check. This logic only needs to guard against a
// capture when the controls were partially or fully scrolled off, in the middle
// of motion, before the view became dirty.
if (mCompositorInMotionSupplier != null) {
Boolean compositorInMotion = mCompositorInMotionSupplier.get();
if (Boolean.TRUE.equals(compositorInMotion)) {
CaptureReadinessResult.logCaptureReasonFromResult(
CaptureReadinessResult.notReady(
TopToolbarBlockCaptureReason.COMPOSITOR_IN_MOTION));
return false;
}
}
}
}
return checkCaptureReadinessResult();
}
/**
* @return Whether a dirty check for invalidation makes sense at this time.
* <p>False if either the toolbar is not dirty, or the toolbar is dirty but a capture
* isn't required at this moment (see {@link TopToolbarBlockCaptureReason})
* <p>True if the toolbar is dirty and a new capture is needed.
*/
private boolean checkCaptureReadinessResult() {
CaptureReadinessResult isReadyResult =
mToolbar == null ? null : mToolbar.isReadyForTextureCapture();
if (isReadyResult != null
&& isReadyResult.blockReason == TopToolbarBlockCaptureReason.SNAPSHOT_SAME) {
// If our view was invalidated but no meaningful properties have changed (which is
// what SNAPSHOT_SAME implies), we can safely avoid re-checking until the next view
// invalidation.
setDirtyRectEmpty();
}
CaptureReadinessResult.logCaptureReasonFromResult(isReadyResult);
return isReadyResult == null ? false : isReadyResult.isReady;
}
@Override
public void onCaptureStart(Canvas canvas, Rect dirtyRect) {
RecordHistogram.recordEnumeratedHistogram(
"Android.Toolbar.BitmapCapture",
ToolbarCaptureType.TOP,
ToolbarCaptureType.NUM_ENTRIES);
// Erase the canvas because assets drawn are not fully opaque and therefore painting
// twice would be bad.
canvas.save();
canvas.clipRect(0, 0, mToolbarContainer.getWidth(), mToolbarContainer.getHeight());
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
canvas.restore();
dirtyRect.set(0, 0, mToolbarContainer.getWidth(), mToolbarContainer.getHeight());
mToolbar.setTextureCaptureMode(true);
super.onCaptureStart(canvas, dirtyRect);
}
@Override
public void onCaptureEnd() {
mToolbar.setTextureCaptureMode(false);
// Forcing a texture capture should only be done for one draw. Turn off forced
// texture capture.
mToolbar.setForceTextureCapture(false);
}
@Override
public long createNativeResource() {
mToolbar.getPositionRelativeToContainer(mToolbarContainer, mTempPosition);
mToolbarRect.set(
mTempPosition[0],
mTempPosition[1],
mToolbarContainer.getWidth(),
mTempPosition[1] + mToolbar.getHeight());
mToolbar.getLocationBarContentRect(mLocationBarRect);
mLocationBarRect.offset(mTempPosition[0], mTempPosition[1]);
int shadowHeight = mToolbarHairline.getHeight();
return ResourceFactory.createToolbarContainerResource(
mToolbarRect, mLocationBarRect, shadowHeight);
}
public void onPageLoadStopped() {
if (ChromeFeatureList.sBrowserControlsInViz.isEnabled()
&& !ChromeFeatureList.sBcivWithSuppression.isEnabled()) {
// With capture suppression, we don't capture after navigating. Instead, we schedule
// a capture to happen when the controls become unlocked. With BCIV, there is no
// surface sync, so it's more likely to scroll before the capture is complete. To
// fix this, we capture after page load finishes. This is late enough in navigation
// to not delay other important tasks on the main thread, and early enough so we
// have a capture available before the controls are unlocked.
mNeedCaptureAfterPageLoad = true;
onResourceRequested();
mNeedCaptureAfterPageLoad = false;
}
}
public void destroy() {
if (mConstraintsObserver != null) {
mConstraintsObserver.destroy();
}
if (mCompositorInMotionSupplier != null) {
mCompositorInMotionSupplier.removeObserver(mOnCompositorInMotionChange);
}
}
private void onCompositorInMotionChange(Boolean compositorInMotion) {
if (!ToolbarFeatures.shouldSuppressCaptures()
|| mToolbar == null
|| mBrowserStateBrowserControlsVisibilityDelegate == null
|| mControlContainerIsVisibleSupplier == null) {
return;
}
if (ToolbarFeatures.shouldRecordSuppressionMetrics()) {
RecordHistogram.recordEnumeratedHistogram(
"Android.TopToolbar.InMotionStage",
ToolbarInMotionStage.SUPPRESSION_ENABLED,
ToolbarInMotionStage.NUM_ENTRIES);
}
if (!Boolean.TRUE.equals(compositorInMotion)) {
if (mControlsToken == TokenHolder.INVALID_TOKEN) {
// Only needed when the ConstraintsChecker doesn't drive the capture.
// TODO(crbug.com/40244055): Make this post a task similar to
// ConstraintsChecker.
onResourceRequested();
} else {
mBrowserStateBrowserControlsVisibilityDelegate.releasePersistentShowingToken(
mControlsToken);
mControlsToken = TokenHolder.INVALID_TOKEN;
}
} else if (super.isDirty() && mControlContainerIsVisibleSupplier.getAsBoolean()) {
CaptureReadinessResult captureReadinessResult = mToolbar.isReadyForTextureCapture();
if (ToolbarFeatures.shouldRecordSuppressionMetrics()
&& compositorInMotion != null) {
RecordHistogram.recordEnumeratedHistogram(
"Android.TopToolbar.InMotionStage",
ToolbarInMotionStage.READINESS_CHECKED,
ToolbarInMotionStage.NUM_ENTRIES);
}
if (captureReadinessResult.blockReason
== TopToolbarBlockCaptureReason.SNAPSHOT_SAME) {
setDirtyRectEmpty();
} else if (captureReadinessResult.isReady) {
// Motion is starting, and we don't have a good capture. Lock the controls so
// that a new capture doesn't happen and the old capture is not shown. This can
// be fixed once the motion is over.
mControlsToken =
mBrowserStateBrowserControlsVisibilityDelegate
.showControlsPersistentAndClearOldToken(mControlsToken);
// Utilize posted task in ConstraintsChecker to drive new capture.
mConstraintsObserver.scheduleRequestResourceOnUnlock();
CaptureReadinessResult.logCaptureReasonFromResult(
CaptureReadinessResult.notReady(
TopToolbarBlockCaptureReason.COMPOSITOR_IN_MOTION));
}
}
}
private @LayoutType int getCurrentLayoutType() {
return mLayoutStateProvider == null
? LayoutType.NONE
: mLayoutStateProvider.getActiveLayoutType();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Don't eat the event if we don't have a handler.
if (mSwipeGestureListener == null) return false;
// Don't react on touch events if the toolbar container is not fully visible.
if (!isToolbarContainerFullyVisible()) return true;
// If we have ACTION_DOWN in this context, that means either no child consumed the event or
// this class is the top UI at the event position. Then, we don't need to feed the event to
// mGestureDetector here because the event is already once fed in onInterceptTouchEvent().
// Moreover, we have to return true so that this class can continue to intercept all the
// subsequent events.
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && !isOnTabStrip(event)) {
return true;
}
return mSwipeGestureListener.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!isToolbarContainerFullyVisible()) return true;
if (mSwipeGestureListener == null || isOnTabStrip(event)) return false;
return mSwipeGestureListener.onTouchEvent(event);
}
private boolean isOnTabStrip(MotionEvent e) {
return e.getY() <= mToolbar.getTabStripHeight();
}
/**
* @return Whether or not the toolbar container is fully visible on screen.
*/
private boolean isToolbarContainerFullyVisible() {
return Float.compare(0f, getTranslationY()) == 0
&& mToolbarContainer.getVisibility() == VISIBLE;
}
private class SwipeGestureListenerImpl extends SwipeGestureListener {
public SwipeGestureListenerImpl(Context context, SwipeHandler handler) {
super(context, handler);
}
@Override
public boolean shouldRecognizeSwipe(MotionEvent e1, MotionEvent e2) {
if (isOnTabStrip(e1)) return false;
if (mToolbar != null && mToolbar.shouldIgnoreSwipeGesture()) return false;
if (KeyboardVisibilityDelegate.getInstance()
.isKeyboardShowing(getContext(), ToolbarControlContainer.this)) {
return false;
}
return true;
}
}
void setToolbarForTesting(Toolbar testToolbar) {
mToolbar = testToolbar;
}
}