1 /**
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.voice;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.UnsupportedAppUsage;
23 import android.app.Activity;
24 import android.content.Intent;
25 import android.hardware.soundtrigger.IRecognitionStatusCallback;
26 import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
27 import android.hardware.soundtrigger.KeyphraseMetadata;
28 import android.hardware.soundtrigger.SoundTrigger;
29 import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel;
30 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
31 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
32 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
33 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
34 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
35 import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
36 import android.media.AudioFormat;
37 import android.os.AsyncTask;
38 import android.os.Handler;
39 import android.os.Message;
40 import android.os.RemoteException;
41 import android.util.Slog;
42 
43 import com.android.internal.app.IVoiceInteractionManagerService;
44 
45 import java.io.PrintWriter;
46 import java.lang.annotation.Retention;
47 import java.lang.annotation.RetentionPolicy;
48 import java.util.Locale;
49 
50 /**
51  * A class that lets a VoiceInteractionService implementation interact with
52  * always-on keyphrase detection APIs.
53  */
54 public class AlwaysOnHotwordDetector {
55     //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----//
56     /**
57      * Indicates that this hotword detector is no longer valid for any recognition
58      * and should not be used anymore.
59      */
60     private static final int STATE_INVALID = -3;
61 
62     /**
63      * Indicates that recognition for the given keyphrase is not available on the system
64      * because of the hardware configuration.
65      * No further interaction should be performed with the detector that returns this availability.
66      */
67     public static final int STATE_HARDWARE_UNAVAILABLE = -2;
68     /**
69      * Indicates that recognition for the given keyphrase is not supported.
70      * No further interaction should be performed with the detector that returns this availability.
71      */
72     public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;
73     /**
74      * Indicates that the given keyphrase is not enrolled.
75      * The caller may choose to begin an enrollment flow for the keyphrase.
76      */
77     public static final int STATE_KEYPHRASE_UNENROLLED = 1;
78     /**
79      * Indicates that the given keyphrase is currently enrolled and it's possible to start
80      * recognition for it.
81      */
82     public static final int STATE_KEYPHRASE_ENROLLED = 2;
83 
84     /**
85      * Indicates that the detector isn't ready currently.
86      */
87     private static final int STATE_NOT_READY = 0;
88 
89     // Keyphrase management actions. Used in getManageIntent() ----//
90     @Retention(RetentionPolicy.SOURCE)
91     @IntDef(prefix = { "MANAGE_ACTION_" }, value = {
92             MANAGE_ACTION_ENROLL,
93             MANAGE_ACTION_RE_ENROLL,
94             MANAGE_ACTION_UN_ENROLL
95     })
96     private @interface ManageActions {}
97 
98     /**
99      * Indicates that we need to enroll.
100      *
101      * @hide
102      */
103     public static final int MANAGE_ACTION_ENROLL = 0;
104     /**
105      * Indicates that we need to re-enroll.
106      *
107      * @hide
108      */
109     public static final int MANAGE_ACTION_RE_ENROLL = 1;
110     /**
111      * Indicates that we need to un-enroll.
112      *
113      * @hide
114      */
115     public static final int MANAGE_ACTION_UN_ENROLL = 2;
116 
117     //-- Flags for startRecognition    ----//
118     /** @hide */
119     @Retention(RetentionPolicy.SOURCE)
120     @IntDef(flag = true, prefix = { "RECOGNITION_FLAG_" }, value = {
121             RECOGNITION_FLAG_NONE,
122             RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
123             RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
124     })
125     public @interface RecognitionFlags {}
126 
127     /**
128      * Empty flag for {@link #startRecognition(int)}.
129      *
130      * @hide
131      */
132     public static final int RECOGNITION_FLAG_NONE = 0;
133     /**
134      * Recognition flag for {@link #startRecognition(int)} that indicates
135      * whether the trigger audio for hotword needs to be captured.
136      */
137     public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;
138     /**
139      * Recognition flag for {@link #startRecognition(int)} that indicates
140      * whether the recognition should keep going on even after the keyphrase triggers.
141      * If this flag is specified, it's possible to get multiple triggers after a
142      * call to {@link #startRecognition(int)} if the user speaks the keyphrase multiple times.
143      * When this isn't specified, the default behavior is to stop recognition once the
144      * keyphrase is spoken, till the caller starts recognition again.
145      */
146     public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 0x2;
147 
148     //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----//
149     // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.
150 
151     /** @hide */
152     @Retention(RetentionPolicy.SOURCE)
153     @IntDef(flag = true, prefix = { "RECOGNITION_MODE_" }, value = {
154             RECOGNITION_MODE_VOICE_TRIGGER,
155             RECOGNITION_MODE_USER_IDENTIFICATION,
156     })
157     public @interface RecognitionModes {}
158 
159     /**
160      * Simple recognition of the key phrase.
161      * Returned by {@link #getSupportedRecognitionModes()}
162      */
163     public static final int RECOGNITION_MODE_VOICE_TRIGGER
164             = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
165     /**
166      * User identification performed with the keyphrase recognition.
167      * Returned by {@link #getSupportedRecognitionModes()}
168      */
169     public static final int RECOGNITION_MODE_USER_IDENTIFICATION
170             = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;
171 
172     static final String TAG = "AlwaysOnHotwordDetector";
173     static final boolean DBG = false;
174 
175     private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
176     private static final int STATUS_OK = SoundTrigger.STATUS_OK;
177 
178     private static final int MSG_AVAILABILITY_CHANGED = 1;
179     private static final int MSG_HOTWORD_DETECTED = 2;
180     private static final int MSG_DETECTION_ERROR = 3;
181     private static final int MSG_DETECTION_PAUSE = 4;
182     private static final int MSG_DETECTION_RESUME = 5;
183 
184     private final String mText;
185     private final Locale mLocale;
186     /**
187      * The metadata of the Keyphrase, derived from the enrollment application.
188      * This may be null if this keyphrase isn't supported by the enrollment application.
189      */
190     private final KeyphraseMetadata mKeyphraseMetadata;
191     private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
192     private final IVoiceInteractionService mVoiceInteractionService;
193     private final IVoiceInteractionManagerService mModelManagementService;
194     private final SoundTriggerListener mInternalCallback;
195     private final Callback mExternalCallback;
196     private final Object mLock = new Object();
197     private final Handler mHandler;
198 
199     private int mAvailability = STATE_NOT_READY;
200 
201     /**
202      * Additional payload for {@link Callback#onDetected}.
203      */
204     public static class EventPayload {
205         private final boolean mTriggerAvailable;
206         // Indicates if {@code captureSession} can be used to continue capturing more audio
207         // from the DSP hardware.
208         private final boolean mCaptureAvailable;
209         // The session to use when attempting to capture more audio from the DSP hardware.
210         private final int mCaptureSession;
211         private final AudioFormat mAudioFormat;
212         // Raw data associated with the event.
213         // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true.
214         private final byte[] mData;
215 
EventPayload(boolean triggerAvailable, boolean captureAvailable, AudioFormat audioFormat, int captureSession, byte[] data)216         private EventPayload(boolean triggerAvailable, boolean captureAvailable,
217                 AudioFormat audioFormat, int captureSession, byte[] data) {
218             mTriggerAvailable = triggerAvailable;
219             mCaptureAvailable = captureAvailable;
220             mCaptureSession = captureSession;
221             mAudioFormat = audioFormat;
222             mData = data;
223         }
224 
225         /**
226          * Gets the format of the audio obtained using {@link #getTriggerAudio()}.
227          * May be null if there's no audio present.
228          */
229         @Nullable
getCaptureAudioFormat()230         public AudioFormat getCaptureAudioFormat() {
231             return mAudioFormat;
232         }
233 
234         /**
235          * Gets the raw audio that triggered the keyphrase.
236          * This may be null if the trigger audio isn't available.
237          * If non-null, the format of the audio can be obtained by calling
238          * {@link #getCaptureAudioFormat()}.
239          *
240          * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
241          */
242         @Nullable
getTriggerAudio()243         public byte[] getTriggerAudio() {
244             if (mTriggerAvailable) {
245                 return mData;
246             } else {
247                 return null;
248             }
249         }
250 
251         /**
252          * Gets the session ID to start a capture from the DSP.
253          * This may be null if streaming capture isn't possible.
254          * If non-null, the format of the audio that can be captured can be
255          * obtained using {@link #getCaptureAudioFormat()}.
256          *
257          * TODO: Candidate for Public API when the API to start capture with a session ID
258          * is made public.
259          *
260          * TODO: Add this to {@link #getCaptureAudioFormat()}:
261          * "Gets the format of the audio obtained using {@link #getTriggerAudio()}
262          * or {@link #getCaptureSession()}. May be null if no audio can be obtained
263          * for either the trigger or a streaming session."
264          *
265          * TODO: Should this return a known invalid value instead?
266          *
267          * @hide
268          */
269         @Nullable
270         @UnsupportedAppUsage
getCaptureSession()271         public Integer getCaptureSession() {
272             if (mCaptureAvailable) {
273                 return mCaptureSession;
274             } else {
275                 return null;
276             }
277         }
278     }
279 
280     /**
281      * Callbacks for always-on hotword detection.
282      */
283     public static abstract class Callback {
284         /**
285          * Called when the hotword availability changes.
286          * This indicates a change in the availability of recognition for the given keyphrase.
287          * It's called at least once with the initial availability.<p/>
288          *
289          * Availability implies whether the hardware on this system is capable of listening for
290          * the given keyphrase or not. <p/>
291          *
292          * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE
293          * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNSUPPORTED
294          * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED
295          * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED
296          */
onAvailabilityChanged(int status)297         public abstract void onAvailabilityChanged(int status);
298         /**
299          * Called when the keyphrase is spoken.
300          * This implicitly stops listening for the keyphrase once it's detected.
301          * Clients should start a recognition again once they are done handling this
302          * detection.
303          *
304          * @param eventPayload Payload data for the detection event.
305          *        This may contain the trigger audio, if requested when calling
306          *        {@link AlwaysOnHotwordDetector#startRecognition(int)}.
307          */
onDetected(@onNull EventPayload eventPayload)308         public abstract void onDetected(@NonNull EventPayload eventPayload);
309         /**
310          * Called when the detection fails due to an error.
311          */
onError()312         public abstract void onError();
313         /**
314          * Called when the recognition is paused temporarily for some reason.
315          * This is an informational callback, and the clients shouldn't be doing anything here
316          * except showing an indication on their UI if they have to.
317          */
onRecognitionPaused()318         public abstract void onRecognitionPaused();
319         /**
320          * Called when the recognition is resumed after it was temporarily paused.
321          * This is an informational callback, and the clients shouldn't be doing anything here
322          * except showing an indication on their UI if they have to.
323          */
onRecognitionResumed()324         public abstract void onRecognitionResumed();
325     }
326 
327     /**
328      * @param text The keyphrase text to get the detector for.
329      * @param locale The java locale for the detector.
330      * @param callback A non-null Callback for receiving the recognition events.
331      * @param voiceInteractionService The current voice interaction service.
332      * @param modelManagementService A service that allows management of sound models.
333      *
334      * @hide
335      */
AlwaysOnHotwordDetector(String text, Locale locale, Callback callback, KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, IVoiceInteractionService voiceInteractionService, IVoiceInteractionManagerService modelManagementService)336     public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback,
337             KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
338             IVoiceInteractionService voiceInteractionService,
339             IVoiceInteractionManagerService modelManagementService) {
340         mText = text;
341         mLocale = locale;
342         mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
343         mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
344         mExternalCallback = callback;
345         mHandler = new MyHandler();
346         mInternalCallback = new SoundTriggerListener(mHandler);
347         mVoiceInteractionService = voiceInteractionService;
348         mModelManagementService = modelManagementService;
349         new RefreshAvailabiltyTask().execute();
350     }
351 
352     /**
353      * Gets the recognition modes supported by the associated keyphrase.
354      *
355      * @see #RECOGNITION_MODE_USER_IDENTIFICATION
356      * @see #RECOGNITION_MODE_VOICE_TRIGGER
357      *
358      * @throws UnsupportedOperationException if the keyphrase itself isn't supported.
359      *         Callers should only call this method after a supported state callback on
360      *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
361      * @throws IllegalStateException if the detector is in an invalid state.
362      *         This may happen if another detector has been instantiated or the
363      *         {@link VoiceInteractionService} hosting this detector has been shut down.
364      */
getSupportedRecognitionModes()365     public @RecognitionModes int getSupportedRecognitionModes() {
366         if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()");
367         synchronized (mLock) {
368             return getSupportedRecognitionModesLocked();
369         }
370     }
371 
getSupportedRecognitionModesLocked()372     private int getSupportedRecognitionModesLocked() {
373         if (mAvailability == STATE_INVALID) {
374             throw new IllegalStateException(
375                     "getSupportedRecognitionModes called on an invalid detector");
376         }
377 
378         // This method only makes sense if we can actually support a recognition.
379         if (mAvailability != STATE_KEYPHRASE_ENROLLED
380                 && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
381             throw new UnsupportedOperationException(
382                     "Getting supported recognition modes for the keyphrase is not supported");
383         }
384 
385         return mKeyphraseMetadata.recognitionModeFlags;
386     }
387 
388     /**
389      * Starts recognition for the associated keyphrase.
390      *
391      * @see #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
392      * @see #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
393      *
394      * @param recognitionFlags The flags to control the recognition properties.
395      * @return Indicates whether the call succeeded or not.
396      * @throws UnsupportedOperationException if the recognition isn't supported.
397      *         Callers should only call this method after a supported state callback on
398      *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
399      * @throws IllegalStateException if the detector is in an invalid state.
400      *         This may happen if another detector has been instantiated or the
401      *         {@link VoiceInteractionService} hosting this detector has been shut down.
402      */
startRecognition(@ecognitionFlags int recognitionFlags)403     public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
404         if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
405         synchronized (mLock) {
406             if (mAvailability == STATE_INVALID) {
407                 throw new IllegalStateException("startRecognition called on an invalid detector");
408             }
409 
410             // Check if we can start/stop a recognition.
411             if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
412                 throw new UnsupportedOperationException(
413                         "Recognition for the given keyphrase is not supported");
414             }
415 
416             return startRecognitionLocked(recognitionFlags) == STATUS_OK;
417         }
418     }
419 
420     /**
421      * Stops recognition for the associated keyphrase.
422      *
423      * @return Indicates whether the call succeeded or not.
424      * @throws UnsupportedOperationException if the recognition isn't supported.
425      *         Callers should only call this method after a supported state callback on
426      *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
427      * @throws IllegalStateException if the detector is in an invalid state.
428      *         This may happen if another detector has been instantiated or the
429      *         {@link VoiceInteractionService} hosting this detector has been shut down.
430      */
stopRecognition()431     public boolean stopRecognition() {
432         if (DBG) Slog.d(TAG, "stopRecognition()");
433         synchronized (mLock) {
434             if (mAvailability == STATE_INVALID) {
435                 throw new IllegalStateException("stopRecognition called on an invalid detector");
436             }
437 
438             // Check if we can start/stop a recognition.
439             if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
440                 throw new UnsupportedOperationException(
441                         "Recognition for the given keyphrase is not supported");
442             }
443 
444             return stopRecognitionLocked() == STATUS_OK;
445         }
446     }
447 
448     /**
449      * Creates an intent to start the enrollment for the associated keyphrase.
450      * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}.
451      * Starting re-enrollment is only valid if the keyphrase is un-enrolled,
452      * i.e. {@link #STATE_KEYPHRASE_UNENROLLED},
453      * otherwise {@link #createReEnrollIntent()} should be preferred.
454      *
455      * @return An {@link Intent} to start enrollment for the given keyphrase.
456      * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
457      *         Callers should only call this method after a supported state callback on
458      *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
459      * @throws IllegalStateException if the detector is in an invalid state.
460      *         This may happen if another detector has been instantiated or the
461      *         {@link VoiceInteractionService} hosting this detector has been shut down.
462      */
createEnrollIntent()463     public Intent createEnrollIntent() {
464         if (DBG) Slog.d(TAG, "createEnrollIntent");
465         synchronized (mLock) {
466             return getManageIntentLocked(MANAGE_ACTION_ENROLL);
467         }
468     }
469 
470     /**
471      * Creates an intent to start the un-enrollment for the associated keyphrase.
472      * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}.
473      * Starting re-enrollment is only valid if the keyphrase is already enrolled,
474      * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
475      *
476      * @return An {@link Intent} to start un-enrollment for the given keyphrase.
477      * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
478      *         Callers should only call this method after a supported state callback on
479      *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
480      * @throws IllegalStateException if the detector is in an invalid state.
481      *         This may happen if another detector has been instantiated or the
482      *         {@link VoiceInteractionService} hosting this detector has been shut down.
483      */
createUnEnrollIntent()484     public Intent createUnEnrollIntent() {
485         if (DBG) Slog.d(TAG, "createUnEnrollIntent");
486         synchronized (mLock) {
487             return getManageIntentLocked(MANAGE_ACTION_UN_ENROLL);
488         }
489     }
490 
491     /**
492      * Creates an intent to start the re-enrollment for the associated keyphrase.
493      * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}.
494      * Starting re-enrollment is only valid if the keyphrase is already enrolled,
495      * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
496      *
497      * @return An {@link Intent} to start re-enrollment for the given keyphrase.
498      * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
499      *         Callers should only call this method after a supported state callback on
500      *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
501      * @throws IllegalStateException if the detector is in an invalid state.
502      *         This may happen if another detector has been instantiated or the
503      *         {@link VoiceInteractionService} hosting this detector has been shut down.
504      */
createReEnrollIntent()505     public Intent createReEnrollIntent() {
506         if (DBG) Slog.d(TAG, "createReEnrollIntent");
507         synchronized (mLock) {
508             return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL);
509         }
510     }
511 
getManageIntentLocked(int action)512     private Intent getManageIntentLocked(int action) {
513         if (mAvailability == STATE_INVALID) {
514             throw new IllegalStateException("getManageIntent called on an invalid detector");
515         }
516 
517         // This method only makes sense if we can actually support a recognition.
518         if (mAvailability != STATE_KEYPHRASE_ENROLLED
519                 && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
520             throw new UnsupportedOperationException(
521                     "Managing the given keyphrase is not supported");
522         }
523 
524         return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
525     }
526 
527     /**
528      * Invalidates this hotword detector so that any future calls to this result
529      * in an IllegalStateException.
530      *
531      * @hide
532      */
invalidate()533     void invalidate() {
534         synchronized (mLock) {
535             mAvailability = STATE_INVALID;
536             notifyStateChangedLocked();
537         }
538     }
539 
540     /**
541      * Reloads the sound models from the service.
542      *
543      * @hide
544      */
onSoundModelsChanged()545     void onSoundModelsChanged() {
546         synchronized (mLock) {
547             if (mAvailability == STATE_INVALID
548                     || mAvailability == STATE_HARDWARE_UNAVAILABLE
549                     || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) {
550                 Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config");
551                 return;
552             }
553 
554             // Stop the recognition before proceeding.
555             // This is done because we want to stop the recognition on an older model if it changed
556             // or was deleted.
557             // The availability change callback should ensure that the client starts recognition
558             // again if needed.
559             stopRecognitionLocked();
560 
561             // Execute a refresh availability task - which should then notify of a change.
562             new RefreshAvailabiltyTask().execute();
563         }
564     }
565 
startRecognitionLocked(int recognitionFlags)566     private int startRecognitionLocked(int recognitionFlags) {
567         KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1];
568         // TODO: Do we need to do something about the confidence level here?
569         recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id,
570                 mKeyphraseMetadata.recognitionModeFlags, 0, new ConfidenceLevel[0]);
571         boolean captureTriggerAudio =
572                 (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
573         boolean allowMultipleTriggers =
574                 (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0;
575         int code = STATUS_ERROR;
576         try {
577             code = mModelManagementService.startRecognition(mVoiceInteractionService,
578                     mKeyphraseMetadata.id, mLocale.toLanguageTag(), mInternalCallback,
579                     new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
580                             recognitionExtra, null /* additional data */));
581         } catch (RemoteException e) {
582             Slog.w(TAG, "RemoteException in startRecognition!", e);
583         }
584         if (code != STATUS_OK) {
585             Slog.w(TAG, "startRecognition() failed with error code " + code);
586         }
587         return code;
588     }
589 
stopRecognitionLocked()590     private int stopRecognitionLocked() {
591         int code = STATUS_ERROR;
592         try {
593             code = mModelManagementService.stopRecognition(
594                     mVoiceInteractionService, mKeyphraseMetadata.id, mInternalCallback);
595         } catch (RemoteException e) {
596             Slog.w(TAG, "RemoteException in stopRecognition!", e);
597         }
598 
599         if (code != STATUS_OK) {
600             Slog.w(TAG, "stopRecognition() failed with error code " + code);
601         }
602         return code;
603     }
604 
notifyStateChangedLocked()605     private void notifyStateChangedLocked() {
606         Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
607         message.arg1 = mAvailability;
608         message.sendToTarget();
609     }
610 
611     /** @hide */
612     static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub {
613         private final Handler mHandler;
614 
SoundTriggerListener(Handler handler)615         public SoundTriggerListener(Handler handler) {
616             mHandler = handler;
617         }
618 
619         @Override
onKeyphraseDetected(KeyphraseRecognitionEvent event)620         public void onKeyphraseDetected(KeyphraseRecognitionEvent event) {
621             if (DBG) {
622                 Slog.d(TAG, "onDetected(" + event + ")");
623             } else {
624                 Slog.i(TAG, "onDetected");
625             }
626             Message.obtain(mHandler, MSG_HOTWORD_DETECTED,
627                     new EventPayload(event.triggerInData, event.captureAvailable,
628                             event.captureFormat, event.captureSession, event.data))
629                     .sendToTarget();
630         }
631         @Override
onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event)632         public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
633             Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event);
634         }
635 
636         @Override
onError(int status)637         public void onError(int status) {
638             Slog.i(TAG, "onError: " + status);
639             mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
640         }
641 
642         @Override
onRecognitionPaused()643         public void onRecognitionPaused() {
644             Slog.i(TAG, "onRecognitionPaused");
645             mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
646         }
647 
648         @Override
onRecognitionResumed()649         public void onRecognitionResumed() {
650             Slog.i(TAG, "onRecognitionResumed");
651             mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
652         }
653     }
654 
655     class MyHandler extends Handler {
656         @Override
handleMessage(Message msg)657         public void handleMessage(Message msg) {
658             synchronized (mLock) {
659                 if (mAvailability == STATE_INVALID) {
660                     Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector");
661                     return;
662                 }
663             }
664 
665             switch (msg.what) {
666                 case MSG_AVAILABILITY_CHANGED:
667                     mExternalCallback.onAvailabilityChanged(msg.arg1);
668                     break;
669                 case MSG_HOTWORD_DETECTED:
670                     mExternalCallback.onDetected((EventPayload) msg.obj);
671                     break;
672                 case MSG_DETECTION_ERROR:
673                     mExternalCallback.onError();
674                     break;
675                 case MSG_DETECTION_PAUSE:
676                     mExternalCallback.onRecognitionPaused();
677                     break;
678                 case MSG_DETECTION_RESUME:
679                     mExternalCallback.onRecognitionResumed();
680                     break;
681                 default:
682                     super.handleMessage(msg);
683             }
684         }
685     }
686 
687     class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> {
688 
689         @Override
doInBackground(Void... params)690         public Void doInBackground(Void... params) {
691             int availability = internalGetInitialAvailability();
692             boolean enrolled = false;
693             // Fetch the sound model if the availability is one of the supported ones.
694             if (availability == STATE_NOT_READY
695                     || availability == STATE_KEYPHRASE_UNENROLLED
696                     || availability == STATE_KEYPHRASE_ENROLLED) {
697                 enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id, mLocale);
698                 if (!enrolled) {
699                     availability = STATE_KEYPHRASE_UNENROLLED;
700                 } else {
701                     availability = STATE_KEYPHRASE_ENROLLED;
702                 }
703             }
704 
705             synchronized (mLock) {
706                 if (DBG) {
707                     Slog.d(TAG, "Hotword availability changed from " + mAvailability
708                             + " -> " + availability);
709                 }
710                 mAvailability = availability;
711                 notifyStateChangedLocked();
712             }
713             return null;
714         }
715 
716         /**
717          * @return The initial availability without checking the enrollment status.
718          */
internalGetInitialAvailability()719         private int internalGetInitialAvailability() {
720             synchronized (mLock) {
721                 // This detector has already been invalidated.
722                 if (mAvailability == STATE_INVALID) {
723                     return STATE_INVALID;
724                 }
725             }
726 
727             ModuleProperties dspModuleProperties = null;
728             try {
729                 dspModuleProperties =
730                         mModelManagementService.getDspModuleProperties(mVoiceInteractionService);
731             } catch (RemoteException e) {
732                 Slog.w(TAG, "RemoteException in getDspProperties!", e);
733             }
734             // No DSP available
735             if (dspModuleProperties == null) {
736                 return STATE_HARDWARE_UNAVAILABLE;
737             }
738             // No enrollment application supports this keyphrase/locale
739             if (mKeyphraseMetadata == null) {
740                 return STATE_KEYPHRASE_UNSUPPORTED;
741             }
742             return STATE_NOT_READY;
743         }
744 
745         /**
746          * @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
747          */
internalGetIsEnrolled(int keyphraseId, Locale locale)748         private boolean internalGetIsEnrolled(int keyphraseId, Locale locale) {
749             try {
750                 return mModelManagementService.isEnrolledForKeyphrase(
751                         mVoiceInteractionService, keyphraseId, locale.toLanguageTag());
752             } catch (RemoteException e) {
753                 Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!", e);
754             }
755             return false;
756         }
757     }
758 
759     /** @hide */
dump(String prefix, PrintWriter pw)760     public void dump(String prefix, PrintWriter pw) {
761         synchronized (mLock) {
762             pw.print(prefix); pw.print("Text="); pw.println(mText);
763             pw.print(prefix); pw.print("Locale="); pw.println(mLocale);
764             pw.print(prefix); pw.print("Availability="); pw.println(mAvailability);
765             pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata);
766             pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo);
767         }
768     }
769 }
770