chromium/chrome/browser/readaloud/android/java/src/org/chromium/chrome/browser/readaloud/ReadAloudFeatures.java

// Copyright 2023 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.readaloud;

import android.app.Activity;

import androidx.annotation.Nullable;

import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.readaloud.ReadAloudMetrics.IneligibilityReason;
import org.chromium.chrome.browser.search_engines.SearchEngineType;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.signin.services.UnifiedConsentServiceBridge;
import org.chromium.components.search_engines.TemplateUrl;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.components.user_prefs.UserPrefs;

/** Functions for reading feature flags and params and checking eligibility. */
@JNINamespace("readaloud")
public final class ReadAloudFeatures {
    private static final String API_KEY_OVERRIDE_PARAM_NAME = "api_key_override";
    private static final String VOICES_OVERRIDE_PARAM_NAME = "voices_override";
    private static long sKnownReadableTrialPtr;

    private static @IneligibilityReason int sIneligibilityReason = IneligibilityReason.UNKNOWN;

    /** Perform startup tasks. */
    public static void init() {
        ReadAloudFeaturesJni.get().clearStaleSyntheticTrialPrefs();

        // Prepare the "known readable" synthetic trial. If it was active in a previous session and
        // the trial associated with the "ReadAloud" flag hasn't changed since then, reactivate it
        // now, otherwise it will not be active until activateKnownReadableStudy() is called.
        if (sKnownReadableTrialPtr != 0L) {
            return;
        }
        sKnownReadableTrialPtr =
                ReadAloudFeaturesJni.get()
                        .initSyntheticTrial(ChromeFeatureList.READALOUD, "_KnownReadable");
    }

    /** Destroy native components. */
    public static void shutdown() {
        if (sKnownReadableTrialPtr != 0L) {
            ReadAloudFeaturesJni.get().destroySyntheticTrial(sKnownReadableTrialPtr);
            sKnownReadableTrialPtr = 0L;
        }
    }

