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

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionUtil;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.segmentation_platform.proto.SegmentationProto.SegmentId;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.permissions.AndroidPermissionDelegate;

import java.util.List;

/**
 * Central class that determines the state of the toolbar button based on finch configuration, user
 * preference, and segmentation platform backend prediction. This class is used only for the
 * segmentation experiment.
 */
public class AdaptiveToolbarStatePredictor {
    /**
     * Key used to lookup segmentation results for adaptive toolbar. Must be kept in sync with
     * components/segmentation_platform/internal/constants.cc.
     */
    private static List<Integer> sSegmentationResultsForTesting;

    private static Integer sToolbarStateForTesting;
    private final Context mContext;
    @NonNull private final Profile mProfile;
    @Nullable private final AndroidPermissionDelegate mAndroidPermissionDelegate;

    /** The result of the predictor. Contains the UI states specific to the toolbar button. */
    public static class UiState {
        /** Used to determine whether we can show any toolbar shortcut specific UI. */
        public final boolean canShowUi;

        /** Used for showing the toolbar shortcut action in the toolbar UI. */
        public final @AdaptiveToolbarButtonVariant int toolbarButtonState;

        /** Used for the selected radio button in the toolbar shortcut settings page. */
        public final @AdaptiveToolbarButtonVariant int preferenceSelection;

        /** Used for the substring used in the auto option. */
        public final @AdaptiveToolbarButtonVariant int autoButtonCaption;

        /** Constructor. */
        public UiState(
                boolean canShowUi,
                int toolbarButtonState,
                int preferenceSelection,
                int autoButtonCaption) {
            this.canShowUi = canShowUi;
            this.toolbarButtonState = toolbarButtonState;
            this.preferenceSelection = preferenceSelection;
            this.autoButtonCaption = autoButtonCaption;
        }
    }

    /**
     * Constructs {@code AdaptiveToolbarStatePredictor}
     *
     * @param context @{@link Context} to determine form-factor.
     * @param profile The {@link Profile} associated with the toolbar state.
     * @param androidPermissionDelegate used for determining if voice search can be used
     */
    public AdaptiveToolbarStatePredictor(
            Context context,
            Profile profile,
            @Nullable AndroidPermissionDelegate androidPermissionDelegate) {
        mContext = context;
        mProfile = profile;
        mAndroidPermissionDelegate = androidPermissionDelegate;
    }

    /**
     * Called to get the updated state of the UI based on various signals.
     *
     * @param callback The callback containing the result.
     */
    public void recomputeUiState(Callback<UiState> callback) {
        if (sToolbarStateForTesting != null) {
            UiState uiState =
                    new UiState(
                            isValidSegment(sToolbarStateForTesting),
                            sToolbarStateForTesting,
                            sToolbarStateForTesting,
                            sToolbarStateForTesting);
            callback.onResult(uiState);
            return;
        }

        // Early return if the feature isn't enabled.
        if (!AdaptiveToolbarFeatures.isCustomizationEnabled()) {
            callback.onResult(
                    new UiState(
                            false,
                            AdaptiveToolbarButtonVariant.UNKNOWN,
                            AdaptiveToolbarButtonVariant.UNKNOWN,
                            AdaptiveToolbarButtonVariant.UNKNOWN));
            return;
        }

        int manualOverride = readManualOverrideFromPrefs();
        int defaultSegment = AdaptiveToolbarFeatures.getSegmentationDefault(mContext);
        boolean toolbarToggle = readToolbarToggleStateFromPrefs();
        readFromSegmentationPlatform(
                segmentSelectionResults -> {
                    int topSegmentationResult =
                            AdaptiveToolbarFeatures.getTopSegmentationResult(
                                    mContext, segmentSelectionResults);
                    UiState uiState =
                            new UiState(
                                    AdaptiveToolbarFeatures.isCustomizationEnabled(),
                                    replaceVariantIfDisabled(
                                            getToolbarButtonState(
                                                    toolbarToggle,
                                                    manualOverride,
                                                    defaultSegment,
                                                    topSegmentationResult)),
                                    getToolbarPreferenceSelection(manualOverride),
                                    replaceVariantIfDisabled(
                                            getToolbarPreferenceAutoOptionSubtitleSegment(
                                                    defaultSegment, topSegmentationResult)));
                    callback.onResult(uiState);
                });
    }

    private @AdaptiveToolbarButtonVariant int getToolbarButtonState(
            boolean toolbarToggle,
            @AdaptiveToolbarButtonVariant int manualOverride,
            @AdaptiveToolbarButtonVariant int defaultSegment,
            @AdaptiveToolbarButtonVariant int segmentationResult) {
        if (!toolbarToggle) return AdaptiveToolbarButtonVariant.UNKNOWN;
        if (isValidSegment(manualOverride)) return manualOverride;

        return isValidSegment(segmentationResult) ? segmentationResult : defaultSegment;
    }

    private @AdaptiveToolbarButtonVariant int getToolbarPreferenceSelection(
            @AdaptiveToolbarButtonVariant int manualOverride) {
        if (isValidSegment(manualOverride)) return manualOverride;
        return AdaptiveToolbarButtonVariant.AUTO;
    }

