SoundTriggerHelper.java revision 2c0273e50a3162595e9a54030166f2369b039a5a
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    //---- SoundTrigger.StatusListener methods
244    @Override
245    public void onRecognition(RecognitionEvent event) {
246        if (event == null) {
247            Slog.w(TAG, "Invalid recognition event!");
248            return;
249        }
250
251        if (DBG) Slog.d(TAG, "onRecognition: " + event);
252        switch (event.status) {
253            // Fire aborts/failures to all listeners since it's not tied to a keyphrase.
254            case SoundTrigger.RECOGNITION_STATUS_ABORT: // fall-through
255            case SoundTrigger.RECOGNITION_STATUS_FAILURE:
256                try {
257                    synchronized (this) {
258                        for (int i = 0; i < mActiveListeners.size(); i++) {
259                            mActiveListeners.valueAt(i).onError(STATUS_ERROR);
260                        }
261                    }
262                } catch (RemoteException e) {
263                    Slog.w(TAG, "RemoteException in onDetectionStopped");
264                }
265                break;
266            case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
267                if (!(event instanceof KeyphraseRecognitionEvent)) {
268                    Slog.w(TAG, "Invalid recognition event!");
269                    return;
270                }
271
272                KeyphraseRecognitionExtra[] keyphraseExtras =
273                        ((KeyphraseRecognitionEvent) event).keyphraseExtras;
274                if (keyphraseExtras == null || keyphraseExtras.length == 0) {
275                    Slog.w(TAG, "Invalid keyphrase recognition event!");
276                    return;
277                }
278                // TODO: Handle more than one keyphrase extras.
279                int keyphraseId = keyphraseExtras[0].id;
280                try {
281                    synchronized(this) {
282                        // Check which keyphrase triggered, and fire the appropriate event.
283                        IRecognitionStatusCallback listener = mActiveListeners.get(keyphraseId);
284                        if (listener != null) {
285                            listener.onDetected((KeyphraseRecognitionEvent) event);
286                        } else {
287                            Slog.w(TAG, "received onRecognition event without any listener for it");
288                            return;
289                        }
290
291                        // FIXME: Remove this block if the lower layer supports multiple triggers.
292                        if (mRecognitionConfig != null
293                                && mRecognitionConfig.allowMultipleTriggers) {
294                            int status = mModule.startRecognition(
295                                    mCurrentSoundModelHandle, mRecognitionConfig);
296                            if (status != STATUS_OK) {
297                                Slog.w(TAG, "Error in restarting recognition after a trigger");
298                                listener.onError(status);
299                            }
300                        }
301                    }
302                } catch (RemoteException e) {
303                    Slog.w(TAG, "RemoteException in onDetectionStopped");
304                }
305                break;
306        }
307    }
308
309    @Override
310    public void onServiceDied() {
311        synchronized (this) {
312            try {
313                for (int i = 0; i < mActiveListeners.size(); i++) {
314                    mActiveListeners.valueAt(i).onError(SoundTrigger.STATUS_DEAD_OBJECT);
315                }
316            } catch (RemoteException e) {
317                Slog.w(TAG, "RemoteException in onDetectionStopped");
318            }
319            mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
320            mCurrentSoundModelUuid = null;
321            // Remove all listeners.
322            mActiveListeners.clear();
323        }
324    }
325}
326