    /**
     * Returns true if Read Aloud is allowed. All must be true:
     *
     * <ul>
     *   <li>Feature flag enabled
     *   <li>Not incognito mode
     *   <li>User opted into "Make search and browsing better"
     *   <li>Google is the default search engine
     *   <li>Listen to this page policy is enabled
     * </ul>
     */
    public static boolean isAllowed(Profile profile) {
        sIneligibilityReason = IneligibilityReason.UNKNOWN;
        if (profile == null) {
            return false;
        }

        if (profile.isOffTheRecord()) {
            sIneligibilityReason = IneligibilityReason.INCOGNITO_MODE;
            return false;
        }

        // Check whether the user has enabled anonymous URL-keyed data collection.
        // This is surfaced on the relatively new "Make searches and browsing
        // better" user setting.
        if (!UnifiedConsentServiceBridge.isUrlKeyedAnonymizedDataCollectionEnabled(profile)) {
            sIneligibilityReason = IneligibilityReason.MSBB_DISABLED;
            return false;
        }

        TemplateUrlService templateUrlService = TemplateUrlServiceFactory.getForProfile(profile);
        TemplateUrl currentSearchEngine = templateUrlService.getDefaultSearchEngineTemplateUrl();
        if (currentSearchEngine == null) {
            sIneligibilityReason = IneligibilityReason.DEFAULT_SEARCH_ENGINE_GOOGLE_FALSE;
            return false;
        }
        int searchEngineType =
                templateUrlService.getSearchEngineTypeFromTemplateUrl(
                        currentSearchEngine.getKeyword());
        if (searchEngineType != SearchEngineType.SEARCH_ENGINE_GOOGLE) {
            sIneligibilityReason = IneligibilityReason.DEFAULT_SEARCH_ENGINE_GOOGLE_FALSE;
            return false;
        }

        if (!UserPrefs.get(profile).getBoolean(Pref.LISTEN_TO_THIS_PAGE_ENABLED)) {
            sIneligibilityReason = IneligibilityReason.POLICY_DISABLED;
            return false;
        }

        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.READALOUD)) {
            sIneligibilityReason = IneligibilityReason.FEATURE_FLAG_DISABLED;
            return false;
        }

        return true;
    }

    public static @IneligibilityReason int getIneligibilityReason() {
        return sIneligibilityReason;
    }

    /** Returns true if playback is enabled. */
    public static boolean isPlaybackEnabled() {
        return ChromeFeatureList.isEnabled(ChromeFeatureList.READALOUD_PLAYBACK);
    }

    /** Returns true if Read Aloud is allowed to play in the background. */
    public static boolean isBackgroundPlaybackEnabled() {
        return ChromeFeatureList.isEnabled(ChromeFeatureList.READALOUD_BACKGROUND_PLAYBACK);
    }

    /** Returns true if Read Aloud entrypoint can be added to overflow menu in CCT. */
    public static boolean isEnabledForOverflowMenuInCCT() {
        return ChromeFeatureList.isEnabled(ChromeFeatureList.READALOUD_IN_OVERFLOW_MENU_IN_CCT);
    }

    // TODO: b/323238277 Move this check into isAllowed()
    /** Returns true if in multi-window and ReadAloud is disabled for multi-window. */
    public static boolean isInMultiWindowAndDisabled(Activity activity) {
        return activity.isInMultiWindowMode()
                && !ChromeFeatureList.isEnabled(ChromeFeatureList.READALOUD_IN_MULTI_WINDOW);
    }

    /** Returns true if Read Aloud tap to seek is enabled. */
    public static boolean isTapToSeekEnabled() {
        return ChromeFeatureList.sReadAloudTapToSeek.isEnabled();
    }

    /** Returns true if the ReadAloud CCT IPH should highlight the menu button. */
    public static boolean isIPHMenuButtonHighlightCCTEnabled() {
        return ChromeFeatureList.isEnabled(
                ChromeFeatureList.READALOUD_IPH_MENU_BUTTON_HIGHLIGHT_CCT);
    }

    /** Returns the API key override feature param if present, or null otherwise. */
    @Nullable
    public static String getApiKeyOverride() {
        String apiKeyOverride =
                ChromeFeatureList.getFieldTrialParamByFeature(
                        ChromeFeatureList.READALOUD, API_KEY_OVERRIDE_PARAM_NAME);
        return apiKeyOverride.isEmpty() ? null : apiKeyOverride;
    }

    /**
     * Returns the voice list override param value in serialized form, or empty
     * string if the param is absent. Value is a base64-encoded ListVoicesResponse
     * binarypb.
     */
    public static String getVoicesParam() {
        return ChromeFeatureList.getFieldTrialParamByFeature(
                ChromeFeatureList.READALOUD, VOICES_OVERRIDE_PARAM_NAME);
    }

    /**
     * Activate the "known readable" synthetic trial if it isn't already active. It may be
     * reactivated on the next startup as described above. Only the first call to this method has
     * any effect.
     */
    public static void activateKnownReadableTrial() {
        if (sKnownReadableTrialPtr != 0L) {
            ReadAloudFeaturesJni.get().activateSyntheticTrial(sKnownReadableTrialPtr);
        }
    }

    /** Return the metrics client ID or empty string if it isn't available. */
    public static String getMetricsId() {
        return ReadAloudFeaturesJni.get().getMetricsId();
    }

    @NativeMethods
    public interface Natives {
        // Create a native readaloud::SyntheticTrial and return its address. It must be
        // cleaned up with destroySyntheticTrial(). May return nullptr if there is no
        // field trial overriding `featureName`.
        long initSyntheticTrial(String featureName, String syntheticTrialNameSuffix);

        // Activate a synthetic trial if not already active. Pointer must not be null.
        void activateSyntheticTrial(long syntheticTrialPtr);

        // Destroy the synthetic trial native object. Pointer must not be null.
        void destroySyntheticTrial(long syntheticTrialPtr);

        // Check stored synthetic trial reactivation prefs and delete those that don't
        // match current field trial state.
        void clearStaleSyntheticTrialPrefs();

        // Get metrics client ID or empty string if it isn't available.
        String getMetricsId();
    }
}