Redirect all speech recognition traffic through system server.

Test: atest CtsVoiceRecognitionTestCases
Bug: 176578753
Change-Id: I5783257b76fa21c2a6f1d2e589fb843b93753350
This commit is contained in:
Sergey Volnov
2021-02-05 01:03:34 +00:00
parent 091fcad4f8
commit bf2e94deab
9 changed files with 644 additions and 208 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -25,5 +25,5 @@ import android.speech.IRecognitionService;
*/
oneway interface IRecognitionServiceManagerCallback {
void onSuccess(in IRecognitionService service);
void onError();
void onError(int errorCode);
}

View File

@@ -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());

View File

@@ -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