chromium/chrome/browser/ui/android/webid/internal/java/src/org/chromium/chrome/browser/ui/android/webid/AccountSelectionBottomSheetContent.java

// Copyright 2021 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.ui.android.webid;

import android.view.View;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.recyclerview.widget.RecyclerView;

import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.blink.mojom.RpMode;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;

/**
 * This view renders content that gets displayed inside the bottom sheet. This is a simple container
 * for the view which is the current best practice for bottom sheet content.
 */
public class AccountSelectionBottomSheetContent implements BottomSheetContent {
    /**
     * The maximum number of accounts that should be fully visible when the the account picker is
     * displayed. Button mode UI is generally bigger because it requires user interaction to
     * trigger. Therefore, we are able to show more accounts at once compared to widget mode.
     */
    private static final float MAX_VISIBLE_ACCOUNTS_WIDGET_MODE = 2.5f;

    private static final float MAX_VISIBLE_ACCOUNTS_BUTTON_MODE = 3.5f;

    private final View mContentView;
    private final BottomSheetController mBottomSheetController;
    private final Supplier<Integer> mScrollOffsetSupplier;
    private final @RpMode.EnumType int mRpMode;
    private @Nullable Runnable mBackPressHandler;
    private final ObservableSupplierImpl<Boolean> mBackPressStateChangedSupplier =
            new ObservableSupplierImpl<>();

    /** Constructs the AccountSelection bottom sheet view. */
    AccountSelectionBottomSheetContent(
            View contentView,
            BottomSheetController bottomSheetController,
            Supplier<Integer> scrollOffsetSupplier,
            @RpMode.EnumType int rpMode) {
        mContentView = contentView;
        mBottomSheetController = bottomSheetController;
        mScrollOffsetSupplier = scrollOffsetSupplier;
        mRpMode = rpMode;
    }

    /**
     * Updates the sheet content back press handling behavior. This should be invoked during an
     * event that updates the back press handling behavior of the sheet content.
     *
     * @param backPressHandler A runnable that will be invoked by the sheet content to handle a back
     *     press. A null value indicates that back press will not be handled by the content.
     */
    public void setCustomBackPressBehavior(@Nullable Runnable backPressHandler) {
        mBackPressHandler = backPressHandler;
        mBackPressStateChangedSupplier.set(backPressHandler != null);
    }

