SoundTriggerHelper.java revision d3b8223377b8046280e4c09e728edc600171f941
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.voiceinteraction;
18
19import android.hardware.soundtrigger.IRecognitionStatusCallback;
20import android.hardware.soundtrigger.SoundTrigger;
21import android.hardware.soundtrigger.SoundTrigger.Keyphrase;
22import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
23import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
24import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
25import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
26import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
27import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
28import android.hardware.soundtrigger.SoundTrigger.SoundModelEvent;
29import android.hardware.soundtrigger.SoundTriggerModule;
30import android.os.RemoteException;
31import android.util.Slog;
32import android.util.SparseArray;
33
34import java.util.ArrayList;
35import java.util.UUID;
36
37/**
38 * Helper for {@link SoundTrigger} APIs.
39 * Currently this just acts as an abstraction over all SoundTrigger API calls.
40 *
41 * @hide
42 */
43public class SoundTriggerHelper implements SoundTrigger.StatusListener {
44    static final String TAG = "SoundTriggerHelper";
45    // TODO: Set to false.
46    static final boolean DBG = true;
47
48    /**
49     * Return codes for {@link #startRecognition(int, KeyphraseSoundModel,
50     *      IRecognitionStatusCallback, RecognitionConfig)},
51     * {@link #stopRecognition(int, IRecognitionStatusCallback)}
52     */
53    public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
54    public static final int STATUS_OK = SoundTrigger.STATUS_OK;
55
56    private static final int INVALID_SOUND_MODEL_HANDLE = -1;
57
58    /** The {@link DspInfo} for the system, or null if none exists. */
59    final ModuleProperties moduleProperties;
60
61    /** The properties for the DSP module */
62    private final SoundTriggerModule mModule;
63
64    // Use a RemoteCallbackList here?
65    private final SparseArray<IRecognitionStatusCallback> mActiveListeners;
66
67    private int mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
68    private UUID mCurrentSoundModelUuid = null;
69    // FIXME: Ideally this should not be stored if allowMultipleTriggers happens at a lower layer.
70    private RecognitionConfig mRecognitionConfig = null;
71
72    SoundTriggerHelper() {
73        ArrayList <ModuleProperties> modules = new ArrayList<>();
74        int status = SoundTrigger.listModules(modules);
75        mActiveListeners = new SparseArray<>(1);
76        if (status != SoundTrigger.STATUS_OK || modules.size() == 0) {
77            Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size());
78            moduleProperties = null;
79            mModule = null;
80        } else {
81            // TODO: Figure out how to determine which module corresponds to the DSP hardware.
82            moduleProperties = modules.get(0);
83            mModule = SoundTrigger.attachModule(moduleProperties.id, this, null);
84        }
85    }
86
87    /**
88     * @return True, if a recognition for the given {@link Keyphrase} is active.
89     */
90    synchronized boolean isKeyphraseActive(Keyphrase keyphrase) {
91        if (keyphrase == null) {
92            Slog.w(TAG, "isKeyphraseActive requires a non-null keyphrase");
93            return false;
94        }
95        return mActiveListeners.get(keyphrase.id) != null;
96    }
97
98    /**
99     * Starts recognition for the given keyphraseId.
100     *
101     * @param keyphraseId The identifier of the keyphrase for which
102     *        the recognition is to be started.
103     * @param soundModel The sound model to use for recognition.
104     * @param listener The listener for the recognition events related to the given keyphrase.
105     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
106     */
107    synchronized int startRecognition(int keyphraseId,
108            KeyphraseSoundModel soundModel,
109            IRecognitionStatusCallback listener,
110            RecognitionConfig recognitionConfig) {
111        if (DBG) {
112            Slog.d(TAG, "startRecognition for keyphraseId=" + keyphraseId
113                    + " soundModel=" + soundModel + ", listener=" + listener
114                    + ", recognitionConfig=" + recognitionConfig);
115            Slog.d(TAG, "moduleProperties=" + moduleProperties);
116            Slog.d(TAG, "# of current listeners=" + mActiveListeners.size());
117            Slog.d(TAG, "current SoundModel handle=" + mCurrentSoundModelHandle);
118            Slog.d(TAG, "current SoundModel UUID="
119                    + (mCurrentSoundModelUuid == null ? null : mCurrentSoundModelUuid));
120        }
121        if (moduleProperties == null || mModule == null) {
122            Slog.w(TAG, "Attempting startRecognition without the capability");
123            return STATUS_ERROR;
124        }
125
126        if (mCurrentSoundModelHandle != INVALID_SOUND_MODEL_HANDLE
127                && !soundModel.uuid.equals(mCurrentSoundModelUuid)) {
128            Slog.w(TAG, "Unloading previous sound model");
129            int status = mModule.unloadSoundModel(mCurrentSoundModelHandle);
130            if (status != SoundTrigger.STATUS_OK) {
131                Slog.w(TAG, "unloadSoundModel call failed with " + status);
132                return status;
133            }
134            mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
135            mCurrentSoundModelUuid = null;
136        }
137
138        // If the previous recognition was by a different listener,
139        // Notify them that it was stopped.
140        IRecognitionStatusCallback oldListener = mActiveListeners.get(keyphraseId);
141        if (oldListener != null && oldListener.asBinder() != listener.asBinder()) {
142            Slog.w(TAG, "Canceling previous recognition");
143            try {
144                oldListener.onError(STATUS_ERROR);
145            } catch (RemoteException e) {
146                Slog.w(TAG, "RemoteException in onDetectionStopped");
147            }
148            mActiveListeners.remove(keyphraseId);
149        }
150
151        // Load the sound model if the current one is null.
152        int soundModelHandle = mCurrentSoundModelHandle;
153        if (mCurrentSoundModelHandle == INVALID_SOUND_MODEL_HANDLE
154                || mCurrentSoundModelUuid == null) {
155            int[] handle = new int[] { INVALID_SOUND_MODEL_HANDLE };
156            int status = mModule.loadSoundModel(soundModel, handle);
157            if (status != SoundTrigger.STATUS_OK) {
158                Slog.w(TAG, "loadSoundModel call failed with " + status);
159                return status;
160            }
161            if (handle[0] == INVALID_SOUND_MODEL_HANDLE) {
162                Slog.w(TAG, "loadSoundModel call returned invalid sound model handle");
163                return STATUS_ERROR;
164            }
165            soundModelHandle = handle[0];
166        } else {
167            if (DBG) Slog.d(TAG, "Reusing previously loaded sound model");
168        }
169
170        // Start the recognition.
171        int status = mModule.startRecognition(soundModelHandle, recognitionConfig);
172        if (status != SoundTrigger.STATUS_OK) {
173            Slog.w(TAG, "startRecognition failed with " + status);
174            return status;
175        }
176
177        // Everything went well!
178        mCurrentSoundModelHandle = soundModelHandle;
179        mCurrentSoundModelUuid = soundModel.uuid;
180        mRecognitionConfig = recognitionConfig;
181        // Register the new listener. This replaces the old one.
182        // There can only be a maximum of one active listener for a keyphrase
183        // at any given time.
184        mActiveListeners.put(keyphraseId, listener);
185        return STATUS_OK;
186    }
187
188    /**
189     * Stops recognition for the given {@link Keyphrase} if a recognition is
190     * currently active.
191     *
192     * @param keyphraseId The identifier of the keyphrase for which
193     *        the recognition is to be stopped.
194     * @param listener The listener for the recognition events related to the given keyphrase.
195     *
196     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
197     */
198    synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
199        if (DBG) {
200            Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId
201                    + ", listener=" + listener);
202            Slog.d(TAG, "# of current listeners = " + mActiveListeners.size());
203        }
204
205        if (moduleProperties == null || mModule == null) {
206            Slog.w(TAG, "Attempting stopRecognition without the capability");
207            return STATUS_ERROR;
208        }
209
210        IRecognitionStatusCallback currentListener = mActiveListeners.get(keyphraseId);
211        if (listener == null) {
212            Slog.w(TAG, "Attempting stopRecognition without a valid listener");
213            return STATUS_ERROR;
214        } if (currentListener == null) {
215            // startRecognition hasn't been called or it failed.
216            Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
217            return STATUS_ERROR;
218        } else if (currentListener.asBinder() != listener.asBinder()) {
219            // We don't allow a different listener to stop the recognition than the one
220            // that started it.
221            Slog.w(TAG, "Attempting stopRecognition for another recognition");
222            return STATUS_ERROR;
223        } else {
224            // Stop recognition if it's the current one, ignore otherwise.
225            int status = mModule.stopRecognition(mCurrentSoundModelHandle);
226            if (status != SoundTrigger.STATUS_OK) {
227                Slog.w(TAG, "stopRecognition call failed with " + status);
228                return status;
229            }
230            status = mModule.unloadSoundModel(mCurrentSoundModelHandle);
231            if (status != SoundTrigger.STATUS_OK) {
232                Slog.w(TAG, "unloadSoundModel call failed with " + status);
233                return status;
234            }
235
236            mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
237            mCurrentSoundModelUuid = null;
238
239            mActiveListeners.remove(keyphraseId);
240            return STATUS_OK;
241        }
242    }
243
244    synchronized void stopAllRecognitions() {
245        if (moduleProperties == null || mModule == null) {
246            return;
247        }
248
249        if (mCurrentSoundModelHandle == INVALID_SOUND_MODEL_HANDLE) {
250            return;
251        }
252
253        int status = mModule.stopRecognition(mCurrentSoundModelHandle);
254        if (status != SoundTrigger.STATUS_OK) {
255            Slog.w(TAG, "stopRecognition call failed with " + status);
256        }
257        status = mModule.unloadSoundModel(mCurrentSoundModelHandle);
258        if (status != SoundTrigger.STATUS_OK) {
259            Slog.w(TAG, "unloadSoundModel call failed with " + status);
260        }
261
262        mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
263        mCurrentSoundModelUuid = null;
264
265        mActiveListeners.clear();
266    }
267
268    //---- SoundTrigger.StatusListener methods
269    @Override
270    public void onRecognition(RecognitionEvent event) {
271        if (event == null) {
272            Slog.w(TAG, "Invalid recognition event!");
273            return;
274        }
275
276        if (DBG) Slog.d(TAG, "onRecognition: " + event);
277        switch (event.status) {
278            // Fire aborts/failures to all listeners since it's not tied to a keyphrase.
279            case SoundTrigger.RECOGNITION_STATUS_ABORT: // fall-through
280            case SoundTrigger.RECOGNITION_STATUS_FAILURE:
281                try {
282                    synchronized (this) {
283                        for (int i = 0; i < mActiveListeners.size(); i++) {
284                            mActiveListeners.valueAt(i).onError(STATUS_ERROR);
285                        }
286                    }
287                } catch (RemoteException e) {
288                    Slog.w(TAG, "RemoteException in onDetectionStopped");
289                }
290                break;
291            case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
292                if (!(event instanceof KeyphraseRecognitionEvent)) {
293                    Slog.w(TAG, "Invalid recognition event!");
294                    return;
295                }
296
297                KeyphraseRecognitionExtra[] keyphraseExtras =
298                        ((KeyphraseRecognitionEvent) event).keyphraseExtras;
299                if (keyphraseExtras == null || keyphraseExtras.length == 0) {
300                    Slog.w(TAG, "Invalid keyphrase recognition event!");
301                    return;
302                }
303                // TODO: Handle more than one keyphrase extras.
304                int keyphraseId = keyphraseExtras[0].id;
305                try {
306                    synchronized(this) {
307                        // Check which keyphrase triggered, and fire the appropriate event.
308                        IRecognitionStatusCallback listener = mActiveListeners.get(keyphraseId);
309                        if (listener != null) {
310                            listener.onDetected((KeyphraseRecognitionEvent) event);
311                        } else {
312                            Slog.w(TAG, "received onRecognition event without any listener for it");
313                            return;
314                        }
315
316                        // FIXME: Remove this block if the lower layer supports multiple triggers.
317                        if (mRecognitionConfig != null
318                                && mRecognitionConfig.allowMultipleTriggers) {
319                            int status = mModule.startRecognition(
320                                    mCurrentSoundModelHandle, mRecognitionConfig);
321                            if (status != STATUS_OK) {
322                                Slog.w(TAG, "Error in restarting recognition after a trigger");
323                                listener.onError(status);
324                            }
325                        }
326                    }
327                } catch (RemoteException e) {
328                    Slog.w(TAG, "RemoteException in onDetectionStopped");
329                }
330                break;
331        }
332    }
333
334    public void onSoundModelUpdate(SoundModelEvent event) {
335        if (event == null) {
336            Slog.w(TAG, "Invalid sound model event!");
337            return;
338        }
339
340        if (DBG) Slog.d(TAG, "onSoundModelUpdate: " + event);
341
342        //TODO: implement sound model update
343    }
344
345    public void onServiceStateChange(int state) {
346        if (DBG) Slog.d(TAG, "onServiceStateChange, state: " + state);
347
348        //TODO: implement service state update
349    }
350
351    @Override
352    public void onServiceDied() {
353        synchronized (this) {
354            try {
355                for (int i = 0; i < mActiveListeners.size(); i++) {
356                    mActiveListeners.valueAt(i).onError(SoundTrigger.STATUS_DEAD_OBJECT);
357                }
358            } catch (RemoteException e) {
359                Slog.w(TAG, "RemoteException in onDetectionStopped");
360            }
361            mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
362            mCurrentSoundModelUuid = null;
363            // Remove all listeners.
364            mActiveListeners.clear();
365        }
366    }
367}
368