/**
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.service.voice;

import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN;
import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.ActivityThread;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.Intent;
import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
import android.hardware.soundtrigger.KeyphraseMetadata;
import android.hardware.soundtrigger.SoundTrigger;
import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.AudioFormat;
import android.media.permission.Identity;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.app.IVoiceInteractionManagerService;
import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
import com.android.internal.infra.AndroidFuture;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * A class that lets a VoiceInteractionService implementation interact with
 * always-on keyphrase detection APIs.
 *
 * @hide
 * TODO(b/168605867): Once Metalava supports expressing a removed public, but current system API,
 *                    mark and track it as such.
 */
@SystemApi
public class AlwaysOnHotwordDetector extends AbstractDetector {
    //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----//
    /**
     * Indicates that this hotword detector is no longer valid for any recognition
     * and should not be used anymore.
     */
    private static final int STATE_INVALID = -3;

    /**
     * Indicates that recognition for the given keyphrase is not available on the system
     * because of the hardware configuration.
     * No further interaction should be performed with the detector that returns this availability.
     */
    public static final int STATE_HARDWARE_UNAVAILABLE = -2;

    /**
     * Indicates that recognition for the given keyphrase is not supported.
     * No further interaction should be performed with the detector that returns this availability.
     *
     * @deprecated This is no longer a valid state. Enrollment can occur outside of
     * {@link KeyphraseEnrollmentInfo} through another privileged application. We can no longer
     * determine ahead of time if the keyphrase and locale are unsupported by the system.
     */
    @Deprecated
    public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;

    /**
     * Indicates that the given keyphrase is not enrolled.
     * The caller may choose to begin an enrollment flow for the keyphrase.
     */
    public static final int STATE_KEYPHRASE_UNENROLLED = 1;

    /**
     * Indicates that the given keyphrase is currently enrolled and it's possible to start
     * recognition for it.
     */
    public static final int STATE_KEYPHRASE_ENROLLED = 2;

    /**
     * Indicates that the availability state of the active keyphrase can't be known due to an error.
     *
     * <p>NOTE: No further interaction should be performed with the detector that returns this
     * state, it would be better to create {@link AlwaysOnHotwordDetector} again.
     */
    public static final int STATE_ERROR = 3;

    /**
     * Indicates that the detector isn't ready currently.
     */
    private static final int STATE_NOT_READY = 0;

