text:
// Copyright 2018 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.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.speech.RecognizerIntent;
import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.CallbackController;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.MutableFlagWithSafeDefault;
import org.chromium.chrome.browser.flags.PostNativeFlag;
import org.chromium.chrome.browser.omnibox.LocationBarDataProvider;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.translate.TranslateBridge;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.permissions.PermissionCallback;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.List;

/**
 * Class containing functionality related to voice search.
 */
public class VoiceRecognitionHandler {
    private static final String TAG = "VoiceRecognition";

    /**
     * The minimum confidence threshold that will result in navigating directly to a voice search
     * response (as opposed to treating it like a typed string in the Omnibox).
     */
    @VisibleForTesting
    public static final float VOICE_SEARCH_CONFIDENCE_NAVIGATE_THRESHOLD = 0.9f;
    /**
     * Extra containing the languages for the returned voice transcriptions (ArrayList<String>).
     *
     * This language is only returned for queries handled by Assistant.
     */
    @VisibleForTesting
    static final String VOICE_QUERY_RESULT_LANGUAGES = "android.speech.extra.LANGUAGE";
    /**
     * Extra containing an identifier for the current Assistant experiment.
     *
     * This is only populated for intents initiated via the toolbar button, and is not populated for
     * internal Chrome URLs.
     */
    @VisibleForTesting
    static final String EXTRA_EXPERIMENT_ID = "com.android.chrome.voice.EXPERIMENT_ID";
    /**
     * The parameter from the ASSISTANT_INTENT_EXPERIMENT_ID feature that configures the experiment
     * ID attached via the EXTRA_EXPERIMENT_ID extra.
     */
    @VisibleForTesting
    static final String ASSISTANT_EXPERIMENT_ID_PARAM_NAME = "experiment_id";
    /**
     * Extra containing the URL of the current page.
     *
     * This is only populated for intents initiated via the toolbar button, and is not populated for
     * internal Chrome URLs.
     */
    @VisibleForTesting
    static final String EXTRA_PAGE_URL = "com.android.chrome.voice.PAGE_URL";
    /**
     * Extra containing the source language code of the current page.
     *
     * This is only populated for pages that are translatable and only for intents initiated via the
     * toolbar button.
     */
    @VisibleForTesting
    static final String EXTRA_TRANSLATE_ORIGINAL_LANGUAGE =
            "com.android.chrome.voice.TRANSLATE_ORIGINAL_LANGUAGE";
    /**
     * Extra containing the current language code of the current page.
     *
     * This is only populated for pages that are translatable and only for intents initiated via the
     * toolbar button.
     */
    @VisibleForTesting
    static final String EXTRA_TRANSLATE_CURRENT_LANGUAGE =
            "com.android.chrome.voice.TRANSLATE_CURRENT_LANGUAGE";
    /**
     * Extra containing the user's default target language code.
     *
     * This is only populated for pages that are translatable and only for intents initiated via the
     * toolbar button.
     */
    @VisibleForTesting
    static final String EXTRA_TRANSLATE_TARGET_LANGUAGE =
            "com.android.chrome.voice.TRANSLATE_TARGET_LANGUAGE";
    /**
     * Extra containing a string that represents the action taken by Assistant after being opened
     * for voice transcription.
     *
     * See AssistantActionPerformed, below.
     */
    @VisibleForTesting
    static final String EXTRA_ACTION_PERFORMED = "com.android.chrome.voice.ACTION_PERFORMED";
    /**
     * Extra containing the current timestamp (in epoch time) used for tracking intent latency.
     */
    @VisibleForTesting
    static final String EXTRA_INTENT_SENT_TIMESTAMP =
            "com.android.chrome.voice.INTENT_SENT_TIMESTAMP";
    /**
     * Extra containing the email for the signed-in/syncing account on the device.
     */
    @VisibleForTesting
    static final String EXTRA_INTENT_USER_EMAIL = "com.android.chrome.voice.INTENT_USER_EMAIL";
    /**
     * Extra containing an integer that indicates which voice entrypoint the intent was initiated
     * from.
     *
     * See VoiceInteractionEventSource for possible values.
     */
    @VisibleForTesting
    static final String EXTRA_VOICE_ENTRYPOINT = "com.android.chrome.voice.VOICE_ENTRYPOINT";

    private static final MutableFlagWithSafeDefault sToolbarMicIphFlag =
            new MutableFlagWithSafeDefault(ChromeFeatureList.TOOLBAR_MIC_IPH_ANDROID, false);
    private static final MutableFlagWithSafeDefault sAssistantIntentPageUrlFlag =
            new MutableFlagWithSafeDefault(ChromeFeatureList.ASSISTANT_INTENT_PAGE_URL, false);
    private static final MutableFlagWithSafeDefault sAssistantIntentExperimentIdFlag =
            new MutableFlagWithSafeDefault(ChromeFeatureList.ASSISTANT_INTENT_EXPERIMENT_ID, false);
    private static final MutableFlagWithSafeDefault sAssistantNonPersonalizedVoiceSearchFlag =
            new MutableFlagWithSafeDefault(
                    ChromeFeatureList.ASSISTANT_NON_PERSONALIZED_VOICE_SEARCH, true);
    private static final MutableFlagWithSafeDefault sAssistantIntentTranslateInfoFlag =
            new MutableFlagWithSafeDefault(
                    ChromeFeatureList.ASSISTANT_INTENT_TRANSLATE_INFO, false);
    private static final PostNativeFlag sCacheVoiceSearchEnabledFlag =
            new PostNativeFlag(ChromeFeatureList.IS_VOICE_SEARCH_ENABLED_CACHE);

    private static Boolean sIsRecognitionIntentPresentForTesting;
    private final Delegate mDelegate;
    private Long mQueryStartTimeMs;
    private WebContentsObserver mVoiceSearchWebContentsObserver;
    private Supplier<AssistantVoiceSearchService> mAssistantVoiceSearchServiceSupplier;
    private TranslateBridgeWrapper mTranslateBridgeWrapper;
    private final ObserverList<Observer> mObservers = new ObserverList<>();
    private final Runnable mLaunchAssistanceSettingsAction;
    private CallbackController mCallbackController = new CallbackController();
    private ObservableSupplier<Profile> mProfileSupplier;
    private Boolean mIsVoiceSearchEnabledCached;
    private boolean mRegisteredActivityStateListener;

    /**
     * AudioPermissionState defined in tools/metrics/histograms/enums.xml.
     *
     * Do not reorder or remove items, only add new items before NUM_ENTRIES.
     */
    @IntDef({AudioPermissionState.GRANTED, AudioPermissionState.DENIED_CAN_ASK_AGAIN,
            AudioPermissionState.DENIED_CANNOT_ASK_AGAIN})
    public @interface AudioPermissionState {
        // Permissions have been granted and won't be requested this time.
        int GRANTED = 0;
        int DENIED_CAN_ASK_AGAIN = 1;
        int DENIED_CANNOT_ASK_AGAIN = 2;

        // Be sure to also update enums.xml when updating these values.
        int NUM_ENTRIES = 3;
    }

    /**
     * VoiceInteractionEventSource defined in tools/metrics/histograms/enums.xml.
     *
     * Do not reorder or remove items, only add new items before NUM_ENTRIES.
     */
    @IntDef({VoiceInteractionSource.OMNIBOX, VoiceInteractionSource.NTP,
            VoiceInteractionSource.SEARCH_WIDGET, VoiceInteractionSource.TASKS_SURFACE,
            VoiceInteractionSource.TOOLBAR})
    public @interface VoiceInteractionSource {
        int OMNIBOX = 0;
        int NTP = 1;
        int SEARCH_WIDGET = 2;
        int TASKS_SURFACE = 3;
        int TOOLBAR = 4;

        // Be sure to also update enums.xml when updating these values.
        int NUM_ENTRIES = 5;
    }

    /**
     * VoiceIntentTarget defined in tools/metrics/histograms/enums.xml.
     *
     * Do not reorder or remove items, only add new items before NUM_ENTRIES.
     */
    @IntDef({VoiceIntentTarget.SYSTEM, VoiceIntentTarget.ASSISTANT})
    public @interface VoiceIntentTarget {
        int SYSTEM = 0;
        int ASSISTANT = 1;

        // Be sure to also update enums.xml when updating these values.
        int NUM_ENTRIES = 2;
    }

    /**
     * AssistantActionPerformed defined in tools/metrics/histograms/enums.xml.
     *
     * Do not reorder or remove items, only add new items before NUM_ENTRIES.
     */
    @IntDef({AssistantActionPerformed.UNKNOWN, AssistantActionPerformed.TRANSCRIPTION,
            AssistantActionPerformed.TRANSLATE, AssistantActionPerformed.READOUT})
    public @interface AssistantActionPerformed {
        int UNKNOWN = 0;
        int TRANSCRIPTION = 1;
        int TRANSLATE = 2;
        int READOUT = 3;

        // Be sure to also update enums.xml when updating these values.
        int NUM_ENTRIES = 4;
    }

    /**
     * Delegate interface to provide data to this class from the location bar implementation.
     */
    public interface Delegate {
        /**
         * Loads the provided URL, assumes the PageTransition type is TYPED.
         * @param url The URL to load.
         */
        void loadUrlFromVoice(String url);

        /**
         * Sets the query string in the omnibox (ensuring the URL bar has focus and triggering
         * autocomplete for the specified query) as if the user typed it.
         * @param query The query to be set in the omnibox.
         */
        void setSearchQuery(final String query);

        /**
         * Grabs a reference to the location data provider from the location bar.
         * @return The {@link LocationBarDataProvider} currently in use by the
         *         {@link LocationBarLayout}.
         */
        LocationBarDataProvider getLocationBarDataProvider();

        /**
         * Grabs a reference to the autocomplete coordinator from the location bar.
         * @return The {@link AutocompleteCoordinator} currently in use by the
         *         {@link LocationBarLayout}.
         */
        // TODO(tedchoc): Limit the visibility of what is passed in here.  This does not need the
        //                full coordinator.  It simply needs a way to pass voice suggestions to the
        //                AutocompleteController.
        AutocompleteCoordinator getAutocompleteCoordinator();

        /**
         * @return The current {@link WindowAndroid}.
         */
        WindowAndroid getWindowAndroid();

        /** Clears omnibox focus, used to display the dialog when the keyboard is shown. */
        void clearOmniboxFocus();

        /** Notifies the delegate that voice recognition could not complete. */
        void notifyVoiceRecognitionCanceled();
    }

    /** Interface for observers interested in updates to the voice state. */
    public interface Observer {
        /**
         * Triggers when an event occurs that impacts availability of the voice
         * recognition, for example audio permissions or policy values change.
         */
        void onVoiceAvailabilityImpacted();
    }

    /**
     * Wraps our usage of the static methods in the {@link TranslateBridge} into a class that can be
     * mocked for testing.
     */
    public static class TranslateBridgeWrapper {
        /**
         * Returns true iff the current tab can be manually translated.
         * Logging should only be performed when this method is called to show the translate menu
         * item.
         */
        public boolean canManuallyTranslate(Tab tab) {
            return TranslateBridge.canManuallyTranslate(tab, /*menuLogging=*/false);
        }

        /**
         * Returns the source language code of the given tab. Empty string if no language was
         * detected yet.
         */
        public String getSourceLanguage(Tab tab) {
            return TranslateBridge.getSourceLanguage(tab);
        }

