| /** |
| * 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.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 static final int MSG_HOTWORD_TRAINING_DATA = 12; |
| |
| 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 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) { |
| mCaptureAvailable = captureAvailable; |
| mCaptureSession = captureSession; |
| mAudioFormat = audioFormat; |
| mDataFormat = dataFormat; |
| mData = data; |
| mHotwordDetectedResult = hotwordDetectedResult; |
| mAudioStream = audioStream; |
| mKephraseExtras = keyphraseExtras; |
| mHalEventReceivedMillis = halEventReceivedMillis; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| |
| 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; |
| } |
| |
| /** |
| * Builds an {@link EventPayload} instance |
| */ |
| @NonNull |
| public EventPayload build() { |
| return new EventPayload(mCaptureAvailable, mAudioFormat, mCaptureSession, |
| mDataFormat, mData, mHotwordDetectedResult, mAudioStream, |
| mKeyphraseExtras, mHalEventReceivedMillis); |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| * 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)}. |
| */ |
| 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); |
| } |
| } |
| notifyStateChanged(availability); |
| } |
| |
| /** |
| * 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; |
| } |
| notifyStateChanged(STATE_INVALID); |
| 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() { |
| boolean notifyError = false; |
| |
| 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); |
| |
| // Execute a refresh availability task - which should then notify of a change. |
| new RefreshAvailabilityTask().execute(); |
| } 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 { |
| notifyError = true; |
| } |
| } |
| } |
| } |
| |
| if (notifyError) { |
| updateAndNotifyStateChanged(STATE_ERROR); |
| } |
| } |
| |
| @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(); |
| } |
| } |
| |
| private void updateAndNotifyStateChanged(int availability) { |
| synchronized (mLock) { |
| updateAvailabilityLocked(availability); |
| } |
| notifyStateChanged(availability); |
| } |
| |
| @GuardedBy("mLock") |
| private void updateAvailabilityLocked(int availability) { |
| if (DBG) { |
| Slog.d(TAG, "Hotword availability changed from " + mAvailability |
| + " -> " + availability); |
| } |
| if (!mIsAvailabilityOverriddenByTestApi) { |
| mAvailability = availability; |
| } |
| } |
| |
| private void notifyStateChanged(int newAvailability) { |
| Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED); |
| message.arg1 = newAvailability; |
| message.sendToTarget(); |
| } |
| |
| private void sendUnknownFailure(String failureMessage) { |
| synchronized (mLock) { |
| // 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 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 onTrainingData(@NonNull HotwordTrainingData data) { |
| if (DBG) { |
| Slog.d(TAG, "onTrainingData(" + data + ")"); |
| } else { |
| Slog.i(TAG, "onTrainingData"); |
| } |
| Message.obtain(mHandler, MSG_HOTWORD_TRAINING_DATA, data).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; |
| case MSG_HOTWORD_TRAINING_DATA: |
| mExternalCallback.onTrainingData((HotwordTrainingData) 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; |
| } |
| } |
| } |
| updateAndNotifyStateChanged(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); |
| if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { |
| sendUnknownFailure( |
| "Failed to refresh availability: " + Log.getStackTraceString(e)); |
| } else { |
| updateAndNotifyStateChanged(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); |
| } |
| } |
| } |