    //-- Flags for startRecognition    ----//
    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, prefix = { "RECOGNITION_FLAG_" }, value = {
            RECOGNITION_FLAG_NONE,
            RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
            RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS,
            RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION,
            RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION,
            RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER,
    })
    public @interface RecognitionFlags {}

    /**
     * Empty flag for {@link #startRecognition(int)}.
     *
     * @hide
     */
    public static final int RECOGNITION_FLAG_NONE = 0;

    /**
     * Recognition flag for {@link #startRecognition(int)} that indicates
     * whether the trigger audio for hotword needs to be captured.
     */
    public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;

    /**
     * Recognition flag for {@link #startRecognition(int)} that indicates
     * whether the recognition should keep going on even after the keyphrase triggers.
     * If this flag is specified, it's possible to get multiple triggers after a
     * call to {@link #startRecognition(int)} if the user speaks the keyphrase multiple times.
     * When this isn't specified, the default behavior is to stop recognition once the
     * keyphrase is spoken, till the caller starts recognition again.
     */
    public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 0x2;

    /**
     * Audio capabilities flag for {@link #startRecognition(int)} that indicates
     * if the underlying recognition should use AEC.
     * This capability may or may not be supported by the system, and support can be queried
     * by calling {@link #getSupportedAudioCapabilities()}. The corresponding capabilities field for
     * this flag is {@link #AUDIO_CAPABILITY_ECHO_CANCELLATION}. If this flag is passed without the
     * audio capability supported, there will be no audio effect applied.
     */
    public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 0x4;

    /**
     * Audio capabilities flag for {@link #startRecognition(int)} that indicates
     * if the underlying recognition should use noise suppression.
     * This capability may or may not be supported by the system, and support can be queried
     * by calling {@link #getSupportedAudioCapabilities()}. The corresponding capabilities field for
     * this flag is {@link #AUDIO_CAPABILITY_NOISE_SUPPRESSION}. If this flag is passed without the
     * audio capability supported, there will be no audio effect applied.
     */
    public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 0x8;

    /**
     * Recognition flag for {@link #startRecognition(int)} that indicates whether the recognition
     * should continue after battery saver mode is enabled.
     * When this flag is specified, the caller will be checked for
     * {@link android.Manifest.permission#SOUND_TRIGGER_RUN_IN_BATTERY_SAVER} permission granted.
     */
    public static final int RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER = 0x10;

    //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----//
    // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, prefix = { "RECOGNITION_MODE_" }, value = {
            RECOGNITION_MODE_VOICE_TRIGGER,
            RECOGNITION_MODE_USER_IDENTIFICATION,
    })
    public @interface RecognitionModes {}

    /**
     * Simple recognition of the key phrase.
     * Returned by {@link #getSupportedRecognitionModes()}
     */
    public static final int RECOGNITION_MODE_VOICE_TRIGGER
            = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;

    /**
     * User identification performed with the keyphrase recognition.
     * Returned by {@link #getSupportedRecognitionModes()}
     */
    public static final int RECOGNITION_MODE_USER_IDENTIFICATION
            = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;

    //-- Audio capabilities. Values in returned bit field for getSupportedAudioCapabilities() --//

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, prefix = { "AUDIO_CAPABILITY_" }, value = {
            AUDIO_CAPABILITY_ECHO_CANCELLATION,
            AUDIO_CAPABILITY_NOISE_SUPPRESSION,
    })
    public @interface AudioCapabilities {}

    /**
     * If set the underlying module supports AEC.
     * Returned by {@link #getSupportedAudioCapabilities()}
     */
    public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION =
            SoundTrigger.ModuleProperties.AUDIO_CAPABILITY_ECHO_CANCELLATION;

    /**
     * If set, the underlying module supports noise suppression.
     * Returned by {@link #getSupportedAudioCapabilities()}
     */
    public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION =
            SoundTrigger.ModuleProperties.AUDIO_CAPABILITY_NOISE_SUPPRESSION;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, prefix = { "MODEL_PARAM_" }, value = {
            MODEL_PARAM_THRESHOLD_FACTOR,
    })
    public @interface ModelParams {}

    /**
     * Gates returning {@code IllegalStateException} in {@link #initialize(
     * PersistableBundle, SharedMemory, SoundTrigger.ModuleProperties)} when no DSP module
     * is available. If the change is not enabled, the existing behavior of not throwing an
     * exception and delivering {@link STATE_HARDWARE_UNAVAILABLE} is retained.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    static final long THROW_ON_INITIALIZE_IF_NO_DSP = 269165460L;

    /**
     * Gates returning {@link Callback#onFailure} and {@link Callback#onUnknownFailure}
     * when asynchronous exceptions are propagated to the client. If the change is not enabled,
     * the existing behavior of delivering {@link #STATE_ERROR} is retained.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    static final long SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS = 280471513L;

    /**
     * Controls the sensitivity threshold adjustment factor for a given model.
     * Negative value corresponds to less sensitive model (high threshold) and
     * a positive value corresponds to a more sensitive model (low threshold).
     * Default value is 0.
     */
    public static final int MODEL_PARAM_THRESHOLD_FACTOR =
            android.hardware.soundtrigger.ModelParams.THRESHOLD_FACTOR;

    static final String TAG = "AlwaysOnHotwordDetector";
    static final boolean DBG = false;

    private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
    private static final int STATUS_OK = SoundTrigger.STATUS_OK;

    private static final int MSG_AVAILABILITY_CHANGED = 1;
    private static final int MSG_HOTWORD_DETECTED = 2;
    private static final int MSG_DETECTION_ERROR = 3;
    private static final int MSG_DETECTION_PAUSE = 4;
    private static final int MSG_DETECTION_RESUME = 5;
    private static final int MSG_HOTWORD_REJECTED = 6;
    private static final int MSG_HOTWORD_STATUS_REPORTED = 7;
    private static final int MSG_PROCESS_RESTARTED = 8;
    private static final int MSG_DETECTION_HOTWORD_DETECTION_SERVICE_FAILURE = 9;
    private static final int MSG_DETECTION_SOUND_TRIGGER_FAILURE = 10;
    private static final int MSG_DETECTION_UNKNOWN_FAILURE = 11;

    private final String mText;
    private final Locale mLocale;
    /**
     * The metadata of the Keyphrase, derived from the enrollment application.
     * This may be null if this keyphrase isn't supported by the enrollment application.
     */
    @GuardedBy("mLock")
    @Nullable
    private KeyphraseMetadata mKeyphraseMetadata;
    private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
    private final IVoiceInteractionManagerService mModelManagementService;
    private IVoiceInteractionSoundTriggerSession mSoundTriggerSession;
    private final SoundTriggerListener mInternalCallback;
    private final Callback mExternalCallback;
    private final Executor mExternalExecutor;
    private final Handler mHandler;
    private final IBinder mBinder = new Binder();
    private final boolean mSupportSandboxedDetectionService;
    private final String mAttributionTag;

    @GuardedBy("mLock")
    private boolean mIsAvailabilityOverriddenByTestApi = false;
    @GuardedBy("mLock")
    private int mAvailability = STATE_NOT_READY;

    /**
     *  A ModelParamRange is a representation of supported parameter range for a
     *  given loaded model.
     */
    public static final class ModelParamRange {
        private final SoundTrigger.ModelParamRange mModelParamRange;

        /** @hide */
        ModelParamRange(SoundTrigger.ModelParamRange modelParamRange) {
            mModelParamRange = modelParamRange;
        }

        /**
         * Get the beginning of the param range
         *
         * @return The inclusive start of the supported range.
         */
        public int getStart() {
            return mModelParamRange.getStart();
        }

        /**
         * Get the end of the param range
         *
         * @return The inclusive end of the supported range.
         */
        public int getEnd() {
            return mModelParamRange.getEnd();
        }

        @Override
        @NonNull
        public String toString() {
            return mModelParamRange.toString();
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            return mModelParamRange.equals(obj);
        }

        @Override
        public int hashCode() {
            return mModelParamRange.hashCode();
        }
    }

    /**
     * Additional payload for {@link Callback#onDetected}.
     */
    public static class EventPayload {

        /**
         * Flags for describing the data format provided in the event payload.
         *
         * @hide
         */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(prefix = {"DATA_FORMAT_"}, value = {
                DATA_FORMAT_RAW,
                DATA_FORMAT_TRIGGER_AUDIO,
        })
        public @interface DataFormat {
        }

        /**
         * Data format is not strictly defined by the framework, and the
         * {@link android.hardware.soundtrigger.SoundTriggerModule} voice engine may populate this
         * field in any format.
         */
        public static final int DATA_FORMAT_RAW = 0;

        /**
         * Data format is defined as trigger audio.
         *
         * <p>When this format is used, {@link #getCaptureAudioFormat()} can be used to understand
         * further the audio format for reading the data.
         *
         * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
         */
        public static final int DATA_FORMAT_TRIGGER_AUDIO = 1;

        @DataFormat
        private final int mDataFormat;
        // Indicates if {@code captureSession} can be used to continue capturing more audio
        // from the DSP hardware.
        private final boolean mCaptureAvailable;
        // The session to use when attempting to capture more audio from the DSP hardware.
        private final int mCaptureSession;
        private final AudioFormat mAudioFormat;
        // Raw data associated with the event.
        // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true.
        private final byte[] mData;
        private final HotwordDetectedResult mHotwordDetectedResult;
        private final ParcelFileDescriptor mAudioStream;
        private final List<KeyphraseRecognitionExtra> mKephraseExtras;

        @ElapsedRealtimeLong
        private final long mHalEventReceivedMillis;

        private final boolean mIsRecognitionStopped;

        private EventPayload(
                boolean captureAvailable,
                @Nullable AudioFormat audioFormat,
                int captureSession,
                @DataFormat int dataFormat,
                @Nullable byte[] data,
                @Nullable HotwordDetectedResult hotwordDetectedResult,
                @Nullable ParcelFileDescriptor audioStream,
                @NonNull List<KeyphraseRecognitionExtra> keyphraseExtras,
                @ElapsedRealtimeLong long halEventReceivedMillis,
                boolean isRecognitionStopped) {
            mCaptureAvailable = captureAvailable;
            mCaptureSession = captureSession;
            mAudioFormat = audioFormat;
            mDataFormat = dataFormat;
            mData = data;
            mHotwordDetectedResult = hotwordDetectedResult;
            mAudioStream = audioStream;
            mKephraseExtras = keyphraseExtras;
            mHalEventReceivedMillis = halEventReceivedMillis;
            mIsRecognitionStopped = isRecognitionStopped;
        }

        /**
         * Gets the format of the audio obtained using {@link #getTriggerAudio()}.
         * May be null if there's no audio present.
         */
        @Nullable
        public AudioFormat getCaptureAudioFormat() {
            return mAudioFormat;
        }

        /**
         * Gets the raw audio that triggered the keyphrase.
         * This may be null if the trigger audio isn't available.
         * If non-null, the format of the audio can be obtained by calling
         * {@link #getCaptureAudioFormat()}.
         *
         * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
         * @deprecated Use {@link #getData()} instead.
         */
        @Deprecated
        @Nullable
        public byte[] getTriggerAudio() {
            if (mDataFormat == DATA_FORMAT_TRIGGER_AUDIO) {
                return mData;
            } else {
                return null;
            }
        }

        /**
         * Conveys the format of the additional data that is triggered with the keyphrase event.
         *
         * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
         * @see DataFormat
         */
        @DataFormat
        public int getDataFormat() {
            return mDataFormat;
        }

        /**
         * Gets additional raw data that is triggered with the keyphrase event.
         *
         * <p>A {@link android.hardware.soundtrigger.SoundTriggerModule} may populate this
         * field with opaque data for use by system applications who know about voice
         * engine internals. Data may be null if the field is not populated by the
         * {@link android.hardware.soundtrigger.SoundTriggerModule}.
         *
         * <p>If {@link #getDataFormat()} is {@link #DATA_FORMAT_TRIGGER_AUDIO}, then the
         * entirety of this buffer is expected to be of the format from
         * {@link #getCaptureAudioFormat()}.
         *
         * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
         */
        @Nullable
        public byte[] getData() {
            return mData;
        }

        /**
         * Gets the session ID to start a capture from the DSP.
         * This may be null if streaming capture isn't possible.
         * If non-null, the format of the audio that can be captured can be
         * obtained using {@link #getCaptureAudioFormat()}.
         *
         * TODO: Candidate for Public API when the API to start capture with a session ID
         * is made public.
         *
         * TODO: Add this to {@link #getCaptureAudioFormat()}:
         * "Gets the format of the audio obtained using {@link #getTriggerAudio()}
         * or {@link #getCaptureSession()}. May be null if no audio can be obtained
         * for either the trigger or a streaming session."
         *
         * TODO: Should this return a known invalid value instead?
         *
         * @hide
         */
        @Nullable
        @UnsupportedAppUsage
        public Integer getCaptureSession() {
            if (mCaptureAvailable) {
                return mCaptureSession;
            } else {
                return null;
            }
        }

        /**
         * Returns {@link HotwordDetectedResult} associated with the hotword event, passed from
         * {@link HotwordDetectionService}.
         */
        @Nullable
        public HotwordDetectedResult getHotwordDetectedResult() {
            return mHotwordDetectedResult;
        }

        /**
         * Returns a stream with bytes corresponding to the open audio stream with hotword data.
         *
         * <p>This data represents an audio stream in the format returned by
         * {@link #getCaptureAudioFormat}.
         *
         * <p>Clients are expected to start consuming the stream within 1 second of receiving the
         * event.
         *
         * <p>When this method returns a non-null, clients must close this stream when it's no
         * longer needed. Failing to do so will result in microphone being open for longer periods
         * of time, and app being attributed for microphone usage.
         */
        @Nullable
        public ParcelFileDescriptor getAudioStream() {
            return mAudioStream;
        }

        /**
         * Returns the keyphrases recognized by the voice engine with additional confidence
         * information
         *
         * @return List of keyphrase extras describing additional data for each keyphrase the voice
         * engine triggered on for this event. The ordering of the list is preserved based on what
         * the ordering provided by {@link android.hardware.soundtrigger.SoundTriggerModule}.
         */
        @NonNull
        public List<KeyphraseRecognitionExtra> getKeyphraseRecognitionExtras() {
            return mKephraseExtras;
        }

        /**
         * Timestamp of when the trigger event from SoundTriggerHal was received by the framework.
         *
         * Clock monotonic including suspend time or its equivalent on the system,
         * in the same units and timebase as {@link SystemClock#elapsedRealtime()}.
         *
         * @return Elapsed realtime in milliseconds when the event was received from the HAL.
         *      Returns -1 if the event was not generated from the HAL.
         */
        @ElapsedRealtimeLong
        public long getHalEventReceivedMillis() {
            return mHalEventReceivedMillis;
        }

        /** Returns whether the system has stopped hotword recognition because of this detection. */
        @FlaggedApi(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
        public boolean isRecognitionStopped() {
            return mIsRecognitionStopped;
        }

        /**
         * Builder class for {@link EventPayload} objects
         *
         * @hide
         */
        @TestApi
        public static final class Builder {
            private boolean mCaptureAvailable = false;
            private int mCaptureSession = -1;
            private AudioFormat mAudioFormat = null;
            @DataFormat
            private int mDataFormat = DATA_FORMAT_RAW;
            private byte[] mData = null;
            private HotwordDetectedResult mHotwordDetectedResult = null;
            private ParcelFileDescriptor mAudioStream = null;
            private List<KeyphraseRecognitionExtra> mKeyphraseExtras = Collections.emptyList();
            @ElapsedRealtimeLong
            private long mHalEventReceivedMillis = -1;
            // default to true to keep prior behavior
            private boolean mIsRecognitionStopped = true;

            public Builder() {}

            Builder(SoundTrigger.KeyphraseRecognitionEvent keyphraseRecognitionEvent) {
                setCaptureAvailable(keyphraseRecognitionEvent.isCaptureAvailable());
                setCaptureSession(keyphraseRecognitionEvent.getCaptureSession());
                if (keyphraseRecognitionEvent.getCaptureFormat() != null) {
                    setCaptureAudioFormat(keyphraseRecognitionEvent.getCaptureFormat());
                }
                setDataFormat((keyphraseRecognitionEvent.triggerInData) ? DATA_FORMAT_TRIGGER_AUDIO
                        : DATA_FORMAT_RAW);
                if (keyphraseRecognitionEvent.getData() != null) {
                    setData(keyphraseRecognitionEvent.getData());
                }
                if (keyphraseRecognitionEvent.keyphraseExtras != null) {
                    setKeyphraseRecognitionExtras(
                            Arrays.asList(keyphraseRecognitionEvent.keyphraseExtras));
                }
                setHalEventReceivedMillis(keyphraseRecognitionEvent.getHalEventReceivedMillis());
            }

            /**
             * Indicates if {@code captureSession} can be used to continue capturing more audio from
             * the DSP hardware.
             */
            @SuppressLint("MissingGetterMatchingBuilder")
            @NonNull
            public Builder setCaptureAvailable(boolean captureAvailable) {
                mCaptureAvailable = captureAvailable;
                return this;
            }

            /**
             * Sets the session ID to start a capture from the DSP.
             */
            @SuppressLint("MissingGetterMatchingBuilder")
            @NonNull
            public Builder setCaptureSession(int captureSession) {
                mCaptureSession = captureSession;
                return this;
            }

            /**
             * Sets the format of the audio obtained using {@link #getTriggerAudio()}.
             */
            @NonNull
            public Builder setCaptureAudioFormat(@NonNull AudioFormat audioFormat) {
                mAudioFormat = audioFormat;
                return this;
            }

            /**
             * Conveys the format of the additional data that is triggered with the keyphrase event.
             *
             * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
             * @see DataFormat
             */
            @NonNull
            public Builder setDataFormat(@DataFormat int dataFormat) {
                mDataFormat = dataFormat;
                return this;
            }

            /**
             * Sets additional raw data that is triggered with the keyphrase event.
             *
             * <p>A {@link android.hardware.soundtrigger.SoundTriggerModule} may populate this
             * field with opaque data for use by system applications who know about voice
             * engine internals. Data may be null if the field is not populated by the
             * {@link android.hardware.soundtrigger.SoundTriggerModule}.
             *
             * <p>If {@link #getDataFormat()} is {@link #DATA_FORMAT_TRIGGER_AUDIO}, then the
             * entirety of this
             * buffer is expected to be of the format from {@link #getCaptureAudioFormat()}.
             *
             * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
             */
            @NonNull
            public Builder setData(@NonNull byte[] data) {
                mData = data;
                return this;
            }

            /**
             * Sets {@link HotwordDetectedResult} associated with the hotword event, passed from
             * {@link HotwordDetectionService}.
             */
            @NonNull
            public Builder setHotwordDetectedResult(
                    @NonNull HotwordDetectedResult hotwordDetectedResult) {
                mHotwordDetectedResult = hotwordDetectedResult;
                return this;
            }

            /**
             * Sets a stream with bytes corresponding to the open audio stream with hotword data.
             *
             * <p>This data represents an audio stream in the format returned by
             * {@link #getCaptureAudioFormat}.
             *
             * <p>Clients are expected to start consuming the stream within 1 second of receiving
             * the
             * event.
             */
            @NonNull
            public Builder setAudioStream(@NonNull ParcelFileDescriptor audioStream) {
                mAudioStream = audioStream;
                return this;
            }

            /**
             * Sets the keyphrases recognized by the voice engine with additional confidence
             * information
             */
            @NonNull
            public Builder setKeyphraseRecognitionExtras(
                    @NonNull List<KeyphraseRecognitionExtra> keyphraseRecognitionExtras) {
                mKeyphraseExtras = keyphraseRecognitionExtras;
                return this;
            }

            /**
             * Timestamp of when the trigger event from SoundTriggerHal was received by the
             * framework.
             *
             * Clock monotonic including suspend time or its equivalent on the system,
             * in the same units and timebase as {@link SystemClock#elapsedRealtime()}.
             */
            @NonNull
            public Builder setHalEventReceivedMillis(
                    @ElapsedRealtimeLong long halEventReceivedMillis) {
                mHalEventReceivedMillis = halEventReceivedMillis;
                return this;
            }

            /**
             * Sets whether the system has stopped hotword recognition because of this detection.
             */
            @FlaggedApi(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
            @NonNull
            public Builder setIsRecognitionStopped(boolean isRecognitionStopped) {
                mIsRecognitionStopped = isRecognitionStopped;
                return this;
            }

            /**
             * Builds an {@link EventPayload} instance
             */
            @NonNull
            public EventPayload build() {
                return new EventPayload(
                        mCaptureAvailable,
                        mAudioFormat,
                        mCaptureSession,
                        mDataFormat,
                        mData,
                        mHotwordDetectedResult,
                        mAudioStream,
                        mKeyphraseExtras,
                        mHalEventReceivedMillis,
                        mIsRecognitionStopped);
            }
        }
    }

    /**
     * Callbacks for always-on hotword detection.
     */
    public abstract static class Callback implements HotwordDetector.Callback {

        /**
         * Updates the availability state of the active keyphrase and locale on every keyphrase
         * sound model change.
         *
         * <p>This API is called whenever there's a possibility that the keyphrase associated
         * with this detector has been updated. It is not guaranteed that there is in fact any
         * change, as it may be called for other reasons.</p>
         *
         * <p>This API is also guaranteed to be called right after an AlwaysOnHotwordDetector
         * instance is created to updated the current availability state.</p>
         *
         * <p>Availability implies the current enrollment state of the given keyphrase. If the
         * hardware on this system is not capable of listening for the given keyphrase,
         * {@link AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE} will be returned.
         *
         * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE
         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED
         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED
         * @see AlwaysOnHotwordDetector#STATE_ERROR
         */
        public abstract void onAvailabilityChanged(int status);

        /**
         * Called when the keyphrase is spoken.
         *
         * <p>If {@code eventPayload.isRecognitionStopped()} returns true, this implicitly stops
         * listening for the keyphrase once it's detected. Clients should start a recognition again
         * once they are done handling this detection.
         *
         * @param eventPayload Payload data for the detection event. This may contain the trigger
         *     audio, if requested when calling {@link
         *     AlwaysOnHotwordDetector#startRecognition(int)} or if the audio comes from the {@link
         *     android.service.wearable.WearableSensingService}.
         */
        public abstract void onDetected(@NonNull EventPayload eventPayload);

        /**
         * {@inheritDoc}
         *
         * @deprecated On {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above,
         * implement {@link HotwordDetector.Callback#onFailure(HotwordDetectionServiceFailure)},
         * {@link AlwaysOnHotwordDetector.Callback#onFailure(SoundTriggerFailure)},
         * {@link HotwordDetector.Callback#onUnknownFailure(String)} instead.
         */
        @Deprecated
        @Override
        public abstract void onError();

        /**
         * Called when the detection fails due to an error occurs in the
         * {@link com.android.server.soundtrigger.SoundTriggerService} and
         * {@link com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService},
         * {@link SoundTriggerFailure} will be reported to the detector.
         *
         * @param soundTriggerFailure It provides the error code, error message and suggested
         *                            action.
         */
        public void onFailure(@NonNull SoundTriggerFailure soundTriggerFailure) {
            onError();
        }

        /** {@inheritDoc} */
        public abstract void onRecognitionPaused();

        /** {@inheritDoc} */
        public abstract void onRecognitionResumed();

        /** {@inheritDoc} */
        public void onRejected(@NonNull HotwordRejectedResult result) {
        }

        /** {@inheritDoc} */
        public void onHotwordDetectionServiceInitialized(int status) {
        }

        /** {@inheritDoc} */
        public void onHotwordDetectionServiceRestarted() {
        }
    }

    /**
     * @param text The keyphrase text to get the detector for.
     * @param locale The java locale for the detector.
     * @param callback A non-null Callback for receiving the recognition events.
     * @param modelManagementService A service that allows management of sound models.
     * @param targetSdkVersion The target SDK version.
     * @param SupportSandboxedDetectionService {@code true} if HotwordDetectionService should be
     * triggered, otherwise {@code false}.
     * @param attributionTag an optional attribution tag passed form the
     * {@link VoiceInteractionService} context via the
     * {@link createAlwaysOnHotwordDetectorInternal(String, Locale, boolean, PersistableBundle,
     * SharedMemory, ModuleProperties, Executor, Callback)}.
     *
     * @hide
     */
    public AlwaysOnHotwordDetector(String text, Locale locale, Executor executor, Callback callback,
            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
            IVoiceInteractionManagerService modelManagementService, int targetSdkVersion,
            boolean supportSandboxedDetectionService, @Nullable String attributionTag) {
        super(modelManagementService, executor, callback);

        mHandler = new MyHandler(Looper.getMainLooper());
        mText = text;
        mLocale = locale;
        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
        mExternalCallback = callback;
        mExternalExecutor = executor != null ? executor : new HandlerExecutor(
                new Handler(Looper.myLooper()));
        mInternalCallback = new SoundTriggerListener(mHandler);
        mModelManagementService = modelManagementService;
        mSupportSandboxedDetectionService = supportSandboxedDetectionService;
        mAttributionTag = attributionTag;
    }

    // Do nothing. This method should not be abstract.
    // TODO (b/269355519) un-subclass AOHD.
    @Override
    void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) {}

    void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory,
            @Nullable SoundTrigger.ModuleProperties moduleProperties) {
        if (mSupportSandboxedDetectionService) {
            initAndVerifyDetector(options, sharedMemory, mInternalCallback,
                    DETECTOR_TYPE_TRUSTED_HOTWORD_DSP, mAttributionTag);
        }
        try {
            Identity identity = new Identity();
            identity.packageName = ActivityThread.currentOpPackageName();
            if (IS_IDENTITY_WITH_ATTRIBUTION_TAG) {
                identity.attributionTag = mAttributionTag;
            }
            if (moduleProperties == null) {
                moduleProperties = mModelManagementService
                        .listModuleProperties(identity)
                        .stream()
                        .filter(prop -> !prop.getSupportedModelArch()
                                .equals(SoundTrigger.FAKE_HAL_ARCH))
                        .findFirst()
                        .orElse(null);
                if (CompatChanges.isChangeEnabled(THROW_ON_INITIALIZE_IF_NO_DSP) &&
                        moduleProperties == null) {
                    throw new IllegalStateException("No DSP module available to attach to");
                }
            }
            mSoundTriggerSession =
                    mModelManagementService.createSoundTriggerSessionAsOriginator(
                            identity, mBinder, moduleProperties);
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
        new RefreshAvailabilityTask().execute();
    }

    /**
     * {@inheritDoc}
     *
     * @throws IllegalStateException if this AlwaysOnHotwordDetector wasn't specified to use a
     * {@link HotwordDetectionService} when it was created. In addition, if this
     * AlwaysOnHotwordDetector is in an invalid or error state.
     */
    @Override
    public final void updateState(@Nullable PersistableBundle options,
            @Nullable SharedMemory sharedMemory) {
        synchronized (mLock) {
            if (!mSupportSandboxedDetectionService) {
                throw new IllegalStateException(
                        "updateState called, but it doesn't support hotword detection service");
            }
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "updateState called on an invalid detector or error state");
            }
        }

        super.updateState(options, sharedMemory);
    }

    /**
     * Test API for manipulating the voice engine and sound model availability.
     *
     * After overriding the availability status, the client's
     * {@link Callback#onAvailabilityChanged(int)} will be called to reflect the updated state.
     *
     * When this override is set, all system updates to availability will be ignored.
     * @hide
     */
    @TestApi
    public void overrideAvailability(int availability) {
        synchronized (mLock) {
            mAvailability = availability;
            mIsAvailabilityOverriddenByTestApi = true;
            // ENROLLED state requires there to be metadata about the sound model so a fake one
            // is created.
            if (mKeyphraseMetadata == null && mAvailability == STATE_KEYPHRASE_ENROLLED) {
                Set<Locale> fakeSupportedLocales = new HashSet<>();
                fakeSupportedLocales.add(mLocale);
                mKeyphraseMetadata = new KeyphraseMetadata(1, mText, fakeSupportedLocales,
                        AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER);
            }
            notifyStateChangedLocked();
        }
    }

    /**
     * Test API for clearing an availability override set by {@link #overrideAvailability(int)}
     *
     * This method will restore the availability to the current system state.
     * @hide
     */
    @TestApi
    public void resetAvailability() {
        synchronized (mLock) {
            mIsAvailabilityOverriddenByTestApi = false;
        }
        // Execute a refresh availability task - which should then notify of a change.
        new RefreshAvailabilityTask().execute();
    }

    /**
     * Test API to simulate to trigger hardware recognition event for test.
     *
     * @hide
     */
    @TestApi
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public void triggerHardwareRecognitionEventForTest(int status, int soundModelHandle,
            @ElapsedRealtimeLong long halEventReceivedMillis, boolean captureAvailable,
            int captureSession, int captureDelayMs, int capturePreambleMs, boolean triggerInData,
            @NonNull AudioFormat captureFormat, @Nullable byte[] data,
            @NonNull List<KeyphraseRecognitionExtra> keyphraseRecognitionExtras) {
        Log.d(TAG, "triggerHardwareRecognitionEventForTest()");
        synchronized (mLock) {
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException("triggerHardwareRecognitionEventForTest called on"
                        + " an invalid detector or error state");
            }
            try {
                mModelManagementService.triggerHardwareRecognitionEventForTest(
                        new KeyphraseRecognitionEvent(status, soundModelHandle, captureAvailable,
                                captureSession, captureDelayMs, capturePreambleMs, triggerInData,
                                captureFormat, data, keyphraseRecognitionExtras.toArray(
                                new KeyphraseRecognitionExtra[0]), halEventReceivedMillis,
                                new Binder()),
                        mInternalCallback);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Gets the recognition modes supported by the associated keyphrase.
     *
     * @see #RECOGNITION_MODE_USER_IDENTIFICATION
     * @see #RECOGNITION_MODE_VOICE_TRIGGER
     *
     * @throws UnsupportedOperationException if the keyphrase itself isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    public @RecognitionModes int getSupportedRecognitionModes() {
        if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()");
        synchronized (mLock) {
            return getSupportedRecognitionModesLocked();
        }
    }

    @GuardedBy("mLock")
    private int getSupportedRecognitionModesLocked() {
        if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
            throw new IllegalStateException(
                    "getSupportedRecognitionModes called on an invalid detector or error state");
        }

        // This method only makes sense if we can actually support a recognition.
        if (mAvailability != STATE_KEYPHRASE_ENROLLED || mKeyphraseMetadata == null) {
            throw new UnsupportedOperationException(
                    "Getting supported recognition modes for the keyphrase is not supported");
        }

        return mKeyphraseMetadata.getRecognitionModeFlags();
    }

    /**
     * Get the audio capabilities supported by the platform which can be enabled when
     * starting a recognition.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @see #AUDIO_CAPABILITY_ECHO_CANCELLATION
     * @see #AUDIO_CAPABILITY_NOISE_SUPPRESSION
     *
     * @return Bit field encoding of the AudioCapabilities supported.
     */
    @AudioCapabilities
    public int getSupportedAudioCapabilities() {
        if (DBG) Slog.d(TAG, "getSupportedAudioCapabilities()");
        synchronized (mLock) {
            return getSupportedAudioCapabilitiesLocked();
        }
    }

    @GuardedBy("mLock")
    private int getSupportedAudioCapabilitiesLocked() {
        try {
            ModuleProperties properties =
                    mSoundTriggerSession.getDspModuleProperties();
            if (properties != null) {
                return properties.getAudioCapabilities();
            }

            return 0;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Starts recognition for the associated keyphrase.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @see #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
     * @see #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
     *
     * @param recognitionFlags The flags to control the recognition properties.
     * @param data Additional pass-through data to the system voice engine along with the
     *             startRecognition request. This data is intended to provide additional parameters
     *             when starting the opaque sound model.
     * @return Indicates whether the call succeeded or not.
     * @throws UnsupportedOperationException if the recognition isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public boolean startRecognition(@RecognitionFlags int recognitionFlags, @NonNull byte[] data) {
        synchronized (mLock) {
            return startRecognitionLocked(recognitionFlags, data)
                    == STATUS_OK;
        }
    }

    /**
     * Starts recognition for the associated keyphrase.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @see #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
     * @see #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
     *
     * @param recognitionFlags The flags to control the recognition properties.
     * @return Indicates whether the call succeeded or not.
     * @throws UnsupportedOperationException if the recognition isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
        if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
        synchronized (mLock) {
            return startRecognitionLocked(recognitionFlags, null /* data */) == STATUS_OK;
        }
    }

    /**
     * Starts recognition for the associated keyphrase.
     *
     * @see #startRecognition(int)
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    @Override
    public boolean startRecognition() {
        return startRecognition(0);
    }

    /**
     * Stops recognition for the associated keyphrase.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @return Indicates whether the call succeeded or not.
     * @throws UnsupportedOperationException if the recognition isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    // TODO: Remove this RequiresPermission since it isn't actually enforced. Also fix the javadoc
    // about permissions enforcement (when it throws vs when it just returns false) for other
    // methods in this class.
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    @Override
    public boolean stopRecognition() {
        if (DBG) Slog.d(TAG, "stopRecognition()");
        synchronized (mLock) {
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "stopRecognition called on an invalid detector or error state");
            }

            // Check if we can start/stop a recognition.
            if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
                throw new UnsupportedOperationException(
                        "Recognition for the given keyphrase is not supported");
            }

            return stopRecognitionLocked() == STATUS_OK;
        }
    }

    /**
     * Set a model specific {@link ModelParams} with the given value. This
     * parameter will keep its value for the duration the model is loaded regardless of starting and
     * stopping recognition. Once the model is unloaded, the value will be lost.
     * {@link AlwaysOnHotwordDetector#queryParameter} should be checked first before calling this
     * method.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @param modelParam   {@link ModelParams}
     * @param value        Value to set
     * @return - {@link SoundTrigger#STATUS_OK} in case of success
     *         - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
     *         - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
     *         - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
     *           if API is not supported by HAL
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public int setParameter(@ModelParams int modelParam, int value) {
        if (DBG) {
            Slog.d(TAG, "setParameter(" + modelParam + ", " + value + ")");
        }

        synchronized (mLock) {
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "setParameter called on an invalid detector or error state");
            }

            return setParameterLocked(modelParam, value);
        }
    }

    /**
     * Get a model specific {@link ModelParams}. This parameter will keep its value
     * for the duration the model is loaded regardless of starting and stopping recognition.
     * Once the model is unloaded, the value will be lost. If the value is not set, a default
     * value is returned. See {@link ModelParams} for parameter default values.
     * {@link AlwaysOnHotwordDetector#queryParameter} should be checked first before
     * calling this method.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @param modelParam   {@link ModelParams}
     * @return value of parameter
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public int getParameter(@ModelParams int modelParam) {
        if (DBG) {
            Slog.d(TAG, "getParameter(" + modelParam + ")");
        }

        synchronized (mLock) {
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "getParameter called on an invalid detector or error state");
            }

            return getParameterLocked(modelParam);
        }
    }

    /**
     * Determine if parameter control is supported for the given model handle.
     * This method should be checked prior to calling {@link AlwaysOnHotwordDetector#setParameter}
     * or {@link AlwaysOnHotwordDetector#getParameter}.
     * Caller must be the active voice interaction service via
     * Settings.Secure.VOICE_INTERACTION_SERVICE.
     *
     * @param modelParam {@link ModelParams}
     * @return supported range of parameter, null if not supported
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    @Nullable
    public ModelParamRange queryParameter(@ModelParams int modelParam) {
        if (DBG) {
            Slog.d(TAG, "queryParameter(" + modelParam + ")");
        }

        synchronized (mLock) {
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "queryParameter called on an invalid detector or error state");
            }

            return queryParameterLocked(modelParam);
        }
    }

    /**
     * Creates an intent to start the enrollment for the associated keyphrase.
     * This intent must be invoked using {@link Context#startForegroundService(Intent)}.
     * Starting re-enrollment is only valid if the keyphrase is un-enrolled,
     * i.e. {@link #STATE_KEYPHRASE_UNENROLLED},
     * otherwise {@link #createReEnrollIntent()} should be preferred.
     *
     * @return An {@link Intent} to start enrollment for the given keyphrase.
     * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @Nullable
    public Intent createEnrollIntent() {
        if (DBG) Slog.d(TAG, "createEnrollIntent");
        synchronized (mLock) {
            return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_ENROLL);
        }
    }

    /**
     * Creates an intent to start the un-enrollment for the associated keyphrase.
     * This intent must be invoked using {@link Context#startForegroundService(Intent)}.
     * Starting re-enrollment is only valid if the keyphrase is already enrolled,
     * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
     *
     * @return An {@link Intent} to start un-enrollment for the given keyphrase.
     * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @Nullable
    public Intent createUnEnrollIntent() {
        if (DBG) Slog.d(TAG, "createUnEnrollIntent");
        synchronized (mLock) {
            return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_UN_ENROLL);
        }
    }

    /**
     * Creates an intent to start the re-enrollment for the associated keyphrase.
     * This intent must be invoked using {@link Context#startForegroundService(Intent)}.
     * Starting re-enrollment is only valid if the keyphrase is already enrolled,
     * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
     *
     * @return An {@link Intent} to start re-enrollment for the given keyphrase.
     * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
     *         Callers should only call this method after a supported state callback on
     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
     * @throws IllegalStateException if the detector is in an invalid or error state.
     *         This may happen if another detector has been instantiated or the
     *         {@link VoiceInteractionService} hosting this detector has been shut down.
     */
    @Nullable
    public Intent createReEnrollIntent() {
        if (DBG) Slog.d(TAG, "createReEnrollIntent");
        synchronized (mLock) {
            return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_RE_ENROLL);
        }
    }

    @GuardedBy("mLock")
    private Intent getManageIntentLocked(@KeyphraseEnrollmentInfo.ManageActions int action) {
        if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
            throw new IllegalStateException(
                    "getManageIntent called on an invalid detector or error state");
        }

        // This method only makes sense if we can actually support a recognition.
        if (mAvailability != STATE_KEYPHRASE_ENROLLED
                && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
            throw new UnsupportedOperationException(
                    "Managing the given keyphrase is not supported");
        }

        return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
    }

    /** {@inheritDoc} */
    @Override
    public void destroy() {
        synchronized (mLock) {
            detachSessionLocked();

            mAvailability = STATE_INVALID;
            mIsAvailabilityOverriddenByTestApi = false;
            notifyStateChangedLocked();
        }
        super.destroy();
    }

    private void detachSessionLocked() {
        try {
            if (DBG) Slog.d(TAG, "detachSessionLocked() " + mSoundTriggerSession);
            if (mSoundTriggerSession != null) {
                mSoundTriggerSession.detach();
            }
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    @Override
    public boolean isUsingSandboxedDetectionService() {
        return mSupportSandboxedDetectionService;
    }

    /**
     * Reloads the sound models from the service.
     *
     * @hide
     */
    // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector
    void onSoundModelsChanged() {
        synchronized (mLock) {
            if (mAvailability == STATE_INVALID
                    || mAvailability == STATE_HARDWARE_UNAVAILABLE
                    || mAvailability == STATE_ERROR) {
                Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config"
                        + " or in the error state");
                return;
            }

            // Because this method reflects an update from the system service models, we should not
            // update the client of an availability change when the availability has been overridden
            // via a test API.
            if (mIsAvailabilityOverriddenByTestApi) {
                Slog.w(TAG, "Suppressing system availability update. "
                        + "Availability is overridden by test API.");
                return;
            }

            // Stop the recognition before proceeding if we are in the enrolled state.
            // The framework makes the guarantee that an actively used model is present in the
            // system server's enrollment database. For this reason we much stop an actively running
            // model when the underlying sound model in enrollment database no longer match.
            if (mAvailability == STATE_KEYPHRASE_ENROLLED) {
                // A SoundTriggerFailure will be sent to the client if the model state was
                // changed. This is an overloading of the onFailure usage because we are sending a
                // callback even in the successful stop case. If stopRecognition is successful,
                // suggested next action RESTART_RECOGNITION will be sent.
                // TODO(b/281608561): This code path will be removed with other enrollment flows in
                //  this class.
                try {
                    int result = stopRecognitionLocked();
                    if (result == STATUS_OK) {
                        sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN,
                                "stopped recognition because of enrollment update",
                                FailureSuggestedAction.RESTART_RECOGNITION));
                    }
                    // only log to logcat here because many failures can be false positives such as
                    // calling stopRecognition where there is no started session.
                    Log.w(TAG, "Failed to stop recognition after enrollment update: code="
                            + result);
                } catch (Exception e) {
                    Slog.w(TAG, "Failed to stop recognition after enrollment update", e);
                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
                        sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN,
                                "Failed to stop recognition after enrollment update: "
                                        + Log.getStackTraceString(e),
                                FailureSuggestedAction.RECREATE_DETECTOR));
                    } else {
                        updateAndNotifyStateChangedLocked(STATE_ERROR);
                    }
                    return;
                }
            }

            // Execute a refresh availability task - which should then notify of a change.
            new RefreshAvailabilityTask().execute();
        }
    }

    @GuardedBy("mLock")
    private int startRecognitionLocked(int recognitionFlags,
            @Nullable byte[] data) {
        if (DBG) {
            Slog.d(TAG, "startRecognition("
                    + recognitionFlags
                    + ", " + Arrays.toString(data) + ")");
        }
        if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
            throw new IllegalStateException(
                    "startRecognition called on an invalid detector or error state");
        }

        // Check if we can start/stop a recognition.
        if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
            throw new UnsupportedOperationException(
                    "Recognition for the given keyphrase is not supported");
        }

        KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1];
        // TODO: Do we need to do something about the confidence level here?
        recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.getId(),
                mKeyphraseMetadata.getRecognitionModeFlags(), 0, new ConfidenceLevel[0]);
        boolean captureTriggerAudio =
                (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
        boolean allowMultipleTriggers =
                (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0;
        boolean runInBatterySaver = (recognitionFlags&RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER) != 0;

        int audioCapabilities = 0;
        if ((recognitionFlags & RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION) != 0) {
            audioCapabilities |= AUDIO_CAPABILITY_ECHO_CANCELLATION;
        }
        if ((recognitionFlags & RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION) != 0) {
            audioCapabilities |= AUDIO_CAPABILITY_NOISE_SUPPRESSION;
        }

        int code;
        try {
            code = mSoundTriggerSession.startRecognition(
                    mKeyphraseMetadata.getId(), mLocale.toLanguageTag(), mInternalCallback,
                    new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
                            recognitionExtra, data, audioCapabilities),
                    runInBatterySaver);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        if (code != STATUS_OK) {
            Slog.w(TAG, "startRecognition() failed with error code " + code);
        }
        return code;
    }

    @GuardedBy("mLock")
    private int stopRecognitionLocked() {
        int code;
        try {
            code = mSoundTriggerSession.stopRecognition(mKeyphraseMetadata.getId(),
                    mInternalCallback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        if (code != STATUS_OK) {
            Slog.w(TAG, "stopRecognition() failed with error code " + code);
        }
        return code;
    }

    @GuardedBy("mLock")
    private int setParameterLocked(@ModelParams int modelParam, int value) {
        try {
            int code = mSoundTriggerSession.setParameter(mKeyphraseMetadata.getId(), modelParam,
                    value);

            if (code != STATUS_OK) {
                Slog.w(TAG, "setParameter failed with error code " + code);
            }

            return code;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @GuardedBy("mLock")
    private int getParameterLocked(@ModelParams int modelParam) {
        try {
            return mSoundTriggerSession.getParameter(mKeyphraseMetadata.getId(), modelParam);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @GuardedBy("mLock")
    @Nullable
    private ModelParamRange queryParameterLocked(@ModelParams int modelParam) {
        try {
            SoundTrigger.ModelParamRange modelParamRange =
                    mSoundTriggerSession.queryParameter(mKeyphraseMetadata.getId(), modelParam);

            if (modelParamRange == null) {
                return null;
            }

            return new ModelParamRange(modelParamRange);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @GuardedBy("mLock")
    private void updateAndNotifyStateChangedLocked(int availability) {
        updateAvailabilityLocked(availability);
        notifyStateChangedLocked();
    }

    @GuardedBy("mLock")
    private void updateAvailabilityLocked(int availability) {
        if (DBG) {
            Slog.d(TAG, "Hotword availability changed from " + mAvailability
                    + " -> " + availability);
        }
        if (!mIsAvailabilityOverriddenByTestApi) {
            mAvailability = availability;
        }
    }

    @GuardedBy("mLock")
    private void notifyStateChangedLocked() {
        Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
        message.arg1 = mAvailability;
        message.sendToTarget();
    }

    @GuardedBy("mLock")
    private void sendUnknownFailure(String failureMessage) {
        // update but do not call onAvailabilityChanged callback for STATE_ERROR
        updateAvailabilityLocked(STATE_ERROR);
        Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget();
    }

    private void sendSoundTriggerFailure(@NonNull SoundTriggerFailure soundTriggerFailure) {
        Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE, soundTriggerFailure)
                .sendToTarget();
    }

    /** @hide */
    static final class SoundTriggerListener extends IHotwordRecognitionStatusCallback.Stub {
        private final Handler mHandler;

        public SoundTriggerListener(Handler handler) {
            mHandler = handler;
        }

        @Override
        public void onKeyphraseDetected(
                KeyphraseRecognitionEvent event, HotwordDetectedResult result) {
            if (DBG) {
                Slog.d(TAG, "onDetected(" + event + ")");
            } else {
                Slog.i(TAG, "onDetected");
            }
            Message.obtain(mHandler, MSG_HOTWORD_DETECTED,
                    new EventPayload.Builder(event)
                            .setHotwordDetectedResult(result)
                            .build())
                    .sendToTarget();
        }

        @Override
        public void onKeyphraseDetectedFromExternalSource(HotwordDetectedResult result) {
            Slog.i(TAG, "onKeyphraseDetectedFromExternalSource");
            EventPayload.Builder eventPayloadBuilder = new EventPayload.Builder();
            if (android.app.wearable.Flags.enableHotwordWearableSensingApi()) {
                eventPayloadBuilder.setIsRecognitionStopped(false);
            }
            Message.obtain(
                            mHandler,
                            MSG_HOTWORD_DETECTED,
                            eventPayloadBuilder.setHotwordDetectedResult(result).build())
                    .sendToTarget();
        }

        @Override
        public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
            Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event);
        }

        @Override
        public void onRejected(@NonNull HotwordRejectedResult result) {
            if (DBG) {
                Slog.d(TAG, "onRejected(" + result + ")");
            } else {
                Slog.i(TAG, "onRejected");
            }
            Message.obtain(mHandler, MSG_HOTWORD_REJECTED, result).sendToTarget();
        }

        @Override
        public void onHotwordDetectionServiceFailure(
                HotwordDetectionServiceFailure hotwordDetectionServiceFailure) {
            Slog.v(TAG, "onHotwordDetectionServiceFailure: " + hotwordDetectionServiceFailure);
            if (hotwordDetectionServiceFailure != null) {
                Message.obtain(mHandler, MSG_DETECTION_HOTWORD_DETECTION_SERVICE_FAILURE,
                        hotwordDetectionServiceFailure).sendToTarget();
            } else {
                Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE,
                        "Error data is null").sendToTarget();
            }
        }

        @Override
        public void onVisualQueryDetectionServiceFailure(
                VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure)
                throws RemoteException {
            // It should never be called here.
            Slog.w(TAG,
                    "onVisualQueryDetectionServiceFailure: " + visualQueryDetectionServiceFailure);
        }

        @Override
        public void onSoundTriggerFailure(SoundTriggerFailure soundTriggerFailure) {
            Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE,
                    Objects.requireNonNull(soundTriggerFailure)).sendToTarget();
        }

        @Override
        public void onUnknownFailure(String errorMessage) throws RemoteException {
            Slog.v(TAG, "onUnknownFailure: " + errorMessage);
            Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE,
                    !TextUtils.isEmpty(errorMessage) ? errorMessage
                            : "Error data is null").sendToTarget();
        }

        @Override
        public void onRecognitionPaused() {
            Slog.i(TAG, "onRecognitionPaused");
            mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
        }

        @Override
        public void onRecognitionResumed() {
            Slog.i(TAG, "onRecognitionResumed");
            mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
        }

        @Override
        public void onStatusReported(int status) {
            if (DBG) {
                Slog.d(TAG, "onStatusReported(" + status + ")");
            } else {
                Slog.i(TAG, "onStatusReported");
            }
            Message message = Message.obtain(mHandler, MSG_HOTWORD_STATUS_REPORTED);
            message.arg1 = status;
            message.sendToTarget();
        }

        @Override
        public void onProcessRestarted() {
            Slog.i(TAG, "onProcessRestarted");
            mHandler.sendEmptyMessage(MSG_PROCESS_RESTARTED);
        }

        @Override
        public void onOpenFile(String filename, AndroidFuture future) throws RemoteException {
            throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
        }
    }

    void onDetectorRemoteException() {
        Message.obtain(mHandler, MSG_DETECTION_HOTWORD_DETECTION_SERVICE_FAILURE,
                new HotwordDetectionServiceFailure(
                        HotwordDetectionServiceFailure.ERROR_CODE_REMOTE_EXCEPTION,
                        "Detector remote exception occurs")).sendToTarget();
    }

    class MyHandler extends Handler {
        MyHandler(@NonNull Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            synchronized (mLock) {
                if (mAvailability == STATE_INVALID) {
                    Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector");
                    return;
                }
            }
            final Message message = Message.obtain(msg);
            Binder.withCleanCallingIdentity(() -> mExternalExecutor.execute(() -> {
                Slog.i(TAG, "handle message " + message.what);
                switch (message.what) {
                    case MSG_AVAILABILITY_CHANGED:
                        mExternalCallback.onAvailabilityChanged(message.arg1);
                        break;
                    case MSG_HOTWORD_DETECTED:
                        mExternalCallback.onDetected((EventPayload) message.obj);
                        break;
                    case MSG_DETECTION_ERROR:
                        // TODO(b/271534248): After reverting the workaround, this logic is still
                        // necessary.
                        mExternalCallback.onError();
                        break;
                    case MSG_DETECTION_PAUSE:
                        mExternalCallback.onRecognitionPaused();
                        break;
                    case MSG_DETECTION_RESUME:
                        mExternalCallback.onRecognitionResumed();
                        break;
                    case MSG_HOTWORD_REJECTED:
                        mExternalCallback.onRejected((HotwordRejectedResult) message.obj);
                        break;
                    case MSG_HOTWORD_STATUS_REPORTED:
                        mExternalCallback.onHotwordDetectionServiceInitialized(message.arg1);
                        break;
                    case MSG_PROCESS_RESTARTED:
                        mExternalCallback.onHotwordDetectionServiceRestarted();
                        break;
                    case MSG_DETECTION_HOTWORD_DETECTION_SERVICE_FAILURE:
                        mExternalCallback.onFailure((HotwordDetectionServiceFailure) message.obj);
                        break;
                    case MSG_DETECTION_SOUND_TRIGGER_FAILURE:
                        mExternalCallback.onFailure((SoundTriggerFailure) message.obj);
                        break;
                    case MSG_DETECTION_UNKNOWN_FAILURE:
                        mExternalCallback.onUnknownFailure((String) message.obj);
                        break;
                    default:
                        super.handleMessage(message);
                }
                message.recycle();
            }));
        }
    }

    // TODO(b/267681692): remove the AsyncTask usage
    class RefreshAvailabilityTask extends AsyncTask<Void, Void, Void> {

        @Override
        public Void doInBackground(Void... params) {
            try {
                int availability = internalGetInitialAvailability();

                synchronized (mLock) {
                    if (availability == STATE_NOT_READY) {
                        internalUpdateEnrolledKeyphraseMetadata();
                        if (mKeyphraseMetadata != null) {
                            availability = STATE_KEYPHRASE_ENROLLED;
                        } else {
                            availability = STATE_KEYPHRASE_UNENROLLED;
                        }
                    }
                    updateAndNotifyStateChangedLocked(availability);
                }
            } catch (Exception e) {
                // Any exception here not caught will crash the process because AsyncTask does not
                // bubble up the exceptions to the client app, so we must propagate it to the app.
                Slog.w(TAG, "Failed to refresh availability", e);
                synchronized (mLock) {
                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
                        sendUnknownFailure(
                                "Failed to refresh availability: " + Log.getStackTraceString(e));
                    } else {
                        updateAndNotifyStateChangedLocked(STATE_ERROR);
                    }
                }
            }

            return null;
        }

        /**
         * @return The initial availability without checking the enrollment status.
         */
        private int internalGetInitialAvailability() {
            synchronized (mLock) {
                // This detector has already been invalidated.
                if (mAvailability == STATE_INVALID) {
                    return STATE_INVALID;
                }
            }

            if (!CompatChanges.isChangeEnabled(THROW_ON_INITIALIZE_IF_NO_DSP)) {
                ModuleProperties dspModuleProperties;
                try {
                    dspModuleProperties =
                            mSoundTriggerSession.getDspModuleProperties();
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }

                // No DSP available
                if (dspModuleProperties == null) {
                    return STATE_HARDWARE_UNAVAILABLE;
                }
            }

            return STATE_NOT_READY;
        }

        private void internalUpdateEnrolledKeyphraseMetadata() {
            try {
                mKeyphraseMetadata = mModelManagementService.getEnrolledKeyphraseMetadata(
                        mText, mLocale.toLanguageTag());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) {
            if (!(obj instanceof AlwaysOnHotwordDetector)) {
                return false;
            }
            AlwaysOnHotwordDetector other = (AlwaysOnHotwordDetector) obj;
            return TextUtils.equals(mText, other.mText) && mLocale.equals(other.mLocale);
        }

        return super.equals(obj);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mText, mLocale);
    }

    /** @hide */
    @Override
    public void dump(String prefix, PrintWriter pw) {
        synchronized (mLock) {
            pw.print(prefix); pw.print("Text="); pw.println(mText);
            pw.print(prefix); pw.print("Locale="); pw.println(mLocale);
            pw.print(prefix); pw.print("Availability="); pw.println(mAvailability);
            pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata);
            pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo);
        }
    }
}