        /**
         * Returns the current language code of the given tab. Empty string if no language was
         * detected yet.
         */
        public String getCurrentLanguage(Tab tab) {
            return TranslateBridge.getCurrentLanguage(tab);
        }

        /**
         * Returns the best target language based on what the Translate Service knows about the
         * user.
         */
        public String getTargetLanguage() {
            return TranslateBridge.getTargetLanguage();
        }
    }

    /**
     * A storage class that holds voice recognition string matches and confidence scores.
     */
    public static class VoiceResult {
        private final String mMatch;
        private final float mConfidence;
        @Nullable
        private final String mLanguage;

        public VoiceResult(String match, float confidence) {
            this(match, confidence, null);
        }

        /**
         * Creates an instance of a VoiceResult.
         * @param match The text match from the voice recognition.
         * @param confidence The confidence value of the recognition that should go from 0.0 to 1.0.
         * @param language The language of the returned query.
         */
        public VoiceResult(String match, float confidence, @Nullable String language) {
            mMatch = match;
            mConfidence = confidence;
            mLanguage = language;
        }

        /**
         * @return The text match from the voice recognition.
         */
        public String getMatch() {
            return mMatch;
        }

        /**
         * @return The confidence value of the recognition that should go from 0.0 to 1.0.
         */
        public float getConfidence() {
            return mConfidence;
        }

        /**
         * @return The IETF language tag for this result.
         */
        public @Nullable String getLanguage() {
            return mLanguage;
        }
    }

    public VoiceRecognitionHandler(Delegate delegate,
            Supplier<AssistantVoiceSearchService> assistantVoiceSearchServiceSupplier,
            Runnable launchAssistanceSettingsAction, ObservableSupplier<Profile> profileSupplier) {
        mDelegate = delegate;
        mAssistantVoiceSearchServiceSupplier = assistantVoiceSearchServiceSupplier;
        mLaunchAssistanceSettingsAction = launchAssistanceSettingsAction;
        mTranslateBridgeWrapper = new TranslateBridgeWrapper();
        mProfileSupplier = profileSupplier;
        mProfileSupplier.addObserver(
                mCallbackController.makeCancelable(profile -> notifyVoiceAvailabilityImpacted()));
    }

    public void addObserver(Observer observer) {
        mObservers.addObserver(observer);
    }

    public void removeObserver(Observer observer) {
        mObservers.removeObserver(observer);
    }

    private void notifyVoiceAvailabilityImpacted() {
        for (Observer o : mObservers) {
            o.onVoiceAvailabilityImpacted();
        }
    }

    /**
     * Instantiated when a voice search is performed to monitor the web contents for a navigation
     * to be started so we can notify the render frame that a user gesture has been performed. This
     * allows autoplay of the voice response for search results.
     */
    private final class VoiceSearchWebContentsObserver extends WebContentsObserver {
        public VoiceSearchWebContentsObserver(WebContents webContents) {
            super(webContents);
        }

        /**
         * Forces the user gesture flag to be set on a render frame if the URL being navigated to
         * is a SRP.
         *
         * @param url The URL for the navigation that started, so we can ensure that what we're
         * navigating to is actually a SRP.
         */
        private void setReceivedUserGesture(GURL url) {
            WebContents webContents = mWebContents.get();
            if (webContents == null) return;

            RenderFrameHost renderFrameHost = webContents.getMainFrame();
            if (renderFrameHost == null) return;
            if (TemplateUrlServiceFactory.get().isSearchResultsPageFromDefaultSearchProvider(url)) {
                renderFrameHost.notifyUserActivation();
            }
        }

        @Override
        public void didFinishNavigationInPrimaryMainFrame(NavigationHandle navigation) {
            if (navigation.hasCommitted() && !navigation.isErrorPage()) {
                setReceivedUserGesture(navigation.getUrl());
            }
            destroy();
        }

        @Override
        public void didFinishNavigationNoop(NavigationHandle navigation) {
            if (!navigation.isInPrimaryMainFrame()) return;
        }
    }

    /**
     * Callback for when we receive voice search results after initiating voice recognition.
     */
    class VoiceRecognitionCompleteCallback implements WindowAndroid.IntentCallback {
        @VoiceInteractionSource
        private final int mSource;

        @VoiceIntentTarget
        private final int mTarget;

        private boolean mCallbackComplete;

        public VoiceRecognitionCompleteCallback(
                @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
            mSource = source;
            mTarget = target;
        }

        // WindowAndroid.IntentCallback implementation:
        @Override
        public void onIntentCompleted(int resultCode, Intent data) {
            if (mCallbackComplete) {
                recordVoiceSearchUnexpectedResult(mSource, mTarget);
                return;
            }

            mCallbackComplete = true;
            if (resultCode == Activity.RESULT_CANCELED) {
                recordVoiceSearchDismissedEvent(mSource, mTarget);
                mDelegate.notifyVoiceRecognitionCanceled();
                return;
            }
            if (resultCode != Activity.RESULT_OK || data.getExtras() == null) {
                recordVoiceSearchFailureEvent(mSource, mTarget);
                mDelegate.notifyVoiceRecognitionCanceled();
                return;
            }

            // Log successful voice use for IPH purposes.
            // Log successful voice use for IPH purposes.
            if (sToolbarMicIphFlag.isEnabled() && mProfileSupplier.hasValue()) {
                // mic shouldn't be visible
                assert !mProfileSupplier.get().isOffTheRecord();
                Tracker tracker = TrackerFactory.getTrackerForProfile(mProfileSupplier.get());
                tracker.notifyEvent(EventConstants.SUCCESSFUL_VOICE_SEARCH);
            }
            // Assume transcription by default when the page URL feature is disabled.
            @AssistantActionPerformed
            int actionPerformed = AssistantActionPerformed.TRANSCRIPTION;
            if (sAssistantIntentPageUrlFlag.isEnabled()) {
                actionPerformed = getActionPerformed(data.getExtras());
            }

            recordSuccessMetrics(mSource, mTarget, actionPerformed);

            if (actionPerformed == AssistantActionPerformed.TRANSCRIPTION) {
                handleTranscriptionResult(data);
            }
        }

        /**
         * Processes the transcription results within the given Intent, potentially initiating a
         * search or navigation.
         * @param data The {@link Intent} with returned transcription data.
         */
        private void handleTranscriptionResult(Intent data) {
            AutocompleteCoordinator autocompleteCoordinator =
                    mDelegate.getAutocompleteCoordinator();
            assert autocompleteCoordinator != null;

            List<VoiceResult> voiceResults = convertBundleToVoiceResults(data.getExtras());
            autocompleteCoordinator.onVoiceResults(voiceResults);
            VoiceResult topResult =
                    (voiceResults != null && voiceResults.size() > 0) ? voiceResults.get(0) : null;
            if (topResult == null) {
                recordVoiceSearchResult(mTarget, false);
                return;
            }

            String topResultQuery = topResult.getMatch();
            if (TextUtils.isEmpty(topResultQuery)) {
                recordVoiceSearchResult(mTarget, false);
                return;
            }

            recordVoiceSearchResult(mTarget, true);
            recordVoiceSearchConfidenceValue(mTarget, topResult.getConfidence());

            if (topResult.getConfidence() < VOICE_SEARCH_CONFIDENCE_NAVIGATE_THRESHOLD) {
                mDelegate.setSearchQuery(topResultQuery);
                return;
            }

            // Since voice was used, we need to let the frame know that there was a user gesture.
            LocationBarDataProvider locationBarDataProvider =
                    mDelegate.getLocationBarDataProvider();
            Tab currentTab =
                    locationBarDataProvider != null ? locationBarDataProvider.getTab() : null;
            if (currentTab != null) {
                if (mVoiceSearchWebContentsObserver != null) {
                    mVoiceSearchWebContentsObserver.destroy();
                    mVoiceSearchWebContentsObserver = null;
                }
                if (currentTab.getWebContents() != null) {
                    mVoiceSearchWebContentsObserver =
                            new VoiceSearchWebContentsObserver(currentTab.getWebContents());
                }
            }

            AutocompleteMatch match = null;
            if (mProfileSupplier.hasValue()) {
                match = AutocompleteCoordinator.classify(mProfileSupplier.get(), topResultQuery);
            }

            String url;
            if (match == null || match.isSearchSuggestion()) {
                url = TemplateUrlServiceFactory.get()
                              .getUrlForVoiceSearchQuery(topResultQuery)
                              .getSpec();
                // If a language was returned to us from voice recognition, then use it. Currently,
                // this is only returned when Google is the search engine. Since Google always has
                // the query as a url parameter so appending this param will always be safe.
                if (topResult.getLanguage() != null) {
                    // TODO(crbug.com/1117271): Cleanup these assertions when Assistant launches.
                    assert url.contains("?") : "URL must contain at least one URL param.";
                    assert !url.contains("#") : "URL must not contain a fragment.";
                    url += "&hl=" + topResult.getLanguage();
                }
            } else {
                url = match.getUrl().getSpec();
            }

            mDelegate.loadUrlFromVoice(url);
        }
    }

    /**
     * Returns the action performed by Assistant from the Assistant Intent callback bundle.
     *
     * If the extra is unavailable, assume TRANSCRIPTION.
     */
    private static @AssistantActionPerformed int getActionPerformed(Bundle extras) {
        assert extras != null;
        String actionPerformed = extras.getString(EXTRA_ACTION_PERFORMED);
        if (actionPerformed == null) {
            // Older versions of Assistant will not set EXTRA_ACTION_PERFORMED. These versions of
            // Assistant also do not handle translate or readout, so we should assume transcription.
            return AssistantActionPerformed.TRANSCRIPTION;
        }
        return parseActionPerformed(actionPerformed);
    }

    /**
     * Parses actionPerformed and returns the associated AssistantActionPerformedValue.
     * @param actionPerformed A String representation of the action enum.
     * @return The parsed action or AssistantActionPerformed.UNKNOWN if no match was found.
     */
    private static @AssistantActionPerformed int parseActionPerformed(String actionPerformed) {
        switch (actionPerformed) {
            case "TRANSCRIPTION":
                return AssistantActionPerformed.TRANSCRIPTION;
            case "TRANSLATE":
                return AssistantActionPerformed.TRANSLATE;
            case "READOUT":
                return AssistantActionPerformed.READOUT;
            default:
                return AssistantActionPerformed.UNKNOWN;
        }
    }

    /**
     * Returns a String for use as a histogram suffix for histograms split by
     * AssistantActionPerformed.
     * @param action The action performed by the Assistant.
     * @return The histogram suffix for the given action. No '.' separator is included.
     */
    private static String getHistogramSuffixForAction(@AssistantActionPerformed int action) {
        switch (action) {
            case AssistantActionPerformed.TRANSCRIPTION:
                return "Transcription";
            case AssistantActionPerformed.TRANSLATE:
                return "Translate";
            case AssistantActionPerformed.READOUT:
                return "Readout";
            default:
                return "Unknown";
        }
    }

