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