chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionUtil.java

// Copyright 2020 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.omnibox.voice;

import android.Manifest;
import android.content.Intent;
import android.speech.RecognizerIntent;

import androidx.annotation.Nullable;

import org.chromium.base.PackageManagerUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.ui.permissions.AndroidPermissionDelegate;

/** Utilities related to voice recognition. */
public class VoiceRecognitionUtil {
    private static Boolean sHasRecognitionIntentHandler;
    private static Boolean sIsVoiceSearchEnabledForTesting;

    /**
     * Returns whether voice search is enabled.
     *
     * <p>Evaluates voice search eligibility based on
     *
     * <ul>
     *   <li>Android permissions (user consent),
     *   <li>Enterprise policies,
     *   <li>Presence of a speech-to-text service in the system.
     * </ul>
     *
     * <p>Note: Requires native libraries to be loaded and initialized for proper execution. When
     * called prematurely, certain signals may be unavailable, making the system fall back to
     * best-effort defaults.
     *
     * <p>Note: this check does not perform strict policy checking.
     *
     * @return true if all the conditions permit execution of a voice search.
     */
    public static boolean isVoiceSearchEnabled(
            AndroidPermissionDelegate androidPermissionDelegate) {
        if (sIsVoiceSearchEnabledForTesting != null) {
            return sIsVoiceSearchEnabledForTesting.booleanValue();
        }

        if (androidPermissionDelegate == null) return false;
        if (!androidPermissionDelegate.hasPermission(Manifest.permission.RECORD_AUDIO)
                && !androidPermissionDelegate.canRequestPermission(
                        Manifest.permission.RECORD_AUDIO)) {
            return false;
        }

        if (!isVoiceSearchPermittedByPolicy(/* strictPolicyCheck= */ false)) return false;

        return isRecognitionIntentPresent(true);
    }

    /**
     * Returns whether enterprise policies permit voice search.
     *
     * <p>Note: Requires native libraries to be loaded and initialized for proper execution. When
     * called prematurely, certain signals may be unavailable, making the system fall back to
     * best-effort defaults.
     *
     * @param strictPolicyCheck Whether to fail if the policy verification cannot be performed at
     *     this time. May be set to false by the UI code if there is a possibility that the call is
     *     made early (eg. before native libraries are initialized). Must be set to true ahead of
     *     actual check.
     * @return true if the Enterprise policies permit execution of a voice search.
     */
    public static boolean isVoiceSearchPermittedByPolicy(boolean strictPolicyCheck) {
        if (ChromeFeatureList.sVoiceSearchAudioCapturePolicy.isEnabled()) {
            // If the PrefService isn't initialized yet we won't know here whether or not voice
            // search is allowed by policy. In that case, treat voice search as enabled but check
            // again when a Profile is set and PrefService becomes available.
            PrefService prefService = getPrefService();

            // Fail if strict policy checking is requested but we do not have the way to verify.
            if (strictPolicyCheck && prefService == null) return false;

            return prefService == null || prefService.getBoolean(Pref.AUDIO_CAPTURE_ALLOWED);
        }
        return true;
    }

    /**
     * Set whether voice search is enabled. Should be reset back to null after the test has
     * finished.
     *
     * @param isVoiceSearchEnabled
     */
    public static void setIsVoiceSearchEnabledForTesting(@Nullable Boolean isVoiceSearchEnabled) {
        sIsVoiceSearchEnabledForTesting = isVoiceSearchEnabled;
        ResettersForTesting.register(() -> sIsVoiceSearchEnabledForTesting = null);
    }

    static void setHasRecognitionIntentHandlerForTesting(@Nullable Boolean hasIntentHandler) {
        var oldValue = sHasRecognitionIntentHandler;
        sHasRecognitionIntentHandler = hasIntentHandler;
        ResettersForTesting.register(() -> sHasRecognitionIntentHandler = oldValue);
    }

    /** Returns the PrefService for the active Profile, or null if no profile has been loaded. */
    private static @Nullable PrefService getPrefService() {
        if (!ProfileManager.isInitialized()) return null;
        return UserPrefs.get(ProfileManager.getLastUsedRegularProfile());
    }

    /**
     * Determines whether or not the {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} {@link Intent}
     * is handled by any {@link android.app.Activity}s in the system. The result will be cached for
     * future calls. Passing {@code false} to {@code useCachedValue} will force it to re-query any
     * {@link android.app.Activity}s that can process the {@link Intent}.
     *
     * @param useCachedValue Whether or not to use the cached value from a previous result.
     * @return {@code true} if recognition is supported. {@code false} otherwise.
     */
    public static boolean isRecognitionIntentPresent(boolean useCachedValue) {
        ThreadUtils.assertOnUiThread();
        if (sHasRecognitionIntentHandler == null || !useCachedValue) {
            sHasRecognitionIntentHandler =
                    PackageManagerUtils.canResolveActivity(
                            new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH));
        }

        return sHasRecognitionIntentHandler;
    }
}