    /**
     * Returns a String for use as a histogram suffix for histograms split by
     * VoiceInteractionSource.
     * @param source The source of the voice interaction.
     * @return The histogram suffix for the given source or null if unknown. No '.' separator is
     *         included.
     */
    private static @Nullable String getHistogramSuffixForSource(
            @VoiceInteractionSource int source) {
        switch (source) {
            case VoiceInteractionSource.OMNIBOX:
                return "Omnibox";
            case VoiceInteractionSource.NTP:
                return "NTP";
            case VoiceInteractionSource.SEARCH_WIDGET:
                return "SearchWidget";
            case VoiceInteractionSource.TASKS_SURFACE:
                return "TasksSurface";
            case VoiceInteractionSource.TOOLBAR:
                return "Toolbar";
            default:
                assert false : "Unknown VoiceInteractionSource: " + source;
                return null;
        }
    }

    /**
     * Returns a String for use as a histogram suffix for histograms split by VoiceIntentTarget.
     * @param target The target of the voice search intent.
     * @return The histogram suffix for the given target. No '.' separator is included.
     */
    private static String getHistogramSuffixForTarget(@VoiceIntentTarget int target) {
        switch (target) {
            case VoiceIntentTarget.SYSTEM:
                return "System";
            case VoiceIntentTarget.ASSISTANT:
                return "Assistant";
            default:
                assert false : "Unknown VoiceIntentTarget: " + target;
                return null;
        }
    }

    /** Convert the android voice intent bundle to a list of result objects. */
    @VisibleForTesting
    protected List<VoiceResult> convertBundleToVoiceResults(Bundle extras) {
        if (extras == null) return null;

        ArrayList<String> strings = extras.getStringArrayList(RecognizerIntent.EXTRA_RESULTS);
        float[] confidences = extras.getFloatArray(RecognizerIntent.EXTRA_CONFIDENCE_SCORES);
        ArrayList<String> languages = extras.getStringArrayList(VOICE_QUERY_RESULT_LANGUAGES);

        if (strings == null || confidences == null) return null;
        if (strings.size() != confidences.length) return null;
        // Langues is optional, so only check the size when it's non-null.
        if (languages != null && languages.size() != strings.size()) return null;

        List<VoiceResult> results = new ArrayList<>();
        for (int i = 0; i < strings.size(); ++i) {
            // Remove any spaces in the voice search match when determining whether it
            // appears to be a URL. This is to prevent cases like (
            // "tech crunch.com" and "www. engadget .com" from not appearing like URLs)
            // from not navigating to the URL.
            // If the string appears to be a URL, then use it instead of the string returned from
            // the voice engine.
            String culledString = strings.get(i).replaceAll(" ", "");

            AutocompleteMatch match = null;
            if (mProfileSupplier.hasValue()) {
                match = AutocompleteCoordinator.classify(mProfileSupplier.get(), culledString);
            }

            String urlOrSearchQuery;
            if (match == null || match.isSearchSuggestion()) {
                urlOrSearchQuery = strings.get(i);
            } else {
                urlOrSearchQuery = culledString;
            }

            String language = languages == null ? null : languages.get(i);
            results.add(new VoiceResult(urlOrSearchQuery, confidences[i], language));
        }
        return results;
    }

    /**
     * Triggers a voice recognition intent to allow the user to specify a search query.
     *
     * @param source The source of the voice recognition initiation, such as NTP or omnibox.
     */
    public void startVoiceRecognition(@VoiceInteractionSource int source) {
        ThreadUtils.assertOnUiThread();
        startTrackingQueryDuration();
        WindowAndroid windowAndroid = mDelegate.getWindowAndroid();
        if (windowAndroid == null) {
            mDelegate.notifyVoiceRecognitionCanceled();
            return;
        }
        Activity activity = windowAndroid.getActivity().get();
        if (activity == null) {
            mDelegate.notifyVoiceRecognitionCanceled();
            return;
        }

        if (!VoiceRecognitionUtil.isVoiceSearchPermittedByPolicy(/* strictPolicyCheck=*/true)) {
            mDelegate.notifyVoiceRecognitionCanceled();
            return;
        }

        if (mAssistantVoiceSearchServiceSupplier.hasValue()) {
            mAssistantVoiceSearchServiceSupplier.get().reportMicPressUserEligibility();
            if (startAGSAForAssistantVoiceSearch(activity, windowAndroid, source)) return;
        }

        if (!startSystemForVoiceSearch(activity, windowAndroid, source)) {
            // TODO(wylieb): Emit histogram here to identify how many users are attempting to use
            // voice search, but fail completely.
            Log.w(TAG, "Couldn't find suitable provider for voice searching");
        }
    }

    /**
     * Requests the audio permission and resolves the voice recognition request if necessary.
     *
     * In a situation when permissions can't be requested anymore, or have been requested
     * and the result was a denial without an option to request them again, voice
     * functionality will become unavailable.
     *
     * @param activity The current {@link Activity} that we're requesting permission for.
     * @param windowAndroid Used to request audio permissions from the Android system.
     * @param source The source of the mic button click, used to record metrics.
     * @return Whether audio permissions are granted.
     */
    private boolean ensureAudioPermissionGranted(
            Activity activity, WindowAndroid windowAndroid, @VoiceInteractionSource int source) {
        if (windowAndroid.hasPermission(Manifest.permission.RECORD_AUDIO)) {
            recordAudioPermissionStateEvent(AudioPermissionState.GRANTED);
            return true;
        }
        // If we don't have permission and also can't ask, then there's no more work left other
        // than telling the delegate to update the mic state.
        if (!windowAndroid.canRequestPermission(Manifest.permission.RECORD_AUDIO)) {
            recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CANNOT_ASK_AGAIN);
            notifyVoiceAvailabilityImpacted();
            return false;
        }

