SoundTriggerHelper.java revision 452a642430e3f8abfa053e48893dd0edfb12799b
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.KeyphraseSoundModel;
23import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
24import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
25import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent;
26import android.hardware.soundtrigger.SoundTriggerModule;
27import android.os.RemoteException;
28import android.util.Slog;
29import android.util.SparseArray;
30
31import java.util.ArrayList;
32
33/**
34 * Helper for {@link SoundTrigger} APIs.
35 * Currently this just acts as an abstraction over all SoundTrigger API calls.
36 *
37 * @hide
38 */
39public class SoundTriggerHelper implements SoundTrigger.StatusListener {
40    static final String TAG = "SoundTriggerHelper";
41    // TODO: Set to false.
42    static final boolean DBG = true;
43    // TODO: Remove this.
44    static final int TEMP_KEYPHRASE_ID = 100;
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, "Canceling previous recognition");
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            try {
131                oldListener.onDetectionStopped();
132            } catch (RemoteException e) {
133                Slog.w(TAG, "RemoteException in onDetectionStopped");
134            }
135            mActiveListeners.remove(keyphraseId);
136        }
137
138        int[] handle = new int[] { INVALID_SOUND_MODEL_HANDLE };
139        int status = mModule.loadSoundModel(soundModel, handle);
140        if (status != SoundTrigger.STATUS_OK) {
141            Slog.w(TAG, "loadSoundModel call failed with " + status);
142            return STATUS_ERROR;
143        }
144        if (handle[0] == INVALID_SOUND_MODEL_HANDLE) {
145            Slog.w(TAG, "loadSoundModel call returned invalid sound model handle");
146            return STATUS_ERROR;
147        }
148
149        // Start the recognition.
150        status = mModule.startRecognition(handle[0], recognitionConfig);
151        if (status != SoundTrigger.STATUS_OK) {
152            Slog.w(TAG, "startRecognition failed with " + status);
153            return STATUS_ERROR;
154        }
155
156        // Everything went well!
157        mCurrentSoundModelHandle = handle[0];
158        // Register the new listener. This replaces the old one.
159        // There can only be a maximum of one active listener for a keyphrase
160        // at any given time.
161        mActiveListeners.put(keyphraseId, listener);
162        return STATUS_OK;
163    }
164
165    /**
166     * Stops recognition for the given {@link Keyphrase} if a recognition is
167     * currently active.
168     *
169     * @param keyphraseId The identifier of the keyphrase for which
170     *        the recognition is to be stopped.
171     * @param listener The listener for the recognition events related to the given keyphrase.
172     *
173     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
174     */
175    synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
176        if (DBG) {
177            Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId
178                    + ", listener=" + listener);
179            Slog.d(TAG, "# of current listeners = " + mActiveListeners.size());
180        }
181
182        if (moduleProperties == null || mModule == null) {
183            Slog.w(TAG, "Attempting stopRecognition without the capability");
184            return STATUS_ERROR;
185        }
186
187        IRecognitionStatusCallback currentListener = mActiveListeners.get(keyphraseId);
188        if (listener == null) {
189            Slog.w(TAG, "Attempting stopRecognition without a valid listener");
190            return STATUS_ERROR;
191        } if (currentListener == null) {
192            // startRecognition hasn't been called or it failed.
193            Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition");
194            return STATUS_ERROR;
195        } else if (currentListener.asBinder() != listener.asBinder()) {
196            // TODO: Figure out if this should match the listener that was passed in during
197            // startRecognition, or should we allow a different listener to stop the recognition,
198            // in which case we don't need to pass in a listener here.
199            Slog.w(TAG, "Attempting stopRecognition for another recognition");
200            return STATUS_ERROR;
201        } else {
202            // Stop recognition if it's the current one, ignore otherwise.
203            // TODO: Inspect the return codes here.
204            int status = mModule.stopRecognition(mCurrentSoundModelHandle);
205            if (status != SoundTrigger.STATUS_OK) {
206                Slog.w(TAG, "stopRecognition call failed with " + status);
207                return STATUS_ERROR;
208            }
209            status = mModule.unloadSoundModel(mCurrentSoundModelHandle);
210            if (status != SoundTrigger.STATUS_OK) {
211                Slog.w(TAG, "unloadSoundModel call failed with " + status);
212                return STATUS_ERROR;
213            }
214
215            mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE;
216            mActiveListeners.remove(keyphraseId);
217            return STATUS_OK;
218        }
219    }
220
221    //---- SoundTrigger.StatusListener methods
222    @Override
223    public void onRecognition(RecognitionEvent event) {
224        // Check which keyphrase triggered, and fire the appropriate event.
225        // TODO: Get the keyphrase out of the event and fire events on it.
226        // For now, as a nasty workaround, we fire all events to the listener for
227        // keyphrase with TEMP_KEYPHRASE_ID.
228        IRecognitionStatusCallback listener = null;
229        synchronized(this) {
230            // TODO: The keyphrase should come from the recognition event
231            // as it may be for a different keyphrase than the current one.
232            listener = mActiveListeners.get(TEMP_KEYPHRASE_ID);
233        }
234        if (listener == null) {
235            Slog.w(TAG, "received onRecognition event without any listener for it");
236            return;
237        }
238
239        switch (event.status) {
240            case SoundTrigger.RECOGNITION_STATUS_SUCCESS:
241                // TODO: Pass the captured audio back.
242                try {
243                    listener.onDetected(null);
244                } catch (RemoteException e) {
245                    Slog.w(TAG, "RemoteException in onDetected");
246                }
247                break;
248            case SoundTrigger.RECOGNITION_STATUS_ABORT:
249                try {
250                    listener.onDetectionStopped();
251                } catch (RemoteException e) {
252                    Slog.w(TAG, "RemoteException in onDetectionStopped");
253                }
254                break;
255            case SoundTrigger.RECOGNITION_STATUS_FAILURE:
256                try {
257                    listener.onDetectionStopped();
258                } catch (RemoteException e) {
259                    Slog.w(TAG, "RemoteException in onDetectionStopped");
260                }
261                break;
262        }
263    }
264
265    @Override
266    public void onServiceDied() {
267        // TODO: Figure out how to restart the recognition here.
268    }
269}
270