blob: 068626588745fbcece0b737db740e71683f97cf1 [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.server.speech;
import static com.android.internal.infra.AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.AttributionSource;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.RemoteException;
import android.speech.IRecognitionListener;
import android.speech.IRecognitionService;
import android.speech.RecognitionService;
import android.speech.SpeechRecognizer;
import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.ServiceConnector;
final class RemoteSpeechRecognitionService extends ServiceConnector.Impl<IRecognitionService> {
private static final String TAG = RemoteSpeechRecognitionService.class.getSimpleName();
private static final boolean DEBUG = false;
private final Object mLock = new Object();
private boolean mConnected = false;
@Nullable
private IRecognitionListener mListener;
@Nullable
@GuardedBy("mLock")
private DelegatingListener mDelegatingListener;
// Makes sure we can block startListening() if session is still in progress.
@GuardedBy("mLock")
private boolean mSessionInProgress = false;
// Makes sure we call startProxyOp / finishProxyOp at right times and only once per session.
@GuardedBy("mLock")
private boolean mRecordingInProgress = false;
private final int mCallingUid;
private final ComponentName mComponentName;
RemoteSpeechRecognitionService(
Context context, ComponentName serviceName, int userId, int callingUid) {
super(context,
new Intent(RecognitionService.SERVICE_INTERFACE).setComponent(serviceName),
Context.BIND_AUTO_CREATE
| Context.BIND_FOREGROUND_SERVICE
| Context.BIND_INCLUDE_CAPABILITIES
| Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
userId,
IRecognitionService.Stub::asInterface);
mCallingUid = callingUid;
mComponentName = serviceName;
if (DEBUG) {
Slog.i(TAG, "Bound to recognition service at: " + serviceName.flattenToString());
}
}
ComponentName getServiceComponentName() {
return mComponentName;
}
void startListening(Intent recognizerIntent, IRecognitionListener listener,
@NonNull AttributionSource attributionSource) {
if (DEBUG) {
Slog.i(TAG, String.format("#startListening for package: %s, feature=%s, callingUid=%d",
attributionSource.getPackageName(), attributionSource.getAttributionTag(),
mCallingUid));
}
if (listener == null) {
Log.w(TAG, "#startListening called with no preceding #setListening - ignoring");
return;
}
if (!mConnected) {
tryRespondWithError(listener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
return;
}
synchronized (mLock) {
if (mSessionInProgress) {
Slog.i(TAG, "#startListening called while listening is in progress.");
tryRespondWithError(listener, SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
return;
}
mSessionInProgress = true;
mRecordingInProgress = true;
mListener = listener;
mDelegatingListener = new DelegatingListener(listener, () -> {
// To be invoked in terminal calls of the callback: results() or error()
if (DEBUG) {
Slog.i(TAG, "Recognition session complete");
}
synchronized (mLock) {
resetStateLocked();
}
});
// Eager local evaluation to avoid reading a different or null value at closure-run-time
final DelegatingListener listenerToStart = this.mDelegatingListener;
run(service ->
service.startListening(
recognizerIntent,
listenerToStart,
attributionSource));
}
}
void stopListening(IRecognitionListener listener) {
if (DEBUG) {
Slog.i(TAG, "#stopListening");
}
if (!mConnected) {
tryRespondWithError(listener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
return;
}
synchronized (mLock) {
if (mListener == null) {
Log.w(TAG, "#stopListening called with no preceding #startListening - ignoring");
tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
return;
}
if (mListener.asBinder() != listener.asBinder()) {
Log.w(TAG, "#stopListening called with an unexpected listener");
tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
return;
}
if (!mRecordingInProgress) {
Slog.i(TAG, "#stopListening called while listening isn't in progress, ignoring.");
return;
}
mRecordingInProgress = false;
// Eager local evaluation to avoid reading a different or null value at closure-run-time
final DelegatingListener listenerToStop = this.mDelegatingListener;
run(service -> service.stopListening(listenerToStop));
}
}
void cancel(IRecognitionListener listener, boolean isShutdown) {
if (DEBUG) {
Slog.i(TAG, "#cancel");
}
if (!mConnected) {
tryRespondWithError(listener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
}
synchronized (mLock) {
if (mListener == null) {
if (DEBUG) {
Log.w(TAG, "#cancel called with no preceding #startListening - ignoring");
}
return;
}
if (mListener.asBinder() != listener.asBinder()) {
Log.w(TAG, "#cancel called with an unexpected listener");
tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
return;
}
// Temporary reference to allow for resetting the hard link mDelegatingListener to null.
IRecognitionListener delegatingListener = mDelegatingListener;
run(service -> service.cancel(delegatingListener, isShutdown));
mRecordingInProgress = false;
mSessionInProgress = false;
mDelegatingListener = null;
mListener = null;
// Schedule to unbind after cancel is delivered.
if (isShutdown) {
run(service -> unbind());
}
}
}
void shutdown() {
synchronized (mLock) {
if (this.mListener == null) {
if (DEBUG) {
Slog.i(TAG, "Package died, but session wasn't initialized. "
+ "Not invoking #cancel");
}
return;
}
}
cancel(mListener, true /* isShutdown */);
}
@Override // from ServiceConnector.Impl
protected void onServiceConnectionStatusChanged(
IRecognitionService service, boolean connected) {
mConnected = connected;
if (DEBUG) {
if (connected) {
Slog.i(TAG, "Connected to speech recognition service");
} else {
Slog.w(TAG, "Disconnected from speech recognition service");
}
}
synchronized (mLock) {
if (!connected) {
if (mListener == null) {
Slog.i(TAG, "Connection to speech recognition service lost, but no "
+ "#startListening has been invoked yet.");
return;
}
tryRespondWithError(mListener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
resetStateLocked();
}
}
}
@Override // from AbstractRemoteService
protected long getAutoDisconnectTimeoutMs() {
return PERMANENT_BOUND_TIMEOUT_MS;
}
private void resetStateLocked() {
mListener = null;
mDelegatingListener = null;
mSessionInProgress = false;
mRecordingInProgress = false;
}
private static void tryRespondWithError(IRecognitionListener listener, int errorCode) {
if (DEBUG) {
Slog.i(TAG, "Responding with error " + errorCode);
}
try {
if (listener != null) {
listener.onError(errorCode);
}
} catch (RemoteException e) {
Slog.w(TAG,
String.format("Failed to respond with an error %d to the client", errorCode),
e);
}
}
private static class DelegatingListener extends IRecognitionListener.Stub {
private final IRecognitionListener mRemoteListener;
private final Runnable mOnSessionComplete;
DelegatingListener(IRecognitionListener listener, Runnable onSessionComplete) {
mRemoteListener = listener;
mOnSessionComplete = onSessionComplete;
}
@Override
public void onReadyForSpeech(Bundle params) throws RemoteException {
mRemoteListener.onReadyForSpeech(params);
}
@Override
public void onBeginningOfSpeech() throws RemoteException {
mRemoteListener.onBeginningOfSpeech();
}
@Override
public void onRmsChanged(float rmsdB) throws RemoteException {
mRemoteListener.onRmsChanged(rmsdB);
}
@Override
public void onBufferReceived(byte[] buffer) throws RemoteException {
mRemoteListener.onBufferReceived(buffer);
}
@Override
public void onEndOfSpeech() throws RemoteException {
mRemoteListener.onEndOfSpeech();
}
@Override
public void onError(int error) throws RemoteException {
if (DEBUG) {
Slog.i(TAG, String.format("Error %d during recognition session", error));
}
mOnSessionComplete.run();
mRemoteListener.onError(error);
}
@Override
public void onResults(Bundle results) throws RemoteException {
if (DEBUG) {
Slog.i(TAG, "#onResults invoked for a recognition session");
}
mOnSessionComplete.run();
mRemoteListener.onResults(results);
}
@Override
public void onPartialResults(Bundle results) throws RemoteException {
mRemoteListener.onPartialResults(results);
}
@Override
public void onEvent(int eventType, Bundle params) throws RemoteException {
mRemoteListener.onEvent(eventType, params);
}
}
}