        PermissionCallback callback = (permissions, grantResults) -> {
            if (grantResults.length != 1) {
                recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CAN_ASK_AGAIN);
                mDelegate.notifyVoiceRecognitionCanceled();
                return;
            }

            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Don't record granted permission here, it will get logged from
                // within startSystemForVoiceSearch call.
                startSystemForVoiceSearch(activity, windowAndroid, source);
            } else if (!windowAndroid.canRequestPermission(Manifest.permission.RECORD_AUDIO)) {
                recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CANNOT_ASK_AGAIN);
                notifyVoiceAvailabilityImpacted();
                mDelegate.notifyVoiceRecognitionCanceled();
            } else {
                recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CAN_ASK_AGAIN);
                mDelegate.notifyVoiceRecognitionCanceled();
            }
        };
        windowAndroid.requestPermissions(new String[] {Manifest.permission.RECORD_AUDIO}, callback);

        return false;
    }

    /** Start the system-provided service to fulfill the current voice search. */
    private boolean startSystemForVoiceSearch(
            Activity activity, WindowAndroid windowAndroid, @VoiceInteractionSource int source) {
        // Check if we need to request audio permissions. If we don't, then trigger a permissions
        // prompt will appear and startVoiceRecognition will be called again.
        if (!ensureAudioPermissionGranted(activity, windowAndroid, source)) {
            mDelegate.notifyVoiceRecognitionCanceled();
            return false;
        }

        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(
                RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
                activity.getComponentName().flattenToString());
        intent.putExtra(RecognizerIntent.EXTRA_WEB_SEARCH_ONLY, true);

        if (!showSpeechRecognitionIntent(windowAndroid, intent, source, VoiceIntentTarget.SYSTEM)) {
            // Requery whether or not the recognition intent can be handled.
            isRecognitionIntentPresent(false);
            notifyVoiceAvailabilityImpacted();
            recordVoiceSearchFailureEvent(source, VoiceIntentTarget.SYSTEM);

            return false;
        }
        return true;
    }

    /**
     * Start AGSA to fulfill the current voice search.
     *
     * @return Whether AGSA was actually started, when false we should fallback to
     *         {@link startSystemForVoiceSearch}.
     */
    private boolean startAGSAForAssistantVoiceSearch(
            Activity activity, WindowAndroid windowAndroid, @VoiceInteractionSource int source) {
        AssistantVoiceSearchService assistantVoiceSearchService =
                mAssistantVoiceSearchServiceSupplier.get();
        if (assistantVoiceSearchService == null) return false;

        // Check if the device meets the requirements to use Assistant voice search.
        if (!assistantVoiceSearchService.canRequestAssistantVoiceSearch()) return false;

        // Check if the consent prompt needs to be shown.
        if (assistantVoiceSearchService.needsEnabledCheck()) {
            mDelegate.clearOmniboxFocus();
            AssistantVoiceSearchConsentController.show(windowAndroid,
                    SharedPreferencesManager.getInstance(), mLaunchAssistanceSettingsAction,
                    BottomSheetControllerProvider.from(windowAndroid),
                    windowAndroid.getModalDialogManager(), (useAssistant) -> {
                        // Notify the service about the consent completion.
                        assistantVoiceSearchService.onAssistantConsentDialogComplete(useAssistant);

                        if (useAssistant) {
                            if (!startAGSAForAssistantVoiceSearch(
                                        activity, windowAndroid, source)) {
                                // Fallback to system voice search.
                                startSystemForVoiceSearch(activity, windowAndroid, source);
                            }
                        } else {
                            startSystemForVoiceSearch(activity, windowAndroid, source);
                        }
                    });

            return true;
        }

        // Final check to see if Assistant voice search should be used.
        if (!assistantVoiceSearchService.shouldRequestAssistantVoiceSearch()) return false;

        Intent intent = assistantVoiceSearchService.getAssistantVoiceSearchIntent();
        intent.putExtra(EXTRA_VOICE_ENTRYPOINT, source);
        // Allows Assistant to track intent latency.
        intent.putExtra(EXTRA_INTENT_SENT_TIMESTAMP, System.currentTimeMillis());

        if (sAssistantIntentExperimentIdFlag.isEnabled()) {
            attachAssistantExperimentId(intent);
        }

        if (!sAssistantNonPersonalizedVoiceSearchFlag.isEnabled()) {
            // TODO(crbug.com/1344574): This is currently still needed by AGSA.
            intent.putExtra(EXTRA_INTENT_USER_EMAIL, assistantVoiceSearchService.getUserEmail());

            if (shouldAddPageUrl(source)) {
                String url = getUrl();
                if (url != null) {
                    intent.putExtra(EXTRA_PAGE_URL, url);
                }
            }

            if (source == VoiceInteractionSource.TOOLBAR
                    && sAssistantIntentTranslateInfoFlag.isEnabled()) {
                boolean attached = attachTranslateExtras(intent);
                recordTranslateExtrasAttachResult(attached);
            }
        }
        if (!showSpeechRecognitionIntent(
                    windowAndroid, intent, source, VoiceIntentTarget.ASSISTANT)) {
            notifyVoiceAvailabilityImpacted();
            recordVoiceSearchFailureEvent(source, VoiceIntentTarget.ASSISTANT);
            return false;
        }

        return true;
    }

    /**
     * Returns true if the current tab's URL should be included with an Assistant intent initiated
     * via the given source.
     */
    private static boolean shouldAddPageUrl(@VoiceInteractionSource int source) {
        // Non-toolbar entrypoints (Omnibox, NTP, etc) obscure the current page contents and make it
        // less obvious that user actions in Assistant may interact with the current page. Omit the
        // page URL in those cases to signal to Assistant that page-actions (e.g. translate,
        // readout) should be disallowed.
        return source == VoiceInteractionSource.TOOLBAR && sAssistantIntentPageUrlFlag.isEnabled();
    }

    /**
     * Returns the URL of the tab associated with this VoiceRecognitionHandler or null if it is not
     * available.
     */
    @Nullable
    private String getUrl() {
        LocationBarDataProvider locationBarDataProvider = mDelegate.getLocationBarDataProvider();
        if (locationBarDataProvider == null) return null;

        Tab currentTab = locationBarDataProvider.getTab();
        if (currentTab == null || currentTab.isIncognito()) {
            return null;
        }

        GURL pageUrl = currentTab.getUrl();
        if (!UrlUtilities.isHttpOrHttps(pageUrl)) return null;
        return pageUrl.getSpec();
    }

    /** Adds an Extra used to indicate to Assistant which experiment arm the user is in. */
    private void attachAssistantExperimentId(Intent intent) {
        String experimentId = ChromeFeatureList.getFieldTrialParamByFeature(
                ChromeFeatureList.ASSISTANT_INTENT_EXPERIMENT_ID,
                ASSISTANT_EXPERIMENT_ID_PARAM_NAME);
        if (!TextUtils.isEmpty(experimentId)) {
            intent.putExtra(EXTRA_EXPERIMENT_ID, experimentId);
        }
    }

    /**
     * Includes translate information, if available, as Extras in the given Assistant intent.
     *
     * @return True if the Extras were attached successfully (page was translatable, languages
     *         detected, etc), false otherwise.
     */
    private boolean attachTranslateExtras(Intent intent) {
        LocationBarDataProvider locationBarDataProvider = mDelegate.getLocationBarDataProvider();
        Tab currentTab = locationBarDataProvider != null ? locationBarDataProvider.getTab() : null;
        if (currentTab == null || currentTab.isIncognito()
                || !mTranslateBridgeWrapper.canManuallyTranslate(currentTab)) {
            return false;
        }

        // The page's URL is used to ensure the Translate intent doesn't translate the wrong page.
        String url = getUrl();
        // The presence of source and current language fields are used to indicate whether the
        // page is translated and/or is translatable. Only include them if they are both available.
        String sourceLanguageCode = mTranslateBridgeWrapper.getSourceLanguage(currentTab);
        String currentLanguageCode = mTranslateBridgeWrapper.getCurrentLanguage(currentTab);
        if (TextUtils.isEmpty(url) || TextUtils.isEmpty(sourceLanguageCode)
                || TextUtils.isEmpty(currentLanguageCode)) {
            return false;
        }

        intent.putExtra(EXTRA_TRANSLATE_ORIGINAL_LANGUAGE, sourceLanguageCode);
        intent.putExtra(EXTRA_TRANSLATE_CURRENT_LANGUAGE, currentLanguageCode);

        // If ASSISTANT_INTENT_PAGE_URL is enabled, the URL may have already been added.
        if (!intent.hasExtra(EXTRA_PAGE_URL)) {
            intent.putExtra(EXTRA_PAGE_URL, url);
        }

        // The target language is not necessary for Assistant to decide whether to show the
        // translate UI.
        String targetLanguageCode = mTranslateBridgeWrapper.getTargetLanguage();
        if (!TextUtils.isEmpty(targetLanguageCode)) {
            intent.putExtra(EXTRA_TRANSLATE_TARGET_LANGUAGE, targetLanguageCode);
        }
        return true;
    }

    /**
     * Shows a cancelable speech recognition intent, returning a boolean that indicates if it was
     * successfully shown.
     *
     * @param windowAndroid The {@link WindowAndroid} associated with the current {@link Tab}.
     * @param intent The speech recognition {@link Intent}.
     * @param source Where the request to launch this {@link Intent} originated, such as NTP or
     *        omnibox.
     * @param target The intended destination of this {@link Intent}, such as the system voice
     *        transcription service or Assistant.
     * @return True if showing the {@link Intent} was successful.
     */
    private boolean showSpeechRecognitionIntent(WindowAndroid windowAndroid, Intent intent,
            @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
        recordVoiceSearchStartEvent(source, target);
        return windowAndroid.showCancelableIntent(intent,
                       new VoiceRecognitionCompleteCallback(source, target),
                       R.string.voice_search_error)
                >= 0;
    }

    /** Returns whether voice search is enabled on the current tab. */
    public boolean isVoiceSearchEnabled() {
        LocationBarDataProvider locationBarDataProvider = mDelegate.getLocationBarDataProvider();
        if (locationBarDataProvider == null) return false;
        if (locationBarDataProvider.isIncognito()) return false;
        WindowAndroid windowAndroid = mDelegate.getWindowAndroid();
        if (windowAndroid == null) return false;
        if (windowAndroid.getActivity().get() == null) return false;
        if (!VoiceRecognitionUtil.isVoiceSearchPermittedByPolicy(false)) return false;

        if (!sCacheVoiceSearchEnabledFlag.isEnabled()) {
            return VoiceRecognitionUtil.isVoiceSearchEnabled(windowAndroid);
        }

        if (mIsVoiceSearchEnabledCached == null) {
            mIsVoiceSearchEnabledCached = VoiceRecognitionUtil.isVoiceSearchEnabled(windowAndroid);

            // isVoiceSearchEnabled depends on whether or not the user gives permissions to
            // record audio. This permission can be changed either when we display a UI prompt
            // to request permissions, or when the permissions are changed in Android settings.
            // In both scenarios, the state of the application will change to being paused before
            // the permission is changed, so we invalidate the cache here.
            if (!mRegisteredActivityStateListener) {
                ApplicationStatus.registerApplicationStateListener(newState -> {
                    if (newState == ApplicationState.HAS_PAUSED_ACTIVITIES) {
                        mIsVoiceSearchEnabledCached = null;
                    }
                });
                mRegisteredActivityStateListener = true;
            }
        }

        return mIsVoiceSearchEnabledCached;
    }

    /** Start tracking query duration by capturing when it started */
    private void startTrackingQueryDuration() {
        mQueryStartTimeMs = SystemClock.elapsedRealtime();
    }

    /** Record metrics that are only logged for successful intent responses. */
    @VisibleForTesting
    protected void recordSuccessMetrics(@VoiceInteractionSource int source,
            @VoiceIntentTarget int target, @AssistantActionPerformed int action) {
        // Defensive check to guard against onIntentResult being called more than once. This only
        // happens with assistant experiments. See crbug.com/1116927 for details.
        if (mQueryStartTimeMs == null) return;
        long elapsedTimeMs = SystemClock.elapsedRealtime() - mQueryStartTimeMs;
        mQueryStartTimeMs = null;

        recordVoiceSearchFinishEvent(source, target);
        recordVoiceSearchOpenDuration(target, elapsedTimeMs);

        // We should only record per-action metrics when the page URL feature is enabled. When
        // disabled, only transcription should occur.
        if (sAssistantIntentPageUrlFlag.isEnabled()) {
            recordAssistantActionPerformed(source, action);
            recordPerActionVoiceSearchOpenDuration(action, elapsedTimeMs);
        }
    }

    /**
     * Records the source of a voice search initiation.
     * @param source The source of the voice search, such as NTP or omnibox. Values taken from the
     *        enum VoiceInteractionEventSource in enums.xml.
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     */
    @VisibleForTesting
    protected void recordVoiceSearchStartEvent(
            @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.StartEventSource", source, VoiceInteractionSource.NUM_ENTRIES);
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.StartEventTarget", target, VoiceIntentTarget.NUM_ENTRIES);
    }

    /**
     * Records the source of a successful voice search completion.
     * @param source The source of the voice search, such as NTP or omnibox. Values taken from the
     *        enum VoiceInteractionEventSource in enums.xml.
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     */
    @VisibleForTesting
    protected void recordVoiceSearchFinishEvent(
            @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.FinishEventSource", source, VoiceInteractionSource.NUM_ENTRIES);
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.FinishEventTarget", target, VoiceIntentTarget.NUM_ENTRIES);
    }

    /**
     * Records the source of a dismissed voice search.
     * @param source The source of the voice search, such as NTP or omnibox. Values taken from the
     *        enum VoiceInteractionEventSource in enums.xml.
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     */
    @VisibleForTesting
    protected void recordVoiceSearchDismissedEvent(
            @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
        RecordHistogram.recordEnumeratedHistogram("VoiceInteraction.DismissedEventSource", source,
                VoiceInteractionSource.NUM_ENTRIES);
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.DismissedEventTarget", target, VoiceIntentTarget.NUM_ENTRIES);
    }

    /**
     * Records the source of a failed voice search.
     * @param source The source of the voice search, such as NTP or omnibox. Values taken from the
     *        enum VoiceInteractionEventSource in enums.xml.
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     */
    @VisibleForTesting
    protected void recordVoiceSearchFailureEvent(
            @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.FailureEventSource", source, VoiceInteractionSource.NUM_ENTRIES);
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.FailureEventTarget", target, VoiceIntentTarget.NUM_ENTRIES);
    }

    /**
     * Records the source of an unexpected voice search result. Ideally this will always be 0.
     * @param source The source of the voice search, such as NTP or omnibox. Values taken from the
     *        enum VoiceInteractionEventSource in enums.xml.
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     */
    @VisibleForTesting
    protected void recordVoiceSearchUnexpectedResult(
            @VoiceInteractionSource int source, @VoiceIntentTarget int target) {
        RecordHistogram.recordEnumeratedHistogram("VoiceInteraction.UnexpectedResultSource", source,
                VoiceInteractionSource.NUM_ENTRIES);
        RecordHistogram.recordEnumeratedHistogram(
                "VoiceInteraction.UnexpectedResultTarget", target, VoiceIntentTarget.NUM_ENTRIES);
    }

    /**
     * Records the action performed by Assistant as a result of the voice search intent.
     * @param action The action performed, such as transcription or translation. Values taken from
     *        the enum AssistantActionPerformed in enums.xml.
     */
    @VisibleForTesting
    protected void recordAssistantActionPerformed(
            @VoiceInteractionSource int source, @AssistantActionPerformed int action) {
        String sourceSuffix = getHistogramSuffixForSource(source);
        if (sourceSuffix != null) {
            RecordHistogram.recordEnumeratedHistogram(
                    "VoiceInteraction.AssistantActionPerformed." + sourceSuffix, action,
                    AssistantActionPerformed.NUM_ENTRIES);
        }
    }

    /**
     * Records the result of a voice search.
     *
     * This also records submetrics split by the intent target.
     *
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     * @param result The result of a voice search, true if results were successfully returned.
     */
    @VisibleForTesting
    protected void recordVoiceSearchResult(@VoiceIntentTarget int target, boolean result) {
        RecordHistogram.recordBooleanHistogram("VoiceInteraction.VoiceSearchResult", result);

        String targetSuffix = getHistogramSuffixForTarget(target);
        if (targetSuffix != null) {
            RecordHistogram.recordBooleanHistogram(
                    "VoiceInteraction.VoiceSearchResult." + targetSuffix, result);
        }
    }

    /**
     * Records the voice search confidence value as a percentage, instead of the 0.0 to 1.0 range
     * we receive.
     *
     * This also records submetrics split by the intent target.
     *
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     * @param value The voice search confidence value we received from 0.0 to 1.0.
     */
    @VisibleForTesting
    protected void recordVoiceSearchConfidenceValue(@VoiceIntentTarget int target, float value) {
        int percentage = Math.round(value * 100f);
        RecordHistogram.recordPercentageHistogram(
                "VoiceInteraction.VoiceResultConfidenceValue", percentage);

        String targetSuffix = getHistogramSuffixForTarget(target);
        if (targetSuffix != null) {
            RecordHistogram.recordPercentageHistogram(
                    "VoiceInteraction.VoiceResultConfidenceValue." + targetSuffix, percentage);
        }
    }

    /**
     * Records the end-to-end voice search duration.
     *
     * This also records submetrics split by the intent target.
     *
     * @param target The intended recipient of the intent, such as the system voice transcription
     *        service or Assistant.
     * @param openDurationMs The duration, in milliseconds, between when a voice intent was
     *        initiated and when its result was returned.
     */
    private void recordVoiceSearchOpenDuration(@VoiceIntentTarget int target, long openDurationMs) {
        RecordHistogram.recordMediumTimesHistogram(
                "VoiceInteraction.QueryDuration.Android", openDurationMs);

        String targetSuffix = getHistogramSuffixForTarget(target);
        if (targetSuffix != null) {
            RecordHistogram.recordMediumTimesHistogram(
                    "VoiceInteraction.QueryDuration.Android.Target." + targetSuffix,
                    openDurationMs);
        }
    }

    /** Records end-to-end voice search duration split by the action performed. */
    private void recordPerActionVoiceSearchOpenDuration(
            @AssistantActionPerformed int action, long openDurationMs) {
        String actionSuffix = getHistogramSuffixForAction(action);
        RecordHistogram.recordMediumTimesHistogram(
                "VoiceInteraction.QueryDuration.Android." + actionSuffix, openDurationMs);
    }

    /** Records whether translate information was successfully attached to an Assistant intent. */
    @VisibleForTesting
    protected void recordTranslateExtrasAttachResult(boolean result) {
        RecordHistogram.recordBooleanHistogram(
                "VoiceInteraction.AssistantIntent.TranslateExtrasAttached", result);
    }

    /**
     * Records audio permissions state when a system voice recognition is requested.
     * @param permissionsState The current RECORD_AUDIO permission state.
     */
    @VisibleForTesting
    protected void recordAudioPermissionStateEvent(@AudioPermissionState int permissionsState) {
        RecordHistogram.recordEnumeratedHistogram("VoiceInteraction.AudioPermissionEvent",
                permissionsState, AudioPermissionState.NUM_ENTRIES);
    }

    /** Allows for overriding the TranslateBridgeWrapper for test purposes. */
    @VisibleForTesting
    protected void setTranslateBridgeWrapper(TranslateBridgeWrapper wrapper) {
        mTranslateBridgeWrapper = wrapper;
    }

    /**
     * Calls into {@link VoiceRecognitionUtil} to determine whether or not the
     * {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} {@link Intent} is handled by any
     * {@link android.app.Activity}s in the system.
     *
     * @param useCachedValue Whether or not to use the cached value from a previous result.
     * @return {@code true} if recognition is supported.  {@code false} otherwise.
     */
    @VisibleForTesting
    protected static boolean isRecognitionIntentPresent(boolean useCachedValue) {
        if (sIsRecognitionIntentPresentForTesting != null) {
            return sIsRecognitionIntentPresentForTesting;
        }
        return VoiceRecognitionUtil.isRecognitionIntentPresent(useCachedValue);
    }

    @VisibleForTesting
    /*package*/ static void setIsRecognitionIntentPresentForTesting(
            Boolean isRecognitionIntentPresent) {
        sIsRecognitionIntentPresentForTesting = isRecognitionIntentPresent;
    }

    @VisibleForTesting
    protected void setIsVoiceSearchEnabledCacheForTesting(Boolean value) {
        mIsVoiceSearchEnabledCached = value;
    }

    /** Sets the start time for testing. */
    void setQueryStartTimeForTesting(Long queryStartTimeMs) {
        mQueryStartTimeMs = queryStartTimeMs;
    }
}


