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