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