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