chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarFeatures.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.toolbar.adaptive;

import android.content.Context;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.FeatureList;
import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.readaloud.ReadAloudFeatures;
import org.chromium.ui.base.DeviceFormFactor;

import java.util.HashMap;
import java.util.List;

/** A utility class for handling feature flags used by {@link AdaptiveToolbarButtonController}. */
public class AdaptiveToolbarFeatures {
    /** Finch default group for new tab variation. */
    static final String NEW_TAB = "new-tab";

    /** Finch default group for share variation. */
    static final String SHARE = "share";

    /** Finch default group for voice search variation. */
    static final String VOICE = "voice";

    /** Default minimum width to show the optional button. */
    public static final int DEFAULT_MIN_WIDTH_DP = 360;

    /** Default delay between action chip expansion and collapse. */
    public static final int DEFAULT_CONTEXTUAL_PAGE_ACTION_CHIP_DELAY_MS = 3000;

    /** Default action chip delay for price tracking. */
    public static final int DEFAULT_PRICE_TRACKING_ACTION_CHIP_DELAY_MS = 6000;

    /** Default action chip delay for reader mode. */
    public static final int DEFAULT_READER_MODE_ACTION_CHIP_DELAY_MS = 3000;

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static final String CONTEXTUAL_PAGE_ACTION_TEST_FEATURE_NAME =
            "CONTEXTUAL_PAGE_ACTION_TEST_FEATURE_NAME";

    @AdaptiveToolbarButtonVariant private static Integer sButtonVariant;

    /** For testing only. */
    private static String sDefaultSegmentForTesting;

    private static HashMap<Integer, Boolean> sActionChipOverridesForTesting;
    private static HashMap<Integer, Boolean> sAlternativeColorOverridesForTesting;
    private static HashMap<Integer, Boolean> sIsDynamicActionOverridesForTesting;

    /**
     * @return Whether the button variant is a dynamic action.
     */
    public static boolean isDynamicAction(@AdaptiveToolbarButtonVariant int variant) {
        if (sIsDynamicActionOverridesForTesting != null
                && sIsDynamicActionOverridesForTesting.containsKey(variant)) {
            return Boolean.TRUE.equals(sIsDynamicActionOverridesForTesting.get(variant));
        }

        switch (variant) {
            case AdaptiveToolbarButtonVariant.UNKNOWN:
            case AdaptiveToolbarButtonVariant.NONE:
            case AdaptiveToolbarButtonVariant.NEW_TAB:
            case AdaptiveToolbarButtonVariant.SHARE:
            case AdaptiveToolbarButtonVariant.VOICE:
            case AdaptiveToolbarButtonVariant.AUTO:
                return false;
            case AdaptiveToolbarButtonVariant.PRICE_TRACKING:
            case AdaptiveToolbarButtonVariant.READER_MODE:
            case AdaptiveToolbarButtonVariant.PRICE_INSIGHTS:
                return true;
        }
        return false;
    }

    /**
     * Returns whether the adaptive toolbar is enabled with segmentation and customization.
     *
     * <p>Must be called with the {@link FeatureList} initialized.
     */
    public static boolean isCustomizationEnabled() {
        return ChromeFeatureList.sAdaptiveButtonInTopToolbarCustomizationV2.isEnabled();
    }

    /**
     * @return Whether the contextual page action should show an action chip when appearing.
     *     <li>If true, it will use the action chip animation using rate limiting from the
     *         "IPH_ContextualPageActions_ActionChip" feature.
     *     <li>If false, we'll show the button's IPH bubble specified on its ButtonData.
     */
    public static boolean shouldShowActionChip(@AdaptiveToolbarButtonVariant int buttonVariant) {
        if (!isDynamicAction(buttonVariant)) return false;
        if (sActionChipOverridesForTesting != null
                && sActionChipOverridesForTesting.containsKey(buttonVariant)) {
            return Boolean.TRUE.equals(sActionChipOverridesForTesting.get(buttonVariant));
        }

        // Price tracking, price insights and reader mode launched with the action chip variant.
        switch (buttonVariant) {
            case AdaptiveToolbarButtonVariant.PRICE_TRACKING:
            case AdaptiveToolbarButtonVariant.READER_MODE:
            case AdaptiveToolbarButtonVariant.PRICE_INSIGHTS:
            case AdaptiveToolbarButtonVariant.TEST_BUTTON:
                return true;
            default:
                assert false : "Unknown button variant " + buttonVariant;
                return false;
        }
    }

