diff options
author | Valentin Kravtsov <valentink@google.com> | 2010-01-28 14:53:41 +0000 |
---|---|---|
committer | Valentin Kravtsov <valentink@google.com> | 2010-02-05 14:21:07 +0000 |
commit | 3da3cad97269d694a6153771fb4a0c3775ca6ab5 (patch) | |
tree | 54c6e90eb5f20661d7bfd508125e3f6123a796b0 /core/java | |
parent | f6e003bc4a885c186a023bd46beaaafa3842cf51 (diff) | |
download | frameworks_base-3da3cad97269d694a6153771fb4a0c3775ca6ab5.zip frameworks_base-3da3cad97269d694a6153771fb4a0c3775ca6ab5.tar.gz frameworks_base-3da3cad97269d694a6153771fb4a0c3775ca6ab5.tar.bz2 |
Adding speech recognition service public API
Change-Id: Ia2c13d4c7993d646956090aa5c56d1a441af9e5a
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/android/speech/IRecognitionListener.aidl | 12 | ||||
-rw-r--r-- | core/java/android/speech/IRecognitionService.aidl | 12 | ||||
-rw-r--r-- | core/java/android/speech/RecognitionListener.java | 17 | ||||
-rw-r--r-- | core/java/android/speech/RecognitionManager.java | 389 | ||||
-rw-r--r-- | core/java/android/speech/RecognitionService.java | 295 |
5 files changed, 569 insertions, 156 deletions
diff --git a/core/java/android/speech/IRecognitionListener.aidl b/core/java/android/speech/IRecognitionListener.aidl index 5b48bd2..2a0f986 100644 --- a/core/java/android/speech/IRecognitionListener.aidl +++ b/core/java/android/speech/IRecognitionListener.aidl @@ -19,12 +19,12 @@ package android.speech; import android.os.Bundle; /** - * Listener for speech recognition events, used with RecognitionService. + * Listener for speech recognition events, used with RecognitionService. * This gives you both the final recognition results, as well as various * intermediate events that can be used to show visual feedback to the user. * {@hide} */ -interface IRecognitionListener { +oneway interface IRecognitionListener { /** * Called when the endpointer is ready for the user to start speaking. * @@ -76,4 +76,12 @@ interface IRecognitionListener { * @param results a Bundle containing the current most likely result. */ void onPartialResults(in Bundle results); + + /** + * Reserved for adding future events. + * + * @param eventType the type of the occurred event + * @param params a Bundle containing the passed parameters + */ + void onEvent(in int eventType, in Bundle params); } diff --git a/core/java/android/speech/IRecognitionService.aidl b/core/java/android/speech/IRecognitionService.aidl index ca9af15..8b27e63 100644 --- a/core/java/android/speech/IRecognitionService.aidl +++ b/core/java/android/speech/IRecognitionService.aidl @@ -29,7 +29,7 @@ import android.speech.IRecognitionListener; * accessing recognition service. * {@hide} */ -interface IRecognitionService { +oneway interface IRecognitionService { /** * Starts listening for speech. Please note that the recognition service supports * one listener only, therefore, if this function is called from two different threads, @@ -38,7 +38,7 @@ interface IRecognitionService { * @param recognizerIntent the intent from which the invocation occurred. Additionally, * this intent can contain extra parameters to manipulate the behavior of the recognition * client. For more information see {@link RecognizerIntent}. - * @param listener to receive callbacks + * @param listener to receive callbacks, note that this must be non-null */ void startListening(in Intent recognizerIntent, in IRecognitionListener listener); @@ -46,11 +46,15 @@ interface IRecognitionService { * Stops listening for speech. Speech captured so far will be recognized as * if the user had stopped speaking at this point. The function has no effect unless it * is called during the speech capturing. + * + * @param listener to receive callbacks, note that this must be non-null */ - void stopListening(); + void stopListening(in IRecognitionListener listener); /** * Cancels the speech recognition. + * + * @param listener to receive callbacks, note that this must be non-null */ - void cancel(); + void cancel(in IRecognitionListener listener); } diff --git a/core/java/android/speech/RecognitionListener.java b/core/java/android/speech/RecognitionListener.java index eab3f40..5434887 100644 --- a/core/java/android/speech/RecognitionListener.java +++ b/core/java/android/speech/RecognitionListener.java @@ -24,12 +24,6 @@ import android.os.Bundle; * Application main thread. */ public interface RecognitionListener { - - /** - * Called when RecognitionManager is successfully initialized - */ - void onInit(); - /** * Called when the endpointer is ready for the user to start speaking. * @@ -76,7 +70,7 @@ public interface RecognitionListener { * * @param results the recognition results. To retrieve the results in {@code * ArrayList<String>} format use {@link Bundle#getStringArrayList(String)} with - * {@link RecognitionManager#RECOGNITION_RESULTS_STRING_ARRAY} as a parameter + * {@link RecognitionManager#RESULTS_RECOGNITION} as a parameter */ void onResults(Bundle results); @@ -89,8 +83,15 @@ public interface RecognitionListener { * * @param partialResults the returned results. To retrieve the results in * ArrayList<String> format use {@link Bundle#getStringArrayList(String)} with - * {@link RecognitionManager#RECOGNITION_RESULTS_STRING_ARRAY} as a parameter + * {@link RecognitionManager#RESULTS_RECOGNITION} as a parameter */ void onPartialResults(Bundle partialResults); + /** + * Reserved for adding future events. + * + * @param eventType the type of the occurred event + * @param params a Bundle containing the passed parameters + */ + void onEvent(int eventType, Bundle params); } diff --git a/core/java/android/speech/RecognitionManager.java b/core/java/android/speech/RecognitionManager.java index 79ae480..0d25b2f 100644 --- a/core/java/android/speech/RecognitionManager.java +++ b/core/java/android/speech/RecognitionManager.java @@ -22,17 +22,23 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; /** * This class provides access to the speech recognition service. This service allows access to the * speech recognizer. Do not instantiate this class directly, instead, call - * {@link RecognitionManager#createRecognitionManager(Context, RecognitionListener, Intent)}. This - * class is not thread safe and must be synchronized externally if accessed from multiple threads. + * {@link RecognitionManager#createRecognitionManager(Context)}. This class's methods must be + * invoked only from the main application thread. Please note that the application must have + * {@link android.Manifest.permission#RECORD_AUDIO} permission to use this class. */ public class RecognitionManager { /** DEBUG value to enable verbose debug prints */ @@ -47,92 +53,118 @@ public class RecognitionManager { * {@link RecognitionListener#onPartialResults(Bundle)} methods. These strings are the possible * recognition results, where the first element is the most likely candidate. */ - public static final String RECOGNITION_RESULTS_STRING_ARRAY = - "recognition_results_string_array"; - - /** The actual RecognitionService endpoint */ - private IRecognitionService mService; - - /** The connection to the actual service */ - private Connection mConnection; - - /** Context with which the manager was created */ - private final Context mContext; - - /** Listener that will receive all the callbacks */ - private final RecognitionListener mListener; - - /** Helper class wrapping the IRecognitionListener */ - private final InternalRecognitionListener mInternalRecognitionListener; + public static final String RESULTS_RECOGNITION = "results_recognition"; /** Network operation timed out. */ - public static final int NETWORK_TIMEOUT_ERROR = 1; + public static final int ERROR_NETWORK_TIMEOUT = 1; /** Other network related errors. */ - public static final int NETWORK_ERROR = 2; + public static final int ERROR_NETWORK = 2; /** Audio recording error. */ - public static final int AUDIO_ERROR = 3; + public static final int ERROR_AUDIO = 3; /** Server sends error status. */ - public static final int SERVER_ERROR = 4; + public static final int ERROR_SERVER = 4; /** Other client side errors. */ - public static final int CLIENT_ERROR = 5; + public static final int ERROR_CLIENT = 5; /** No speech input */ - public static final int SPEECH_TIMEOUT_ERROR = 6; + public static final int ERROR_SPEECH_TIMEOUT = 6; /** No recognition result matched. */ - public static final int NO_MATCH_ERROR = 7; + public static final int ERROR_NO_MATCH = 7; /** RecognitionService busy. */ - public static final int SERVER_BUSY_ERROR = 8; + public static final int ERROR_RECOGNIZER_BUSY = 8; + + /** Insufficient permissions */ + public static final int ERROR_INSUFFICIENT_PERMISSIONS = 9; + + /** action codes */ + private final static int MSG_START = 1; + private final static int MSG_STOP = 2; + private final static int MSG_CANCEL = 3; + private final static int MSG_CHANGE_LISTENER = 4; + + /** The actual RecognitionService endpoint */ + private IRecognitionService mService; + + /** The connection to the actual service */ + private Connection mConnection; + + /** Context with which the manager was created */ + private final Context mContext; + + /** Handler that will execute the main tasks */ + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START: + handleStartListening((Intent) msg.obj); + break; + case MSG_STOP: + handleStopMessage(); + break; + case MSG_CANCEL: + handleCancelMessage(); + break; + case MSG_CHANGE_LISTENER: + handleChangeListener((RecognitionListener) msg.obj); + break; + } + } + }; /** - * RecognitionManager was not initialized yet, most probably because - * {@link RecognitionListener#onInit()} was not called yet. + * Temporary queue, saving the messages until the connection will be established, afterwards, + * only mHandler will receive the messages */ - public static final int MANAGER_NOT_INITIALIZED_ERROR = 9; + private final Queue<Message> mPendingTasks = new LinkedList<Message>(); + + /** The Listener that will receive all the callbacks */ + private final InternalListener mListener = new InternalListener(); /** - * The right way to create a RecognitionManager is by using + * The right way to create a {@code RecognitionManager} is by using * {@link #createRecognitionManager} static factory method */ - private RecognitionManager(final RecognitionListener listener, final Context context) { - mInternalRecognitionListener = new InternalRecognitionListener(); + private RecognitionManager(final Context context) { mContext = context; - mListener = listener; } /** - * Basic ServiceConnection which just records mService variable. + * Basic ServiceConnection that records the mService variable. Additionally, on creation it + * invokes the {@link IRecognitionService#startListening(Intent, IRecognitionListener)}. */ private class Connection implements ServiceConnection { - public synchronized void onServiceConnected(final ComponentName name, - final IBinder service) { + public void onServiceConnected(final ComponentName name, final IBinder service) { + // always done on the application main thread, so no need to send message to mHandler mService = IRecognitionService.Stub.asInterface(service); - if (mListener != null) { - mListener.onInit(); - } if (DBG) Log.d(TAG, "onServiceConnected - Success"); + while (!mPendingTasks.isEmpty()) { + mHandler.sendMessage(mPendingTasks.poll()); + } } public void onServiceDisconnected(final ComponentName name) { + // always done on the application main thread, so no need to send message to mHandler mService = null; mConnection = null; + mPendingTasks.clear(); if (DBG) Log.d(TAG, "onServiceDisconnected - Success"); } } /** * Checks whether a speech recognition service is available on the system. If this method - * returns {@code false}, - * {@link RecognitionManager#createRecognitionManager(Context, RecognitionListener, Intent)} - * will fail. + * returns {@code false}, {@link RecognitionManager#createRecognitionManager(Context)} will + * fail. * - * @param context with which RecognitionManager will be created + * @param context with which {@code RecognitionManager} will be created * @return {@code true} if recognition is available, {@code false} otherwise */ public static boolean isRecognitionAvailable(final Context context) { @@ -142,78 +174,61 @@ public class RecognitionManager { } /** - * Factory method to create a new RecognitionManager + * Factory method to create a new {@code RecognitionManager}, please note that + * {@link #setRecognitionListener(RecognitionListener)} must be called before dispatching any + * command to the created {@code RecognitionManager}. * - * @param context in which to create RecognitionManager - * @param listener that will receive all the callbacks from the created - * {@link RecognitionManager} - * @param recognizerIntent contains initialization parameters for the speech recognizer. The - * intent action should be {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH}. Future - * versions of this API may add startup parameters for speech recognizer. - * @return null if a recognition service implementation is not installed or if speech - * recognition is not supported by the device, otherwise a new RecognitionManager is - * returned. The created RecognitionManager can only be used after the - * {@link RecognitionListener#onInit()} method has been called. + * @param context in which to create {@code RecognitionManager} + * @return a new {@code RecognitionManager} */ - public static RecognitionManager createRecognitionManager(final Context context, - final RecognitionListener listener, final Intent recognizerIntent) { - if (context == null || recognizerIntent == null) { - throw new IllegalArgumentException( - "Context and recognizerListener argument cannot be null)"); + public static RecognitionManager createRecognitionManager(final Context context) { + if (context == null) { + throw new IllegalArgumentException("Context cannot be null)"); } - RecognitionManager manager = new RecognitionManager(listener, context); - manager.mConnection = manager.new Connection(); - if (!context.bindService(recognizerIntent, manager.mConnection, Context.BIND_AUTO_CREATE)) { - Log.e(TAG, "bind to recognition service failed"); - listener.onError(CLIENT_ERROR); - return null; - } - return manager; + checkIsCalledFromMainThread(); + return new RecognitionManager(context); } /** - * Checks whether the service is connected - * - * @param functionName from which the call originated - * @return {@code true} if the service was successfully initialized, {@code false} otherwise + * Sets the listener that will receive all the callbacks. The previous unfinished commands will + * be executed with the old listener, while any following command will be executed with the new + * listener. + * + * @param listener listener that will receive all the callbacks from the created + * {@link RecognitionManager}, this must not be null. */ - private boolean connectToService(final String functionName) { - if (mService != null) { - return true; - } - if (mConnection == null) { - if (DBG) Log.d(TAG, "restarting connection to the recognition service"); - mConnection = new Connection(); - mContext.bindService(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), mConnection, - Context.BIND_AUTO_CREATE); - } - mInternalRecognitionListener.onError(MANAGER_NOT_INITIALIZED_ERROR); - Log.e(TAG, functionName + " was called before service connection was initialized"); - return false; + public void setRecognitionListener(RecognitionListener listener) { + checkIsCalledFromMainThread(); + putMessage(Message.obtain(mHandler, MSG_CHANGE_LISTENER, listener)); } /** - * Starts listening for speech. + * Starts listening for speech. Please note that + * {@link #setRecognitionListener(RecognitionListener)} must be called beforehand, otherwise a + * {@link RuntimeException} will be thrown. * * @param recognizerIntent contains parameters for the recognition to be performed. The intent - * action should be {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH}. The intent may also - * contain optional extras, see {@link RecognizerIntent}. If these values are not set - * explicitly, default values will be used by the recognizer. + * may also contain optional extras, see {@link RecognizerIntent}. If these values are + * not set explicitly, default values will be used by the recognizer. */ - public void startListening(Intent recognizerIntent) { + public void startListening(final Intent recognizerIntent) { if (recognizerIntent == null) { - throw new IllegalArgumentException("recognizerIntent argument cannot be null"); + throw new IllegalArgumentException("intent must not be null"); } - if (!connectToService("startListening")) { - return; // service is not connected yet, reconnect in progress - } - try { - mService.startListening(recognizerIntent, mInternalRecognitionListener); - if (DBG) Log.d(TAG, "service start listening command succeded"); - } catch (final RemoteException e) { - Log.e(TAG, "startListening() failed", e); - mInternalRecognitionListener.onError(CLIENT_ERROR); + checkIsCalledFromMainThread(); + checkIsCommandAllowed(); + if (mConnection == null) { // first time connection + mConnection = new Connection(); + if (!mContext.bindService(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), + mConnection, Context.BIND_AUTO_CREATE)) { + Log.e(TAG, "bind to recognition service failed"); + mConnection = null; + mService = null; + mListener.onError(ERROR_CLIENT); + return; + } } + putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent)); } /** @@ -222,100 +237,190 @@ public class RecognitionManager { * called, as the speech endpointer will automatically stop the recognizer listening when it * determines speech has completed. However, you can manipulate endpointer parameters directly * using the intent extras defined in {@link RecognizerIntent}, in which case you may sometimes - * want to manually call this method to stop listening sooner. + * want to manually call this method to stop listening sooner. Please note that + * {@link #setRecognitionListener(RecognitionListener)} must be called beforehand, otherwise a + * {@link RuntimeException} will be thrown. */ public void stopListening() { + checkIsCalledFromMainThread(); + checkIsCommandAllowed(); + putMessage(Message.obtain(mHandler, MSG_STOP)); + } + + /** + * Cancels the speech recognition. Please note that + * {@link #setRecognitionListener(RecognitionListener)} must be called beforehand, otherwise a + * {@link RuntimeException} will be thrown. + */ + public void cancel() { + checkIsCalledFromMainThread(); + checkIsCommandAllowed(); + putMessage(Message.obtain(mHandler, MSG_CANCEL)); + } + + private static void checkIsCalledFromMainThread() { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new RuntimeException( + "RecognitionManager should be used only from the application's main thread"); + } + } + + private void checkIsCommandAllowed() { + if (mService == null && mPendingTasks.isEmpty()) { // setListener message must be there + throw new IllegalStateException("Listener must be set before any command is called"); + } + } + + private void putMessage(Message msg) { if (mService == null) { - return; // service is not connected, but no need to reconnect at this point + mPendingTasks.offer(msg); + } else { + mHandler.sendMessage(msg); + } + } + + /** sends the actual message to the service */ + private void handleStartListening(Intent recognizerIntent) { + try { + mService.startListening(recognizerIntent, mListener); + if (DBG) Log.d(TAG, "service start listening command succeded"); + } catch (final RemoteException e) { + Log.e(TAG, "startListening() failed", e); + mListener.onError(ERROR_CLIENT); } + } + + /** sends the actual message to the service */ + private void handleStopMessage() { try { - mService.stopListening(); + mService.stopListening(mListener); if (DBG) Log.d(TAG, "service stop listening command succeded"); } catch (final RemoteException e) { Log.e(TAG, "stopListening() failed", e); - mInternalRecognitionListener.onError(CLIENT_ERROR); + mListener.onError(ERROR_CLIENT); } } - /** - * Cancels the speech recognition. - */ - public void cancel() { - if (mService == null) { - return; // service is not connected, but no need to reconnect at this point - } + /** sends the actual message to the service */ + private void handleCancelMessage() { try { - mService.cancel(); + mService.cancel(mListener); if (DBG) Log.d(TAG, "service cancel command succeded"); } catch (final RemoteException e) { Log.e(TAG, "cancel() failed", e); - mInternalRecognitionListener.onError(CLIENT_ERROR); + mListener.onError(ERROR_CLIENT); } } + /** changes the listener */ + private void handleChangeListener(RecognitionListener listener) { + if (DBG) Log.d(TAG, "handleChangeListener, listener=" + listener); + mListener.mInternalListener = listener; + } + /** - * Destroys the RecognitionManager object. Note that after calling this method all method calls - * on this object will fail, triggering {@link RecognitionListener#onError}. + * Destroys the {@code RecognitionManager} object. Note that after calling this method all + * method calls on this object will fail, triggering {@link RecognitionListener#onError}. */ public void destroy() { if (mConnection != null) { mContext.unbindService(mConnection); } + mPendingTasks.clear(); mService = null; + mConnection = null; } /** - * Internal wrapper of IRecognitionListener which will propagate the results - * to RecognitionListener + * Internal wrapper of IRecognitionListener which will propagate the results to + * RecognitionListener */ - private class InternalRecognitionListener extends IRecognitionListener.Stub { + private class InternalListener extends IRecognitionListener.Stub { + private RecognitionListener mInternalListener; + + private final static int MSG_BEGINNING_OF_SPEECH = 1; + private final static int MSG_BUFFER_RECEIVED = 2; + private final static int MSG_END_OF_SPEECH = 3; + private final static int MSG_ERROR = 4; + private final static int MSG_READY_FOR_SPEECH = 5; + private final static int MSG_RESULTS = 6; + private final static int MSG_PARTIAL_RESULTS = 7; + private final static int MSG_RMS_CHANGED = 8; + private final static int MSG_ON_EVENT = 9; + + private final Handler mInternalHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (mInternalListener == null) { + return; + } + switch (msg.what) { + case MSG_BEGINNING_OF_SPEECH: + mInternalListener.onBeginningOfSpeech(); + break; + case MSG_BUFFER_RECEIVED: + mInternalListener.onBufferReceived((byte[]) msg.obj); + break; + case MSG_END_OF_SPEECH: + mInternalListener.onEndOfSpeech(); + break; + case MSG_ERROR: + mInternalListener.onError((Integer) msg.obj); + break; + case MSG_READY_FOR_SPEECH: + mInternalListener.onReadyForSpeech((Bundle) msg.obj); + break; + case MSG_RESULTS: + mInternalListener.onResults((Bundle) msg.obj); + break; + case MSG_PARTIAL_RESULTS: + mInternalListener.onPartialResults((Bundle) msg.obj); + break; + case MSG_RMS_CHANGED: + mInternalListener.onRmsChanged((Float) msg.obj); + break; + case MSG_ON_EVENT: + mInternalListener.onEvent(msg.arg1, (Bundle) msg.obj); + break; + } + } + }; public void onBeginningOfSpeech() { - if (mListener != null) { - mListener.onBeginningOfSpeech(); - } + Message.obtain(mInternalHandler, MSG_BEGINNING_OF_SPEECH).sendToTarget(); } public void onBufferReceived(final byte[] buffer) { - if (mListener != null) { - mListener.onBufferReceived(buffer); - } + Message.obtain(mInternalHandler, MSG_BUFFER_RECEIVED, buffer).sendToTarget(); } public void onEndOfSpeech() { - if (mListener != null) { - mListener.onEndOfSpeech(); - } + Message.obtain(mInternalHandler, MSG_END_OF_SPEECH).sendToTarget(); } public void onError(final int error) { - if (mListener != null) { - mListener.onError(error); - } + Message.obtain(mInternalHandler, MSG_ERROR, error).sendToTarget(); } public void onReadyForSpeech(final Bundle noiseParams) { - if (mListener != null) { - mListener.onReadyForSpeech(noiseParams); - } + Message.obtain(mInternalHandler, MSG_READY_FOR_SPEECH, noiseParams).sendToTarget(); } public void onResults(final Bundle results) { - if (mListener != null) { - mListener.onResults(results); - } + Message.obtain(mInternalHandler, MSG_RESULTS, results).sendToTarget(); } public void onPartialResults(final Bundle results) { - if (mListener != null) { - mListener.onPartialResults(results); - } + Message.obtain(mInternalHandler, MSG_PARTIAL_RESULTS, results).sendToTarget(); } public void onRmsChanged(final float rmsdB) { - if (mListener != null) { - mListener.onRmsChanged(rmsdB); - } + Message.obtain(mInternalHandler, MSG_RMS_CHANGED, rmsdB).sendToTarget(); + } + + public void onEvent(final int eventType, final Bundle params) { + Message.obtain(mInternalHandler, MSG_ON_EVENT, eventType, eventType, params) + .sendToTarget(); } } } diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java new file mode 100644 index 0000000..0d74960 --- /dev/null +++ b/core/java/android/speech/RecognitionService.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.speech; + +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +/** + * This class provides a base class for recognition service implementations. This class should be + * extended only in case you wish to implement a new speech recognizer. Please not that the + * implementation of this service is state-less. + */ +public abstract class RecognitionService extends Service { + + /** Log messages identifier */ + private static final String TAG = "RecognitionService"; + + /** Debugging flag */ + private static final boolean DBG = false; + + /** + * The current callback of an application that invoked the + * {@link RecognitionService#onStartListening(Intent, Callback)} method + */ + private Callback mCurrentCallback = null; + + private static final int MSG_START_LISTENING = 1; + + private static final int MSG_STOP_LISTENING = 2; + + private static final int MSG_CANCEL = 3; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START_LISTENING: + StartListeningArgs args = (StartListeningArgs) msg.obj; + dispatchStartListening(args.mIntent, args.mListener); + break; + case MSG_STOP_LISTENING: + dispatchStopListening((IRecognitionListener) msg.obj); + break; + case MSG_CANCEL: + dispatchCancel((IRecognitionListener) msg.obj); + } + } + }; + + private void dispatchStartListening(Intent intent, IRecognitionListener listener) { + if (mCurrentCallback == null) { + if (DBG) Log.d(TAG, "created new mCurrentCallback, listener = " + listener.asBinder()); + mCurrentCallback = new Callback(listener); + RecognitionService.this.onStartListening(intent, mCurrentCallback); + } else { + try { + listener.onError(RecognitionManager.ERROR_RECOGNIZER_BUSY); + } catch (RemoteException e) { + Log.d(TAG, "onError call from startListening failed"); + } + Log.i(TAG, "concurrent startListening received - ignoring this call"); + } + } + + private void dispatchStopListening(IRecognitionListener listener) { + try { + if (mCurrentCallback == null) { + listener.onError(RecognitionManager.ERROR_CLIENT); + Log.w(TAG, "stopListening called with no preceding startListening - ignoring"); + } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) { + listener.onError(RecognitionManager.ERROR_RECOGNIZER_BUSY); + Log.w(TAG, "stopListening called by other caller than startListening - ignoring"); + } else { // the correct state + RecognitionService.this.onStopListening(mCurrentCallback); + } + } catch (RemoteException e) { // occurs if onError fails + Log.d(TAG, "onError call from stopListening failed"); + } + } + + private void dispatchCancel(IRecognitionListener listener) { + if (mCurrentCallback == null) { + Log.w(TAG, "cancel called with no preceding startListening - ignoring"); + } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) { + Log.w(TAG, "cancel called by client who did not call startListening - ignoring"); + } else { // the correct state + RecognitionService.this.onCancel(mCurrentCallback); + mCurrentCallback = null; + if (DBG) Log.d(TAG, "canceling - setting mCurrentCallback to null"); + } + } + + private class StartListeningArgs { + public final Intent mIntent; + + public final IRecognitionListener mListener; + + public StartListeningArgs(Intent intent, IRecognitionListener listener) { + this.mIntent = intent; + this.mListener = listener; + } + } + + /** Binder of the recognition service */ + private final IRecognitionService.Stub mBinder = new IRecognitionService.Stub() { + public void startListening(Intent recognizerIntent, IRecognitionListener listener) { + if (DBG) Log.d(TAG, "startListening called by:" + listener.asBinder()); + if (checkPermissions(listener)) { + mHandler.sendMessage(Message.obtain(mHandler, MSG_START_LISTENING, + new StartListeningArgs(recognizerIntent, listener))); + } + } + + public void stopListening(IRecognitionListener listener) { + if (DBG) Log.d(TAG, "stopListening called by:" + listener.asBinder()); + if (checkPermissions(listener)) { + mHandler.sendMessage(Message.obtain(mHandler, MSG_STOP_LISTENING, listener)); + } + } + + public void cancel(IRecognitionListener listener) { + if (DBG) Log.d(TAG, "cancel called by:" + listener.asBinder()); + if (checkPermissions(listener)) { + mHandler.sendMessage(Message.obtain(mHandler, MSG_CANCEL, listener)); + } + } + }; + + /** + * Checks whether the caller has sufficient permissions + * + * @param listener to send the error message to in case of error + * @return {@code true} if the caller has enough permissions, {@code false} otherwise + */ + private boolean checkPermissions(IRecognitionListener listener) { + if (DBG) Log.d(TAG, "checkPermissions"); + if (RecognitionService.this.checkCallingOrSelfPermission(android.Manifest.permission. + RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + return true; + } + try { + Log.e(TAG, "call for recognition service without RECORD_AUDIO permissions"); + listener.onError(RecognitionManager.ERROR_INSUFFICIENT_PERMISSIONS); + } catch (RemoteException re) { + Log.e(TAG, "sending ERROR_INSUFFICIENT_PERMISSIONS message failed", re); + } + return false; + } + + /** + * Notifies the service that it should start listening for speech. + * + * @param recognizerIntent contains parameters for the recognition to be performed. The intent + * may also contain optional extras, see {@link RecognizerIntent}. If these values are + * not set explicitly, default values should be used by the recognizer. + * @param listener that will receive the service's callbacks + */ + protected abstract void onStartListening(Intent recognizerIntent, Callback listener); + + /** + * Notifies the service that it should cancel the speech recognition. + */ + protected abstract void onCancel(Callback listener); + + /** + * Notifies the service that it should stop listening for speech. Speech captured so far should + * be recognized as if the user had stopped speaking at this point. This method is only called + * if the application calls it explicitly. + */ + protected abstract void onStopListening(Callback listener); + + @Override + public final IBinder onBind(final Intent intent) { + if (DBG) Log.d(TAG, "onBind, intent=" + intent); + return mBinder; + } + + /** + * This class receives callbacks from the speech recognition service and forwards them to the + * user. An instance of this class is passed to the + * {@link RecognitionService#onStartListening(Intent, Callback)} method. Recognizers may call + * these methods on any thread. + */ + public class Callback { + private final IRecognitionListener mListener; + + private Callback(IRecognitionListener listener) { + mListener = listener; + } + + /** + * The service should call this method when the user has started to speak. + */ + public void beginningOfSpeech() throws RemoteException { + if (DBG) Log.d(TAG, "beginningOfSpeech"); + mListener.onBeginningOfSpeech(); + } + + /** + * The service should call this method when sound has been received. The purpose of this + * function is to allow giving feedback to the user regarding the captured audio. + * + * @param buffer a buffer containing a sequence of big-endian 16-bit integers representing a + * single channel audio stream. The sample rate is implementation dependent. + */ + public void bufferReceived(byte[] buffer) throws RemoteException { + mListener.onBufferReceived(buffer); + } + + /** + * The service should call this method after the user stops speaking. + */ + public void endOfSpeech() throws RemoteException { + mListener.onEndOfSpeech(); + } + + /** + * The service should call this method when a network or recognition error occurred. + * + * @param error code is defined in {@link RecognitionManager} + */ + public void error(int error) throws RemoteException { + mCurrentCallback = null; + mListener.onError(error); + } + + /** + * The service should call this method when partial recognition results are available. This + * method can be called at any time between {@link #beginningOfSpeech()} and + * {@link #results(Bundle)} when partial results are ready. This method may be called zero, + * one or multiple times for each call to {@link RecognitionManager#startListening(Intent)}, + * depending on the speech recognition service implementation. + * + * @param partialResults the returned results. To retrieve the results in + * ArrayList<String> format use {@link Bundle#getStringArrayList(String)} with + * {@link RecognitionManager#RESULTS_RECOGNITION} as a parameter + */ + public void partialResults(Bundle partialResults) throws RemoteException { + mListener.onPartialResults(partialResults); + } + + /** + * The service should call this method when the endpointer is ready for the user to start + * speaking. + * + * @param params parameters set by the recognition service. Reserved for future use. + */ + public void readyForSpeech(Bundle params) throws RemoteException { + mListener.onReadyForSpeech(params); + } + + /** + * The service should call this method when recognition results are ready. + * + * @param results the recognition results. To retrieve the results in {@code + * ArrayList<String>} format use {@link Bundle#getStringArrayList(String)} with + * {@link RecognitionManager#RESULTS_RECOGNITION} as a parameter + */ + public void results(Bundle results) throws RemoteException { + mCurrentCallback = null; + mListener.onResults(results); + } + + /** + * The service should call this method when the sound level in the audio stream has changed. + * There is no guarantee that this method will be called. + * + * @param rmsdB the new RMS dB value + */ + public void rmsChanged(float rmsdB) throws RemoteException { + mListener.onRmsChanged(rmsdB); + } + } +} |