blob: e2f2580f4b1cfc9356c2d117ca34a8b9cf972d4c [file] [log] [blame]
// 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.autofill_assistant.trigger_scripts;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Space;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.autofill_assistant.AssistantBottomBarDelegate;
import org.chromium.chrome.browser.autofill_assistant.AssistantBottomSheetContent;
import org.chromium.chrome.browser.autofill_assistant.AssistantRootViewContainer;
import org.chromium.chrome.browser.autofill_assistant.BottomSheetUtils;
import org.chromium.chrome.browser.autofill_assistant.LayoutUtils;
import org.chromium.chrome.browser.autofill_assistant.ScrollToHideGestureListener;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChipViewHolder;
import org.chromium.chrome.browser.autofill_assistant.generic_ui.AssistantDimension;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderCoordinator;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderModel;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController.StateChangeReason;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
import java.util.ArrayList;
import java.util.List;
/**
* Represents the UI for a particular trigger script. Notifies a delegate about user interactions.
*/
public class AssistantTriggerScript {
/** Interface for delegates of this class who will be notified of user interactions. */
public interface Delegate {
void onTriggerScriptAction(@TriggerScriptAction int action);
void onBottomSheetClosedWithSwipe();
boolean onBackButtonPressed();
void onFeedbackButtonClicked();
}
@Nullable
private AssistantBottomSheetContent mContent;
private final Context mContext;
private final Delegate mDelegate;
private final WebContents mWebContents;
private final BottomSheetController mBottomSheetController;
private final BottomSheetObserver mBottomSheetObserver;
private final ObservableSupplierImpl<Integer> mInsetSupplier = new ObservableSupplierImpl<>();
private final ApplicationViewportInsetSupplier mApplicationViewportInsetSupplier;
private AssistantHeaderCoordinator mHeaderCoordinator;
private AssistantHeaderModel mHeaderModel;
private ScrollToHideGestureListener mGestureListener;
private LinearLayout mChipsContainer;
private final int mInnerChipSpacing;
/** Height of the bottom sheet's shadow, used to compute the viewport resize offset. */
private final int mShadowHeight;
/** Whether the visual viewport should be resized while the trigger script is shown. */
private boolean mResizeVisualViewport;
private final List<AssistantChip> mLeftAlignedChips = new ArrayList<>();
private final List<AssistantChip> mRightAlignedChips = new ArrayList<>();
private final List<String> mCancelPopupItems = new ArrayList<>();
private final List<Integer> mCancelPopupActions = new ArrayList<>();
private boolean mAnimateBottomSheet = true;
public AssistantTriggerScript(Context context, Delegate delegate, WebContents webContents,
BottomSheetController controller,
ApplicationViewportInsetSupplier applicationViewportInsetSupplier) {
assert delegate != null;
mContext = context;
mDelegate = delegate;
mWebContents = webContents;
mBottomSheetController = controller;
mApplicationViewportInsetSupplier = applicationViewportInsetSupplier;
mApplicationViewportInsetSupplier.addSupplier(mInsetSupplier);
mBottomSheetObserver = new EmptyBottomSheetObserver() {
@Override
public void onSheetClosed(@StateChangeReason int reason) {
if (reason == StateChangeReason.SWIPE) {
mDelegate.onBottomSheetClosedWithSwipe();
}
}
@Override
public void onSheetContentChanged(@Nullable BottomSheetContent newContent) {
// TODO(crbug.com/806868): Make sure this works and does not interfere with Duet
// once we are in ChromeTabbedActivity.
updateVisualViewportHeight();
}
@Override
public void onSheetOffsetChanged(float heightFraction, float offsetPx) {
updateVisualViewportHeight();
}
};
mInnerChipSpacing = mContext.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_actions_spacing);
mShadowHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.bottom_sheet_toolbar_shadow_height);
}
private void createBottomSheetContents() {
mContent =
new AssistantBottomSheetContent(mContext, () -> new AssistantBottomBarDelegate() {
@Override
public boolean onBackButtonPressed() {
return mDelegate.onBackButtonPressed();
}
@Override
public void onBottomSheetClosedWithSwipe() {
// TODO(micantox): introduce a dedicated AssistantBottomSheetContent
// delegate, rather than reuse the bottom bar delegate, as it forces
// implementation of methods that the bottom sheet will never invoke.
assert false : "This should never happen";
mDelegate.onBottomSheetClosedWithSwipe();
}
});
// Allow swipe-to-dismiss.
mContent.setPeekModeDisabled(true);
mChipsContainer = new LinearLayout(mContext);
mChipsContainer.setOrientation(LinearLayout.HORIZONTAL);
int horizontalMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_bottombar_horizontal_spacing);
int verticalMargin = AssistantDimension.getPixelSizeDp(mContext, 16);
mChipsContainer.setPadding(
horizontalMargin, verticalMargin, horizontalMargin, verticalMargin);
AssistantRootViewContainer rootViewContainer =
(AssistantRootViewContainer) LayoutUtils.createInflater(mContext).inflate(
R.layout.autofill_assistant_bottom_sheet_content, /* root= */ null);
rootViewContainer.disableTalkbackViewResizing();
ScrollView scrollableContent = rootViewContainer.findViewById(R.id.scrollable_content);
rootViewContainer.addView(mHeaderCoordinator.getView(), 0);
rootViewContainer.addView(mChipsContainer,
new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
mContent.setContent(rootViewContainer, scrollableContent);
}
public void destroy() {
disableScrollToHide();
mBottomSheetController.removeObserver(mBottomSheetObserver);
if (mHeaderCoordinator != null) {
mHeaderCoordinator.destroy();
}
mApplicationViewportInsetSupplier.removeSupplier(mInsetSupplier);
}
@VisibleForTesting
public void disableBottomSheetAnimationsForTesting(boolean disable) {
mAnimateBottomSheet = !disable;
}
@VisibleForTesting
public List<AssistantChip> getLeftAlignedChipsForTest() {
return mLeftAlignedChips;
}
@VisibleForTesting
public List<AssistantChip> getRightAlignedChipsForTest() {
return mRightAlignedChips;
}
@VisibleForTesting
public AssistantBottomSheetContent getBottomSheetContentForTest() {
return mContent;
}
private void addChipsToContainer(LinearLayout container, List<AssistantChip> chips) {
for (int i = 0; i < chips.size(); ++i) {
AssistantChipViewHolder viewHolder =
AssistantChipViewHolder.create(container, chips.get(i).getType());
viewHolder.bind(chips.get(i));
container.addView(viewHolder.getView());
if (i < chips.size() - 1) {
container.addView(new Space(mContext),
new LinearLayout.LayoutParams(
mInnerChipSpacing, ViewGroup.LayoutParams.MATCH_PARENT));
}
}
}
public AssistantHeaderModel createHeaderAndGetModel() {
mHeaderModel = new AssistantHeaderModel();
if (mHeaderCoordinator != null) {
mHeaderCoordinator.destroy();
}
mHeaderCoordinator = new AssistantHeaderCoordinator(mContext, mHeaderModel);
mHeaderModel.set(
AssistantHeaderModel.FEEDBACK_BUTTON_CALLBACK, mDelegate::onFeedbackButtonClicked);
return mHeaderModel;
}
/** Binds {@code chips} to {@code actions}. */
private void bindChips(List<AssistantChip> chips, int[] actions) {
assert chips.size() == actions.length;
for (int i = 0; i < chips.size(); ++i) {
int index = i;
if (actions[index] == TriggerScriptAction.SHOW_CANCEL_POPUP) {
chips.get(index).setPopupItems(mCancelPopupItems,
result -> mDelegate.onTriggerScriptAction(mCancelPopupActions.get(result)));
} else {
chips.get(index).setSelectedListener(
() -> mDelegate.onTriggerScriptAction(actions[index]));
}
}
}
public void setLeftAlignedChips(List<AssistantChip> chips, int[] actions) {
assert chips.size() == actions.length;
mLeftAlignedChips.clear();
mLeftAlignedChips.addAll(chips);
bindChips(mLeftAlignedChips, actions);
}
public void setRightAlignedChips(List<AssistantChip> chips, int[] actions) {
assert chips.size() == actions.length;
mRightAlignedChips.clear();
mRightAlignedChips.addAll(chips);
bindChips(mRightAlignedChips, actions);
}
public void setCancelPopupMenu(String[] items, int[] actions) {
assert items.length == actions.length;
mCancelPopupItems.clear();
mCancelPopupActions.clear();
for (int i = 0; i < actions.length; ++i) {
mCancelPopupItems.add(items[i]);
mCancelPopupActions.add(actions[i]);
}
}
public void update() {
mChipsContainer.removeAllViews();
addChipsToContainer(mChipsContainer, mLeftAlignedChips);
mChipsContainer.addView(new Space(mContext),
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f));
addChipsToContainer(mChipsContainer, mRightAlignedChips);
}
public boolean show(boolean resizeVisualViewport, boolean scrollToHide) {
if (mHeaderModel == null || mHeaderCoordinator == null) {
assert false : "createHeaderAndGetModel() must be called before show()";
return false;
}
mResizeVisualViewport = resizeVisualViewport;
createBottomSheetContents();
update();
mBottomSheetController.removeObserver(mBottomSheetObserver);
mBottomSheetController.addObserver(mBottomSheetObserver);
BottomSheetUtils.showContentAndMaybeExpand(mBottomSheetController, mContent,
/* shouldExpand = */ true, /* animate = */ mAnimateBottomSheet);
if (scrollToHide) enableScrollToHide();
return true;
}
public void hide() {
disableScrollToHide();
mBottomSheetController.removeObserver(mBottomSheetObserver);
mBottomSheetController.hideContent(mContent, /* animate = */ mAnimateBottomSheet);
mResizeVisualViewport = false;
updateVisualViewportHeight();
}
private void updateVisualViewportHeight() {
if (!mResizeVisualViewport || mBottomSheetController.getCurrentSheetContent() != mContent) {
setVisualViewportResizing(0);
return;
}
// In order to align the bottom of a website with the top of the bottom sheet, we need to
// remove the shadow height from the sheet's current offset. Note that mShadowHeight is
// different from the sheet controller's getTopShadowHeight().
setVisualViewportResizing(mBottomSheetController.getCurrentOffset() - mShadowHeight);
}
/**
* Shrink the visual viewport by {@code resizing} pixels. 0 will restore original size.
*
* Fork of {@code AssistantBottomBarCoordinator}. TODO(arbesser): refactor, share code.
*/
private void setVisualViewportResizing(int resizing) {
int currentInset = mInsetSupplier.get() != null ? mInsetSupplier.get() : 0;
if (resizing == currentInset || mWebContents == null
|| mWebContents.getRenderWidgetHostView() == null) {
return;
}
mInsetSupplier.set(resizing);
}
private void disableScrollToHide() {
if (mGestureListener == null) return;
GestureListenerManager.fromWebContents(mWebContents).removeListener(mGestureListener);
mGestureListener = null;
}
private void enableScrollToHide() {
if (mGestureListener != null) return;
mGestureListener = new ScrollToHideGestureListener(mBottomSheetController, mContent);
GestureListenerManager.fromWebContents(mWebContents).addListener(mGestureListener);
}
}