    /**
     * @return The amount of time the action chip should remain expanded in milliseconds. Default is
     *     3 seconds.
     */
    public static int getContextualPageActionDelayMs(
            @AdaptiveToolbarButtonVariant int buttonVariant) {
        switch (buttonVariant) {
            case AdaptiveToolbarButtonVariant.PRICE_TRACKING:
            case AdaptiveToolbarButtonVariant.PRICE_INSIGHTS:
            case AdaptiveToolbarButtonVariant.TEST_BUTTON:
                return DEFAULT_PRICE_TRACKING_ACTION_CHIP_DELAY_MS;
            case AdaptiveToolbarButtonVariant.READER_MODE:
                return DEFAULT_READER_MODE_ACTION_CHIP_DELAY_MS;
            default:
                assert false : "Unknown button variant " + buttonVariant;
                return DEFAULT_CONTEXTUAL_PAGE_ACTION_CHIP_DELAY_MS;
        }
    }

    /**
     * @return Whether the CPA action chip should use a different background color when expanded.
     */
    public static boolean shouldUseAlternativeActionChipColor(
            @AdaptiveToolbarButtonVariant int buttonVariant) {
        if (sAlternativeColorOverridesForTesting != null
                && sAlternativeColorOverridesForTesting.containsKey(buttonVariant)) {
            return Boolean.TRUE.equals(sAlternativeColorOverridesForTesting.get(buttonVariant));
        }
        // Price tracking, price insights and reader mode launched without using alternative color.
        switch (buttonVariant) {
            case AdaptiveToolbarButtonVariant.PRICE_TRACKING:
            case AdaptiveToolbarButtonVariant.READER_MODE:
            case AdaptiveToolbarButtonVariant.PRICE_INSIGHTS:
                return false;
            default:
                assert false : "Unknown button variant " + buttonVariant;
                return false;
        }
    }

