1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package android.speech;
18
19import android.annotation.SdkConstant;
20import android.annotation.SdkConstant.SdkConstantType;
21import android.app.Service;
22import android.content.Intent;
23import android.content.PermissionChecker;
24import android.os.Binder;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.Message;
29import android.os.RemoteException;
30import android.util.Log;
31
32import java.lang.ref.WeakReference;
33
34/**
35 * This class provides a base class for recognition service implementations. This class should be
36 * extended only in case you wish to implement a new speech recognizer. Please note that the
37 * implementation of this service is stateless.
38 */
39public abstract class RecognitionService extends Service {
40    /**
41     * The {@link Intent} that must be declared as handled by the service.
42     */
43    @SdkConstant(SdkConstantType.SERVICE_ACTION)
44    public static final String SERVICE_INTERFACE = "android.speech.RecognitionService";
45
46    /**
47     * Name under which a RecognitionService component publishes information about itself.
48     * This meta-data should reference an XML resource containing a
49     * <code>&lt;{@link android.R.styleable#RecognitionService recognition-service}&gt;</code> tag.
50     */
51    public static final String SERVICE_META_DATA = "android.speech";
52
53    /** Log messages identifier */
54    private static final String TAG = "RecognitionService";
55
56    /** Debugging flag */
57    private static final boolean DBG = false;
58
59    /** Binder of the recognition service */
60    private RecognitionServiceBinder mBinder = new RecognitionServiceBinder(this);
61
62    /**
63     * The current callback of an application that invoked the
64     * {@link RecognitionService#onStartListening(Intent, Callback)} method
65     */
66    private Callback mCurrentCallback = null;
67
68    private static final int MSG_START_LISTENING = 1;
69
70    private static final int MSG_STOP_LISTENING = 2;
71
72    private static final int MSG_CANCEL = 3;
73
74    private static final int MSG_RESET = 4;
75
76    private final Handler mHandler = new Handler() {
77        @Override
78        public void handleMessage(Message msg) {
79            switch (msg.what) {
80                case MSG_START_LISTENING:
81                    StartListeningArgs args = (StartListeningArgs) msg.obj;
82                    dispatchStartListening(args.mIntent, args.mListener, args.mCallingUid);
83                    break;
84                case MSG_STOP_LISTENING:
85                    dispatchStopListening((IRecognitionListener) msg.obj);
86                    break;
87                case MSG_CANCEL:
88                    dispatchCancel((IRecognitionListener) msg.obj);
89                    break;
90                case MSG_RESET:
91                    dispatchClearCallback();
92                    break;
93            }
94        }
95    };
96
97    private void dispatchStartListening(Intent intent, final IRecognitionListener listener,
98            int callingUid) {
99        if (mCurrentCallback == null) {
100            if (DBG) Log.d(TAG, "created new mCurrentCallback, listener = " + listener.asBinder());
101            try {
102                listener.asBinder().linkToDeath(new IBinder.DeathRecipient() {
103                    @Override
104                    public void binderDied() {
105                        mHandler.sendMessage(mHandler.obtainMessage(MSG_CANCEL, listener));
106                    }
107                }, 0);
108            } catch (RemoteException re) {
109                Log.e(TAG, "dead listener on startListening");
110                return;
111            }
112            mCurrentCallback = new Callback(listener, callingUid);
113            RecognitionService.this.onStartListening(intent, mCurrentCallback);
114        } else {
115            try {
116                listener.onError(SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
117            } catch (RemoteException e) {
118                Log.d(TAG, "onError call from startListening failed");
119            }
120            Log.i(TAG, "concurrent startListening received - ignoring this call");
121        }
122    }
123
124    private void dispatchStopListening(IRecognitionListener listener) {
125        try {
126            if (mCurrentCallback == null) {
127                listener.onError(SpeechRecognizer.ERROR_CLIENT);
128                Log.w(TAG, "stopListening called with no preceding startListening - ignoring");
129            } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) {
130                listener.onError(SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
131                Log.w(TAG, "stopListening called by other caller than startListening - ignoring");
132            } else { // the correct state
133                RecognitionService.this.onStopListening(mCurrentCallback);
134            }
135        } catch (RemoteException e) { // occurs if onError fails
136            Log.d(TAG, "onError call from stopListening failed");
137        }
138    }
139
140    private void dispatchCancel(IRecognitionListener listener) {
141        if (mCurrentCallback == null) {
142            if (DBG) Log.d(TAG, "cancel called with no preceding startListening - ignoring");
143        } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) {
144            Log.w(TAG, "cancel called by client who did not call startListening - ignoring");
145        } else { // the correct state
146            RecognitionService.this.onCancel(mCurrentCallback);
147            mCurrentCallback = null;
148            if (DBG) Log.d(TAG, "canceling - setting mCurrentCallback to null");
149        }
150    }
151
152    private void dispatchClearCallback() {
153        mCurrentCallback = null;
154    }
155
156    private class StartListeningArgs {
157        public final Intent mIntent;
158
159        public final IRecognitionListener mListener;
160        public final int mCallingUid;
161
162        public StartListeningArgs(Intent intent, IRecognitionListener listener, int callingUid) {
163            this.mIntent = intent;
164            this.mListener = listener;
165            this.mCallingUid = callingUid;
166        }
167    }
168
169    /**
170     * Checks whether the caller has sufficient permissions
171     *
172     * @param listener to send the error message to in case of error
173     * @return {@code true} if the caller has enough permissions, {@code false} otherwise
174     */
175    private boolean checkPermissions(IRecognitionListener listener) {
176        if (DBG) Log.d(TAG, "checkPermissions");
177        if (PermissionChecker.checkCallingOrSelfPermission(this,
178                android.Manifest.permission.RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED) {
179            return true;
180        }
181        try {
182            Log.e(TAG, "call for recognition service without RECORD_AUDIO permissions");
183            listener.onError(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS);
184        } catch (RemoteException re) {
185            Log.e(TAG, "sending ERROR_INSUFFICIENT_PERMISSIONS message failed", re);
186        }
187        return false;
188    }
189
190    /**
191     * Notifies the service that it should start listening for speech.
192     *
193     * @param recognizerIntent contains parameters for the recognition to be performed. The intent
194     *        may also contain optional extras, see {@link RecognizerIntent}. If these values are
195     *        not set explicitly, default values should be used by the recognizer.
196     * @param listener that will receive the service's callbacks
197     */
198    protected abstract void onStartListening(Intent recognizerIntent, Callback listener);
199
200    /**
201     * Notifies the service that it should cancel the speech recognition.
202     */
203    protected abstract void onCancel(Callback listener);
204
205    /**
206     * Notifies the service that it should stop listening for speech. Speech captured so far should
207     * be recognized as if the user had stopped speaking at this point. This method is only called
208     * if the application calls it explicitly.
209     */
210    protected abstract void onStopListening(Callback listener);
211
212    @Override
213    public final IBinder onBind(final Intent intent) {
214        if (DBG) Log.d(TAG, "onBind, intent=" + intent);
215        return mBinder;
216    }
217
218    @Override
219    public void onDestroy() {
220        if (DBG) Log.d(TAG, "onDestroy");
221        mCurrentCallback = null;
222        mBinder.clearReference();
223        super.onDestroy();
224    }
225
226    /**
227     * This class receives callbacks from the speech recognition service and forwards them to the
228     * user. An instance of this class is passed to the
229     * {@link RecognitionService#onStartListening(Intent, Callback)} method. Recognizers may call
230     * these methods on any thread.
231     */
232    public class Callback {
233        private final IRecognitionListener mListener;
234        private final int mCallingUid;
235
236        private Callback(IRecognitionListener listener, int callingUid) {
237            mListener = listener;
238            mCallingUid = callingUid;
239        }
240
241        /**
242         * The service should call this method when the user has started to speak.
243         */
244        public void beginningOfSpeech() throws RemoteException {
245            if (DBG) Log.d(TAG, "beginningOfSpeech");
246            mListener.onBeginningOfSpeech();
247        }
248
249        /**
250         * The service should call this method when sound has been received. The purpose of this
251         * function is to allow giving feedback to the user regarding the captured audio.
252         *
253         * @param buffer a buffer containing a sequence of big-endian 16-bit integers representing a
254         *        single channel audio stream. The sample rate is implementation dependent.
255         */
256        public void bufferReceived(byte[] buffer) throws RemoteException {
257            mListener.onBufferReceived(buffer);
258        }
259
260        /**
261         * The service should call this method after the user stops speaking.
262         */
263        public void endOfSpeech() throws RemoteException {
264            mListener.onEndOfSpeech();
265        }
266
267        /**
268         * The service should call this method when a network or recognition error occurred.
269         *
270         * @param error code is defined in {@link SpeechRecognizer}
271         */
272        public void error(int error) throws RemoteException {
273            Message.obtain(mHandler, MSG_RESET).sendToTarget();
274            mListener.onError(error);
275        }
276
277        /**
278         * The service should call this method when partial recognition results are available. This
279         * method can be called at any time between {@link #beginningOfSpeech()} and
280         * {@link #results(Bundle)} when partial results are ready. This method may be called zero,
281         * one or multiple times for each call to {@link SpeechRecognizer#startListening(Intent)},
282         * depending on the speech recognition service implementation.
283         *
284         * @param partialResults the returned results. To retrieve the results in
285         *        ArrayList&lt;String&gt; format use {@link Bundle#getStringArrayList(String)} with
286         *        {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter
287         */
288        public void partialResults(Bundle partialResults) throws RemoteException {
289            mListener.onPartialResults(partialResults);
290        }
291
292        /**
293         * The service should call this method when the endpointer is ready for the user to start
294         * speaking.
295         *
296         * @param params parameters set by the recognition service. Reserved for future use.
297         */
298        public void readyForSpeech(Bundle params) throws RemoteException {
299            mListener.onReadyForSpeech(params);
300        }
301
302        /**
303         * The service should call this method when recognition results are ready.
304         *
305         * @param results the recognition results. To retrieve the results in {@code
306         *        ArrayList<String>} format use {@link Bundle#getStringArrayList(String)} with
307         *        {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter
308         */
309        public void results(Bundle results) throws RemoteException {
310            Message.obtain(mHandler, MSG_RESET).sendToTarget();
311            mListener.onResults(results);
312        }
313
314        /**
315         * The service should call this method when the sound level in the audio stream has changed.
316         * There is no guarantee that this method will be called.
317         *
318         * @param rmsdB the new RMS dB value
319         */
320        public void rmsChanged(float rmsdB) throws RemoteException {
321            mListener.onRmsChanged(rmsdB);
322        }
323
324        /**
325         * Return the Linux uid assigned to the process that sent you the current transaction that
326         * is being processed. This is obtained from {@link Binder#getCallingUid()}.
327         */
328        public int getCallingUid() {
329            return mCallingUid;
330        }
331    }
332
333    /** Binder of the recognition service */
334    private static final class RecognitionServiceBinder extends IRecognitionService.Stub {
335        private final WeakReference<RecognitionService> mServiceRef;
336
337        public RecognitionServiceBinder(RecognitionService service) {
338            mServiceRef = new WeakReference<RecognitionService>(service);
339        }
340
341        @Override
342        public void startListening(Intent recognizerIntent, IRecognitionListener listener) {
343            if (DBG) Log.d(TAG, "startListening called by:" + listener.asBinder());
344            final RecognitionService service = mServiceRef.get();
345            if (service != null && service.checkPermissions(listener)) {
346                service.mHandler.sendMessage(Message.obtain(service.mHandler,
347                        MSG_START_LISTENING, service.new StartListeningArgs(
348                                recognizerIntent, listener, Binder.getCallingUid())));
349            }
350        }
351
352        @Override
353        public void stopListening(IRecognitionListener listener) {
354            if (DBG) Log.d(TAG, "stopListening called by:" + listener.asBinder());
355            final RecognitionService service = mServiceRef.get();
356            if (service != null && service.checkPermissions(listener)) {
357                service.mHandler.sendMessage(Message.obtain(service.mHandler,
358                        MSG_STOP_LISTENING, listener));
359            }
360        }
361
362        @Override
363        public void cancel(IRecognitionListener listener) {
364            if (DBG) Log.d(TAG, "cancel called by:" + listener.asBinder());
365            final RecognitionService service = mServiceRef.get();
366            if (service != null && service.checkPermissions(listener)) {
367                service.mHandler.sendMessage(Message.obtain(service.mHandler,
368                        MSG_CANCEL, listener));
369            }
370        }
371
372        public void clearReference() {
373            mServiceRef.clear();
374        }
375    }
376}
377