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