blob: fcca1a54ace0b434b0b0868a50a975c7d3f78d54 [file] [log] [blame]
// 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.autofill_assistant;
import android.app.Activity;
import android.content.Context;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantActionsCarouselCoordinator;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantCarouselCoordinator;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantChip;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantSuggestionsCarouselCoordinator;
import org.chromium.chrome.browser.autofill_assistant.details.AssistantDetailsCoordinator;
import org.chromium.chrome.browser.autofill_assistant.form.AssistantFormCoordinator;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderCoordinator;
import org.chromium.chrome.browser.autofill_assistant.header.AssistantHeaderModel;
import org.chromium.chrome.browser.autofill_assistant.infobox.AssistantInfoBoxCoordinator;
import org.chromium.chrome.browser.autofill_assistant.overlay.AssistantOverlayModel;
import org.chromium.chrome.browser.autofill_assistant.overlay.AssistantOverlayState;
import org.chromium.chrome.browser.autofill_assistant.payment.AssistantPaymentRequestCoordinator;
import org.chromium.chrome.browser.autofill_assistant.payment.AssistantPaymentRequestModel;
import org.chromium.chrome.browser.compositor.CompositorViewResizer;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.ui.modelutil.ListModel;
/**
* Coordinator responsible for the Autofill Assistant bottom bar.
*/
class AssistantBottomBarCoordinator
implements CompositorViewResizer, AssistantPeekHeightCoordinator.Delegate {
private final AssistantModel mModel;
private final BottomSheetController mBottomSheetController;
private final AssistantBottomSheetContent mContent;
// Child coordinators.
private final AssistantHeaderCoordinator mHeaderCoordinator;
private final AssistantDetailsCoordinator mDetailsCoordinator;
private final AssistantFormCoordinator mFormCoordinator;
private final AssistantCarouselCoordinator mSuggestionsCoordinator;
private final AssistantCarouselCoordinator mActionsCoordinator;
private final AssistantPeekHeightCoordinator mPeekHeightCoordinator;
private AssistantInfoBoxCoordinator mInfoBoxCoordinator;
private AssistantPaymentRequestCoordinator mPaymentRequestCoordinator;
private final ObserverList<CompositorViewResizer.Observer> mSizeObservers =
new ObserverList<>();
private boolean mResizeViewport;
@Nullable
private ScrollView mOnboardingScrollView;
AssistantBottomBarCoordinator(
Activity activity, AssistantModel model, BottomSheetController controller) {
mModel = model;
mBottomSheetController = controller;
mContent = new AssistantBottomSheetContent(activity);
// Instantiate child components.
mHeaderCoordinator = new AssistantHeaderCoordinator(
activity, mContent.mBottomBarView, model.getHeaderModel());
mInfoBoxCoordinator = new AssistantInfoBoxCoordinator(activity, model.getInfoBoxModel());
mDetailsCoordinator = new AssistantDetailsCoordinator(activity, model.getDetailsModel());
mPaymentRequestCoordinator =
new AssistantPaymentRequestCoordinator(activity, model.getPaymentRequestModel());
mFormCoordinator = new AssistantFormCoordinator(activity, model.getFormModel());
mSuggestionsCoordinator =
new AssistantSuggestionsCarouselCoordinator(activity, model.getSuggestionsModel());
mActionsCoordinator =
new AssistantActionsCarouselCoordinator(activity, model.getActionsModel());
BottomSheet bottomSheet = controller.getBottomSheet();
mPeekHeightCoordinator = new AssistantPeekHeightCoordinator(activity, this, bottomSheet,
mContent.mToolbarView, mContent.mBottomBarView, mSuggestionsCoordinator.getView(),
mActionsCoordinator.getView(), AssistantPeekHeightCoordinator.PeekMode.HANDLE);
// Add child views to bottom bar container.
mContent.mBottomBarView.addView(mInfoBoxCoordinator.getView());
mContent.mBottomBarView.addView(mDetailsCoordinator.getView());
mContent.mBottomBarView.addView(mPaymentRequestCoordinator.getView());
mContent.mBottomBarView.addView(mFormCoordinator.getView());
mContent.mBottomBarView.addView(mSuggestionsCoordinator.getView());
mContent.mBottomBarView.addView(mActionsCoordinator.getView());
// Set children top margins to have a spacing between them. For the carousels, we set their
// margin only when they are not empty given that they are always shown, even if empty. We
// do not hide them because there is an incompatibility bug between the animateLayoutChanges
// attribute set on mBottomBarView and the animations ran by the carousels
// RecyclerView.
int childSpacing = activity.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_bottombar_vertical_spacing);
setChildMarginTop(mDetailsCoordinator.getView(), childSpacing);
setChildMarginTop(mPaymentRequestCoordinator.getView(), childSpacing);
setChildMarginTop(mFormCoordinator.getView(), childSpacing);
setCarouselMarginTop(mSuggestionsCoordinator.getView(),
model.getSuggestionsModel().getChipsModel(), childSpacing);
setCarouselMarginTop(mActionsCoordinator.getView(), model.getActionsModel().getChipsModel(),
childSpacing);
// Set the horizontal margins of children. We don't set them on the payment request and the
// carousels to allow them to take the full width of the sheet.
setHorizontalMargins(mInfoBoxCoordinator.getView());
setHorizontalMargins(mDetailsCoordinator.getView());
setHorizontalMargins(mFormCoordinator.getView());
View bottomSheetContainer =
bottomSheet.findViewById(org.chromium.chrome.R.id.bottom_sheet_content);
bottomSheet.addObserver(new EmptyBottomSheetObserver() {
@Override
public void onSheetClosed(int reason) {
// When scrolling with y < peekHeight, the BottomSheet will make the content
// invisible. This is a workaround to prevent that as our toolbar is transparent and
// we want to sheet content to stay visible.
if ((bottomSheet.getSheetState() == BottomSheet.SheetState.SCROLLING
|| bottomSheet.getSheetState() == BottomSheet.SheetState.PEEK)
&& bottomSheet.getCurrentSheetContent() == mContent
&& model.get(AssistantModel.VISIBLE)) {
bottomSheetContainer.setVisibility(View.VISIBLE);
}
}
@Override
public void onSheetStateChanged(int newState) {
if (newState == BottomSheet.SheetState.PEEK
&& bottomSheet.getCurrentSheetContent() == mContent) {
// When in the peek state, the BottomSheet hides the content view. We override
// that because we artificially increase the height of the transparent toolbar
// to show parts of the content view.
bottomSheetContainer.setVisibility(View.VISIBLE);
}
}
@Override
public void onSheetContentChanged(@Nullable BottomSheet.BottomSheetContent newContent) {
// TODO(crbug.com/806868): Make sure this works and does not interfere with Duet
// once we are in ChromeTabbedActivity.
notifyAutofillAssistantSizeChanged();
}
});
// Show or hide the bottom sheet content when the Autofill Assistant visibility is changed.
model.addObserver((source, propertyKey) -> {
if (AssistantModel.VISIBLE == propertyKey) {
if (model.get(AssistantModel.VISIBLE)) {
showAndExpand();
} else {
hide();
}
}
});
}
/**
* Cleanup resources when this goes out of scope.
*/
public void destroy() {
mInfoBoxCoordinator.destroy();
mInfoBoxCoordinator = null;
mPaymentRequestCoordinator.destroy();
mPaymentRequestCoordinator = null;
mHeaderCoordinator.destroy();
}
/**
* Show the onboarding screen and call {@code callback} with {@code true} if the user agreed to
* proceed, false otherwise.
*/
public void showOnboarding(String experimentIds, Callback<Boolean> callback) {
mModel.getHeaderModel().set(AssistantHeaderModel.VISIBLE, false);
// Show overlay to prevent user from interacting with the page during onboarding.
mModel.getOverlayModel().set(AssistantOverlayModel.STATE, AssistantOverlayState.FULL);
View onboardingView = AssistantOnboardingCoordinator.show(experimentIds,
mContent.mBottomBarView.getContext(), mContent.mBottomBarView, accepted -> {
mOnboardingScrollView = null;
if (!accepted) {
callback.onResult(false);
return;
}
mModel.getHeaderModel().set(AssistantHeaderModel.VISIBLE, true);
// Hide overlay.
mModel.getOverlayModel().set(
AssistantOverlayModel.STATE, AssistantOverlayState.HIDDEN);
callback.onResult(true);
});
mOnboardingScrollView = onboardingView.findViewById(R.id.onboarding_scroll_view);
}
/** Request showing the Assistant bottom bar view and expand the sheet. */
public void showAndExpand() {
if (mBottomSheetController.requestShowContent(mContent, /* animate= */ true)) {
mBottomSheetController.expandSheet();
}
}
/** Hide the Assistant bottom bar view. */
public void hide() {
mBottomSheetController.hideContent(mContent, /* animate= */ true);
}
void setResizeViewport(boolean resizeViewport) {
if (resizeViewport == mResizeViewport) return;
mResizeViewport = resizeViewport;
notifyAutofillAssistantSizeChanged();
}
/** Set the peek mode. */
void setPeekMode(@AssistantPeekHeightCoordinator.PeekMode int peekMode) {
mPeekHeightCoordinator.setPeekMode(peekMode);
}
@Override
public void setShowOnlyCarousels(boolean showOnlyCarousels) {
mDetailsCoordinator.setForceInvisible(showOnlyCarousels);
mModel.getPaymentRequestModel().set(
AssistantPaymentRequestModel.FORCE_INVISIBLE, showOnlyCarousels);
mFormCoordinator.setForceInvisible(showOnlyCarousels);
}
@Override
public void onPeekHeightChanged() {
notifyAutofillAssistantSizeChanged();
}
private void setChildMarginTop(View child, int marginTop) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) child.getLayoutParams();
params.topMargin = marginTop;
child.setLayoutParams(params);
}
/**
* Observe {@code model} such that we set the topMargin of {@code carouselView} to {@code
* marginTop} when {@code model} is not empty and set it to 0 otherwise.
*/
private void setCarouselMarginTop(
View carouselView, ListModel<AssistantChip> chipsModel, int marginTop) {
chipsModel.addObserver(new AbstractListObserver<Void>() {
@Override
public void onDataSetChanged() {
setChildMarginTop(carouselView, chipsModel.size() > 0 ? marginTop : 0);
}
});
}
@VisibleForTesting
public AssistantCarouselCoordinator getSuggestionsCoordinator() {
return mSuggestionsCoordinator;
}
@VisibleForTesting
public AssistantCarouselCoordinator getActionsCoordinator() {
return mActionsCoordinator;
}
private void setHorizontalMargins(View view) {
LinearLayout.MarginLayoutParams layoutParams =
(LinearLayout.MarginLayoutParams) view.getLayoutParams();
int horizontalMargin = view.getContext().getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_bottombar_horizontal_spacing);
layoutParams.setMarginStart(horizontalMargin);
layoutParams.setMarginEnd(horizontalMargin);
view.setLayoutParams(layoutParams);
}
private void notifyAutofillAssistantSizeChanged() {
int height = getHeight();
for (Observer observer : mSizeObservers) {
observer.onHeightChanged(height);
}
}
// Implementation of methods from AutofillAssistantSizeManager.
@Override
public int getHeight() {
if (mResizeViewport
&& mBottomSheetController.getBottomSheet().getCurrentSheetContent() == mContent)
return mPeekHeightCoordinator.getPeekHeight();
return 0;
}
@Override
public void addObserver(Observer observer) {
mSizeObservers.addObserver(observer);
}
@Override
public void removeObserver(Observer observer) {
mSizeObservers.removeObserver(observer);
}
// TODO(crbug.com/806868): Move this class at the top of the file once it is a static class.
private class AssistantBottomSheetContent implements BottomSheet.BottomSheetContent {
private final ViewGroup mToolbarView;
private final SizeListenableLinearLayout mBottomBarView;
public AssistantBottomSheetContent(Context context) {
mToolbarView = (ViewGroup) LayoutInflater.from(context).inflate(
R.layout.autofill_assistant_bottom_sheet_toolbar, /* root= */ null);
mBottomBarView = (SizeListenableLinearLayout) LayoutInflater.from(context).inflate(
R.layout.autofill_assistant_bottom_sheet_content, /* root= */ null);
}
@Override
public View getContentView() {
return mBottomBarView;
}
@Nullable
@Override
public View getToolbarView() {
return mToolbarView;
}
@Override
public int getVerticalScrollOffset() {
// TODO(crbug.com/806868): Have a single ScrollView container that contains all child
// views (except carousels) instead.
if (mOnboardingScrollView != null && mOnboardingScrollView.isShown()) {
return mOnboardingScrollView.getScrollY();
}
if (mPaymentRequestCoordinator.getScrollView().isShown()) {
return mPaymentRequestCoordinator.getScrollView().getScrollY();
}
if (mFormCoordinator.getView().isShown()) {
return mFormCoordinator.getView().getScrollY();
}
return 0;
}
@Override
public void destroy() {}
@Override
public int getPriority() {
return BottomSheet.ContentPriority.HIGH;
}
@Override
public boolean swipeToDismissEnabled() {
return false;
}
@Override
public boolean isPeekStateEnabled() {
return true;
}
@Override
public boolean wrapContentEnabled() {
return true;
}
@Override
public boolean hasCustomLifecycle() {
return true;
}
@Override
public boolean hasCustomScrimLifecycle() {
return true;
}
@Override
public boolean setContentSizeListener(@Nullable BottomSheet.ContentSizeListener listener) {
mBottomBarView.setContentSizeListener(listener);
return true;
}
@Override
public boolean hideOnScroll() {
return false;
}
@Override
public int getSheetContentDescriptionStringId() {
return R.string.autofill_assistant_sheet_content_description;
}
@Override
public int getSheetHalfHeightAccessibilityStringId() {
return R.string.autofill_assistant_sheet_half_height;
}
@Override
public int getSheetFullHeightAccessibilityStringId() {
return R.string.autofill_assistant_sheet_full_height;
}
@Override
public int getSheetClosedAccessibilityStringId() {
return R.string.autofill_assistant_sheet_closed;
}
}
}