text:
package clang

// #include "./clang-c/Index.h"
// #include "go-clang.h"
import "C"
import "fmt"

/*
	Describes the kind of a template argument.

	See the definition of llvm::clang::TemplateArgument::ArgKind for full
	element descriptions.
*/
type TemplateArgumentKind uint32

const (
	TemplateArgumentKind_Null              TemplateArgumentKind = C.CXTemplateArgumentKind_Null
	TemplateArgumentKind_Type                                   = C.CXTemplateArgumentKind_Type
	TemplateArgumentKind_Declaration                            = C.CXTemplateArgumentKind_Declaration
	TemplateArgumentKind_NullPtr                                = C.CXTemplateArgumentKind_NullPtr
	TemplateArgumentKind_Integral                               = C.CXTemplateArgumentKind_Integral
	TemplateArgumentKind_Template                               = C.CXTemplateArgumentKind_Template
	TemplateArgumentKind_TemplateExpansion                      = C.CXTemplateArgumentKind_TemplateExpansion
	TemplateArgumentKind_Expression                             = C.CXTemplateArgumentKind_Expression
	TemplateArgumentKind_Pack                                   = C.CXTemplateArgumentKind_Pack
	TemplateArgumentKind_Invalid                                = C.CXTemplateArgumentKind_Invalid
)

func (tak TemplateArgumentKind) Spelling() string {
	switch tak {
	case TemplateArgumentKind_Null:
		return "TemplateArgumentKind=Null"
	case TemplateArgumentKind_Type:
		return "TemplateArgumentKind=Type"
	case TemplateArgumentKind_Declaration:
		return "TemplateArgumentKind=Declaration"
	case TemplateArgumentKind_NullPtr:
		return "TemplateArgumentKind=NullPtr"
	case TemplateArgumentKind_Integral:
		return "TemplateArgumentKind=Integral"
	case TemplateArgumentKind_Template:
		return "TemplateArgumentKind=Template"
	case TemplateArgumentKind_TemplateExpansion:
		return "TemplateArgumentKind=TemplateExpansion"
	case TemplateArgumentKind_Expression:
		return "TemplateArgumentKind=Expression"
	case TemplateArgumentKind_Pack:
		return "TemplateArgumentKind=Pack"
	case TemplateArgumentKind_Invalid:
		return "TemplateArgumentKind=Invalid"
	}

	return fmt.Sprintf("TemplateArgumentKind unkown %d", int(tak))
}

func (tak TemplateArgumentKind) String() string {
	return tak.Spelling()
}

redpajama_set_name: RedPajamaGithub


text:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTK;
using OpenTK.Input;
using System.Windows.Forms;
using System.Drawing;
namespace SharpGL.Input
{
    /// <summary>
    /// Handles mouse movement as well as mouse button events.
    /// </summary>
	public class MouseHandler
	{
        /// <summary>
        /// Shows or hides the mouse cursor
        /// </summary>
        public static bool CursorVisible
        {
            get
            {
                return _window.CursorVisible;
            }
            set
            {
                _window.CursorVisible = value;
            }
        }
        /// <summary>
        /// If true, the mouse will be locked to the center of the screen.
        /// </summary>
        public static bool MouseLocked { get; set; }
		private static Dictionary<MouseButton, bool> pressedButtons;
		private static Dictionary<MouseButton, Action> buttonActions;
		private static Vector2 mPos, mDelta;
        private static GameWindow _window;
        /// <summary>
        /// X-Position of the mouse cursor
        /// </summary>
		public static float X
		{
			get
			{
				return mPos.X;
			}
		}
        /// <summary>
        /// Y-Position of the mouse cursor
        /// </summary>
		public static float Y
		{
			get
			{
				return mPos.Y;
			}
		}
		public delegate void MouseMoveHandler(Vector2 position, Vector2 delta);
        /// <summary>
        /// Called when the mouse cursor is moved.
        /// </summary>
		public static event MouseMoveHandler OnMouseMove;
		internal static void Init(GameWindow window)
		{
            _window = window;
			window.Mouse.ButtonDown += Mouse_ButtonDown;
			window.Mouse.ButtonUp += Mouse_ButtonUp;

			pressedButtons = new Dictionary<MouseButton, bool>();
			buttonActions = new Dictionary<MouseButton, Action>();

		}

		static void Mouse_ButtonUp(object sender, MouseButtonEventArgs e)
		{
			if(pressedButtons.ContainsKey(e.Button))
            {
                pressedButtons[e.Button] = false;
            }
		}

		static void Mouse_ButtonDown(object sender, MouseButtonEventArgs e)
		{
			if (pressedButtons.ContainsKey(e.Button))
            {
                pressedButtons[e.Button] = true;
            }
		}
        /// <summary>
        /// Registers an action to be called when the specified mouse button is pressed
        /// </summary>
        /// <param name="b">Mouse button</param>
        /// <param name="action">Event to be called</param>
		public static void RegisterButtonDown(MouseButton b, Action action)
		{
			pressedButtons.Add(b, false);
			buttonActions.Add(b, action);
		}
		internal static void Update()
		{
            mDelta.X = mPos.X - Cursor.Position.X;
            mDelta.Y = mPos.Y - Cursor.Position.Y;
            if (mDelta.LengthSquared > 0)
            {
                if (MouseLocked)
                    Cursor.Position = new Point(_window.X + _window.Width / 2, _window.Y + _window.Height / 2);
                mPos.X = Cursor.Position.X;
                mPos.Y = Cursor.Position.Y;
                if (OnMouseMove != null)
                    OnMouseMove(mPos, mDelta);
            }
			foreach (MouseButton k in buttonActions.Keys)
			{
				if (pressedButtons[k])
					buttonActions[k]();
			}
		}
	}
}


text:
A decorator is a simple way  to a dynamically enhance and existing behavior using composition.

Let say we have an interface `Coffee` that describe a Coffee by its cost and its ingredients

```java
interface Coffee {
  long cost();      // cost of the coffee, in cents
  String ingredients();
}
```

We can write a simple implementation using a record.

```mermaid
classDiagram
class Coffee {
  <<interface>>
  long cost()
  String ingredients()
}
class SimpleCoffee {
  <<record>>
  long cost
  String ingredients()
}
Coffee <|.. SimpleCoffee
```

```java
record SimpleCoffee(long cost) implements Coffee {
  @Override
  public String ingredients() {
    return "Coffee";
  }
}
```

Let say we now want to represent a coffee with milk, this is a `Coffee` so it should implement the interface
`Coffee`, it's cost is the cost of a simple coffee plus 50 cents and the ingredients is coffee and milk.

Because the behavior of a coffee with milk depends on the behavior of a coffee, a coffee with milk is created
using a coffee and delegates to that instance to get the cost of a coffee and the ingredient of a coffee.

```java
record WithMilk(Coffee coffee) implements Coffee {
  @Override
  public long cost() {
    return coffee.cost() + 50;
  }

  @Override
  public String ingredients() {
    return coffee.ingredients() + ", Milk";
  }
}
```

```mermaid
classDiagram
class Coffee {
  <<interface>>
  long cost()
  String ingredients()
}
class SimpleCoffee {
  <<record>>
  long cost
  String ingredients()
}
class WithMilk {
  <<record>>
  long cost()
  String ingredients()
}
Coffee <|.. SimpleCoffee
Coffee <|.. WithMilk
WithMilk --> "1" Coffee : coffee
```

So `WithMilk` is simultaneously, a Coffee because it implements the interface `Coffee`and decorates an existing coffee
by taking an instance of a Coffee as record component.

If we repeat the pattern with `WithSprinkles`.

```java
record WithSprinkles(Coffee coffee) implements Coffee {
  @Override
  public long cost() {
    return coffee.cost() + 20;
  }

  @Override
  public String ingredients() {
    return coffee.ingredients() + ", Sprinkles";
  }
}
```

