chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/partialcustomtab/PartialCustomTabDisplayManager.java

// Copyright 2022 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.customtabs.features.partialcustomtab;

import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.ACTIVITY_SIDE_SHEET_SLIDE_IN_FROM_BOTTOM;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.ACTIVITY_SIDE_SHEET_SLIDE_IN_FROM_SIDE;

import android.app.Activity;
import android.content.res.Configuration;
import android.os.Handler;
import android.util.SparseBooleanArray;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.AnimRes;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabBaseStrategy.PartialCustomTabType;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.ConfigurationChangedObserver;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.browser_ui.widget.TouchEventProvider;

/**
 * Class responsible for how the Partial Chrome Custom Tabs are displayed on the screen.
 * It creates and handles the supported size strategies for Partial Chrome Custom Tabs based on the
 * intent extras values provided by the embedder, the window size, and the device state.
 */
public class PartialCustomTabDisplayManager extends CustomTabHeightStrategy
        implements ConfigurationChangedObserver {
    static final int CREATE_STRATEGY_DELAY_CONFIG_CHANGE_MS = 150;
    static final int WINDOW_WIDTH_COMPACT_CUTOFF_DP = 600;

    private final Activity mActivity;
    private final BrowserServicesIntentDataProvider mIntentData;
    private final int mBreakPointDp;
    private final OnResizedCallback mOnResizedCallback;
    private final OnActivityLayoutCallback mOnActivityLayoutCallback;
    private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
    private final FullscreenManager mFullscreenManager;
    private final boolean mIsTablet;
    private final PartialCustomTabVersionCompat mVersionCompat;
    private final SparseBooleanArray mLastMaximizeState = new SparseBooleanArray();

    // Simple factory interface creating a new SizeStrategy. Facilitates testing.
    interface SizeStrategyCreator {
        PartialCustomTabBaseStrategy createForType(
                @PartialCustomTabType int type,
                BrowserServicesIntentDataProvider intentData,
                boolean startMaximized);
    }

    private PartialCustomTabBaseStrategy mStrategy;
    private @PartialCustomTabType int mCurrentPartialCustomTabType;

    private View mToolbarCoordinatorView;
    private CustomTabToolbar mCustomTabToolbar;
    private int mToolbarCornerRadius;
    private PartialCustomTabHandleStrategyFactory mHandleStrategyFactory;
    private SizeStrategyCreator mSizeStrategyCreator = this::createSizeStrategy;
    private Supplier<TouchEventProvider> mTouchEventProvider;
    private Supplier<Tab> mTab;

    public PartialCustomTabDisplayManager(
            Activity activity,
            BrowserServicesIntentDataProvider intentData,
            Supplier<TouchEventProvider> touchEventProvider,
            Supplier<Tab> tab,
            OnResizedCallback onResizedCallback,
            OnActivityLayoutCallback onActivityLayoutCallback,
            ActivityLifecycleDispatcher lifecycleDispatcher,
            FullscreenManager fullscreenManager,
            boolean isTablet) {
        mActivity = activity;
        mIntentData = intentData;
        mTouchEventProvider = touchEventProvider;
        mTab = tab;
        mOnResizedCallback = onResizedCallback;
        mOnActivityLayoutCallback = onActivityLayoutCallback;
        mFullscreenManager = fullscreenManager;
        mIsTablet = isTablet;
        mActivityLifecycleDispatcher = lifecycleDispatcher;
        lifecycleDispatcher.register(this);

        mVersionCompat = PartialCustomTabVersionCompat.create(mActivity, this::updatePosition);
        mHandleStrategyFactory = new PartialCustomTabHandleStrategyFactory();
        mBreakPointDp = calculateBreakPoint(intentData.getActivityBreakPoint());
        mCurrentPartialCustomTabType = calculatePartialCustomTabType();
        mStrategy =
                mSizeStrategyCreator.createForType(mCurrentPartialCustomTabType, intentData, false);
    }

    public @PartialCustomTabType int getActiveStrategyType() {
        return mStrategy.getStrategyType();
    }

    public void onShowSoftInput(Runnable softKeyboardRunnable) {
        mStrategy.onShowSoftInput(softKeyboardRunnable);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        int type = calculatePartialCustomTabType();
        if (type != mCurrentPartialCustomTabType) {
            if (mStrategy != null) {
                mStrategy.destroy(); // May update the internal states.
                mLastMaximizeState.put(mStrategy.getStrategyType(), mStrategy.isMaximized());
            }
            boolean startMaximized = mLastMaximizeState.get(type, false);
            mStrategy = mSizeStrategyCreator.createForType(type, mIntentData, startMaximized);
            mCurrentPartialCustomTabType = type;
            mStrategy.setToolbar(mToolbarCoordinatorView, mCustomTabToolbar);
            new Handler()
                    .postDelayed(
                            () -> {
                                mStrategy.onToolbarInitialized(
                                        mToolbarCoordinatorView,
                                        mCustomTabToolbar,
                                        mToolbarCornerRadius);
                                mStrategy.onPostInflationStartup();
                                // TODO(http://crbug.com/1406107): Creating a new strategy type is
                                // basically a resize so we need to make sure to call
                                // #onActivityResized here as
                                // well
                            },
                            CREATE_STRATEGY_DELAY_CONFIG_CHANGE_MS);
        } else {
            // If the type of PCCT strategy did not change we can just call into the equivalent
            // method for the given strategy.
            mStrategy.onConfigurationChanged(newConfig.orientation);
        }
    }

    /**
     * @see {@link org.chromium.chrome.browser.lifecycle.InflationObserver#onPostInflationStartup()}
     */
    @Override
    public void onPostInflationStartup() {
        mStrategy.onPostInflationStartup();
    }

    /** Returns false if we didn't change the Window background color, true otherwise. */
    @Override
    public boolean changeBackgroundColorForResizing() {
        return mStrategy.changeBackgroundColorForResizing();
    }

    /**
     * Provide this class with the required views and values so it can set up the strategy.
     *
     * @param coordinatorView Coordinator view to insert the UI handle for the users to resize the
     *                        custom tab.
     * @param toolbar The {@link CustomTabToolbar} to set up the strategy.
     * @param toolbarCornerRadius The custom tab corner radius in pixels.
     */
    @Override
    public void onToolbarInitialized(
            View coordinatorView, CustomTabToolbar toolbar, @Px int toolbarCornerRadius) {
        mToolbarCoordinatorView = coordinatorView;
        mCustomTabToolbar = toolbar;
        mToolbarCornerRadius = toolbarCornerRadius;

        mStrategy.onToolbarInitialized(coordinatorView, toolbar, toolbarCornerRadius);
    }

    /**
     * @see {@link BaseCustomTabRootUiCoordinator#handleCloseAnimation()}
     */
    @Override
    public boolean handleCloseAnimation(Runnable finishRunnable) {
        return mStrategy.handleCloseAnimation(finishRunnable);
    }

    /**
     * Set the scrim value to apply to partial CCT UI.
     * @param scrimFraction Scrim fraction.
     */
    @Override
    public void setScrimFraction(float scrimFraction) {
        mStrategy.setScrimFraction(scrimFraction);
    }

    // FindToolbarObserver implementation.

    @Override
    public void onFindToolbarShown() {
        mStrategy.onFindToolbarShown();
    }

    @Override
    public void onFindToolbarHidden() {
        mStrategy.onFindToolbarHidden();
    }

    /** Destroy the strategy object. */
    @Override
    public void destroy() {
        mStrategy.destroy();
    }

    private static int calculateBreakPoint(int unclampedBreakPointDp) {
        return Math.max(unclampedBreakPointDp, WINDOW_WIDTH_COMPACT_CUTOFF_DP);
    }

    private @PartialCustomTabType int calculatePartialCustomTabType() {
        int initialWidth = mIntentData.getInitialActivityWidth();
        int initialHeight = mIntentData.getInitialActivityHeight();
        return calculatePartialCustomTabType(
                mActivity,
                initialWidth,
                initialHeight,
                mVersionCompat::getDisplayWidthDp,
                mBreakPointDp);
    }

    @VisibleForTesting
    static @PartialCustomTabType int calculatePartialCustomTabType(
            Activity activity,
            int initialWidth,
            int initialHeight,
            Supplier<Integer> displayWidthDpSupplier,
            int breakPointDp) {
        if (MultiWindowUtils.getInstance().isInMultiWindowMode(activity)) {
            return PartialCustomTabType.FULL_SIZE;
        }
        if (initialWidth == 0 && initialHeight == 0) {
            return PartialCustomTabType.FULL_SIZE;
        }
        int displayWidthDp = -1;
        if (initialWidth > 0 && initialHeight > 0) {
            if (displayWidthDp < 0) displayWidthDp = displayWidthDpSupplier.get();
            return displayWidthDp < breakPointDp
                    ? PartialCustomTabType.BOTTOM_SHEET
                    : PartialCustomTabType.SIDE_SHEET;
        }
        if (initialWidth > 0) {
            if (displayWidthDp < 0) displayWidthDp = displayWidthDpSupplier.get();
            return displayWidthDp < breakPointDp
                    ? PartialCustomTabType.FULL_SIZE
                    : PartialCustomTabType.SIDE_SHEET;
        }
        if (initialHeight > 0) {
            return PartialCustomTabType.BOTTOM_SHEET;
        }
        assert false : "Unreachable";
        return PartialCustomTabType.FULL_SIZE;
    }

    /**
     * Get the start animation resource ID to override the default with.
     * @param activity Activity to get window resource from.
     * @param provider Intent data provider from which to extract necessary info.
     * @param defaultResId Default start animation resource ID.
     * @return Start resource ID if an override was found, or the default one if not.
     */
    public static @AnimRes int getStartAnimationOverride(
            Activity activity,
            BrowserServicesIntentDataProvider provider,
            @AnimRes int defaultResId) {
        // Initialize VersionCompat lazily using a supplier since in many cases (for normal CCTs)
        // |calculatePartialCustomTabType| won't need the object and will early out.
        Supplier<Integer> displayWidthDpSupplier =
                () -> {
                    var versionCompat = PartialCustomTabVersionCompat.create(activity, () -> {});
                    return versionCompat.getDisplayWidthDp();
                };
        @PartialCustomTabType
        int type =
                calculatePartialCustomTabType(
                        activity,
                        provider.getInitialActivityWidth(),
                        provider.getInitialActivityHeight(),
                        displayWidthDpSupplier,
                        calculateBreakPoint(provider.getActivityBreakPoint()));

        @AnimRes int start_anim_id = defaultResId;
        if (type == PartialCustomTabType.BOTTOM_SHEET || type == PartialCustomTabType.FULL_SIZE) {
            start_anim_id = R.anim.slide_in_up;
        } else if (type == PartialCustomTabType.SIDE_SHEET) {
            int behavior = provider.getSideSheetSlideInBehavior();
            if (behavior == ACTIVITY_SIDE_SHEET_SLIDE_IN_FROM_BOTTOM) {
                start_anim_id = R.anim.slide_in_up;
            } else if (behavior == ACTIVITY_SIDE_SHEET_SLIDE_IN_FROM_SIDE) {
                boolean sheetOnRight =
                        PartialCustomTabSideSheetStrategy.isSheetOnRight(
                                provider.getSideSheetPosition());
                start_anim_id = sheetOnRight ? R.anim.slide_in_right : R.anim.slide_in_left;
            } else {
                assert false : "Invalide slide-in behavior";
            }
        }
        return start_anim_id;
    }

    private PartialCustomTabBaseStrategy createSizeStrategy(
            @PartialCustomTabType int type,
            BrowserServicesIntentDataProvider intentData,
            boolean maximized) {
        RecordHistogram.recordEnumeratedHistogram(
                "CustomTabs.PartialCustomTabType", type, PartialCustomTabType.COUNT);

        return switch (type) {
            case PartialCustomTabType.BOTTOM_SHEET -> new PartialCustomTabBottomSheetStrategy(
                    mActivity,
                    mIntentData,
                    mTouchEventProvider,
                    mTab,
                    mOnResizedCallback,
                    mOnActivityLayoutCallback,
                    mActivityLifecycleDispatcher,
                    mFullscreenManager,
                    mIsTablet,
                    maximized,
                    mHandleStrategyFactory);
            case PartialCustomTabType.SIDE_SHEET -> new PartialCustomTabSideSheetStrategy(
                    mActivity,
                    mIntentData,
                    mOnResizedCallback,
                    mOnActivityLayoutCallback,
                    mFullscreenManager,
                    mIsTablet,
                    maximized,
                    mHandleStrategyFactory);
            case PartialCustomTabType.FULL_SIZE -> new PartialCustomTabFullSizeStrategy(
                    mActivity,
                    mIntentData,
                    mOnResizedCallback,
                    mOnActivityLayoutCallback,
                    mFullscreenManager,
                    mIsTablet,
                    mHandleStrategyFactory);
            default -> {
                assert false : "Partial Custom Tab type not supported: " + type;
                yield null;
            }
        };
    }

    private void updatePosition() {}

    SizeStrategyCreator getSizeStrategyCreatorForTesting() {
        return mSizeStrategyCreator;
    }

    PartialCustomTabBaseStrategy getSizeStrategyForTesting() {
        return mStrategy;
    }

    int getBreakPointDpForTesting() {
        return mBreakPointDp;
    }

    void setMocksForTesting(
            ViewGroup coordinatorLayout,
            CustomTabToolbar toolbar,
            View toolbarCoordinator,
            PartialCustomTabHandleStrategyFactory handleStrategyFactory,
            SizeStrategyCreator sizeStrategyCreator) {
        mToolbarCoordinatorView = toolbarCoordinator;
        mCustomTabToolbar = toolbar;
        mHandleStrategyFactory = handleStrategyFactory;
        mSizeStrategyCreator = sizeStrategyCreator;
        mStrategy.setMockViewForTesting(coordinatorLayout, toolbar, toolbarCoordinator);
    }
}