Redirect all speech recognition traffic through system server.
Test: atest CtsVoiceRecognitionTestCases Bug: 176578753 Change-Id: I5783257b76fa21c2a6f1d2e589fb843b93753350
This commit is contained in:
@@ -43,7 +43,7 @@ oneway interface IRecognitionService {
|
||||
* @param featureId The feature in the package
|
||||
*/
|
||||
void startListening(in Intent recognizerIntent, in IRecognitionListener listener,
|
||||
String packageName, String featureId);
|
||||
String packageName, String featureId, int callingUid);
|
||||
|
||||
/**
|
||||
* Stops listening for speech. Speech captured so far will be recognized as
|
||||
@@ -62,6 +62,7 @@ oneway interface IRecognitionService {
|
||||
* @param listener to receive callbacks, note that this must be non-null
|
||||
* @param packageName the package name calling this API
|
||||
* @param featureId The feature in the package
|
||||
* @param isShutdown Whether the cancellation is caused by a client calling #shutdown
|
||||
*/
|
||||
void cancel(in IRecognitionListener listener, String packageName, String featureId);
|
||||
void cancel(in IRecognitionListener listener, String packageName, String featureId, boolean isShutdown);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package android.speech;
|
||||
|
||||
import android.content.ComponentName;
|
||||
|
||||
import android.speech.IRecognitionServiceManagerCallback;
|
||||
|
||||
/**
|
||||
@@ -23,6 +25,10 @@ import android.speech.IRecognitionServiceManagerCallback;
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
interface IRecognitionServiceManager {
|
||||
void createSession(in IRecognitionServiceManagerCallback callback);
|
||||
oneway interface IRecognitionServiceManager {
|
||||
void createSession(
|
||||
in ComponentName componentName,
|
||||
in IBinder clientToken,
|
||||
boolean onDevice,
|
||||
in IRecognitionServiceManagerCallback callback);
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ import android.speech.IRecognitionService;
|
||||
*/
|
||||
oneway interface IRecognitionServiceManagerCallback {
|
||||
void onSuccess(in IRecognitionService service);
|
||||
void onError();
|
||||
void onError(int errorCode);
|
||||
}
|
||||
|
||||
@@ -105,17 +105,6 @@ public abstract class RecognitionService extends Service {
|
||||
int callingUid) {
|
||||
if (mCurrentCallback == null) {
|
||||
if (DBG) Log.d(TAG, "created new mCurrentCallback, listener = " + listener.asBinder());
|
||||
try {
|
||||
listener.asBinder().linkToDeath(new IBinder.DeathRecipient() {
|
||||
@Override
|
||||
public void binderDied() {
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_CANCEL, listener));
|
||||
}
|
||||
}, 0);
|
||||
} catch (RemoteException re) {
|
||||
Log.e(TAG, "dead listener on startListening");
|
||||
return;
|
||||
}
|
||||
mCurrentCallback = new Callback(listener, callingUid);
|
||||
RecognitionService.this.onStartListening(intent, mCurrentCallback);
|
||||
} else {
|
||||
@@ -352,7 +341,6 @@ public abstract class RecognitionService extends Service {
|
||||
* Return the Linux uid assigned to the process that sent you the current transaction that
|
||||
* is being processed. This is obtained from {@link Binder#getCallingUid()}.
|
||||
*/
|
||||
// TODO(b/176578753): need to make sure this is fixed when proxied through system.
|
||||
public int getCallingUid() {
|
||||
return mCallingUid;
|
||||
}
|
||||
@@ -368,7 +356,7 @@ public abstract class RecognitionService extends Service {
|
||||
|
||||
@Override
|
||||
public void startListening(Intent recognizerIntent, IRecognitionListener listener,
|
||||
String packageName, String featureId) {
|
||||
String packageName, String featureId, int callingUid) {
|
||||
Preconditions.checkNotNull(packageName);
|
||||
|
||||
if (DBG) Log.d(TAG, "startListening called by:" + listener.asBinder());
|
||||
@@ -377,7 +365,7 @@ public abstract class RecognitionService extends Service {
|
||||
packageName, featureId)) {
|
||||
service.mHandler.sendMessage(Message.obtain(service.mHandler,
|
||||
MSG_START_LISTENING, service.new StartListeningArgs(
|
||||
recognizerIntent, listener, Binder.getCallingUid())));
|
||||
recognizerIntent, listener, callingUid)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +385,7 @@ public abstract class RecognitionService extends Service {
|
||||
|
||||
@Override
|
||||
public void cancel(IRecognitionListener listener, String packageName,
|
||||
String featureId) {
|
||||
String featureId, boolean isShutdown) {
|
||||
Preconditions.checkNotNull(packageName);
|
||||
|
||||
if (DBG) Log.d(TAG, "cancel called by:" + listener.asBinder());
|
||||
|
||||
@@ -20,8 +20,8 @@ import android.annotation.NonNull;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
@@ -32,10 +32,9 @@ import android.os.ServiceManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Slog;
|
||||
|
||||
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
|
||||
@@ -107,6 +106,12 @@ public class SpeechRecognizer {
|
||||
/** Insufficient permissions */
|
||||
public static final int ERROR_INSUFFICIENT_PERMISSIONS = 9;
|
||||
|
||||
/** Too many requests from the same client. */
|
||||
public static final int ERROR_TOO_MANY_REQUESTS = 10;
|
||||
|
||||
/** Server has been disconnected, e.g. because the app has crashed. */
|
||||
public static final int ERROR_SERVER_DISCONNECTED = 11;
|
||||
|
||||
/** action codes */
|
||||
private final static int MSG_START = 1;
|
||||
private final static int MSG_STOP = 2;
|
||||
@@ -116,9 +121,6 @@ public class SpeechRecognizer {
|
||||
/** 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;
|
||||
|
||||
@@ -151,15 +153,11 @@ public class SpeechRecognizer {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Temporary queue, saving the messages until the connection will be established, afterwards,
|
||||
* only mHandler will receive the messages
|
||||
*/
|
||||
private final Queue<Message> mPendingTasks = new LinkedList<Message>();
|
||||
|
||||
/** The Listener that will receive all the callbacks */
|
||||
private final InternalListener mListener = new InternalListener();
|
||||
|
||||
private final IBinder mClientToken = new Binder();
|
||||
|
||||
/**
|
||||
* The right way to create a {@code SpeechRecognizer} is by using
|
||||
* {@link #createSpeechRecognizer} static factory method
|
||||
@@ -180,30 +178,6 @@ public class SpeechRecognizer {
|
||||
mOnDevice = onDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic ServiceConnection that records the mService variable. Additionally, on creation it
|
||||
* invokes the {@link IRecognitionService#startListening(Intent, IRecognitionListener)}.
|
||||
*/
|
||||
private class Connection implements ServiceConnection {
|
||||
|
||||
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 (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 SpeechRecognizer#createSpeechRecognizer(Context)} will
|
||||
@@ -303,87 +277,52 @@ public class SpeechRecognizer {
|
||||
throw new IllegalArgumentException("intent must not be null");
|
||||
}
|
||||
checkIsCalledFromMainThread();
|
||||
if (mConnection == null) { // first time connection
|
||||
// TODO(b/176578753): both flows should go through system service.
|
||||
if (mOnDevice) {
|
||||
connectToSystemService();
|
||||
} else {
|
||||
connectToService();
|
||||
|
||||
if (DBG) {
|
||||
Slog.i(TAG, "#startListening called");
|
||||
if (mService == null) {
|
||||
Slog.i(TAG, "Connection is not established yet");
|
||||
}
|
||||
}
|
||||
putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent));
|
||||
}
|
||||
|
||||
private void connectToSystemService() {
|
||||
mManagerService = IRecognitionServiceManager.Stub.asInterface(
|
||||
ServiceManager.getService(Context.SPEECH_RECOGNITION_SERVICE));
|
||||
|
||||
if (mManagerService == null) {
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO(b/176578753): this has to supply information on whether to use on-device impl.
|
||||
mManagerService.createSession(new IRecognitionServiceManagerCallback.Stub(){
|
||||
@Override
|
||||
public void onSuccess(IRecognitionService service) throws RemoteException {
|
||||
mService = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() throws RemoteException {
|
||||
Log.e(TAG, "Bind to system recognition service failed");
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
private void connectToService() {
|
||||
mConnection = new Connection();
|
||||
|
||||
Intent serviceIntent = new Intent(RecognitionService.SERVICE_INTERFACE);
|
||||
|
||||
if (mServiceComponent == null) {
|
||||
String serviceComponent = Settings.Secure.getString(mContext.getContentResolver(),
|
||||
Settings.Secure.VOICE_RECOGNITION_SERVICE);
|
||||
|
||||
if (TextUtils.isEmpty(serviceComponent)) {
|
||||
Log.e(TAG, "no selected voice recognition service");
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
return;
|
||||
}
|
||||
|
||||
serviceIntent.setComponent(
|
||||
ComponentName.unflattenFromString(serviceComponent));
|
||||
if (mService == null) {
|
||||
// First time connection: first establish a connection, then dispatch #startListening.
|
||||
connectToSystemService(
|
||||
() -> putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent)));
|
||||
} else {
|
||||
serviceIntent.setComponent(mServiceComponent);
|
||||
}
|
||||
if (!mContext.bindService(serviceIntent, mConnection,
|
||||
Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) {
|
||||
Log.e(TAG, "bind to recognition service failed");
|
||||
mConnection = null;
|
||||
mService = null;
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
return;
|
||||
putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening for speech. Speech captured so far will be recognized as if the user had
|
||||
* stopped speaking at this point. Note that in the default case, this does not need to be
|
||||
* 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. Please note that
|
||||
* stopped speaking at this point.
|
||||
*
|
||||
* <p>Note that in the default case, this does not need to be 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.
|
||||
*
|
||||
* <p>Upon invocation clients must wait until {@link RecognitionListener#onResults} or
|
||||
* {@link RecognitionListener#onError} are invoked before calling
|
||||
* {@link SpeechRecognizer#startListening} again. Otherwise such an attempt would be rejected by
|
||||
* recognition service.
|
||||
*
|
||||
* <p>Please note that
|
||||
* {@link #setRecognitionListener(RecognitionListener)} should be called beforehand, otherwise
|
||||
* no notifications will be received.
|
||||
*/
|
||||
public void stopListening() {
|
||||
checkIsCalledFromMainThread();
|
||||
|
||||
if (DBG) {
|
||||
Slog.i(TAG, "#stopListening called");
|
||||
if (mService == null) {
|
||||
Slog.i(TAG, "Connection is not established yet");
|
||||
}
|
||||
}
|
||||
|
||||
putMessage(Message.obtain(mHandler, MSG_STOP));
|
||||
}
|
||||
|
||||
@@ -405,11 +344,7 @@ public class SpeechRecognizer {
|
||||
}
|
||||
|
||||
private void putMessage(Message msg) {
|
||||
if (mService == null) {
|
||||
mPendingTasks.offer(msg);
|
||||
} else {
|
||||
mHandler.sendMessage(msg);
|
||||
}
|
||||
mHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
/** sends the actual message to the service */
|
||||
@@ -419,7 +354,7 @@ public class SpeechRecognizer {
|
||||
}
|
||||
try {
|
||||
mService.startListening(recognizerIntent, mListener, mContext.getOpPackageName(),
|
||||
mContext.getAttributionTag());
|
||||
mContext.getAttributionTag(), android.os.Process.myUid());
|
||||
if (DBG) Log.d(TAG, "service start listening command succeded");
|
||||
} catch (final RemoteException e) {
|
||||
Log.e(TAG, "startListening() failed", e);
|
||||
@@ -448,7 +383,11 @@ public class SpeechRecognizer {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mService.cancel(mListener, mContext.getOpPackageName(), mContext.getAttributionTag());
|
||||
mService.cancel(
|
||||
mListener,
|
||||
mContext.getOpPackageName(),
|
||||
mContext.getAttributionTag(),
|
||||
false /* isShutdown */);
|
||||
if (DBG) Log.d(TAG, "service cancel command succeded");
|
||||
} catch (final RemoteException e) {
|
||||
Log.e(TAG, "cancel() failed", e);
|
||||
@@ -471,28 +410,94 @@ public class SpeechRecognizer {
|
||||
mListener.mInternalListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the {@code SpeechRecognizer} object.
|
||||
*/
|
||||
/** Destroys the {@code SpeechRecognizer} object. */
|
||||
public void destroy() {
|
||||
if (mService != null) {
|
||||
try {
|
||||
mService.cancel(mListener, mContext.getOpPackageName(),
|
||||
mContext.getAttributionTag());
|
||||
mContext.getAttributionTag(), true /* isShutdown */);
|
||||
} catch (final RemoteException e) {
|
||||
// Not important
|
||||
}
|
||||
}
|
||||
|
||||
if (mConnection != null) {
|
||||
mContext.unbindService(mConnection);
|
||||
}
|
||||
mPendingTasks.clear();
|
||||
mService = null;
|
||||
mConnection = null;
|
||||
mListener.mInternalListener = null;
|
||||
}
|
||||
|
||||
/** Establishes a connection to system server proxy and initializes the session. */
|
||||
private void connectToSystemService(Runnable onSuccess) {
|
||||
mManagerService = IRecognitionServiceManager.Stub.asInterface(
|
||||
ServiceManager.getService(Context.SPEECH_RECOGNITION_SERVICE));
|
||||
|
||||
if (mManagerService == null) {
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
return;
|
||||
}
|
||||
|
||||
ComponentName componentName = getSpeechRecognizerComponentName();
|
||||
|
||||
if (!mOnDevice && componentName == null) {
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mManagerService.createSession(
|
||||
componentName,
|
||||
mClientToken,
|
||||
mOnDevice,
|
||||
new IRecognitionServiceManagerCallback.Stub(){
|
||||
@Override
|
||||
public void onSuccess(IRecognitionService service) throws RemoteException {
|
||||
mService = service;
|
||||
onSuccess.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode) throws RemoteException {
|
||||
Log.e(TAG, "Bind to system recognition service failed");
|
||||
mListener.onError(errorCode);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component name to be used for establishing a connection, based on the parameters
|
||||
* used during initialization.
|
||||
*
|
||||
* <p>Note the 3 different scenarios:
|
||||
* <ol>
|
||||
* <li>On-device speech recognizer which is determined by the manufacturer and not
|
||||
* changeable by the user
|
||||
* <li>Default user-selected speech recognizer as specified by
|
||||
* {@code Settings.Secure.VOICE_RECOGNITION_SERVICE}
|
||||
* <li>Custom speech recognizer supplied by the client.
|
||||
*/
|
||||
private ComponentName getSpeechRecognizerComponentName() {
|
||||
if (mOnDevice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mServiceComponent != null) {
|
||||
return mServiceComponent;
|
||||
}
|
||||
|
||||
String serviceComponent = Settings.Secure.getString(mContext.getContentResolver(),
|
||||
Settings.Secure.VOICE_RECOGNITION_SERVICE);
|
||||
|
||||
if (TextUtils.isEmpty(serviceComponent)) {
|
||||
Log.e(TAG, "no selected voice recognition service");
|
||||
mListener.onError(ERROR_CLIENT);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ComponentName.unflattenFromString(serviceComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal wrapper of IRecognitionListener which will propagate the results to
|
||||
* RecognitionListener
|
||||
|
||||
Reference in New Issue
Block a user