We can see why this pattern is interesting because we have not only created a coffee with sprinkles but also
a coffee with milk and with sprinkles.

```java
Coffee coffee = new SimpleCoffee(100);
Coffee coffeeWithMilk = new WithMilk(coffee);
Coffee coffeeWithMilkAndSprinkles = new WithSprinkles(coffeeWithMilk);
System.out.println("ingredients: " + coffeeWithMilkAndSprinkles.ingredients());
System.out.println("cost: " + coffeeWithMilkAndSprinkles.cost() + " cents");
```

This pattern is more powerful than the inheritance because it relies on the delegation which is a dynamic relation
while the inheritance/extends is a static relation.

## Improving the API if the hierarchy is sealed

We can improve the API if we do not allow users to define their own coffee,
we can sealed the interface and provide instance methods (`withMilk`` and `withSprinkles` here)
to create a decorator from an existing instance more easily.

```java
sealed interface Coffee permits SimpleCoffee, WithMilk, WithSprinkles {
  long cost();
  String ingredients();

  static Coffee simple(long cost) {
    return new SimpleCoffee(cost);
  }
  default Coffee withMilk() {
    return new WithMilk(this);
  }
  default Coffee withSprinkles() {
    return new WithSprinkles(this);
  }
}
```

Here is the same example as above, written using the new API

```java
Coffee coffeeWithMilkAndSprinkles = Coffee.simple(100)
    .withMilk()
    .withSprinkles();
System.out.println("ingredients: " + coffeeWithMilkAndSprinkles.ingredients());
System.out.println("cost: " + coffeeWithMilkAndSprinkles.cost() + " cents");
```

Apart from being easier in terms of method discovery, this API also does not explicitly reference the names
of the implementations so `SimpleCoffee`, `WithMilk` and `WithSprinkles` can be hidden and non-public.

## Relation with the Proxy Pattern

A proxy is a decorator, so it implements an interface and delegate to an instance of that interface
but it has a different intent, the idea to intercept method calls to do cross-cutting operations
like logging, authentication, caching, etc.

Here is an example of a proxy, a Coffee that logs its cost.

```java
record LoggingCoffee(Coffee coffee) {
  @Override
  public long cost() {
    var cost = coffee.cost();
    System.out.println("coffee cost " + cost);  // simulate logging
    return cost;
  }

  @Override
  public String ingredients() {
    return coffee.ingredients();
  }
}
```


text:
SCREEN_SOURCES = screen_agent.c

SOURCES += $(SCREEN_SOURCES:%.c=ncurses/screen/%.c)


text:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS -Wall #-}

module Main (
    main
) where

import Control.Monad (foldM)
import Control.Monad.Trans (liftIO)
import qualified Data.ByteString.Char8 as C
import Data.Default
import qualified Data.IntMap as IM
import Data.Time.Clock (getCurrentTime)
import System.Console.GetOpt
import UI.Command

import Data.ZoomCache
import Data.ZoomCache.Dump
import Data.ZoomCache.Multichannel()

------------------------------------------------------------

data Config = Config
    { noRaw    :: Bool
    , channels :: Int
    , wmLevel  :: Int
    , track    :: TrackNo
    , intData  :: Bool
    , variable :: Bool
    , spec     :: TrackSpec
    }

instance Default Config where
    def = defConfig

defConfig :: Config
defConfig = Config
    { noRaw    = False
    , channels = 1
    , wmLevel  = 1024
    , track    = 1
    , intData  = False
    , variable = False
    , spec     = def { specDeltaEncode = False
                     , specZlibCompress = False
                     , specName = "gen"
                     }
    }

data Option = NoRaw
            | Channels String
            | Watermark String
            | Track String
            | Delta
            | ZLib
            | Variable
            | IntData
            | Rate String
            | Label String
    deriving (Eq)

options :: [OptDescr Option]
options = genOptions

genOptions :: [OptDescr Option]
genOptions =
    [ Option ['z'] ["no-raw"] (NoArg NoRaw)
             "Do NOT include raw data in the output"
    , Option ['c'] ["channels"] (ReqArg Channels "channels")
             "Set number of channels"
    , Option ['w'] ["watermark"] (ReqArg Watermark "watermark")
             "Set high-watermark level"
    , Option ['t'] ["track"] (ReqArg Track "trackNo")
             "Set or select track number"
    , Option ['d'] ["delta"] (NoArg Delta)
             "Delta-encode data"
    , Option ['Z'] ["zlib"] (NoArg ZLib)
             "Zlib-compress data"
    , Option ['b'] ["variable"] (NoArg Variable)
             "Generate variable-rate data"
    , Option ['i'] ["integer"] (NoArg IntData)
             "Generate integer data"
    , Option ['r'] ["rate"] (ReqArg Rate "data-rate")
             "Set track rate"
    , Option ['l'] ["label"] (ReqArg Label "label")
             "Set track label"
    ]

processArgs :: [String] -> IO (Config, [String])
processArgs args = do
    case getOpt RequireOrder options args of
        (opts, args', [] ) -> do
            config <- processConfig def opts
            return (config, args')
        (_,    _,     _:_) -> return (def, args)

processConfig :: Config -> [Option] -> IO Config
processConfig = foldM processOneOption
    where
        processOneOption config NoRaw = do
            return $ config {noRaw = True}
        processOneOption config (Channels s) = do
            return $ config {channels = read s}
        processOneOption config (Watermark s) = do
            return $ config {wmLevel = read s}
        processOneOption config (Track s) = do
            return $ config {track = read s}

        processOneOption config Delta = do
            return $ config { spec = (spec config){specDeltaEncode = True} }
        processOneOption config ZLib = do
            return $ config { spec = (spec config){specZlibCompress = True} }
        processOneOption config Variable = do
            return $ config { variable = True
                            , spec = (spec config){specSRType = VariableSR}
                            }
        processOneOption config IntData = do
            return $ config { intData = True
                            , spec = setCodec (undefined :: Int) (spec config)
                            }
        processOneOption config (Rate s) = do
            return $ config { spec = (spec config){specRate = fromInteger $ read s} }
        processOneOption config (Label s) = do
            return $ config { spec = (spec config){specName = C.pack s} }

------------------------------------------------------------

zoomGen :: Command ()
zoomGen = defCmd {
          cmdName = "gen"
        , cmdHandler = zoomGenHandler
        , cmdCategory = "Writing"
        , cmdShortDesc = "Generate zoom-cache data"
        , cmdExamples = [("Generate a file called foo.zoom", "foo.zoom")]
        }

zoomGenHandler :: App () ()
zoomGenHandler = do
    (config, filenames) <- liftIO . processArgs =<< appArgs
    liftIO $ mapM_ (zoomWriteFile config) filenames

zoomWriteFile :: Config -> FilePath -> IO ()
zoomWriteFile Config{..} path
    | intData   = w ints
    | otherwise = w doubles
    where
    w :: (ZoomReadable a, ZoomWrite a, ZoomWritable a, ZoomWrite (SampleOffset, a))
      => [a] -> IO ()
    w d
        | variable && channels == 1 =
            writeData (sW >> mapM_ (write track) (zip (map SO [1,3..]) d))
        | channels == 1             =
            writeData (sW >> mapM_ (write track) d)
        | variable                  =
            writeData (sW >> mapM_ (write track)
                                   (zip (map SO [1,3..])
                                        (map (replicate channels) d)))
        | otherwise                 =
            writeData (sW >> mapM_ (write track) (map (replicate channels) d))

    writeData ds = do
        now <- getCurrentTime
        withFileWrite trackMap (Just now) (not noRaw) ds path

    sW = setWatermark track wmLevel
    trackMap = IM.singleton track spec'
    spec' | channels == 1 && intData = setCodec (undefined :: Int) spec
          | channels == 1            = setCodec (undefined :: Double) spec
          | intData                  = setCodecMultichannel channels (undefined :: Int) spec
          | otherwise                = setCodecMultichannel channels (undefined :: Double) spec

------------------------------------------------------------

doubles :: [Double]
doubles = take 10000000 $ map ((* 1000.0) . sin) [0.0, 0.01 ..]

ints :: [Int]
ints = map round doubles

------------------------------------------------------------

zoomInfo :: Command ()
zoomInfo = defCmd {
          cmdName = "info"
        , cmdHandler = zoomInfoHandler
        , cmdCategory = "Reading"
        , cmdShortDesc = "Display basic info about a zoom-cache file"
        , cmdExamples = [("Display info about foo.zoom", "foo.zoom")]
        }

zoomInfoHandler :: App () ()
zoomInfoHandler = mapM_ (liftIO . zoomInfoFile standardIdentifiers) =<< appArgs

------------------------------------------------------------

zoomDump :: Command ()
zoomDump = defCmd {
          cmdName = "dump"
        , cmdHandler = zoomDumpHandler
        , cmdCategory = "Reading"
        , cmdShortDesc = "Read zoom-cache data"
        , cmdExamples = [("Yo", "")]
        }

zoomDumpHandler :: App () ()
zoomDumpHandler = do
    (config, filenames) <- liftIO . processArgs =<< appArgs
    mapM_ (liftIO . zoomDumpFile standardIdentifiers (track config)) filenames

------------------------------------------------------------

zoomSummary :: Command ()
zoomSummary = defCmd {
          cmdName = "summary"
        , cmdHandler = zoomSummaryHandler
        , cmdCategory = "Reading"
        , cmdShortDesc = "Read zoom-cache summary data"
        , cmdExamples = [("Read summary level 3 from foo.zoom", "3 foo.zoom")]
        }

zoomSummaryHandler :: App () ()
zoomSummaryHandler = do
    (config, filenames) <- liftIO . processArgs =<< appArgs
    liftIO . (f (track config)) $ filenames
    where
        f trackNo (lvl:paths) = mapM_ (zoomDumpSummaryLevel (read lvl)
                                       standardIdentifiers trackNo) paths
        f _ _ = putStrLn "Usage: zoom-cache summary n file.zoom"

------------------------------------------------------------
-- The Application
--

zoom :: Application () ()
zoom = def {
          appName = "zoom"
        , appVersion = "0.1"
        , appAuthors = ["Conrad Parker"]
        , appBugEmail = "conrad@metadecks.org"
        , appShortDesc = "Trivial zoom-cache inspection tools"
        , appLongDesc = longDesc
        , appCategories = ["Reading", "Writing"]
        , appSeeAlso = [""]
        , appProject = "Zoom"
        , appCmds = [ zoomGen
                    , zoomInfo
                    , zoomDump
                    , zoomSummary
                    ]
	}

longDesc :: String
longDesc = "Manipulate zoom-cache files"

------------------------------------------------------------
-- Main
--

main :: IO ()
main = appMain zoom


text:
package org.jhaws.common.io.media.ytdl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jhaws.common.io.FilePath;
import org.jhaws.common.io.Utils;
import org.jhaws.common.io.Utils.OSGroup;
import org.jhaws.common.io.console.Processes.ExitValueException;
import org.jhaws.common.io.console.Processes.Lines;
import org.jhaws.common.io.media.Tool;
import org.jhaws.common.lang.StringValue;
import org.jhaws.common.lang.Value;

// https://github.com/ytdl-org/youtube-dl/blob/master/README.md
//
// youtube-dl.exe --list-extractors
//
// youtube-dl.exe --extractor-descriptions
//
// https://github.com/ytdl-org/youtube-dl
// https://ytdl-org.github.io/youtube-dl/supportedsites.html
// --write-info-json
//
// https://askubuntu.com/questions/673442/downloading-youtube-playlist-with-youtube-dl-skipping-existing-files
//
// --restrict-filenames Restrict filenames to only ASCII
// characters, and avoid "&" and spaces in
// filenames
// -w, --no-overwrites Do not overwrite files
// -c, --continue Force resume of partially downloaded files.
// By default, youtube-dl will resume
// downloads if possible.
// --write-info-json Write video metadata to a .info.json file
//
//
// cookies
// https://github.com/ytdl-org/youtube-dl/issues/26152
//
//
// Map<String, Object> extraConfig = new LinkedHashMap<>();
// extraConfig.put("--sub-lang", "english");
// extraConfig.put("--http-chunk-size", "1M");
// extraConfig.put("--buffer-size", "16K");
// extraConfig.put("--ffmpeg-location", ffmpegTool.getFfmpeg().getAbsolutePath());
// extraConfig.put("--write-info-json", null);
// extraConfig.put("--no-progress", null);
// if(ytdlp) {
// if (Utils.osgroup == OSGroup.Windows) {
// extraConfig.put("--windows-filenames", null);
// }
// extraConfig.put("--concurrent-fragments", "3");
// extraConfig.put("--cookies-from-browser", "firefox");
// }
public class YTDL extends Tool {
	public static String UNSUPPORTED = "ERROR: Unsupported URL: ";

	protected static final String UTF_8 = "utf-8";

	public static final String EXE = "youtube-dl";

	public static final String URL = "https://yt-dl.org/downloads/latest/";

	protected String userAgent;

	protected String escapeChar;

	protected boolean autoUpdate = true;

	public YTDL() {
		super(System.getenv("YOUTUBEDL"));
	}

	public YTDL(String s) {
		super(s);
	}

	public YTDL(boolean disableAuto) {
		super(disableAuto);
	}

	@Override
	public String toString() {
		return String.valueOf(executable);
	}

	@Override
	protected void setPathImpl(String path) {
		if (StringUtils.isBlank(path)) {
			new NullPointerException().printStackTrace();
			return;
		}
		FilePath f = new FilePath(path);
		if (f.exists()) {
			if (f.isFile()) {
				executable = f;
			} else {
				if (Utils.osgroup == OSGroup.Windows) {
					executable = f.child(EXE).appendExtension("exe");
				} else {
					executable = f.child(EXE);
				}
			}
		} else {
			f.createDirectory();
			if (Utils.osgroup == OSGroup.Windows) {
				executable = f.child(EXE).appendExtension("exe");
			} else {
				executable = f.child(EXE);
			}
		}

		if (executable.exists()) {
			FilePath update = executable.appendExtension("update");
			if (autoUpdate || update.notExists() || (update.getLastModifiedTime().toMillis() + 24 * 3_600_000) < System.currentTimeMillis()) {
				Tool.call(Duration.ofSeconds(60), null, new Lines(), executable.getParentPath(), Arrays.asList(Tool.command(executable), "-U"), true, null, true, null);
				update.write("" + System.currentTimeMillis());
			}
		} else {
			String tmp = EXE;
			if (Utils.osgroup == OSGroup.Windows) {
				tmp = tmp + ".exe";
			}
			try (InputStream in = new URL(URL + tmp).openConnection().getInputStream(); OutputStream out = executable.newBufferedOutputStream()) {
				IOUtils.copy(in, out);
			} catch (IOException ex) {
				logger.error("{}", ex);
				return;
			}
		}
	}

	@Override
	protected String getVersionImpl() {
		List<String> command = new ArrayList<>();
		command.add(Tool.command(executable));
		command.add("--version");
		Lines lines = new Lines();
		Tool.call(null, lines, executable.getParentPath(), command);
		return lines.lines().get(0);
	}

	public String fileName(String url, String cookiesLoc, Map<String, Object> extraConfig) {
		if (executable == null || executable.notExists())
			throw new NullPointerException();
		List<String> command = new ArrayList<>();
		command.add(Tool.command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		extraConfig(command, extraConfig);
		command.add("--get-filename");
		command.add(url);
		StringValue fn = new StringValue();
		List<String> full = new ArrayList<>();
		try {
			Lines lines = new Lines() {
				@Override
				public void accept(String t) {
					fn.set(t);
					full.add(t);
				}
			};
			call(null, lines, null, command);
			return fn.get();
		} catch (RuntimeException ex) {
			full.forEach(System.out::println);
			ex.printStackTrace(System.out);
			throw ex;
		}
	}

	public String metadata(String url, String cookiesLoc, Map<String, Object> extraConfig) {
		if (executable == null || executable.notExists())
			throw new NullPointerException();
		List<String> command = new ArrayList<>();
		command.add(Tool.command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		extraConfig(command, extraConfig);
		command.add("--write-info-json");
		command.add("--skip-download");
		command.add("--no-clean-info-json");
		command.add(url);
		StringValue fn = new StringValue();
		List<String> full = new ArrayList<>();
		try {
			Lines l = new Lines() {
				@Override
				public void accept(String t) {
					String pre = "[info] Writing video metadata as JSON to: ";
					if (t.startsWith(pre)) {
						lines.add(t.substring(pre.length()).trim());
					}
				}
			};
			FilePath fp = FilePath.getTempDirectory();
			call(null, l, fp, command);
			return fp.child(l.lines().get(0)).readAll();
		} catch (RuntimeException ex) {
			full.forEach(System.out::println);
			ex.printStackTrace(System.out);
			throw ex;
		}
	}

	public FilePath downloadAudio(String url, FilePath tmpFolder, FilePath targetFolder, String cookiesLoc, Map<String, Object> extraConfig) {
		if (executable == null || executable.notExists())
			throw new NullPointerException();
		if (tmpFolder == null)
			tmpFolder = FilePath.getTempDirectory();
		if (targetFolder == null)
			targetFolder = FilePath.getTempDirectory();
		tmpFolder = tmpFolder.child(String.valueOf(System.currentTimeMillis())).createDirectory();
		List<String> command = new ArrayList<>();
		command.add(command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		// command.add("--verbose");
		command.add("-f");
		command.add("bestaudio[ext=m4a]");
		// command.add("--embed-thumbnail");
		// command.add("--ignore-errors");
		// command.add("--add-metadata");
		extraConfig(command, extraConfig);
		command.add("-o");
		command.add(getEscapeChar() + tmpFolder.getAbsolutePath() + "/" + "%(title)s.f%(format_id)s.%(ext)s" + getEscapeChar());
		command.add(url);
		List<String> dl = new ArrayList<>();
		try {
			dl(executable.getParentPath(), command, dl);
		} catch (ExitValueException ex) {
			//
		}
		if (dl.isEmpty()) {
			throw new NullPointerException();
		}
		if (dl.size() == 1) {
			FilePath from = tmpFolder.child(dl.get(0));
			FilePath to = targetFolder.child(from.getFileNameString()).newFileIndex();
			from.renameTo(to);
			return to;
		}
		throw new UnsupportedOperationException(url);
	}

	protected void extraConfig(List<String> command, Map<String, Object> extraConfig) {
		Optional.ofNullable(extraConfig).ifPresent(_extraConfig -> extraConfig.entrySet().forEach(entry -> {
			command.add(entry.getKey());
			if (entry.getValue() != null) {
				command.add(getEscapeChar() + String.valueOf(entry.getValue()) + getEscapeChar());
			}
		}));
	}

	protected void userAgent(List<String> command) {
		if (StringUtils.isNotBlank(userAgent)) {
			command.add("--user-agent");
			command.add(getEscapeChar() + userAgent + getEscapeChar());
		}
	}

	public List<FilePath> download(String url, FilePath tmpFolder, FilePath targetFolder, String cookiesLoc, Map<String, Object> extraConfig) {
		if (executable == null || executable.notExists())
			throw new NullPointerException();
		if (tmpFolder == null)
			tmpFolder = FilePath.getTempDirectory();
		if (targetFolder == null)
			targetFolder = FilePath.getTempDirectory();
		tmpFolder = tmpFolder.child(String.valueOf(System.currentTimeMillis())).createDirectory();
		List<String> dl = new ArrayList<>();
		try {
			if (!(url.contains("youtube") || url.contains("youtu.be"))) {
				throw new ExitValueException(0);
			}
			downloadYT(url, tmpFolder, cookiesLoc, dl, extraConfig);
		} catch (ExitValueException ex) {
			try {
				downloadOther(url, tmpFolder, cookiesLoc, dl, false, extraConfig);
			} catch (Exception ex2) {
				downloadOther(url, tmpFolder, cookiesLoc, dl, true, extraConfig);
			}
		}
		if (dl.isEmpty()) {
			throw new NullPointerException();
		}
		FilePath _tmpFolder = tmpFolder;
		return dl.stream().map(a -> {
			FilePath fp = new FilePath(a);
			if (fp.exists())
				return fp;
			fp = _tmpFolder.child(a);
			if (fp.exists())
				return fp;
			return null;
		}).filter(Objects::nonNull).collect(Collectors.toList());
	}

	public void downloadOther(String url, FilePath tmpFolder, String cookiesLoc, List<String> dl, boolean onlyId, Map<String, Object> extraConfig) {
		List<String> command = new ArrayList<>();
		command.add(command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		// command.add("--verbose");
		// command.add("--embed-thumbnail");
		// command.add("--ignore-errors");
		// command.add("--add-metadata");
		extraConfig(command, extraConfig);
		command.add("-o");
		// https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template
		command.add(getEscapeChar() + tmpFolder.getAbsolutePath() + "/" + (onlyId ? "f%(id)s.%(ext)s" : "%(title)s.f%(format_id)s.%(ext)s") + getEscapeChar());
		command.add(url);
		System.out.println(command.stream().collect(Collectors.joining(" ")));
		dl(tmpFolder, command, dl);
	}

	protected void common(List<String> command) {
		command.add("--no-check-certificate");
		command.add("--restrict-filenames");
		command.add("--encoding");
		command.add(UTF_8);
	}

	public void downloadYT(String url, FilePath tmpFolder, String cookiesLoc, List<String> dl, Map<String, Object> extraConfig) {
		List<String> command = new ArrayList<>();
		command.add(command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		extraConfig(command, extraConfig);
		// --compat-options format-sort
		command.add("-f");
		// command.add("bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best");
		command.add("bestvideo,bestaudio");
		command.add("-o");
		command.add(getEscapeChar() + tmpFolder.getAbsolutePath() + "/" + "%(title)s.f%(format_id)s.%(ext)s" + getEscapeChar());
		command.add(url);
		dl(tmpFolder, command, dl);
	}

	protected void cookies(List<String> command, String cookiesLoc) {
		FilePath cookies;
		if (StringUtils.isNotBlank(cookiesLoc)) {
			cookies = new FilePath(cookiesLoc);
		} else {
			cookies = executable.getParentPath().child("cookies.txt");
		}
		if (cookies.exists()) {
			command.add("--cookies");
			command.add(Tool.command(cookies));
		}
	}

	protected void dl(FilePath tmpFolder, List<String> command, List<String> dl) {
		// logger.info("\n" + dl.stream().collect(Collectors.joining(" ")));
		Set<String> u = new HashSet<>();
		Lines lines = new Lines() {
			@Override
			public void accept(String t) {
				System.out.println(t);
				{
					String prefix = "[download] Destination: ";
					if (t != null && t.startsWith(prefix)) {
						u.add(t.substring(prefix.length()));
					}
				}
				{
					String prefix = "[download] ";
					String suffix = " has already been downloaded";
					if (t != null && t.startsWith(prefix) && t.endsWith(suffix)) {
						u.add(t.substring(prefix.length(), t.length() - suffix.length()));
					}
				}
				{
					String prefix = "[atomicparsley] Adding thumbnail to \"";
					if (t != null && t.startsWith(prefix)) {
						u.add(t.substring(prefix.length(), t.length() - 1));
					}
				}
				{
					String prefix = "[ffmpeg] Adding metadata to '";
					if (t != null && t.startsWith(prefix)) {
						u.add(t.substring(prefix.length(), t.length() - 1));
					}
				}

				super.accept(t);
				logger.info("> " + t);
			}
		};
		Value<Process> processHolder = null;
		boolean log = true;
		Consumer<String> listener = null;
		boolean throwExitValue = true;
		List<FilePath> paths = Arrays.asList(executable.getParentPath());
		System.out.println(tmpFolder.getAbsolutePath() + ">\n" + command.stream().collect(Collectors.joining(" ")));
		try {
			call(processHolder, lines, tmpFolder, command, log, listener, throwExitValue, paths);
		} catch (RuntimeException ex) {
			if (lines.lines() != null && !lines.lines().isEmpty()) {
				String l = lines.lines().get(lines.lines().size() - 1);
				if (l.startsWith(UNSUPPORTED)) {
					throw new UnsupportedOperationException(l.substring(UNSUPPORTED.length()));
				}
			}
			throw ex;
		}
		if (dl != null) {
			dl.addAll(u);
		}
	}

	public List<YTDLFormat> formats(String url, String cookiesLoc, Map<String, Object> extraConfig) {
		if (executable == null || executable.notExists())
			throw new NullPointerException();
		List<String> command = new ArrayList<>();
		command.add(command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		extraConfig(command, extraConfig);
		command.add("--list-formats");
		// --compat-options list-formats
		command.add(url);
		Value<Process> processHolder = null;
		boolean log = true;
		Consumer<String> listener = null;
		boolean throwExitValue = true;
		List<FilePath> paths = Arrays.asList(executable.getParentPath());
		FilePath tmpFolder = getExecutable().getParentPath();
		List<YTDLFormat> rv = new ArrayList<>();
		Lines lines = new Lines() {
			@Override
			public void accept(String t) {
				System.out.println(t);
				if (!(t.startsWith("[youtube]") || t.startsWith("[info]") || t.startsWith("format code"))) {
					YTDLFormat f = new YTDLFormat();
					f.formatCode = t.substring(0, 13).trim();
					f.extension = t.substring(13, 24).trim();
					f.resolution = t.substring(24, 35).trim();
					f.note = t.substring(35).trim();
					String tmp = t.substring(t.lastIndexOf(",") + 1).trim();
					int p = tmp.indexOf("KiB");
					if (p != -1) {
						f.size = (long) (Double.parseDouble(tmp.substring(0, p)) * 1024.0);
					} else {
						p = tmp.indexOf("MiB");
						if (p != -1) {
							f.size = (long) (Double.parseDouble(tmp.substring(0, p)) * 1024.0 * 1024.0);
						} else {
							p = tmp.indexOf("GiB");
							if (p != -1) {
								f.size = (long) (Double.parseDouble(tmp.substring(0, p)) * 1024.0 * 1024.0 * 1024.0);
							}
						}
					}
					if (f.resolution.contains("x")) {
						String[] pa = f.resolution.split("x");
						f.width = Integer.parseInt(pa[0]);
						f.height = Integer.parseInt(pa[1]);
					}
					rv.add(f);
				}
				super.accept(t);
			}
		};
		call(processHolder, lines, tmpFolder, command, log, listener, throwExitValue, paths);
		return rv;
	}

	public FilePath downloadFormat(String url, YTDLFormat format, FilePath tmpFolder, FilePath targetFolder, String cookiesLoc, Map<String, Object> extraConfig) {
		if (executable == null || executable.notExists())
			throw new NullPointerException();
		if (tmpFolder == null)
			tmpFolder = FilePath.getTempDirectory();
		if (targetFolder == null)
			targetFolder = FilePath.getTempDirectory();
		tmpFolder = tmpFolder.child(String.valueOf(System.currentTimeMillis())).createDirectory();
		List<String> dl = new ArrayList<>();
		List<String> command = new ArrayList<>();
		command.add(command(executable));
		cookies(command, cookiesLoc);
		userAgent(command);
		common(command);
		command.add("--format");
		command.add(format.formatCode);
		extraConfig(command, extraConfig);
		command.add(url);
		dl(tmpFolder, command, dl);
		FilePath s = tmpFolder.listFiles().get(0);
		if (tmpFolder.equals(targetFolder)) {
			return s;
		}
		return s.moveTo(targetFolder);
	}

	public String getUserAgent() {
		return this.userAgent;
	}

	public void setUserAgent(String userAgent) {
		this.userAgent = userAgent;
	}

	public String getEscapeChar() {
		if (escapeChar == null) {
			escapeChar = Utils.osgroup == OSGroup.Windows ? "\"" : "'";
		}
		return this.escapeChar;
	}

	public void setEscapeChar(String escapeChar) {
		this.escapeChar = escapeChar;
	}

	public boolean isAutoUpdate() {
		return this.autoUpdate;
	}

	public void setAutoUpdate(boolean autoUpdate) {
		this.autoUpdate = autoUpdate;
	}

	@SuppressWarnings("serial")
	public static class YTDLFormat implements Serializable {
		private Integer height;
		private Integer width;
		private String formatCode;
		private String extension;
		private String resolution;
		private String note;
		private Long size;

		public String getFormatCode() {
			return this.formatCode;
		}

		public void setFormatCode(String formatCode) {
			this.formatCode = formatCode;
		}

		public String getExtension() {
			return this.extension;
		}

		public void setExtension(String extension) {
			this.extension = extension;
		}

		public String getResolution() {
			return this.resolution;
		}

		public void setResolution(String resolution) {
			this.resolution = resolution;
		}

		public String getNote() {
			return this.note;
		}

		public void setNote(String note) {
			this.note = note;
		}

		public Long getSize() {
			return this.size;
		}

		public void setSize(Long size) {
			this.size = size;
		}

		public Integer getHeight() {
			return this.height;
		}

		public void setHeight(Integer height) {
			this.height = height;
		}

		public Integer getWidth() {
			return this.width;
		}

		public void setWidth(Integer width) {
			this.width = width;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((this.formatCode == null) ? 0 : this.formatCode.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			YTDLFormat other = (YTDLFormat) obj;
			if (this.formatCode == null) {
				if (other.formatCode != null)
					return false;
			} else if (!this.formatCode.equals(other.formatCode))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "YTDLFormat [height=" + this.height + ", width=" + this.width + ", formatCode=" + this.formatCode + ", extension=" + this.extension + ", resolution="
					+ this.resolution + ", note=" + this.note + ", size=" + this.size + "]";
		}
	}
}


text:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
class admin_model extends CI_Model {

	function admin_model()
	{
		parent::__construct();
	}

	function idByEmail($email){
		$this->db->select('id');
		$this->db->where('email', $email);
		$query = $this->db->get('users');
		return $query->result();
	}

	function user_list(){
		$this->db->select('*');
		$query = $this->db->get('users');
		return $query->result();
	}

	function user_list_by($val){
		$this->db->select('*');
		$this->db->like('email', strtolower($val));
		$this->db->or_like('site_url',strtolower($val));
		$query = $this->db->get('users');
		return $query->result();
	}

	function user_modify(){
		$data = $this->input->post(NULL, TRUE);
		print_r($data);
		$this->db->where('id', $data['id']);
		$this->db->update('users', $data);
	}

	function user_delete($var){
		$data = $this->input->post(NULL, TRUE);
		$this->db->where('id', $var);
		$this->db->delete('users');
	}

	function user_site_list($email){
		$this->db->select('*');
		$this->db->where('email', $email);
		$query = $this->db->get('user_site_info');
		return $query->result();
	}

	function report_insert($var){
		$data = $this->input->post(NULL, TRUE);
		$data['minimum_payment_amount'] = $var;
		$this->db->insert('report_list', $data);
	}

	function report_list($type, $email)
	{
		$this->db->select('*');
		if($type == '1'){
			$this->db->where('user_email', $email);
		}
		$this->db->order_by('date desc');
		$query = $this->db->get('report_list');
		return $query->result();
	}

	function report_modify(){
		$data = $this->input->post(NULL, TRUE);
		$this->db->where('id', $data['id']);
		$this->db->update('report_list', $data);
	}

	function report_delete($var){
		$data = $this->input->post(NULL, TRUE);
		$this->db->where('id', $var);
		$this->db->delete('report_list');
	}

	function approval_list(){
		$this->db->select('*');
		$this->db->where('activated', '0');
		$query = $this->db->get('users');
		return $query->result();
	}

	function activation($email){
		$data['new_email_key'] = md5(rand().microtime());
		$this->db->where('email', $email);
		$this->db->update('users', $data);
		return true;
	}

	function activation_user_info($email){
		$this->db->select('*');
		$this->db->where('email', $email);
		$query = $this->db->get('users');
		return $query->result();
	}
}
?>


text:
<?php
require_once 'config.php';

class Friends extends Controller
{
	public function add()
	{
		if(!$_SESSION['id']){
			header("Location: ".ROOT_DIR."/home/index");
		}
		$data = array();
		if(isset($_POST['trainer_name'])){
			$trainer = $this->model('Trainer');
			$trainer->id = $_SESSION['id'];
			$friendTrainerName = filter_var(trim($_POST['trainer_name']), FILTER_SANITIZE_STRING);
			$trainer->sendFriendRequest($friendTrainerName);
			if($trainer->error){
				$data['error'] = $trainer->error;
			}
			else{
				header("Location: ".ROOT_DIR."/friends/index");
			}
		}
		$this->view('friends/add', $data);
	}

	public function confirm($id)
	{
		if(!$_SESSION['id']){
			header("Location: ".ROOT_DIR."/home/index");
		}
		$data = array();
		$trainer = $this->model('Trainer');
		$trainer->id = $_SESSION['id'];
		$trainer->confirmFriend($id);
		header("Location: ".ROOT_DIR."/friends/index");
	}

	public function deleteFriend($id)
	{
		if(!$_SESSION['id']){
			header("Location: ".ROOT_DIR."/home/index");
		}
		$data = array();
		$trainer = $this->model('Trainer');
		$trainer->id = $_SESSION['id'];
		$trainer->deleteFriend($id);
		//echo "added";die;
		header("Location: ".ROOT_DIR."/friends/index");
	}

	public function index()
	{
		if(!$_SESSION['id']){
			header("Location: ".ROOT_DIR."/home/index");
		}
		$trainer = $this->model('Trainer');
		$trainer->id = $_SESSION['id'];
		$data['friends'] = $trainer->getFriendList();
		$data['unconfirmedFriends'] = $trainer->getUnconfirmedFriends();
		$data['pendingFriends'] = $trainer->getPendingFriends();
		$this->view('friends/index', $data);
	}
}