SoundTriggerHelper.java revision 37f9776ab9aec865fe2ef6544fc84c37e48aca1a
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
17package com.android.server.soundtrigger;
18
19import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
20
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.hardware.soundtrigger.IRecognitionStatusCallback;
26import android.hardware.soundtrigger.SoundTrigger;
27import android.hardware.soundtrigger.SoundTrigger.GenericRecognitionEvent;
28import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
29import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
30import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
31import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
32import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
33import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
34import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
35import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
36import android.hardware.soundtrigger.SoundTrigger.SoundModel;
37import android.hardware.soundtrigger.SoundTrigger.SoundModelEvent;
38import android.hardware.soundtrigger.SoundTriggerModule;
39import android.os.PowerManager;
40import android.os.RemoteException;
41import android.telephony.PhoneStateListener;
42import android.telephony.TelephonyManager;
43import android.util.Slog;
44import com.android.internal.logging.MetricsLogger;
45
46import java.io.FileDescriptor;
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import java.util.HashMap;
50import java.util.UUID;
51
52/**
53 * Helper for {@link SoundTrigger} APIs. Supports two types of models:
54 * (i) A voice model which is exported via the {@link VoiceInteractionService}. There can only be
55 * a single voice model running on the DSP at any given time.
56 *
57 * (ii) Generic sound-trigger models: Supports multiple of these.
58 *
59 * Currently this just acts as an abstraction over all SoundTrigger API calls.
60 * @hide
61 */
62public class SoundTriggerHelper implements SoundTrigger.StatusListener {
63    static final String TAG = "SoundTriggerHelper";
64    static final boolean DBG = false;
65
66    /**
67     * Return codes for {@link #startRecognition(int, KeyphraseSoundModel,
68     *      IRecognitionStatusCallback, RecognitionConfig)},
69     * {@link #stopRecognition(int, IRecognitionStatusCallback)}
70     */
71    public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
72    public static final int STATUS_OK = SoundTrigger.STATUS_OK;
73
74    private static final int INVALID_VALUE = Integer.MIN_VALUE;
75
76    /** The {@link ModuleProperties} for the system, or null if none exists. */
77    final ModuleProperties mModuleProperties;
78
79    /** The properties for the DSP module */
80    private SoundTriggerModule mModule;
81    private final Object mLock = new Object();
82    private final Context mContext;
83    private final TelephonyManager mTelephonyManager;
84    private final PhoneStateListener mPhoneStateListener;
85    private final PowerManager mPowerManager;
86
87    // The SoundTriggerManager layer handles multiple recognition models of type generic and
88    // keyphrase. We store the ModelData here in a hashmap.
89    private final HashMap<UUID, ModelData> mModelDataMap;
90
91    // An index of keyphrase sound models so that we can reach them easily. We support indexing
92    // keyphrase sound models with a keyphrase ID. Sound model with the same keyphrase ID will
93    // replace an existing model, thus there is a 1:1 mapping from keyphrase ID to a voice
94    // sound model.
95    private HashMap<Integer, UUID> mKeyphraseUuidMap;
96
97    private boolean mCallActive = false;
98    private boolean mIsPowerSaveMode = false;
99    // Indicates if the native sound trigger service is disabled or not.
100    // This is an indirect indication of the microphone being open in some other application.
101    private boolean mServiceDisabled = false;
102
103    // Whether we have ANY recognition (keyphrase or generic) running.
104    private boolean mRecognitionRunning = false;
105
106    private PowerSaveModeListener mPowerSaveModeListener;
107
108    SoundTriggerHelper(Context context) {
109        ArrayList <ModuleProperties> modules = new ArrayList<>();
110        int status = SoundTrigger.listModules(modules);
111        mContext = context;
112        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
113        mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
114        mModelDataMap = new HashMap<UUID, ModelData>();
115        mKeyphraseUuidMap = new HashMap<Integer, UUID>();
116        mPhoneStateListener = new MyCallStateListener();
117        if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
118            Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size());
119            mModuleProperties = null;
120            mModule = null;
121        } else {
122            // TODO: Figure out how to determine which module corresponds to the DSP hardware.
123            mModuleProperties = modules.get(0);
124        }
125    }
126
127    /**
128     * Starts recognition for the given generic sound model ID. This is a wrapper around {@link
129     * startRecognition()}.
130     *
131     * @param modelId UUID of the sound model.
132     * @param soundModel The generic sound model to use for recognition.
133     * @param callback Callack for the recognition events related to the given keyphrase.
134     * @param recognitionConfig Instance of RecognitionConfig containing the parameters for the
135     * recognition.
136     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
137     */
138    int startGenericRecognition(UUID modelId, GenericSoundModel soundModel,
139            IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
140        MetricsLogger.count(mContext, "sth_start_recognition", 1);
141        if (modelId == null || soundModel == null || callback == null ||
142                recognitionConfig == null) {
143            Slog.w(TAG, "Passed in bad data to startGenericRecognition().");
144            return STATUS_ERROR;
145        }
146
147        synchronized (mLock) {
148            ModelData modelData = getOrCreateGenericModelDataLocked(modelId);
149            if (modelData == null) {
150                Slog.w(TAG, "Irrecoverable error occurred, check UUID / sound model data.");
151                return STATUS_ERROR;
152            }
153            return startRecognition(soundModel, modelData, callback, recognitionConfig,
154                    INVALID_VALUE /* keyphraseId */);
155        }
156    }
157
158    /**
159     * Starts recognition for the given keyphraseId.
160     *
161     * @param keyphraseId The identifier of the keyphrase for which
162     *        the recognition is to be started.
163     * @param soundModel The sound model to use for recognition.
164     * @param callback The callback for the recognition events related to the given keyphrase.
165     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
166     */
167    int startKeyphraseRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
168            IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig) {
169        synchronized (mLock) {
170            MetricsLogger.count(mContext, "sth_start_recognition", 1);
171            if (soundModel == null || callback == null || recognitionConfig == null) {
172                return STATUS_ERROR;
173            }
174
175            if (DBG) {
176                Slog.d(TAG, "startKeyphraseRecognition for keyphraseId=" + keyphraseId
177                        + " soundModel=" + soundModel + ", callback=" + callback.asBinder()
178                        + ", recognitionConfig=" + recognitionConfig);
179                Slog.d(TAG, "moduleProperties=" + mModuleProperties);
180                dumpModelStateLocked();
181            }
182
183            ModelData model = getKeyphraseModelDataLocked(keyphraseId);
184            if (model != null && !model.isKeyphraseModel()) {
185                Slog.e(TAG, "Generic model with same UUID exists.");
186                return STATUS_ERROR;
187            }
188
189            // Process existing model first.
190            if (model != null && !model.getModelId().equals(soundModel.uuid)) {
191                // The existing model has a different UUID, should be replaced.
192                int status = cleanUpExistingKeyphraseModel(model);
193                if (status != STATUS_OK) {
194                    return status;
195                }
196                removeKeyphraseModelLocked(keyphraseId);
197                model = null;
198            }
199
200            // We need to create a new one: either no previous models existed for given keyphrase id
201            // or the existing model had a different UUID and was cleaned up.
202            if (model == null) {
203                model = createKeyphraseModelDataLocked(soundModel.uuid, keyphraseId);
204            }
205
206            return startRecognition(soundModel, model, callback, recognitionConfig,
207                    keyphraseId);
208        }
209    }
210
211    private int cleanUpExistingKeyphraseModel(ModelData modelData) {
212        // Stop and clean up a previous ModelData if one exists. This usually is used when the
213        // previous model has a different UUID for the same keyphrase ID.
214        int status = tryStopAndUnloadLocked(modelData, true /* stop */, true /* unload */);
215        if (status != STATUS_OK) {
216            Slog.w(TAG, "Unable to stop or unload previous model: " +
217                    modelData.toString());
218        }
219        return status;
220    }
221
222    /**
223     * Starts recognition for the given sound model. A single routine for both keyphrase and
224     * generic sound models.
225     *
226     * @param soundModel The sound model to use for recognition.
227     * @param modelData Instance of {@link #ModelData} for the given model.
228     * @param callback Callback for the recognition events related to the given keyphrase.
229     * @param recognitionConfig Instance of {@link RecognitionConfig} containing the parameters
230     * @param keyphraseId Keyphrase ID for keyphrase models only. Pass in INVALID_VALUE for other
231     * models.
232     * for the recognition.
233     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
234     */
235    int startRecognition(SoundModel soundModel, ModelData modelData,
236            IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig,
237            int keyphraseId) {
238        synchronized (mLock) {
239            if (mModuleProperties == null) {
240                Slog.w(TAG, "Attempting startRecognition without the capability");
241                return STATUS_ERROR;
242            }
243            if (mModule == null) {
244                mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null);
245                if (mModule == null) {
246                    Slog.w(TAG, "startRecognition cannot attach to sound trigger module");
247                    return STATUS_ERROR;
248                }
249            }
250
251            // Initialize power save, call active state monitoring logic.
252            if (!mRecognitionRunning) {
253                initializeTelephonyAndPowerStateListeners();
254            }
255
256            // If the existing SoundModel is different (for the same UUID for Generic and same
257            // keyphrase ID for voice), ensure that it is unloaded and stopped before proceeding.
258            // This works for both keyphrase and generic models. This logic also ensures that a
259            // previously loaded (or started) model is appropriately stopped. Since this is a
260            // generalization of the previous logic with a single keyphrase model, we should have
261            // no regression with the previous version of this code as was given in the
262            // startKeyphrase() routine.
263            if (modelData.getSoundModel() != null) {
264                boolean stopModel = false; // Stop the model after checking that it is started.
265                boolean unloadModel = false;
266                if (modelData.getSoundModel().equals(soundModel) && modelData.isModelStarted()) {
267                    // The model has not changed, but the previous model is "started".
268                    // Stop the previously running model.
269                    stopModel = true;
270                    unloadModel = false; // No need to unload if the model hasn't changed.
271                } else if (!modelData.getSoundModel().equals(soundModel)) {
272                    // We have a different model for this UUID. Stop and unload if needed. This
273                    // helps maintain the singleton restriction for keyphrase sound models.
274                    stopModel = modelData.isModelStarted();
275                    unloadModel = modelData.isModelLoaded();
276                }
277                if (stopModel || unloadModel) {
278                    int status = tryStopAndUnloadLocked(modelData, stopModel, unloadModel);
279                    if (status != STATUS_OK) {
280                        Slog.w(TAG, "Unable to stop or unload previous model: " +
281                                modelData.toString());
282                        return status;
283                    }
284                }
285            }
286
287            IRecognitionStatusCallback oldCallback = modelData.getCallback();
288            if (oldCallback != null && oldCallback.asBinder() != callback.asBinder()) {
289                Slog.w(TAG, "Canceling previous recognition for model id: " +
290                        modelData.getModelId());
291                try {
292                    oldCallback.onError(STATUS_ERROR);
293                } catch (RemoteException e) {
294                    Slog.w(TAG, "RemoteException in onDetectionStopped", e);
295                }
296                modelData.clearCallback();
297            }
298
299            // Load the model if it is not loaded.
300            if (!modelData.isModelLoaded()) {
301                // Load the model
302                int[] handle = new int[] { INVALID_VALUE };
303                int status = mModule.loadSoundModel(soundModel, handle);
304                if (status != SoundTrigger.STATUS_OK) {
305                    Slog.w(TAG, "loadSoundModel call failed with " + status);
306                    return status;
307                }
308                if (handle[0] == INVALID_VALUE) {
309                    Slog.w(TAG, "loadSoundModel call returned invalid sound model handle");
310                    return STATUS_ERROR;
311                }
312                modelData.setHandle(handle[0]);
313                modelData.setLoaded();
314                Slog.d(TAG, "Sound model loaded with handle:" + handle[0]);
315            }
316            modelData.setCallback(callback);
317            modelData.setRequested(true);
318            modelData.setRecognitionConfig(recognitionConfig);
319            modelData.setSoundModel(soundModel);
320
321            return startRecognitionLocked(modelData,
322                    false /* Don't notify for synchronous calls */);
323        }
324    }
325
326    /**
327     * Stops recognition for the given generic sound model. This is a wrapper for {@link
328     * #stopRecognition}.
329     *
330     * @param modelId The identifier of the generic sound model for which
331     *        the recognition is to be stopped.
332     * @param callback The callback for the recognition events related to the given sound model.
333     *
334     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
335     */
336    int stopGenericRecognition(UUID modelId, IRecognitionStatusCallback callback) {
337        synchronized (mLock) {
338            MetricsLogger.count(mContext, "sth_stop_recognition", 1);
339            if (callback == null || modelId == null) {
340                Slog.e(TAG, "Null callbackreceived for stopGenericRecognition() for modelid:" +
341                        modelId);
342                return STATUS_ERROR;
343            }
344
345            ModelData modelData = mModelDataMap.get(modelId);
346            if (modelData == null || !modelData.isGenericModel()) {
347                Slog.w(TAG, "Attempting stopRecognition on invalid model with id:" + modelId);
348                return STATUS_ERROR;
349            }
350
351            int status = stopRecognition(modelData, callback);
352            if (status != SoundTrigger.STATUS_OK) {
353                Slog.w(TAG, "stopGenericRecognition failed: " + status);
354            }
355            return status;
356        }
357    }
358
359    /**
360     * Stops recognition for the given {@link Keyphrase} if a recognition is
361     * currently active. This is a wrapper for {@link #stopRecognition()}.
362     *
363     * @param keyphraseId The identifier of the keyphrase for which
364     *        the recognition is to be stopped.
365     * @param callback The callback for the recognition events related to the given keyphrase.
366     *
367     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
368     */
369    int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback callback) {
370        synchronized (mLock) {
371            MetricsLogger.count(mContext, "sth_stop_recognition", 1);
372            if (callback == null) {
373                Slog.e(TAG, "Null callback received for stopKeyphraseRecognition() for keyphraseId:" +
374                        keyphraseId);
375                return STATUS_ERROR;
376            }
377
378            ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
379            if (modelData == null || !modelData.isKeyphraseModel()) {
380                Slog.e(TAG, "No model exists for given keyphrase Id.");
381                return STATUS_ERROR;
382            }
383
384            if (DBG) {
385                Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId + ", callback =" +
386                        callback.asBinder());
387                Slog.d(TAG, "current callback=" + (modelData == null ? "null" :
388                            modelData.getCallback().asBinder()));
389            }
390            int status = stopRecognition(modelData, callback);
391            if (status != SoundTrigger.STATUS_OK) {
392                return status;
393            }
394
395            return status;
396        }
397    }
398
399    /**
400     * Stops recognition for the given ModelData instance.
401     *
402     * @param modelData Instance of {@link #ModelData} sound model.
403     * @param callback The callback for the recognition events related to the given keyphrase.
404     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
405     */
406    private int stopRecognition(ModelData modelData, IRecognitionStatusCallback callback) {
407        synchronized (mLock) {
408            if (callback == null) {
409                return STATUS_ERROR;
410            }
411            if (mModuleProperties == null || mModule == null) {
412                Slog.w(TAG, "Attempting stopRecognition without the capability");
413                return STATUS_ERROR;
414            }
415
416            IRecognitionStatusCallback currentCallback = modelData.getCallback();
417            if (modelData == null || currentCallback == null ||
418                    (!modelData.isRequested() && !modelData.isModelStarted())) {
419                // startGenericRecognition hasn't been called or it failed.
420                Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
421                return STATUS_ERROR;
422            }
423
424            if (currentCallback.asBinder() != callback.asBinder()) {
425                // We don't allow a different listener to stop the recognition than the one
426                // that started it.
427                Slog.w(TAG, "Attempting stopRecognition for another recognition");
428                return STATUS_ERROR;
429            }
430
431            // Request stop recognition via the update() method.
432            modelData.setRequested(false);
433            int status = updateRecognitionLocked(modelData, isRecognitionAllowed(),
434                    false /* don't notify for synchronous calls */);
435            if (status != SoundTrigger.STATUS_OK) {
436                return status;
437            }
438
439            // We leave the sound model loaded but not started, this helps us when we start back.
440            // Also clear the internal state once the recognition has been stopped.
441            modelData.setLoaded();
442            modelData.clearCallback();
443            modelData.setRecognitionConfig(null);
444
445            if (!computeRecognitionRunningLocked()) {
446                internalClearGlobalStateLocked();
447            }
448
449            return status;
450        }
451    }
452
453    // Stop a previously started model if it was started. Optionally, unload if the previous model
454    // is stale and is about to be replaced.
455    // Needs to be called with the mLock held.
456    private int tryStopAndUnloadLocked(ModelData modelData, boolean stopModel,
457            boolean unloadModel) {
458        int status = STATUS_OK;
459        if (modelData.isModelNotLoaded()) {
460            return status;
461        }
462        if (stopModel && modelData.isModelStarted()) {
463            status = stopRecognitionLocked(modelData,
464                    false /* don't notify for synchronous calls */);
465            if (status != SoundTrigger.STATUS_OK) {
466                Slog.w(TAG, "stopRecognition failed: " + status);
467                return status;
468            }
469        }
470
471        if (unloadModel && modelData.isModelLoaded()) {
472            Slog.d(TAG, "Unloading previously loaded stale model.");
473            status = mModule.unloadSoundModel(modelData.getHandle());
474            MetricsLogger.count(mContext, "sth_unloading_stale_model", 1);
475            if (status != SoundTrigger.STATUS_OK) {
476                Slog.w(TAG, "unloadSoundModel call failed with " + status);
477            } else {
478                // Clear the ModelData state if successful.
479                modelData.clearState();
480            }
481        }
482        return status;
483    }
484
485    /**
486     * Stops all recognitions active currently and clears the internal state.
487     */
488    void stopAllRecognitions() {
489        synchronized (mLock) {
490            MetricsLogger.count(mContext, "sth_stop_all_recognitions", 1);
491            if (mModuleProperties == null || mModule == null) {
492                return;
493            }
494
495            // Stop all recognition models.
496            for (ModelData model : mModelDataMap.values()) {
497                if (model.isModelStarted()) {
498                    int status = stopRecognitionLocked(model,
499                            false /* do not notify for synchronous calls */);
500                    if (status != STATUS_OK) {
501                        Slog.w(TAG, "Error stopping model: " + model.getHandle());
502                    }
503                    model.setStopped();
504                    model.setRequested(false);
505                    model.clearCallback();
506                    model.setRecognitionConfig(null);
507                }
508            }
509            internalClearGlobalStateLocked();
510        }
511    }
512
513    public ModuleProperties getModuleProperties() {
514        return mModuleProperties;
515    }
516
517    int unloadKeyphraseSoundModel(int keyphraseId) {
518        synchronized (mLock) {
519            MetricsLogger.count(mContext, "sth_unload_keyphrase_sound_model", 1);
520            ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
521            if (mModule == null || modelData == null || modelData.getHandle() == INVALID_VALUE ||
522                    !modelData.isKeyphraseModel()) {
523                return STATUS_ERROR;
524            }
525
526            // Stop recognition if it's the current one.
527            modelData.setRequested(false);
528            int status = updateRecognitionLocked(modelData, isRecognitionAllowed(),
529                    false /* don't notify */);
530            if (status != SoundTrigger.STATUS_OK) {
531                Slog.w(TAG, "Stop recognition failed for keyphrase ID:" + status);
532            }
533
534            status = mModule.unloadSoundModel(modelData.getHandle());
535            if (status != SoundTrigger.STATUS_OK) {
536                Slog.w(TAG, "unloadKeyphraseSoundModel call failed with " + status);
537            }
538
539            // Remove it from existence.
540            removeKeyphraseModelLocked(keyphraseId);
541            return status;
542        }
543    }
544
545    int unloadGenericSoundModel(UUID modelId) {
546        synchronized (mLock) {
547            MetricsLogger.count(mContext, "sth_unload_generic_sound_model", 1);
548            if (modelId == null || mModule == null) {
549                return STATUS_ERROR;
550            }
551            ModelData modelData = mModelDataMap.get(modelId);
552            if (modelData == null || !modelData.isGenericModel()) {
553                Slog.w(TAG, "Unload error: Attempting unload invalid generic model with id:" +
554                        modelId);
555                return STATUS_ERROR;
556            }
557            if (!modelData.isModelLoaded()) {
558                // Nothing to do here.
559                Slog.i(TAG, "Unload: Given generic model is not loaded:" + modelId);
560                return STATUS_OK;
561            }
562            if (modelData.isModelStarted()) {
563                int status = stopRecognitionLocked(modelData,
564                        false /* don't notify for synchronous calls */);
565                if (status != SoundTrigger.STATUS_OK) {
566                    Slog.w(TAG, "stopGenericRecognition failed: " + status);
567                }
568            }
569
570            int status = mModule.unloadSoundModel(modelData.getHandle());
571            if (status != SoundTrigger.STATUS_OK) {
572                Slog.w(TAG, "unloadGenericSoundModel() call failed with " + status);
573                Slog.w(TAG, "unloadGenericSoundModel() force-marking model as unloaded.");
574            }
575
576            // Remove it from existence.
577            mModelDataMap.remove(modelId);
578            if (DBG) dumpModelStateLocked();
579            return status;
580        }
581    }
582
583    //---- SoundTrigger.StatusListener methods
584    @Override
585    public void onRecognition(RecognitionEvent event) {
586        if (event == null) {
587            Slog.w(TAG, "Null recognition event!");
588            return;
589        }
590
591        if (!(event instanceof KeyphraseRecognitionEvent) &&
592                !(event instanceof GenericRecognitionEvent)) {
593            Slog.w(TAG, "Invalid recognition event type (not one of generic or keyphrase)!");
594            return;
595        }
596
597        if (DBG) Slog.d(TAG, "onRecognition: " + event);
598        synchronized (mLock) {
599            switch (event.status) {
600                case SoundTrigger.RECOGNITION_STATUS_ABORT:
601                    onRecognitionAbortLocked(event);
602                    break;
603                case SoundTrigger.RECOGNITION_STATUS_FAILURE:
604                    // Fire failures to all listeners since it's not tied to a keyphrase.
605                    onRecognitionFailureLocked();
606                    break;
607                case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
608                    if (isKeyphraseRecognitionEvent(event)) {
609                        onKeyphraseRecognitionSuccessLocked((KeyphraseRecognitionEvent) event);
610                    } else {
611                        onGenericRecognitionSuccessLocked((GenericRecognitionEvent) event);
612                    }
613                    break;
614            }
615        }
616    }
617
618    private boolean isKeyphraseRecognitionEvent(RecognitionEvent event) {
619        return event instanceof KeyphraseRecognitionEvent;
620    }
621
622    private void onGenericRecognitionSuccessLocked(GenericRecognitionEvent event) {
623        MetricsLogger.count(mContext, "sth_generic_recognition_event", 1);
624        if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS) {
625            return;
626        }
627        ModelData model = getModelDataForLocked(event.soundModelHandle);
628        if (model == null || !model.isGenericModel()) {
629            Slog.w(TAG, "Generic recognition event: Model does not exist for handle: " +
630                    event.soundModelHandle);
631            return;
632        }
633
634        IRecognitionStatusCallback callback = model.getCallback();
635        if (callback == null) {
636            Slog.w(TAG, "Generic recognition event: Null callback for model handle: " +
637                    event.soundModelHandle);
638            return;
639        }
640
641        try {
642            callback.onGenericSoundTriggerDetected((GenericRecognitionEvent) event);
643        } catch (RemoteException e) {
644            Slog.w(TAG, "RemoteException in onGenericSoundTriggerDetected", e);
645        }
646
647        model.setStopped();
648        RecognitionConfig config = model.getRecognitionConfig();
649        if (config == null) {
650            Slog.w(TAG, "Generic recognition event: Null RecognitionConfig for model handle: " +
651                    event.soundModelHandle);
652            return;
653        }
654
655        model.setRequested(config.allowMultipleTriggers);
656        // TODO: Remove this block if the lower layer supports multiple triggers.
657        if (model.isRequested()) {
658            updateRecognitionLocked(model, isRecognitionAllowed() /* isAllowed */,
659                    true /* notify */);
660        }
661    }
662
663    @Override
664    public void onSoundModelUpdate(SoundModelEvent event) {
665        if (event == null) {
666            Slog.w(TAG, "Invalid sound model event!");
667            return;
668        }
669        if (DBG) Slog.d(TAG, "onSoundModelUpdate: " + event);
670        synchronized (mLock) {
671            MetricsLogger.count(mContext, "sth_sound_model_updated", 1);
672            onSoundModelUpdatedLocked(event);
673        }
674    }
675
676    @Override
677    public void onServiceStateChange(int state) {
678        if (DBG) Slog.d(TAG, "onServiceStateChange, state: " + state);
679        synchronized (mLock) {
680            onServiceStateChangedLocked(SoundTrigger.SERVICE_STATE_DISABLED == state);
681        }
682    }
683
684    @Override
685    public void onServiceDied() {
686        Slog.e(TAG, "onServiceDied!!");
687        MetricsLogger.count(mContext, "sth_service_died", 1);
688        synchronized (mLock) {
689            onServiceDiedLocked();
690        }
691    }
692
693    private void onCallStateChangedLocked(boolean callActive) {
694        if (mCallActive == callActive) {
695            // We consider multiple call states as being active
696            // so we check if something really changed or not here.
697            return;
698        }
699        mCallActive = callActive;
700        updateAllRecognitionsLocked(true /* notify */);
701    }
702
703    private void onPowerSaveModeChangedLocked(boolean isPowerSaveMode) {
704        if (mIsPowerSaveMode == isPowerSaveMode) {
705            return;
706        }
707        mIsPowerSaveMode = isPowerSaveMode;
708        updateAllRecognitionsLocked(true /* notify */);
709    }
710
711    private void onSoundModelUpdatedLocked(SoundModelEvent event) {
712        // TODO: Handle sound model update here.
713    }
714
715    private void onServiceStateChangedLocked(boolean disabled) {
716        if (disabled == mServiceDisabled) {
717            return;
718        }
719        mServiceDisabled = disabled;
720        updateAllRecognitionsLocked(true /* notify */);
721    }
722
723    private void onRecognitionAbortLocked(RecognitionEvent event) {
724        Slog.w(TAG, "Recognition aborted");
725        MetricsLogger.count(mContext, "sth_recognition_aborted", 1);
726        ModelData modelData = getModelDataForLocked(event.soundModelHandle);
727        if (modelData != null) {
728            modelData.setStopped();
729        }
730    }
731
732    private void onRecognitionFailureLocked() {
733        Slog.w(TAG, "Recognition failure");
734        MetricsLogger.count(mContext, "sth_recognition_failure_event", 1);
735        try {
736            sendErrorCallbacksToAll(STATUS_ERROR);
737        } catch (RemoteException e) {
738            Slog.w(TAG, "RemoteException in onError", e);
739        } finally {
740            internalClearModelStateLocked();
741            internalClearGlobalStateLocked();
742        }
743    }
744
745    private int getKeyphraseIdFromEvent(KeyphraseRecognitionEvent event) {
746        if (event == null) {
747            Slog.w(TAG, "Null RecognitionEvent received.");
748            return INVALID_VALUE;
749        }
750        KeyphraseRecognitionExtra[] keyphraseExtras =
751                ((KeyphraseRecognitionEvent) event).keyphraseExtras;
752        if (keyphraseExtras == null || keyphraseExtras.length == 0) {
753            Slog.w(TAG, "Invalid keyphrase recognition event!");
754            return INVALID_VALUE;
755        }
756        // TODO: Handle more than one keyphrase extras.
757        return keyphraseExtras[0].id;
758    }
759
760    private void onKeyphraseRecognitionSuccessLocked(KeyphraseRecognitionEvent event) {
761        Slog.i(TAG, "Recognition success");
762        MetricsLogger.count(mContext, "sth_keyphrase_recognition_event", 1);
763        int keyphraseId = getKeyphraseIdFromEvent(event);
764        ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
765
766        if (modelData == null || !modelData.isKeyphraseModel()) {
767            Slog.e(TAG, "Keyphase model data does not exist for ID:" + keyphraseId);
768            return;
769        }
770
771        if (modelData.getCallback() == null) {
772            Slog.w(TAG, "Received onRecognition event without callback for keyphrase model.");
773            return;
774        }
775
776        try {
777            modelData.getCallback().onKeyphraseDetected((KeyphraseRecognitionEvent) event);
778        } catch (RemoteException e) {
779            Slog.w(TAG, "RemoteException in onKeyphraseDetected", e);
780        }
781
782        modelData.setStopped();
783
784        RecognitionConfig config = modelData.getRecognitionConfig();
785        if (config != null) {
786            // Whether we should continue by starting this again.
787            modelData.setRequested(config.allowMultipleTriggers);
788        }
789        // TODO: Remove this block if the lower layer supports multiple triggers.
790        if (modelData.isRequested()) {
791            updateRecognitionLocked(modelData, isRecognitionAllowed(), true /* notify */);
792        }
793    }
794
795    private void updateAllRecognitionsLocked(boolean notify) {
796        boolean isAllowed = isRecognitionAllowed();
797        for (ModelData modelData : mModelDataMap.values()) {
798            updateRecognitionLocked(modelData, isAllowed, notify);
799        }
800    }
801
802    private int updateRecognitionLocked(ModelData model, boolean isAllowed,
803        boolean notify) {
804        boolean start = model.isRequested() && isAllowed;
805        if (start == model.isModelStarted()) {
806            // No-op.
807            return STATUS_OK;
808        }
809        if (start) {
810            return startRecognitionLocked(model, notify);
811        } else {
812            return stopRecognitionLocked(model, notify);
813        }
814    }
815
816    private void onServiceDiedLocked() {
817        try {
818          MetricsLogger.count(mContext, "sth_service_died", 1);
819            sendErrorCallbacksToAll(SoundTrigger.STATUS_DEAD_OBJECT);
820        } catch (RemoteException e) {
821            Slog.w(TAG, "RemoteException in onError", e);
822        } finally {
823            internalClearModelStateLocked();
824            internalClearGlobalStateLocked();
825            if (mModule != null) {
826                mModule.detach();
827                mModule = null;
828            }
829        }
830    }
831
832    // internalClearGlobalStateLocked() cleans up the telephony and power save listeners.
833    private void internalClearGlobalStateLocked() {
834        // Unregister from call state changes.
835        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
836
837        // Unregister from power save mode changes.
838        if (mPowerSaveModeListener != null) {
839            mContext.unregisterReceiver(mPowerSaveModeListener);
840            mPowerSaveModeListener = null;
841        }
842    }
843
844    // Clears state for all models (generic and keyphrase).
845    private void internalClearModelStateLocked() {
846        for (ModelData modelData : mModelDataMap.values()) {
847            modelData.clearState();
848        }
849    }
850
851    class MyCallStateListener extends PhoneStateListener {
852        @Override
853        public void onCallStateChanged(int state, String arg1) {
854            if (DBG) Slog.d(TAG, "onCallStateChanged: " + state);
855            synchronized (mLock) {
856                onCallStateChangedLocked(TelephonyManager.CALL_STATE_IDLE != state);
857            }
858        }
859    }
860
861    class PowerSaveModeListener extends BroadcastReceiver {
862        @Override
863        public void onReceive(Context context, Intent intent) {
864            if (!PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) {
865                return;
866            }
867            boolean active = mPowerManager.isPowerSaveMode();
868            if (DBG) Slog.d(TAG, "onPowerSaveModeChanged: " + active);
869            synchronized (mLock) {
870                onPowerSaveModeChangedLocked(active);
871            }
872        }
873    }
874
875    void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
876        synchronized (mLock) {
877            pw.print("  module properties=");
878            pw.println(mModuleProperties == null ? "null" : mModuleProperties);
879
880            pw.print("  call active="); pw.println(mCallActive);
881            pw.print("  power save mode active="); pw.println(mIsPowerSaveMode);
882            pw.print("  service disabled="); pw.println(mServiceDisabled);
883        }
884    }
885
886    private void initializeTelephonyAndPowerStateListeners() {
887        // Get the current call state synchronously for the first recognition.
888        mCallActive = mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
889
890        // Register for call state changes when the first call to start recognition occurs.
891        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
892
893        // Register for power saver mode changes when the first call to start recognition
894        // occurs.
895        if (mPowerSaveModeListener == null) {
896            mPowerSaveModeListener = new PowerSaveModeListener();
897            mContext.registerReceiver(mPowerSaveModeListener,
898                    new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
899        }
900        mIsPowerSaveMode = mPowerManager.isPowerSaveMode();
901    }
902
903    // Sends an error callback to all models with a valid registered callback.
904    private void sendErrorCallbacksToAll(int errorCode) throws RemoteException {
905        for (ModelData modelData : mModelDataMap.values()) {
906            IRecognitionStatusCallback callback = modelData.getCallback();
907            if (callback != null) {
908                callback.onError(STATUS_ERROR);
909            }
910        }
911    }
912
913    private ModelData getOrCreateGenericModelDataLocked(UUID modelId) {
914        ModelData modelData = mModelDataMap.get(modelId);
915        if (modelData == null) {
916            modelData = ModelData.createGenericModelData(modelId);
917            mModelDataMap.put(modelId, modelData);
918        } else if (!modelData.isGenericModel()) {
919            Slog.e(TAG, "UUID already used for non-generic model.");
920            return null;
921        }
922        return modelData;
923    }
924
925    private void removeKeyphraseModelLocked(int keyphraseId) {
926        UUID uuid = mKeyphraseUuidMap.get(keyphraseId);
927        if (uuid == null) {
928            return;
929        }
930        mModelDataMap.remove(uuid);
931        mKeyphraseUuidMap.remove(keyphraseId);
932    }
933
934    private ModelData getKeyphraseModelDataLocked(int keyphraseId) {
935        UUID uuid = mKeyphraseUuidMap.get(keyphraseId);
936        if (uuid == null) {
937            return null;
938        }
939        return mModelDataMap.get(uuid);
940    }
941
942    // Use this to create a new ModelData entry for a keyphrase Id. It will overwrite existing
943    // mapping if one exists.
944    private ModelData createKeyphraseModelDataLocked(UUID modelId, int keyphraseId) {
945        mKeyphraseUuidMap.remove(keyphraseId);
946        mModelDataMap.remove(modelId);
947        mKeyphraseUuidMap.put(keyphraseId, modelId);
948        ModelData modelData = ModelData.createKeyphraseModelData(modelId);
949        mModelDataMap.put(modelId, modelData);
950        return modelData;
951    }
952
953    // Instead of maintaining a second hashmap of modelHandle -> ModelData, we just
954    // iterate through to find the right object (since we don't expect 100s of models
955    // to be stored).
956    private ModelData getModelDataForLocked(int modelHandle) {
957        // Fetch ModelData object corresponding to the model handle.
958        for (ModelData model : mModelDataMap.values()) {
959            if (model.getHandle() == modelHandle) {
960                return model;
961            }
962        }
963        return null;
964    }
965
966    // Whether we are allowed to run any recognition at all. The conditions that let us run
967    // a recognition include: no active phone call or not being in a power save mode. Also,
968    // the native service should be enabled.
969    private boolean isRecognitionAllowed() {
970        return !mCallActive && !mServiceDisabled && !mIsPowerSaveMode;
971    }
972
973    // A single routine that implements the start recognition logic for both generic and keyphrase
974    // models.
975    private int startRecognitionLocked(ModelData modelData, boolean notify) {
976        IRecognitionStatusCallback callback = modelData.getCallback();
977        int handle = modelData.getHandle();
978        RecognitionConfig config = modelData.getRecognitionConfig();
979        if (callback == null || handle == INVALID_VALUE || config == null) {
980            // Nothing to do here.
981            Slog.w(TAG, "startRecognition: Bad data passed in.");
982            MetricsLogger.count(mContext, "sth_start_recognition_error", 1);
983            return STATUS_ERROR;
984        }
985
986        if (!isRecognitionAllowed()) {
987            // Nothing to do here.
988            Slog.w(TAG, "startRecognition requested but not allowed.");
989            MetricsLogger.count(mContext, "sth_start_recognition_not_allowed", 1);
990            return STATUS_OK;
991        }
992
993        int status = mModule.startRecognition(handle, config);
994        if (status != SoundTrigger.STATUS_OK) {
995            Slog.w(TAG, "startRecognition failed with " + status);
996            MetricsLogger.count(mContext, "sth_start_recognition_error", 1);
997            // Notify of error if needed.
998            if (notify) {
999                try {
1000                    callback.onError(status);
1001                } catch (RemoteException e) {
1002                    Slog.w(TAG, "RemoteException in onError", e);
1003                }
1004            }
1005        } else {
1006            Slog.i(TAG, "startRecognition successful.");
1007            MetricsLogger.count(mContext, "sth_start_recognition_success", 1);
1008            modelData.setStarted();
1009            // Notify of resume if needed.
1010            if (notify) {
1011                try {
1012                    callback.onRecognitionResumed();
1013                } catch (RemoteException e) {
1014                    Slog.w(TAG, "RemoteException in onRecognitionResumed", e);
1015                }
1016            }
1017        }
1018        if (DBG) {
1019            Slog.d(TAG, "Model being started :" + modelData.toString());
1020        }
1021        return status;
1022    }
1023
1024    private int stopRecognitionLocked(ModelData modelData, boolean notify) {
1025        IRecognitionStatusCallback callback = modelData.getCallback();
1026
1027        // Stop recognition.
1028        int status = STATUS_OK;
1029
1030        status = mModule.stopRecognition(modelData.getHandle());
1031
1032        if (status != SoundTrigger.STATUS_OK) {
1033            Slog.w(TAG, "stopRecognition call failed with " + status);
1034            MetricsLogger.count(mContext, "sth_stop_recognition_error", 1);
1035            if (notify) {
1036                try {
1037                    callback.onError(status);
1038                } catch (RemoteException e) {
1039                    Slog.w(TAG, "RemoteException in onError", e);
1040                }
1041            }
1042        } else {
1043            modelData.setStopped();
1044            MetricsLogger.count(mContext, "sth_stop_recognition_success", 1);
1045            // Notify of pause if needed.
1046            if (notify) {
1047                try {
1048                    callback.onRecognitionPaused();
1049                } catch (RemoteException e) {
1050                    Slog.w(TAG, "RemoteException in onRecognitionPaused", e);
1051                }
1052            }
1053        }
1054        if (DBG) {
1055            Slog.d(TAG, "Model being stopped :" + modelData.toString());
1056        }
1057        return status;
1058    }
1059
1060    private void dumpModelStateLocked() {
1061        for (UUID modelId : mModelDataMap.keySet()) {
1062            ModelData modelData = mModelDataMap.get(modelId);
1063            Slog.i(TAG, "Model :" + modelData.toString());
1064        }
1065    }
1066
1067    // Computes whether we have any recognition running at all (voice or generic). Sets
1068    // the mRecognitionRunning variable with the result.
1069    private boolean computeRecognitionRunningLocked() {
1070        if (mModuleProperties == null || mModule == null) {
1071            mRecognitionRunning = false;
1072            return mRecognitionRunning;
1073        }
1074        for (ModelData modelData : mModelDataMap.values()) {
1075            if (modelData.isModelStarted()) {
1076                mRecognitionRunning = true;
1077                return mRecognitionRunning;
1078            }
1079        }
1080        mRecognitionRunning = false;
1081        return mRecognitionRunning;
1082    }
1083
1084    // This class encapsulates the callbacks, state, handles and any other information that
1085    // represents a model.
1086    private static class ModelData {
1087        // Model not loaded (and hence not started).
1088        static final int MODEL_NOTLOADED = 0;
1089
1090        // Loaded implies model was successfully loaded. Model not started yet.
1091        static final int MODEL_LOADED = 1;
1092
1093        // Started implies model was successfully loaded and start was called.
1094        static final int MODEL_STARTED = 2;
1095
1096        // One of MODEL_NOTLOADED, MODEL_LOADED, MODEL_STARTED (which implies loaded).
1097        private int mModelState;
1098        private UUID mModelId;
1099
1100        // mRequested captures the explicit intent that a start was requested for this model. We
1101        // continue to capture and retain this state even after the model gets started, so that we
1102        // know when a model gets stopped due to "other" reasons, that we should start it again.
1103        // This was the intended behavior of the "mRequested" variable in the previous version of
1104        // this code that we are replicating here.
1105        //
1106        // The "other" reasons include power save, abort being called from the lower layer (due
1107        // to concurrent capture not being supported) and phone call state. Once we recover from
1108        // these transient disruptions, we would start such models again where mRequested == true.
1109        // Thus, mRequested gets reset only when there is an explicit intent to stop the model
1110        // coming from the SoundTriggerService layer that uses this class (and thus eventually
1111        // from the app that manages this model).
1112        private boolean mRequested = false;
1113
1114        // One of SoundModel.TYPE_GENERIC or SoundModel.TYPE_KEYPHRASE. Initially set
1115        // to SoundModel.TYPE_UNKNOWN;
1116        private int mModelType = SoundModel.TYPE_UNKNOWN;
1117
1118        private IRecognitionStatusCallback mCallback = null;
1119        private RecognitionConfig mRecognitionConfig = null;
1120
1121        // Model handle is an integer used by the HAL as an identifier for sound
1122        // models.
1123        private int mModelHandle = INVALID_VALUE;
1124
1125        // The SoundModel instance, one of KeyphraseSoundModel or GenericSoundModel.
1126        private SoundModel mSoundModel = null;
1127
1128        private ModelData(UUID modelId, int modelType) {
1129            mModelId = modelId;
1130            // Private constructor, since we require modelType to be one of TYPE_GENERIC,
1131            // TYPE_KEYPHRASE or TYPE_UNKNOWN.
1132            mModelType = modelType;
1133        }
1134
1135        static ModelData createKeyphraseModelData(UUID modelId) {
1136            return new ModelData(modelId, SoundModel.TYPE_KEYPHRASE);
1137        }
1138
1139        static ModelData createGenericModelData(UUID modelId) {
1140            return new ModelData(modelId, SoundModel.TYPE_GENERIC_SOUND);
1141        }
1142
1143        // Note that most of the functionality in this Java class will not work for
1144        // SoundModel.TYPE_UNKNOWN nevertheless we have it since lower layers support it.
1145        static ModelData createModelDataOfUnknownType(UUID modelId) {
1146            return new ModelData(modelId, SoundModel.TYPE_UNKNOWN);
1147        }
1148
1149        synchronized void setCallback(IRecognitionStatusCallback callback) {
1150            mCallback = callback;
1151        }
1152
1153        synchronized IRecognitionStatusCallback getCallback() {
1154            return mCallback;
1155        }
1156
1157        synchronized boolean isModelLoaded() {
1158            return (mModelState == MODEL_LOADED || mModelState == MODEL_STARTED);
1159        }
1160
1161        synchronized boolean isModelNotLoaded() {
1162            return mModelState == MODEL_NOTLOADED;
1163        }
1164
1165        synchronized void setStarted() {
1166            mModelState = MODEL_STARTED;
1167        }
1168
1169        synchronized void setStopped() {
1170            mModelState = MODEL_LOADED;
1171        }
1172
1173        synchronized void setLoaded() {
1174            mModelState = MODEL_LOADED;
1175        }
1176
1177        synchronized boolean isModelStarted() {
1178            return mModelState == MODEL_STARTED;
1179        }
1180
1181        synchronized void clearState() {
1182            mModelState = MODEL_NOTLOADED;
1183            mModelHandle = INVALID_VALUE;
1184            mRecognitionConfig = null;
1185            mRequested = false;
1186            mCallback = null;
1187        }
1188
1189        synchronized void clearCallback() {
1190            mCallback = null;
1191        }
1192
1193        synchronized void setHandle(int handle) {
1194            mModelHandle = handle;
1195        }
1196
1197        synchronized void setRecognitionConfig(RecognitionConfig config) {
1198            mRecognitionConfig = config;
1199        }
1200
1201        synchronized int getHandle() {
1202            return mModelHandle;
1203        }
1204
1205        synchronized UUID getModelId() {
1206            return mModelId;
1207        }
1208
1209        synchronized RecognitionConfig getRecognitionConfig() {
1210            return mRecognitionConfig;
1211        }
1212
1213        // Whether a start recognition was requested.
1214        synchronized boolean isRequested() {
1215            return mRequested;
1216        }
1217
1218        synchronized void setRequested(boolean requested) {
1219            mRequested = requested;
1220        }
1221
1222        synchronized void setSoundModel(SoundModel soundModel) {
1223            mSoundModel = soundModel;
1224        }
1225
1226        synchronized SoundModel getSoundModel() {
1227            return mSoundModel;
1228        }
1229
1230        synchronized int getModelType() {
1231            return mModelType;
1232        }
1233
1234        synchronized boolean isKeyphraseModel() {
1235            return mModelType == SoundModel.TYPE_KEYPHRASE;
1236        }
1237
1238        synchronized boolean isGenericModel() {
1239            return mModelType == SoundModel.TYPE_GENERIC_SOUND;
1240        }
1241
1242        synchronized String stateToString() {
1243            switch(mModelState) {
1244                case MODEL_NOTLOADED: return "NOT_LOADED";
1245                case MODEL_LOADED: return "LOADED";
1246                case MODEL_STARTED: return "STARTED";
1247            }
1248            return "Unknown state";
1249        }
1250
1251        synchronized String requestedToString() {
1252            return "Requested: " + (mRequested ? "Yes" : "No");
1253        }
1254
1255        synchronized String callbackToString() {
1256            return "Callback: " + (mCallback != null ? mCallback.asBinder() : "null");
1257        }
1258
1259        synchronized String uuidToString() {
1260            return "UUID: " + mModelId;
1261        }
1262
1263        synchronized public String toString() {
1264            return "Handle: " + mModelHandle + "\n" +
1265                    "ModelState: " + stateToString() + "\n" +
1266                    requestedToString() + "\n" +
1267                    callbackToString() + "\n" +
1268                    uuidToString() + "\n" + modelTypeToString();
1269        }
1270
1271        synchronized String modelTypeToString() {
1272            String type = null;
1273            switch (mModelType) {
1274                case SoundModel.TYPE_GENERIC_SOUND: type = "Generic"; break;
1275                case SoundModel.TYPE_UNKNOWN: type = "Unknown"; break;
1276                case SoundModel.TYPE_KEYPHRASE: type = "Keyphrase"; break;
1277            }
1278            return "Model type: " + type + "\n";
1279        }
1280    }
1281}
1282