    private @AdaptiveToolbarButtonVariant int getToolbarPreferenceAutoOptionSubtitleSegment(
            @AdaptiveToolbarButtonVariant int defaultSegment,
            @AdaptiveToolbarButtonVariant int segmentationResult) {
        return isValidSegment(segmentationResult) ? segmentationResult : defaultSegment;
    }

    /**
     * @return Given a segment, whether it is a valid segment that can be shown to the user.
     */
    private boolean isValidSegment(@AdaptiveToolbarButtonVariant int variant) {
        switch (variant) {
            case AdaptiveToolbarButtonVariant.NEW_TAB:
            case AdaptiveToolbarButtonVariant.SHARE:
            case AdaptiveToolbarButtonVariant.VOICE:
            case AdaptiveToolbarButtonVariant.TRANSLATE:
            case AdaptiveToolbarButtonVariant.ADD_TO_BOOKMARKS:
            case AdaptiveToolbarButtonVariant.READ_ALOUD:
                return true;
            case AdaptiveToolbarButtonVariant.UNKNOWN:
            case AdaptiveToolbarButtonVariant.NONE:
            case AdaptiveToolbarButtonVariant.AUTO:
            case AdaptiveToolbarButtonVariant.PRICE_TRACKING:
            case AdaptiveToolbarButtonVariant.READER_MODE:
            case AdaptiveToolbarButtonVariant.PRICE_INSIGHTS:
                return false;
            default:
                assert false : "Invalid adaptive toolbar button variant: " + variant;
                return false;
        }
    }

    @VisibleForTesting
    @AdaptiveToolbarButtonVariant
    int readManualOverrideFromPrefs() {
        return AdaptiveToolbarPrefs.getCustomizationSetting();
    }

    @VisibleForTesting
    boolean readToolbarToggleStateFromPrefs() {
        return AdaptiveToolbarPrefs.isCustomizationPreferenceEnabled();
    }

    /**
     * Called to read results from the segmentation backend. The result contains a list of {@link
     * AdaptiveToolbarButtonVariant} indicating rank-ordered segments. Caller can determine which
     * result should be shown.
     *
     * @param callback A callback for results.
     */
    public void readFromSegmentationPlatform(Callback<List<Integer>> callback) {
        if (sSegmentationResultsForTesting != null) {
            callback.onResult(sSegmentationResultsForTesting);
            return;
        }

        boolean useRawResults = DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext);
        AdaptiveToolbarBridge.getSessionVariantButtons(
                mProfile, useRawResults, result -> callback.onResult(result.second));
    }

    /**
     * Returns the default segment if {@code variant} is not available on this system. Otherwise
     * returns {@code variant} unchanged.
     */
    private @AdaptiveToolbarButtonVariant int replaceVariantIfDisabled(
            @AdaptiveToolbarButtonVariant int variant) {
        if (isVariantEnabled(variant)) return variant;
        variant = AdaptiveToolbarFeatures.getSegmentationDefault(mContext);
        if (isVariantEnabled(variant)) return variant;
        // Fallback in the unlikely situation the default is disabled.
        return AdaptiveToolbarButtonVariant.UNKNOWN;
    }

    private boolean isVariantEnabled(@AdaptiveToolbarButtonVariant int variant) {
        switch (variant) {
            case AdaptiveToolbarButtonVariant.VOICE:
                if (mAndroidPermissionDelegate == null) return true;
                return VoiceRecognitionUtil.isVoiceSearchEnabled(mAndroidPermissionDelegate);
            case AdaptiveToolbarButtonVariant.TRANSLATE:
                return AdaptiveToolbarFeatures.isAdaptiveToolbarTranslateEnabled();
            case AdaptiveToolbarButtonVariant.ADD_TO_BOOKMARKS:
                return AdaptiveToolbarFeatures.isAdaptiveToolbarAddToBookmarksEnabled();
            case AdaptiveToolbarButtonVariant.READ_ALOUD:
                return AdaptiveToolbarFeatures.isAdaptiveToolbarReadAloudEnabled(mProfile);
            default:
                return true;
        }
    }

    /**
     * Conversion method between {@link SegmentId} and {@link
     * AdaptiveToolbarButtonVariant}.
     */
    public static @AdaptiveToolbarButtonVariant int getAdaptiveToolbarButtonVariantFromSegmentId(
            SegmentId segmentId) {
        switch (segmentId) {
            case OPTIMIZATION_TARGET_SEGMENTATION_NEW_TAB:
                return AdaptiveToolbarButtonVariant.NEW_TAB;
            case OPTIMIZATION_TARGET_SEGMENTATION_SHARE:
                return AdaptiveToolbarButtonVariant.SHARE;
            case OPTIMIZATION_TARGET_SEGMENTATION_VOICE:
                return AdaptiveToolbarButtonVariant.VOICE;
            case OPTIMIZATION_TARGET_CONTEXTUAL_PAGE_ACTION_PRICE_TRACKING:
                return AdaptiveToolbarButtonVariant.PRICE_TRACKING;
            default:
                return AdaptiveToolbarButtonVariant.UNKNOWN;
        }
    }

    /** For testing only. */
    public static void setSegmentationResultsForTesting(Pair<Boolean, List<Integer>> results) {
        sSegmentationResultsForTesting = results == null ? null : results.second;
    }

    /** For testing only. */
    public static void setToolbarStateForTesting(Integer toolbarState) {
        sToolbarStateForTesting = toolbarState;
    }
}