// 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.compositor.bottombar.contextualsearch;
import android.app.Activity;
import android.content.Context;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ActivityState;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.CompositorViewHolder;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelContent;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager.PanelPriority;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.compositor.scene_layer.ContextualSearchSceneLayer;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchManagementDelegate;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchUma;
import org.chromium.chrome.browser.contextualsearch.ResolvedSearchTerm.CardTag;
import org.chromium.chrome.browser.layouts.scene_layer.SceneOverlayLayer;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ToolbarManager;
import org.chromium.chrome.browser.toolbar.top.ToolbarLayout;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeController;
import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
import org.chromium.components.browser_ui.widget.scrim.ScrimProperties;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.util.ColorUtils;
import java.util.List;
/**
* Controls the Contextual Search Panel, primarily the Bar - the {@link ContextualSearchBarControl}
* - and the content area that shows the Search Result.
*/
public class ContextualSearchPanel extends OverlayPanel {
/** Allows controls that appear in this panel to call back with requests or notifications. */
interface ContextualSearchPanelSectionHost {
/** Returns the current Y position of the panel section. */
float getYPositionPx();
/** Notifies the panel that the caller's section is changing its size. */
void onPanelSectionSizeChange(boolean hasStarted);
}
/** The interface that the Opt-in promo uses to communicate with this Panel. */
interface ContextualSearchPromoHost extends ContextualSearchPanelSectionHost {
/** Notifies the host that the promo was shown. */
void onPromoShown();
/** Notifies the host whether the user enabled the feature via the promotion. */
void setContextualSearchPromoCardSelection(boolean enabled);
}
/** The interface that the Related Searches section uses to communicate with this Panel. */
interface RelatedSearchesSectionHost extends ContextualSearchPanelSectionHost {
/**
* Notifies that the user has clicked on a suggestions in this section of the panel.
* @param suggestionIndex The 0-based index into the list of suggestions provided by the
* panel and presented in the UI. E.g. if the user clicked the second chit this value
* would be 1.
*/
void onSuggestionClicked(int suggestionIndex);
}
/** Restricts the maximized panel height to the given fraction of a tab. */
private static final float MAXIMIZED_HEIGHT_FRACTION = 0.95f;
/** Used for logging state changes. */
private final ContextualSearchPanelMetrics mPanelMetrics;
/** Used to query toolbar state. */
private final ToolbarManager mToolbarManager;
/** The distance of the divider from the end of the bar, in dp. */
private final float mEndButtonWidthDp;
/** Whether the contextual search panel can be promoted to a new tab. */
private final boolean mCanPromoteToNewTab;
/** Supplies a {@link EdgeToEdgeController} that adjusts for more screen-bottom space. */
private Supplier<EdgeToEdgeController> mEdgeToEdgeControllerSupplier;
/** Whether the Panel should be promoted to a new tab after being maximized. */
private boolean mShouldPromoteToTabAfterMaximizing;
/** The object for handling global Contextual Search management duties */
private ContextualSearchManagementDelegate mManagementDelegate;
/** Whether the content view has been touched. */
private boolean mHasContentBeenTouched;
/** The compositor layer used for drawing the panel. */
private ContextualSearchSceneLayer mSceneLayer;
/**
* A ScrimCoordinator for adjusting the Status Bar's brightness when a scrim is present (when
* the panel is open).
*/
private ScrimCoordinator mScrimCoordinator;
/**
* Params that configure our use of the ScrimCoordinator for adjusting the Status Bar's
* brightness when a scrim is present (when the panel is open).
*/
private PropertyModel mScrimProperties;
/** Whether we have started collapsing the panel. */
private boolean mDidStartCollapsing;
// ============================================================================================
// Constructor
// ============================================================================================
/**
* @param context The current Android {@link Context}.
* @param layoutManager A layout manager for observing scene changes.
* @param panelManager The object managing the how different panels are shown.
* @param browserControlsStateProvider Used to measure the browser controls.
* @param windowAndroid The {@link WindowAndroid} for the current activity.
* @param profile The Profile this ContextualSearchPanel is associated with.
* @param compositorViewHolder The {@link CompositorViewHolder} for the current activity.
* @param toolbarHeightDp The height of the toolbar in dp.
* @param toolbarManager The {@link ToolbarManager}, used to query for colors.
* @param canPromoteToNewTab Whether the panel can be promoted to a new tab.
* @param currentTabSupplier Supplies the current activity tab.
* @param edgeToEdgeControllerSupplier Controller for edge-to-edge drawing.
*/
public ContextualSearchPanel(
@NonNull Context context,
@NonNull LayoutManagerImpl layoutManager,
@NonNull OverlayPanelManager panelManager,
@NonNull BrowserControlsStateProvider browserControlsStateProvider,
@NonNull WindowAndroid windowAndroid,
@NonNull Profile profile,
@NonNull CompositorViewHolder compositorViewHolder,
float toolbarHeightDp,
@NonNull ToolbarManager toolbarManager,
boolean canPromoteToNewTab,
@NonNull Supplier<Tab> currentTabSupplier,
@NonNull Supplier<EdgeToEdgeController> edgeToEdgeControllerSupplier) {
super(
context,
layoutManager,
panelManager,
browserControlsStateProvider,
windowAndroid,
profile,
compositorViewHolder,
toolbarHeightDp,
currentTabSupplier);
mSceneLayer = createNewContextualSearchSceneLayer();
mPanelMetrics = new ContextualSearchPanelMetrics();
mToolbarManager = toolbarManager;
mCanPromoteToNewTab = canPromoteToNewTab;
mEdgeToEdgeControllerSupplier = edgeToEdgeControllerSupplier;
mEndButtonWidthDp =
mContext.getResources()
.getDimensionPixelSize(
R.dimen.contextual_search_padded_button_width)
* mPxToDp;
}
@Override
public OverlayPanelContent createNewOverlayPanelContent() {
return new OverlayPanelContent(
mManagementDelegate.getOverlayPanelContentDelegate(),
new PanelProgressObserver(),
mActivity,
getProfile(),
getBarHeight(),
getCompositorViewHolder(),
getWindowAndroid(),
getCurrentTabSupplier());
}
// ============================================================================================
// Scene Overlay
// ============================================================================================
/** Create a new scene layer for this panel. This should be overridden by tests as necessary. */
protected ContextualSearchSceneLayer createNewContextualSearchSceneLayer() {
return new ContextualSearchSceneLayer(
getProfile(), mContext.getResources().getDisplayMetrics().density);
}
@Override
public SceneOverlayLayer getUpdatedSceneOverlayTree(
RectF viewport, RectF visibleViewport, ResourceManager resourceManager, float yOffset) {
super.getUpdatedSceneOverlayTree(viewport, visibleViewport, resourceManager, yOffset);
mSceneLayer.update(
resourceManager,
this,
getSearchBarControl(),
getPromoControl(),
getRelatedSearchesInBarControl(),
getImageControl());
return mSceneLayer;
}
// ============================================================================================
// Contextual Search Manager Integration
// ============================================================================================
/**
* Sets the {@code ContextualSearchManagementDelegate} associated with this panel.
*
* @param delegate The {@code ContextualSearchManagementDelegate}.
*/
public void setManagementDelegate(ContextualSearchManagementDelegate delegate) {
if (mManagementDelegate != delegate) {
mManagementDelegate = delegate;
if (delegate != null) {
setActivity(mManagementDelegate.getActivity());
}
}
}
/**
* Notifies that the preference state has changed.
*
* @param isEnabled Whether the feature is enabled.
*/
public void onContextualSearchPrefChanged(boolean isEnabled) {
if (!isShowing()) return;
getPromoControl().onContextualSearchPrefChanged(isEnabled);
}
// ============================================================================================
// Panel State
// ============================================================================================
@Override
public void setPanelState(@PanelState int toState, @StateChangeReason int reason) {
@PanelState int fromState = getPanelState();
mPanelMetrics.onPanelStateChanged(fromState, toState, reason, getProfile());
if (toState == PanelState.CLOSED || toState == PanelState.UNDEFINED) {
mManagementDelegate.onPanelFinishedShowing();
}
super.setPanelState(toState, reason);
mDidStartCollapsing = false;
}
@Override
protected @PanelState int getProjectedState(float velocity) {
@PanelState int projectedState = super.getProjectedState(velocity);
// Prevent the fling gesture from moving the Panel from PEEKED to MAXIMIZED. This is to
// make sure the Promo will be visible, considering that the EXPANDED state is the only
// one that will show the Promo.
if (getPromoControl().isVisible()
&& projectedState == PanelState.MAXIMIZED
&& getPanelState() == PanelState.PEEKED) {
projectedState = PanelState.EXPANDED;
}
// If we're swiping the panel down from MAXIMIZED skip the EXPANDED state and go all the
// way to PEEKED.
if (getPanelState() == PanelState.MAXIMIZED && projectedState == PanelState.EXPANDED) {
projectedState = PanelState.PEEKED;
}
return projectedState;
}
@Override
public boolean onBackPressed() {
if (!isShowing()) return false;
mManagementDelegate.hideContextualSearch(StateChangeReason.BACK_PRESS);
return true;
}
// ============================================================================================
// Contextual Search Manager Integration
// ============================================================================================
@Override
protected void onClosed(@StateChangeReason int reason) {
// Must be called before destroying Content because unseen visits should be removed from
// history, and if the Content gets destroyed there won't be a Webcontents to do that.
mManagementDelegate.onCloseContextualSearch(reason);
setProgressBarCompletion(0);
setProgressBarVisible(false);
getImageControl().hideCustomImage(false);
super.onClosed(reason);
if (mSceneLayer != null) mSceneLayer.hideTree();
if (mScrimCoordinator != null) mScrimCoordinator.hideScrim(false);
mDidStartCollapsing = false;
}
// ============================================================================================
// Generic Event Handling
// ============================================================================================
private boolean isCoordinateInsideActionTarget(float x) {
if (LocalizationUtils.isLayoutRtl()) {
return x >= getContentX() + mEndButtonWidthDp;
} else {
return x <= getContentX() + getWidth() - mEndButtonWidthDp;
}
}
/** Handles a bar click. The position is given in dp. */
@Override
public void handleBarClick(float x, float y) {
getSearchBarControl().onSearchBarClick(x);
if (isPeeking()) {
if (getSearchBarControl().getQuickActionControl().hasQuickAction()
&& isCoordinateInsideActionTarget(x)) {
getSearchBarControl()
.getQuickActionControl()
.sendIntent(getCurrentTabSupplier().get());
} else {
// super takes care of expanding the Panel when peeking.
super.handleBarClick(x, y);
}
} else if (isExpanded() || isMaximized()) {
if (canPromoteToNewTab() && isCoordinateInsideOpenTabButton(x)) {
mManagementDelegate.promoteToTab();
} else {
peekPanel(StateChangeReason.UNKNOWN);
}
}
}
@Override
public boolean onInterceptBarClick() {
return onInterceptOpeningPanel();
}
@Override
public boolean onInterceptBarSwipe() {
return onInterceptOpeningPanel();
}
/**
* @return True if the event on the bar was intercepted.
*/
private boolean onInterceptOpeningPanel() {
if (mManagementDelegate.isRunningInCompatibilityMode()) {
mManagementDelegate.openResolvedSearchUrlInNewTab();
return true;
}
return false;
}
@Override
public void onShowPress(float x, float y) {
if (isCoordinateInsideBar(x, y)) getSearchBarControl().onShowPress(x);
super.onShowPress(x, y);
}
// ============================================================================================
// Panel base methods
// ============================================================================================
@Override
protected void destroyComponents() {
super.destroyComponents();
destroyPromoControl();
destroyInBarRelatedSearchesControl();
destroySearchBarControl();
}
@Override
public void onActivityStateChange(Activity activity, int newState) {
super.onActivityStateChange(activity, newState);
if (newState == ActivityState.PAUSED) {
mManagementDelegate.logCurrentState();
}
}
@Override
public @PanelPriority int getPriority() {
return PanelPriority.HIGH;
}
@Override
public boolean canBeSuppressed() {
// The selected text on the page is lost when the panel is closed, thus, this panel cannot
// be restored if it is suppressed.
return false;
}
@Override
public void notifyBarTouched(float x) {
getOverlayPanelContent().showContent();
}
@Override
public float getOpenTabIconX() {
if (LocalizationUtils.isLayoutRtl()) {
return getOffsetX() + getBarMarginSide();
} else {
return getOffsetX() + getWidth() - getBarMarginSide() - getCloseIconDimension();
}
}
@Override
protected boolean isCoordinateInsideCloseButton(float x) {
return false;
}
@Override
protected boolean isCoordinateInsideOpenTabButton(float x) {
return getOpenTabIconX() - getButtonPaddingDps() <= x
&& x <= getOpenTabIconX() + getOpenTabIconDimension() + getButtonPaddingDps();
}
@Override
public float getContentY() {
return getOffsetY() + getBarContainerHeight() + getPromoHeightPx() * mPxToDp;
}
@Override
public float getBarContainerHeight() {
return getBarHeight();
}
@Override
protected float getPeekedHeight() {
return getBarHeight();
}
@Override
protected float getMaximizedHeight() {
// Max height does not cover the entire content screen.
return getTabHeight() * MAXIMIZED_HEIGHT_FRACTION;
}
@Override
public float getBarMarginBottomPx() {
// When Edge To Edge is enabled and drawing to the bottom edge, pass in the bottom inset
// to pad the search bar (specifically, the caption's bottom padding). Use 0 otherwise.
// TODO(crbug.com/332543636) Remove padding when it's no longer needed in EXPANDED and
// MAXIMIZED states
@Nullable EdgeToEdgeController edgeToEdgeController = mEdgeToEdgeControllerSupplier.get();
return edgeToEdgeController != null ? edgeToEdgeController.getBottomInsetPx() : 0;
}
@Override
public float getBarHeight() {
// If the font is scaled, the preset bar height obtained from super.getBarHeight() may be
// smaller than the height required to display the bar's content. In such cases, it is
// necessary to select the larger value between the preset height and the actual content
// height.
float baseBarHeight = super.getBarHeight();
// When Edge To Edge is enabled and drawing to the bottom edge, increase the base bar height
// to properly account for the extra bottom inset when positioning the peek height. The
// padding will appear in the search bar control min height after a delay, once the view has
// inflated, but that's too late for initial positioning.
if (mEdgeToEdgeControllerSupplier.get() != null) {
baseBarHeight += mEdgeToEdgeControllerSupplier.get().getBottomInset();
}
return Math.max(baseBarHeight, getSearchBarControlMinHeightDps())
+ getInBarRelatedSearchesAnimatedHeightDps();
}
@Override
public void setClampedPanelHeight(float height) {
super.setClampedPanelHeight(height);
}
// ============================================================================================
// Animation Handling
// ============================================================================================
@Override
protected void onHeightAnimationFinished() {
super.onHeightAnimationFinished();
if (mShouldPromoteToTabAfterMaximizing && getPanelState() == PanelState.MAXIMIZED) {
mShouldPromoteToTabAfterMaximizing = false;
mManagementDelegate.promoteToTab();
}
}
@Override
@VisibleForTesting
public void animatePanelToState(
@Nullable @PanelState Integer state, @StateChangeReason int reason, long duration) {
// If the in bar chip showing animation is running, do not run the new panel animation
// unless it needs to animate to a different state.
if (state == getPanelState()
&& haveSearchBarControl()
&& getSearchBarControl().inBarRelatedSearchesAnimationIsRunning()) {
return;
}
if (state == PanelState.PEEKED
&& (getPanelState() == PanelState.EXPANDED
|| getPanelState() == PanelState.MAXIMIZED)) {
mManagementDelegate.onPanelCollapsing();
getRelatedSearchesInBarControl().onPanelCollapsing();
}
super.animatePanelToState(state, reason, duration);
}
// ============================================================================================
// Contextual Search Panel API
// ============================================================================================
/** Notify the panel that the content was seen. */
public void setWasSearchContentViewSeen() {
mPanelMetrics.setWasSearchContentViewSeen();
}
/**
* @param isActive Whether the promo is active.
*/
public void setIsPromoActive(boolean isActive) {
if (isActive) {
getPromoControl().show();
} else {
getPromoControl().hide();
}
mPanelMetrics.setIsPromoActive(isActive);
}
public void clearRelatedSearches() {
getRelatedSearchesInBarControl().hide();
}
/**
* Maximizes the Contextual Search Panel.
* @param reason The {@code StateChangeReason} behind the maximization.
*/
@Override
public void maximizePanel(@StateChangeReason int reason) {
mShouldPromoteToTabAfterMaximizing = false;
super.maximizePanel(reason);
}
/**
* Maximizes the Contextual Search Panel, then promotes it to a regular Tab.
*
* @param reason The {@code StateChangeReason} behind the maximization and promotion to tab.
*/
public void maximizePanelThenPromoteToTab(@StateChangeReason int reason) {
mShouldPromoteToTabAfterMaximizing = true;
super.maximizePanel(reason);
if (reason == StateChangeReason.SERP_NAVIGATION) {
RelatedSearchesControl activeRelatedSearches = getRelatedSearchesInBarControl();
ContextualSearchUma.logSerpResultClicked(
activeRelatedSearches.isShowingRelatedSearchSerp());
}
}
@Override
public void peekPanel(@StateChangeReason int reason) {
super.peekPanel(reason);
if (getPanelState() == PanelState.CLOSED || getPanelState() == PanelState.PEEKED) {
mHasContentBeenTouched = false;
}
if ((getPanelState() == PanelState.UNDEFINED || getPanelState() == PanelState.CLOSED)
&& reason == StateChangeReason.TEXT_SELECT_TAP) {
mPanelMetrics.onPanelTriggeredFromTap();
}
}
@Override
public void closePanel(@StateChangeReason int reason, boolean animate) {
super.closePanel(reason, animate);
mHasContentBeenTouched = false;
if (reason == StateChangeReason.TAB_PROMOTION) {
RelatedSearchesControl activeRelatedSearches = getRelatedSearchesInBarControl();
ContextualSearchUma.logTabPromotion(activeRelatedSearches.isShowingRelatedSearchSerp());
}
}
@Override
public void expandPanel(@StateChangeReason int reason) {
super.expandPanel(reason);
}
@Override
public void requestPanelShow(@StateChangeReason int reason) {
// If a re-tap is causing the panel to show when already shown, the superclass may ignore
// that, but we want to be sure to capture search metrics for each tap.
if (isShowing() && getPanelState() == PanelState.PEEKED) {
peekPanel(reason);
}
super.requestPanelShow(reason);
}
/** Gets whether a touch on the content view has been done yet or not. */
public boolean didTouchContent() {
return mHasContentBeenTouched;
}
/**
* Sets the search term to display in the SearchBar. This should be called when the search term
* is set without search term resolution.
*
* @param searchTerm The string that represents the search term.
*/
public void setSearchTerm(String searchTerm) {
setSearchTerm(searchTerm, null);
}
/**
* Sets the search term to display in the SearchBar. This should be called when the search term
* is set after search term resolution completed.
*
* @param searchTerm The string that represents the search term.
* @param pronunciation A string for the pronunciation when a Definition is shown.
*/
public void setSearchTerm(String searchTerm, @Nullable String pronunciation) {
getImageControl().hideCustomImage(true);
getSearchBarControl().setSearchTerm(searchTerm, pronunciation);
mPanelMetrics.onSearchRequestStarted();
// Make sure the new Search Term draws.
requestUpdate();
}
/**
* Sets the search context details to display in the SearchBar.
*
* @param selection The portion of the context that represents the user's selection.
* @param end The portion of the context from the selection to its end.
*/
public void setContextDetails(String selection, String end) {
getImageControl().hideCustomImage(true);
getSearchBarControl().setContextDetails(selection, end);
mPanelMetrics.onSearchRequestStarted();
// Make sure the new Context draws.
requestUpdate();
}
/**
* Sets the caption to display in the SearchBar. When the caption is displayed, the Search Term
* is pushed up and the caption shows below.
*
* @param caption The string to show in as the caption.
*/
public void setCaption(String caption) {
getSearchBarControl().setCaption(caption);
}
/** Ensures that we have a Caption to display in the SearchBar. */
public void ensureCaption() {
if (getSearchBarControl().hasCaption()) return;
getSearchBarControl()
.setCaption(
mContext.getResources()
.getString(R.string.contextual_search_default_caption));
}
/** Hides the caption. */
public void hideCaption() {
getSearchBarControl().hideCaption();
}
/**
* Handles showing the resolved search term in the SearchBar.
*
* @param searchTerm The string that represents the search term.
* @param thumbnailUrl The URL of the thumbnail to display.
* @param quickActionUri The URI for the intent associated with the quick action.
* @param quickActionCategory The {@code QuickActionCategory} for the quick action.
* @param cardTagEnum The {@link CardTag} that the server returned if there was a card, or
* {@code 0}.
* @param relatedSearchesInBar Related Searches suggestions to be displayed in the Bar.
*/
@VisibleForTesting
public void onSearchTermResolved(
String searchTerm,
String thumbnailUrl,
String quickActionUri,
int quickActionCategory,
@CardTag int cardTagEnum,
@Nullable List<String> relatedSearchesInBar) {
onSearchTermResolved(
searchTerm,
null,
thumbnailUrl,
quickActionUri,
quickActionCategory,
cardTagEnum,
relatedSearchesInBar);
}
/**
* Handles showing the resolved search term in the SearchBar.
*
* @param searchTerm The string that represents the search term.
* @param pronunciation A string for the pronunciation when a Definition is shown.
* @param thumbnailUrl The URL of the thumbnail to display.
* @param quickActionUri The URI for the intent associated with the quick action.
* @param quickActionCategory The {@code QuickActionCategory} for the quick action.
* @param cardTagEnum The {@link CardTag} that the server returned if there was a card, or
* {@code 0}.
* @param relatedSearchesInBar Related Searches suggestions to be displayed in the Bar.
*/
public void onSearchTermResolved(
String searchTerm,
@Nullable String pronunciation,
String thumbnailUrl,
String quickActionUri,
int quickActionCategory,
@CardTag int cardTagEnum,
@Nullable List<String> relatedSearchesInBar) {
boolean hadInBarSuggestions = getRelatedSearchesInBarControl().hasReleatedSearchesToShow();
getRelatedSearchesInBarControl().setRelatedSearchesSuggestions(relatedSearchesInBar);
if (getRelatedSearchesInBarControl().hasReleatedSearchesToShow() != hadInBarSuggestions) {
getSearchBarControl().animateInBarRelatedSearches(!hadInBarSuggestions);
}
if (cardTagEnum == CardTag.CT_DEFINITION
|| cardTagEnum == CardTag.CT_CONTEXTUAL_DEFINITION) {
getSearchBarControl().setVectorDrawableDefinitionIcon();
} else {
getImageControl().setThumbnailUrl(thumbnailUrl);
}
getSearchBarControl().setSearchTerm(searchTerm, pronunciation);
getSearchBarControl().animateSearchTermResolution();
// TODO(donnd): this can probably be removed or changed to an assert.
if (mActivity == null || mToolbarManager == null) return;
getSearchBarControl()
.setQuickAction(
quickActionUri, quickActionCategory, mToolbarManager.getPrimaryColor());
}
/**
* @return The padding used for each side of the button in the Bar.
*/
public float getButtonPaddingDps() {
return mButtonPaddingDps;
}
// ============================================================================================
// Panel Metrics
// ============================================================================================
// TODO(pedrosimonetti): replace proxy methods with direct PanelMetrics usage
/**
* @return The {@link ContextualSearchPanelMetrics}.
*/
public ContextualSearchPanelMetrics getPanelMetrics() {
return mPanelMetrics;
}
/** Sets that the contextual search involved the promo. */
public void setDidSearchInvolvePromo() {
mPanelMetrics.setDidSearchInvolvePromo();
}
// ============================================================================================
// Panel Rendering
// ============================================================================================
// TODO(pedrosimonetti): generalize the dispatching of panel updates.
@Override
protected void updatePanelForCloseOrPeek(float percentage) {
super.updatePanelForCloseOrPeek(percentage);
getPromoControl().onUpdateFromCloseToPeek(percentage);
getRelatedSearchesInBarControl().onUpdateFromCloseToPeek(percentage);
getSearchBarControl().onUpdateFromCloseToPeek(percentage);
mDidStartCollapsing = false;
}
@Override
protected void updatePanelForExpansion(float percentage) {
super.updatePanelForExpansion(percentage);
if (getPanelState() == PanelState.EXPANDED && !mDidStartCollapsing && percentage < 0.5f) {
mDidStartCollapsing = true;
mManagementDelegate.onPanelCollapsing();
getRelatedSearchesInBarControl().onPanelCollapsing();
}
getPromoControl().onUpdateFromPeekToExpand(percentage);
getRelatedSearchesInBarControl().onUpdateFromPeekToExpand(percentage);
getSearchBarControl().onUpdateFromPeekToExpand(percentage);
}
@Override
protected void updatePanelForMaximization(float percentage) {
super.updatePanelForMaximization(percentage);
getPromoControl().onUpdateFromExpandToMaximize(percentage);
getRelatedSearchesInBarControl().onUpdateFromExpandToMaximize(percentage);
}
@Override
protected void updatePanelForSizeChange() {
if (getPromoControl().isVisible()) {
getPromoControl().invalidate(true);
}
if (getRelatedSearchesInBarControl().isVisible()) {
getRelatedSearchesInBarControl().invalidate(true);
}
// NOTE(pedrosimonetti): We cannot tell where the selection will be after the
// orientation change, so we are setting the selection position to zero, which
// means the base page will be positioned in its original state and we won't
// try to keep the selection in view.
updateBasePageSelectionYPx(0.f);
updateBasePageTargetY();
super.updatePanelForSizeChange();
}
@Override
protected void updateStatusBar() {
float maxBrightness = getMaxBasePageBrightness();
float minBrightness = getMinBasePageBrightness();
float basePageBrightness = getBasePageBrightness();
// Compute Status Bar alpha based on the base-page brightness range applied by the Overlay.
// TODO(donnd): Create a full-screen sized view and apply the black_alpha_65 color to get
// an exact match between the scrim and the status bar colors instead of adjusting the
// status bar alpha to approximate the native overlay brightness filter.
// Details in https://crbug.com/848922.
float statusBarAlpha =
(maxBrightness - basePageBrightness) / (maxBrightness - minBrightness);
if (!getCanHideAndroidBrowserControls()) scrimAndroidToolbar(statusBarAlpha);
if (statusBarAlpha == 0.0) {
if (mScrimCoordinator != null) mScrimCoordinator.hideScrim(false);
mScrimProperties = null;
mScrimCoordinator = null;
return;
} else {
mScrimCoordinator = mManagementDelegate.getScrimCoordinator();
if (mScrimProperties == null) {
mScrimProperties =
new PropertyModel.Builder(ScrimProperties.REQUIRED_KEYS)
.with(ScrimProperties.TOP_MARGIN, 0)
.with(ScrimProperties.AFFECTS_STATUS_BAR, true)
.with(ScrimProperties.ANCHOR_VIEW, getCompositorViewHolder())
.with(ScrimProperties.SHOW_IN_FRONT_OF_ANCHOR_VIEW, false)
.with(ScrimProperties.VISIBILITY_CALLBACK, null)
.with(ScrimProperties.CLICK_DELEGATE, null)
.build();
mScrimCoordinator.showScrim(mScrimProperties);
}
mScrimCoordinator.setAlpha(statusBarAlpha);
}
}
private void scrimAndroidToolbar(float scrimFraction) {
int toolbarColor = mToolbarManager.getToolbar().getPrimaryColor();
if (scrimFraction > 0.f) {
toolbarColor = getScrimmedColor(mActivity, toolbarColor, scrimFraction);
}
ToolbarLayout toolbarLayout = mActivity.findViewById(R.id.toolbar);
ColorDrawable toolbarBackground = (ColorDrawable) toolbarLayout.getBackground();
toolbarBackground.setColor(toolbarColor);
scrimImage(R.id.drag_handlebar, R.color.drag_handlebar_color_baseline, scrimFraction);
scrimImage(R.id.toolbar_hairline, R.color.divider_line_bg_color_baseline, scrimFraction);
}
private void scrimImage(int viewId, int colorId, float scrimFraction) {
ImageView view = mActivity.findViewById(viewId);
if (view == null) return;
int baseColor = mActivity.getColor(colorId);
if (scrimFraction > 0.f) {
view.setColorFilter(getScrimmedColor(mActivity, baseColor, scrimFraction));
} else {
view.clearColorFilter();
}
}
private static @ColorInt int getScrimmedColor(
Context context, @ColorInt int baseColor, float scrimFraction) {
@ColorInt int scrimColor = context.getColor(R.color.default_scrim_color);
return ColorUtils.overlayColor(baseColor, scrimColor, scrimFraction);
}
// ============================================================================================
// Selection position
// ============================================================================================
/** The approximate Y coordinate of the selection in pixels. */
private float mBasePageSelectionYPx = -1.f;
/**
* Updates the coordinate of the existing selection.
*
* @param y The y coordinate of the selection in pixels.
*/
public void updateBasePageSelectionYPx(float y) {
mBasePageSelectionYPx = y;
}
@Override
protected float calculateBasePageDesiredOffset() {
float offset = 0.f;
if (mBasePageSelectionYPx > 0.f) {
// Convert from px to dp.
final float selectionY = mBasePageSelectionYPx * mPxToDp;
// Calculate the offset to center the selection on the available area.
final float availableHeight = getTabHeight() - getExpandedHeight();
offset = -selectionY + availableHeight / 2;
offset += getLayoutOffsetYDps();
}
return offset;
}
// ============================================================================================
// ContextualSearchBarControl
// ============================================================================================
private ContextualSearchBarControl mSearchBarControl;
/**
* Creates the ContextualSearchBarControl, if needed. The Views are set to INVISIBLE, because
* they won't actually be displayed on the screen (their snapshots will be displayed instead).
*/
public ContextualSearchBarControl getSearchBarControl() {
if (mSearchBarControl == null) {
mSearchBarControl =
new ContextualSearchBarControl(this, mContext, mContainerView, mResourceLoader);
}
return mSearchBarControl;
}
/** Destroys the ContextualSearchBarControl. */
protected void destroySearchBarControl() {
if (mSearchBarControl != null) {
mSearchBarControl.destroy();
mSearchBarControl = null;
}
}
/** Returns whether we currently have a Search Bar created. */
private boolean haveSearchBarControl() {
return mSearchBarControl != null;
}
/** Returns the search bar's minimum required height. */
private float getSearchBarControlMinHeightDps() {
return mSearchBarControl == null ? 0 : mSearchBarControl.getMinHeightDps();
}
// ============================================================================================
// Image Control
// ============================================================================================
/**
* @return The {@link ContextualSearchImageControl} for the panel.
*/
public ContextualSearchImageControl getImageControl() {
return getSearchBarControl().getImageControl();
}
// ============================================================================================
// Promo
// ============================================================================================
private ContextualSearchPromoControl mPromoControl;
private ContextualSearchPromoHost mPromoHost;
/**
* @return Height of the promo in pixels.
*/
private float getPromoHeightPx() {
return getPromoControl().getHeightPx();
}
/** Creates the ContextualSearchPromoControl, if needed. */
private ContextualSearchPromoControl getPromoControl() {
if (mPromoControl == null) {
mPromoControl =
new ContextualSearchPromoControl(
this,
getContextualSearchPromoHost(),
mContext,
getCoordinatorView(),
mResourceLoader);
}
return mPromoControl;
}
/** Destroys the ContextualSearchPromoControl. */
private void destroyPromoControl() {
if (mPromoControl != null) {
mPromoControl.destroy();
mPromoControl = null;
}
}
/**
* @return An implementation of {@link ContextualSearchPromoHost}.
*/
private ContextualSearchPromoHost getContextualSearchPromoHost() {
if (mPromoHost == null) {
// Create a handler for callbacks from the Opt-in promo.
mPromoHost =
new ContextualSearchPromoHost() {
@Override
public float getYPositionPx() {
// Needs to enumerate anything that can appear above it in the panel.
return Math.round((getOffsetY() + getBarContainerHeight()) / mPxToDp);
}
@Override
public void onPanelSectionSizeChange(boolean hasStarted) {
// The promo section is causing movement, but since there's nothing
// below it we don't need to do anything.
}
@Override
public void onPromoShown() {
mManagementDelegate.onPromoShown();
}
@Override
public void setContextualSearchPromoCardSelection(boolean enabled) {
mManagementDelegate.setContextualSearchPromoCardSelection(enabled);
}
};
}
return mPromoHost;
}
private ViewGroup getCoordinatorView() {
ViewGroup result = mContainerView;
// Use the coordinator inside of the container if we can get it. See crbug.com/1258902.
ViewGroup coordinator = mContainerView.findViewById(org.chromium.chrome.R.id.coordinator);
// Returns null in tests. TODO(donnd): figure out why - tests should have the same views.
if (coordinator != null) result = coordinator;
return result;
}
// ============================================================================================
// The Related Searches Control that appears in the Bar
// ============================================================================================
private RelatedSearchesControl mRelatedSearchesInBarControl;
private RelatedSearchesSectionHost mRelatedSearchesInBarHost;
/** Creates the RelatedSearchesControl to be shown in the Bar, if needed. */
@VisibleForTesting
public RelatedSearchesControl getRelatedSearchesInBarControl() {
if (mRelatedSearchesInBarControl == null) {
mRelatedSearchesInBarControl =
new RelatedSearchesControl(
this,
getRelatedSearchesInBarHost(),
mContext,
getCoordinatorView(),
mResourceLoader);
}
return mRelatedSearchesInBarControl;
}
/**
* @return Height of the Related Searches UI as currently show right inside the Bar, in DPs.
*/
public float getInBarRelatedSearchesAnimatedHeightDps() {
return haveSearchBarControl()
? getSearchBarControl().getInBarRelatedSearchesAnimatedHeightDps()
: 0.f;
}
/**
* Returns the amount of padding that is redundant between the Related Searches carousel that is
* shown in the Bar with the content above it. The content above has its own padding that
* provides a space between it and the bottom of the Bar. So when the Bar grows to include the
* Related Searches (which has its own padding above and below) there is redundant padding.
* @return The amount of overlap of padding values that can be removed (in pixels).
*/
public float getInBarRelatedSearchesRedundantPadding() {
return getRelatedSearchesInBarControl().getRedundantPadding();
}
/**
* @return Total height of this section of the Bar in DPs (once fully exposed by animation).
*/
float getInBarRelatedSearchesMaximumHeightDps() {
return getRelatedSearchesInBarControl().getMaximumHeightPx() * mPxToDp;
}
/** Destroys the RelatedSearchesControl. */
private void destroyInBarRelatedSearchesControl() {
if (mRelatedSearchesInBarControl != null) {
mRelatedSearchesInBarControl.destroy();
mRelatedSearchesInBarControl = null;
}
}
/**
* @return An implementation of {@link RelatedSearchesSectionHost}.
*/
private RelatedSearchesSectionHost getRelatedSearchesInBarHost() {
if (mRelatedSearchesInBarHost == null) {
mRelatedSearchesInBarHost =
new RelatedSearchesSectionHost() {
@Override
public float getYPositionPx() {
// Position the carousel at the bottom part of the bar as it animates to
// a taller size.
return Math.round(
(getOffsetY()
+ getBarContainerHeight()
- getInBarRelatedSearchesAnimatedHeightDps())
/ mPxToDp);
}
@Override
public void onPanelSectionSizeChange(boolean hasStarted) {
// This section currently doesn't change size, so we can ignore this.
}
@Override
public void onSuggestionClicked(int selectionIndex) {
mManagementDelegate.onRelatedSearchesSuggestionClicked(selectionIndex);
}
};
}
return mRelatedSearchesInBarHost;
}
// ============================================================================================
// Panel Content
// ============================================================================================
@Override
public void onTouchSearchContentViewAck() {
mHasContentBeenTouched = true;
}
/**
* Destroy the current content in the panel. NOTE(mdjones): This should not be exposed. The only
* use is in ContextualSearchManager for a bug related to loading new panel content.
*/
public void destroyContent() {
super.destroyOverlayPanelContent();
}
/**
* @return Whether the panel content can be displayed in a new tab.
*/
public boolean canPromoteToNewTab() {
return mCanPromoteToNewTab;
}
// ============================================================================================
// Testing Support
// ============================================================================================
/** Simulates a tap on the panel's end button. */
@VisibleForTesting
public void simulateTapOnEndButton() {
endHeightAnimation();
// Determine the x-position for the simulated tap.
float xPosition;
if (LocalizationUtils.isLayoutRtl()) {
xPosition = getContentX() + (mEndButtonWidthDp / 2);
} else {
xPosition = getContentX() + getWidth() - (mEndButtonWidthDp / 2);
}
// Determine the y-position for the simulated tap.
float yPosition = getOffsetY() + (getHeight() / 2);
// Simulate the tap.
handleClick(xPosition, yPosition);
}
/**
* Updates the panel as if a transition from one state to the given state has just been
* completed. The caller should first set the panel to the supplied "to" state. This method just
* makes the panel notify its subcomponents that the transition has been completed.
* @param panelState The "to" state that has just been completed by the test.
*/
public void updatePanelToStateForTest(@PanelState int panelState) {
// Use a switch to just support the implemented state(s) and fail if others are attempted.
switch (panelState) {
case PanelState.EXPANDED:
updatePanelForExpansion(1.0f);
break;
}
}
@Override
@VisibleForTesting
public boolean getCanHideAndroidBrowserControls() {
return super.getCanHideAndroidBrowserControls();
}
@Override
@VisibleForTesting
public OverlayPanelContent getOverlayPanelContent() {
return super.getOverlayPanelContent();
}
public void setEdgeToEdgeControllerSupplierForTesting(
Supplier<EdgeToEdgeController> edgeToEdgeControllerSupplier) {
mEdgeToEdgeControllerSupplier = edgeToEdgeControllerSupplier;
}
}