    /**
     * We guard contextual page actions behind a feature flag, since all segmentation platform
     * powered functionalities require a feature flag.
     *
     * @return Whether contextual page actions are enabled.
     */
    public static boolean isContextualPageActionsEnabled() {
        return ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_PAGE_ACTIONS);
    }

    public static boolean isAdaptiveToolbarTranslateEnabled() {
        return ChromeFeatureList.isEnabled(
                ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_TRANSLATE);
    }

    public static boolean isAdaptiveToolbarAddToBookmarksEnabled() {
        return ChromeFeatureList.isEnabled(
                ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_ADD_TO_BOOKMARKS);
    }

    public static boolean isPriceInsightsPageActionEnabled() {
        return ChromeFeatureList.isEnabled(ChromeFeatureList.PRICE_INSIGHTS);
    }

    public static boolean isAdaptiveToolbarReadAloudEnabled(Profile profile) {
        return ReadAloudFeatures.isAllowed(profile);
    }

    /**
     * Returns top choice from segmentation results based on device form-factor.
     *
     * @param context @{@link Context} to determine form factor.
     * @param segmentationResults List of rank-ordered results obtained from segmentation.
     * @return Top result to use for UI flows.
     */
    public static @AdaptiveToolbarButtonVariant int getTopSegmentationResult(
            Context context, List<Integer> segmentationResults) {
        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(context)) {
            // Exclude NTB and Bookmarks from segmentation results on tablets since these buttons
            // are available on top chrome (on tab strip and omnibox).
            for (int result : segmentationResults) {
                if (AdaptiveToolbarButtonVariant.NEW_TAB == result
                        || AdaptiveToolbarButtonVariant.ADD_TO_BOOKMARKS == result) continue;
                return result;
            }
            return AdaptiveToolbarButtonVariant.UNKNOWN;
        }
        return segmentationResults.get(0);
    }

    /**
     * Returns the default variant to be shown in segmentation experiment when the backend results
     * are unavailable or not configured.
     *
     * @param context @{@link Context} to determine form-factor.
     */
    static @AdaptiveToolbarButtonVariant int getSegmentationDefault(Context context) {
        assert isCustomizationEnabled();
        if (sButtonVariant != null) return sButtonVariant;
        String defaultSegment = getDefaultSegment(context);
        switch (defaultSegment) {
            case NEW_TAB:
                sButtonVariant = AdaptiveToolbarButtonVariant.NEW_TAB;
                break;
            case SHARE:
                sButtonVariant = AdaptiveToolbarButtonVariant.SHARE;
                break;
            case VOICE:
                sButtonVariant = AdaptiveToolbarButtonVariant.VOICE;
                break;
            default:
                sButtonVariant = AdaptiveToolbarButtonVariant.UNKNOWN;
                break;
        }
        return sButtonVariant;
    }

    /**
     * Returns the default segment to be selected in absence of a valid segmentation result.
     *
     * @param context @{@link Context} to determine form-factor. Defaults defer by form-factor.
     */
    static String getDefaultSegment(Context context) {
        if (sDefaultSegmentForTesting != null) return sDefaultSegmentForTesting;
        return DeviceFormFactor.isNonMultiDisplayContextOnTablet(context) ? SHARE : NEW_TAB;
    }

    static void setDefaultSegmentForTesting(String defaultSegment) {
        sDefaultSegmentForTesting = defaultSegment;
        ResettersForTesting.register(() -> sDefaultSegmentForTesting = null);
    }

    public static void setActionChipOverrideForTesting(
            @AdaptiveToolbarButtonVariant int buttonVariant, Boolean useActionChip) {
        if (sActionChipOverridesForTesting == null) {
            sActionChipOverridesForTesting = new HashMap<>();
        }
        sActionChipOverridesForTesting.put(buttonVariant, useActionChip);
        ResettersForTesting.register(() -> sActionChipOverridesForTesting = null);
    }

    public static void setAlternativeColorOverrideForTesting(
            @AdaptiveToolbarButtonVariant int buttonVariant, Boolean useAlternativeColor) {
        if (sAlternativeColorOverridesForTesting == null) {
            sAlternativeColorOverridesForTesting = new HashMap<>();
        }
        sAlternativeColorOverridesForTesting.put(buttonVariant, useAlternativeColor);
        ResettersForTesting.register(() -> sAlternativeColorOverridesForTesting = null);
    }

    public static void setIsDynamicActionForTesting(
            @AdaptiveToolbarButtonVariant int buttonVariant, Boolean isDynamicAction) {
        if (sIsDynamicActionOverridesForTesting == null) {
            sIsDynamicActionOverridesForTesting = new HashMap<>();
        }
        sIsDynamicActionOverridesForTesting.put(buttonVariant, isDynamicAction);
        ResettersForTesting.register(() -> sIsDynamicActionOverridesForTesting = null);
    }

    public static void clearParsedParamsForTesting() {
        sButtonVariant = null;
        sDefaultSegmentForTesting = null;
    }

    private AdaptiveToolbarFeatures() {}

    /** @return The minimum device width below which the toolbar button isn't shown. */
    public static int getDeviceMinimumWidthForShowingButton() {
        return ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
                ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_CUSTOMIZATION_V2,
                "minimum_width_dp",
                DEFAULT_MIN_WIDTH_DP);
    }
}