    public void computeAndUpdateAccountListHeight() {
        // {@link mContentView} is null for some tests.
        if (mContentView == null) return;

        View sheetContainer = mContentView.findViewById(R.id.sheet_item_list_container);
        // When we're in the multi-account chooser and there are more than {@link
        // MAX_VISIBLE_ACCOUNTS_WIDGET_MODE} accounts, resize the list so that only {@link
        // MAX_VISIBLE_ACCOUNTS_WIDGET_MODE}
        // accounts and part of the next one are visible.
        RecyclerView sheetItemListView = sheetContainer.findViewById(R.id.sheet_item_list);
        int numAccounts = sheetItemListView.getAdapter().getItemCount();
        if (numAccounts > MAX_VISIBLE_ACCOUNTS_WIDGET_MODE) {
            sheetItemListView.measure(
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            int measuredHeight = sheetItemListView.getMeasuredHeight();
            int containerHeight =
                    Math.round(
                            ((float) measuredHeight / numAccounts)
                                    * MAX_VISIBLE_ACCOUNTS_WIDGET_MODE);
            sheetContainer.getLayoutParams().height = containerHeight;
        } else {
            // Need to set the height here in case it was changed by a previous {@link
            // computeAndUpdateAccountListHeight()} call.
            sheetContainer.getLayoutParams().height = FrameLayout.LayoutParams.WRAP_CONTENT;
        }
    }

    /**
     * Returns the height of the full state in button mode.
     *
     * @return the full state height in pixels. Never 0. Can theoretically exceed the screen height.
     */
    private @Px int getMaximumButtonModeSheetHeightPx() {
        View accountSelectionSheet = mContentView.findViewById(R.id.account_selection_sheet);
        accountSelectionSheet.measure(
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        return accountSelectionSheet.getMeasuredHeight();
    }

    /**
     * Returns the height of the half state in button mode. For up to 3 accounts, it shows all
     * accounts fully. For 4+ accounts, it shows the first 3.5 accounts to encourage scrolling.
     *
     * @return the half state height in pixels. Never 0. Can theoretically exceed the screen height.
     */
    private @Px int getDesiredButtonModeSheetHeightPx() {
        View sheetContainer = mContentView.findViewById(R.id.sheet_item_list_container);
        // When we're in the multi-account chooser and there are more than {@link
        // MAX_VISIBLE_ACCOUNTS_BUTTON_MODE} accounts, resize the list so that only {@link
        // MAX_VISIBLE_ACCOUNTS_BUTTON_MODE}
        // accounts and part of the next one are visible.
        RecyclerView sheetItemListView = sheetContainer.findViewById(R.id.sheet_item_list);
        int numAccounts = sheetItemListView.getAdapter().getItemCount();
        if (numAccounts > MAX_VISIBLE_ACCOUNTS_BUTTON_MODE) {
            sheetItemListView.measure(
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            View accountRow = sheetItemListView.getChildAt(0);
            @Px int measuredHeight = sheetItemListView.getMeasuredHeight();
            @Px
            int desiredHeight =
                    Math.round(accountRow.getMeasuredHeight() * MAX_VISIBLE_ACCOUNTS_BUTTON_MODE);
            return getMaximumButtonModeSheetHeightPx() - measuredHeight + desiredHeight;
        }
        return getMaximumButtonModeSheetHeightPx();
    }

    @Override
    public void destroy() {}

    @Override
    public View getContentView() {
        return mContentView;
    }

    @Override
    public @Nullable View getToolbarView() {
        return null;
    }

    @Override
    public int getVerticalScrollOffset() {
        return mScrollOffsetSupplier.get();
    }

    @Override
    public int getPriority() {
        return BottomSheetContent.ContentPriority.HIGH;
    }

    @Override
    public boolean hasCustomScrimLifecycle() {
        // For widget mode, return true to ensure no scrim is created behind the view.
        if (mRpMode == RpMode.WIDGET) return true;
        return false;
    }

    @Override
    public boolean swipeToDismissEnabled() {
        return true;
    }

    @Override
    public boolean skipHalfStateOnScrollingDown() {
        return false;
    }

    @Override
    public int getPeekHeight() {
        return BottomSheetContent.HeightMode.DISABLED;
    }

    @Override
    public float getFullHeightRatio() {
        if (mRpMode == RpMode.WIDGET) return HeightMode.WRAP_CONTENT;
        // WRAP_CONTENT would be the right fit but this disables the HALF state.
        return Math.min(
                        getMaximumButtonModeSheetHeightPx(),
                        mBottomSheetController.getContainerHeight())
                / (float) mBottomSheetController.getContainerHeight();
    }

    @Override
    public float getHalfHeightRatio() {
        if (mRpMode == RpMode.WIDGET) {
            computeAndUpdateAccountListHeight();
            return HeightMode.WRAP_CONTENT;
        }
        return Math.min(
                        getDesiredButtonModeSheetHeightPx(),
                        mBottomSheetController.getContainerHeight())
                / (float) mBottomSheetController.getContainerHeight();
    }

    @Override
    public boolean hideOnScroll() {
        return false;
    }

    @Override
    public boolean handleBackPress() {
        if (mBackPressHandler != null) {
            mBackPressHandler.run();
            return true;
        }
        return false;
    }

    @Override
    public ObservableSupplierImpl<Boolean> getBackPressStateChangedSupplier() {
        return mBackPressStateChangedSupplier;
    }

    @Override
    public void onBackPressed() {
        handleBackPress();
    }

    @Override
    public int getSheetContentDescriptionStringId() {
        return R.string.account_selection_content_description;
    }

    @Override
    public int getSheetHalfHeightAccessibilityStringId() {
        return R.string.account_selection_sheet_half_height;
    }

    @Override
    public int getSheetFullHeightAccessibilityStringId() {
        return R.string.account_selection_sheet_full_height;
    }

    @Override
    public int getSheetClosedAccessibilityStringId() {
        return R.string.account_selection_sheet_closed;
    }
}