Merge "Implement preriodic trusted process restarting for hotword detectors." into sc-dev am: 56686819a8

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14679832

Change-Id: I746f8771183b3b0bcea3746410334f94a5cdfd3e
This commit is contained in:
Sergey Volnov
2021-06-22 15:08:06 +00:00
committed by Automerger Merge Worker
9 changed files with 443 additions and 263 deletions

View File

@@ -263,6 +263,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
private static final int MSG_DETECTION_RESUME = 5; private static final int MSG_DETECTION_RESUME = 5;
private static final int MSG_HOTWORD_REJECTED = 6; private static final int MSG_HOTWORD_REJECTED = 6;
private static final int MSG_HOTWORD_STATUS_REPORTED = 7; private static final int MSG_HOTWORD_STATUS_REPORTED = 7;
private static final int MSG_PROCESS_RESTARTED = 8;
private final String mText; private final String mText;
private final Locale mLocale; private final Locale mLocale;
@@ -1212,6 +1213,12 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
message.arg1 = status; message.arg1 = status;
message.sendToTarget(); message.sendToTarget();
} }
@Override
public void onProcessRestarted() {
Slog.i(TAG, "onProcessRestarted");
mHandler.sendEmptyMessage(MSG_PROCESS_RESTARTED);
}
} }
class MyHandler extends Handler { class MyHandler extends Handler {
@@ -1246,6 +1253,9 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
case MSG_HOTWORD_STATUS_REPORTED: case MSG_HOTWORD_STATUS_REPORTED:
mExternalCallback.onHotwordDetectionServiceInitialized(msg.arg1); mExternalCallback.onHotwordDetectionServiceInitialized(msg.arg1);
break; break;
case MSG_PROCESS_RESTARTED:
mExternalCallback.onHotwordDetectionServiceRestarted();
break;
default: default:
super.handleMessage(msg); super.handleMessage(msg);
} }

View File

@@ -291,9 +291,7 @@ public abstract class HotwordDetectionService extends Service {
@Nullable PersistableBundle options, @Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory, @Nullable SharedMemory sharedMemory,
@DurationMillisLong long callbackTimeoutMillis, @DurationMillisLong long callbackTimeoutMillis,
@Nullable IntConsumer statusCallback) { @Nullable IntConsumer statusCallback) {}
// TODO: Handle the unimplemented case by throwing?
}
/** /**
* Called when the {@link VoiceInteractionService} requests that this service * Called when the {@link VoiceInteractionService} requests that this service

View File

@@ -122,7 +122,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
this.mCallback = callback; this.mCallback = callback;
} }
/** TODO: onDetected */ /** Called when the detected result is valid. */
@Override @Override
public void onDetected( public void onDetected(
@Nullable HotwordDetectedResult hotwordDetectedResult, @Nullable HotwordDetectedResult hotwordDetectedResult,
@@ -150,33 +150,45 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
public void onKeyphraseDetected( public void onKeyphraseDetected(
SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, SoundTrigger.KeyphraseRecognitionEvent recognitionEvent,
HotwordDetectedResult result) { HotwordDetectedResult result) {
if (DEBUG) {
Slog.i(TAG, "Ignored #onKeyphraseDetected event");
}
} }
@Override @Override
public void onGenericSoundTriggerDetected( public void onGenericSoundTriggerDetected(
SoundTrigger.GenericRecognitionEvent recognitionEvent) throws RemoteException { SoundTrigger.GenericRecognitionEvent recognitionEvent) throws RemoteException {
if (DEBUG) {
Slog.i(TAG, "Ignored #onGenericSoundTriggerDetected event");
}
} }
@Override @Override
public void onRejected(HotwordRejectedResult result) throws RemoteException { public void onRejected(HotwordRejectedResult result) throws RemoteException {
if (DEBUG) {
Slog.i(TAG, "Ignored #onRejected event");
}
} }
@Override @Override
public void onError(int status) throws RemoteException { public void onError(int status) throws RemoteException {
if (DEBUG) {
Slog.i(TAG, "Ignored #onError (" + status + ") event");
}
} }
@Override @Override
public void onRecognitionPaused() throws RemoteException { public void onRecognitionPaused() throws RemoteException {
if (DEBUG) {
Slog.i(TAG, "Ignored #onRecognitionPaused event");
}
} }
@Override @Override
public void onRecognitionResumed() throws RemoteException { public void onRecognitionResumed() throws RemoteException {
if (DEBUG) {
Slog.i(TAG, "Ignored #onRecognitionResumed event");
}
} }
@Override @Override
@@ -187,6 +199,14 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
mCallback, mCallback,
status)); status));
} }
@Override
public void onProcessRestarted() throws RemoteException {
Slog.v(TAG, "onProcessRestarted()");
mHandler.sendMessage(obtainMessage(
HotwordDetector.Callback::onHotwordDetectionServiceRestarted,
mCallback));
}
} }
/** @hide */ /** @hide */

View File

@@ -78,4 +78,7 @@ oneway interface IHotwordRecognitionStatusCallback {
* @param status The status about the result of requesting update state action. * @param status The status about the result of requesting update state action.
*/ */
void onStatusReported(int status); void onStatusReported(int status);
/** Called when the hotword detection process is restarted */
void onProcessRestarted();
} }

View File

@@ -228,7 +228,7 @@ public interface ServiceConnector<I extends IInterface> {
private final int mBindingFlags; private final int mBindingFlags;
private final @Nullable Function<IBinder, I> mBinderAsInterface; private final @Nullable Function<IBinder, I> mBinderAsInterface;
private final @NonNull Handler mHandler; private final @NonNull Handler mHandler;
private final @NonNull Executor mExecutor; protected final @NonNull Executor mExecutor;
private volatile I mService = null; private volatile I mService = null;
private boolean mBinding = false; private boolean mBinding = false;

View File

@@ -29,11 +29,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger; import android.hardware.soundtrigger.SoundTrigger;
import android.media.AudioAttributes;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
@@ -66,12 +62,16 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/** /**
* A class that provides the communication with the HotwordDetectionService. * A class that provides the communication with the HotwordDetectionService.
@@ -81,33 +81,38 @@ final class HotwordDetectionConnection {
// TODO (b/177502877): Set the Debug flag to false before shipping. // TODO (b/177502877): Set the Debug flag to false before shipping.
private static final boolean DEBUG = true; private static final boolean DEBUG = true;
// Number of bytes per sample of audio (which is a short).
private static final int BYTES_PER_SAMPLE = 2;
// TODO: These constants need to be refined. // TODO: These constants need to be refined.
private static final long VALIDATION_TIMEOUT_MILLIS = 3000; private static final long VALIDATION_TIMEOUT_MILLIS = 3000;
private static final long VOICE_INTERACTION_TIMEOUT_TO_OPEN_MIC_MILLIS = 2000;
private static final int MAX_STREAMING_SECONDS = 10;
private static final int MICROPHONE_BUFFER_LENGTH_SECONDS = 8;
private static final int HOTWORD_AUDIO_LENGTH_SECONDS = 3;
private static final long MAX_UPDATE_TIMEOUT_MILLIS = 6000; private static final long MAX_UPDATE_TIMEOUT_MILLIS = 6000;
private static final Duration MAX_UPDATE_TIMEOUT_DURATION =
Duration.ofMillis(MAX_UPDATE_TIMEOUT_MILLIS);
private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool(); private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool();
// TODO: This may need to be a Handler(looper) // TODO: This may need to be a Handler(looper)
private final ScheduledExecutorService mScheduledExecutorService = private final ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(); Executors.newSingleThreadScheduledExecutor();
private final AtomicBoolean mUpdateStateFinish = new AtomicBoolean(false); private final AtomicBoolean mUpdateStateAfterStartFinished = new AtomicBoolean(false);
private final @NonNull ServiceConnectionFactory mServiceConnectionFactory;
final Object mLock; final Object mLock;
final int mVoiceInteractionServiceUid; final int mVoiceInteractionServiceUid;
final ComponentName mDetectionComponentName; final ComponentName mDetectionComponentName;
final int mUser; final int mUser;
final Context mContext; final Context mContext;
final @NonNull ServiceConnector<IHotwordDetectionService> mRemoteHotwordDetectionService;
boolean mBound;
volatile HotwordDetectionServiceIdentity mIdentity; volatile HotwordDetectionServiceIdentity mIdentity;
private IHotwordRecognitionStatusCallback mCallback;
private IMicrophoneHotwordDetectionVoiceInteractionCallback mSoftwareCallback;
private Instant mLastRestartInstant;
private ScheduledFuture<?> mCancellationTaskFuture;
@GuardedBy("mLock") @GuardedBy("mLock")
private ParcelFileDescriptor mCurrentAudioSink; private ParcelFileDescriptor mCurrentAudioSink;
@GuardedBy("mLock")
private boolean mValidatingDspTrigger = false;
@GuardedBy("mLock")
private boolean mPerformingSoftwareHotwordDetection;
private @NonNull ServiceConnection mRemoteHotwordDetectionService;
HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid, HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid,
ComponentName serviceName, int userId, boolean bindInstantServiceAllowed, ComponentName serviceName, int userId, boolean bindInstantServiceAllowed,
@@ -121,50 +126,36 @@ final class HotwordDetectionConnection {
final Intent intent = new Intent(HotwordDetectionService.SERVICE_INTERFACE); final Intent intent = new Intent(HotwordDetectionService.SERVICE_INTERFACE);
intent.setComponent(mDetectionComponentName); intent.setComponent(mDetectionComponentName);
mRemoteHotwordDetectionService = new ServiceConnector.Impl<IHotwordDetectionService>( mServiceConnectionFactory = new ServiceConnectionFactory(intent, bindInstantServiceAllowed);
mContext, intent, bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0, mUser,
IHotwordDetectionService.Stub::asInterface) {
@Override // from ServiceConnector.Impl
protected void onServiceConnectionStatusChanged(IHotwordDetectionService service,
boolean connected) {
if (DEBUG) {
Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected);
}
synchronized (mLock) {
mBound = connected;
}
}
@Override mRemoteHotwordDetectionService = mServiceConnectionFactory.create();
protected long getAutoDisconnectTimeoutMs() {
return -1;
}
@Override
public void binderDied() {
super.binderDied();
Slog.w(TAG, "binderDied");
try {
callback.onError(-1);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to report onError status: " + e);
}
}
};
mRemoteHotwordDetectionService.connect();
if (callback == null) { if (callback == null) {
updateStateLocked(options, sharedMemory); updateStateLocked(options, sharedMemory);
return; return;
} }
updateAudioFlinger(); mCallback = callback;
updateContentCaptureManager();
updateStateWithCallbackLocked(options, sharedMemory, callback); mLastRestartInstant = Instant.now();
updateStateAfterProcessStart(options, sharedMemory);
// TODO(volnov): we need to be smarter here, e.g. schedule it a bit more often, but wait
// until the current session is closed.
mCancellationTaskFuture = mScheduledExecutorService.scheduleAtFixedRate(() -> {
if (DEBUG) {
Slog.i(TAG, "Time to restart the process, TTL has passed");
}
synchronized (mLock) {
restartProcessLocked();
}
}, 30, 30, TimeUnit.MINUTES);
} }
private void updateStateWithCallbackLocked(PersistableBundle options, private void updateStateAfterProcessStart(
SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) { PersistableBundle options, SharedMemory sharedMemory) {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "updateStateWithCallbackLocked"); Slog.d(TAG, "updateStateAfterProcessStart");
} }
mRemoteHotwordDetectionService.postAsync(service -> { mRemoteHotwordDetectionService.postAsync(service -> {
AndroidFuture<Void> future = new AndroidFuture<>(); AndroidFuture<Void> future = new AndroidFuture<>();
@@ -183,21 +174,21 @@ final class HotwordDetectionConnection {
mIdentity = mIdentity =
new HotwordDetectionServiceIdentity(uid, mVoiceInteractionServiceUid); new HotwordDetectionServiceIdentity(uid, mVoiceInteractionServiceUid);
future.complete(null); future.complete(null);
if (mUpdateStateAfterStartFinished.getAndSet(true)) {
Slog.w(TAG, "call callback after timeout");
return;
}
int status = bundle != null ? bundle.getInt(
KEY_INITIALIZATION_STATUS,
INITIALIZATION_STATUS_UNKNOWN)
: INITIALIZATION_STATUS_UNKNOWN;
// Add the protection to avoid unexpected status
if (status > HotwordDetectionService.getMaxCustomInitializationStatus()
&& status != INITIALIZATION_STATUS_UNKNOWN) {
status = INITIALIZATION_STATUS_UNKNOWN;
}
try { try {
if (mUpdateStateFinish.getAndSet(true)) { mCallback.onStatusReported(status);
Slog.w(TAG, "call callback after timeout");
return;
}
int status = bundle != null ? bundle.getInt(
KEY_INITIALIZATION_STATUS,
INITIALIZATION_STATUS_UNKNOWN)
: INITIALIZATION_STATUS_UNKNOWN;
// Add the protection to avoid unexpected status
if (status > HotwordDetectionService.getMaxCustomInitializationStatus()
&& status != INITIALIZATION_STATUS_UNKNOWN) {
status = INITIALIZATION_STATUS_UNKNOWN;
}
callback.onStatusReported(status);
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.w(TAG, "Failed to report initialization status: " + e); Slog.w(TAG, "Failed to report initialization status: " + e);
} }
@@ -214,13 +205,13 @@ final class HotwordDetectionConnection {
.whenComplete((res, err) -> { .whenComplete((res, err) -> {
if (err instanceof TimeoutException) { if (err instanceof TimeoutException) {
Slog.w(TAG, "updateState timed out"); Slog.w(TAG, "updateState timed out");
if (mUpdateStateAfterStartFinished.getAndSet(true)) {
return;
}
try { try {
if (mUpdateStateFinish.getAndSet(true)) { mCallback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN);
return;
}
callback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN);
} catch (RemoteException e) { } catch (RemoteException e) {
Slog.w(TAG, "Failed to report initialization status: " + e); Slog.w(TAG, "Failed to report initialization status UNKNOWN", e);
} }
} else if (err != null) { } else if (err != null) {
Slog.w(TAG, "Failed to update state: " + err); Slog.w(TAG, "Failed to update state: " + err);
@@ -230,27 +221,9 @@ final class HotwordDetectionConnection {
}); });
} }
private void updateAudioFlinger() {
// TODO: Consider using a proxy that limits the exposed API surface.
IBinder audioFlinger = ServiceManager.getService("media.audio_flinger");
if (audioFlinger == null) {
throw new IllegalStateException("Service media.audio_flinger wasn't found.");
}
mRemoteHotwordDetectionService.post(service -> service.updateAudioFlinger(audioFlinger));
}
private void updateContentCaptureManager() {
IBinder b = ServiceManager
.getService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
IContentCaptureManager binderService = IContentCaptureManager.Stub.asInterface(b);
mRemoteHotwordDetectionService.post(
service -> service.updateContentCaptureManager(binderService,
new ContentCaptureOptions(null)));
}
private boolean isBound() { private boolean isBound() {
synchronized (mLock) { synchronized (mLock) {
return mBound; return mRemoteHotwordDetectionService.isBound();
} }
} }
@@ -258,18 +231,25 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "cancelLocked"); Slog.d(TAG, "cancelLocked");
} }
if (mBound) { if (mRemoteHotwordDetectionService.isBound()) {
mRemoteHotwordDetectionService.unbind(); mRemoteHotwordDetectionService.unbind();
mBound = false;
LocalServices.getService(PermissionManagerServiceInternal.class) LocalServices.getService(PermissionManagerServiceInternal.class)
.setHotwordDetectionServiceProvider(null); .setHotwordDetectionServiceProvider(null);
mIdentity = null; mIdentity = null;
} }
mCancellationTaskFuture.cancel(/* may interrupt */ true);
} }
void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) { void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) {
mRemoteHotwordDetectionService.run( // Prevent doing the init late, so restart is handled equally to a clean process start.
service -> service.updateState(options, sharedMemory, null /* callback */)); // TODO(b/191742511): this logic needs a test
if (!mUpdateStateAfterStartFinished.get()
&& Instant.now().minus(MAX_UPDATE_TIMEOUT_DURATION).isBefore(mLastRestartInstant)) {
updateStateAfterProcessStart(options, sharedMemory);
} else {
mRemoteHotwordDetectionService.run(
service -> service.updateState(options, sharedMemory, null /* callback */));
}
} }
void startListeningFromMic( void startListeningFromMic(
@@ -278,7 +258,20 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "startListeningFromMic"); Slog.d(TAG, "startListeningFromMic");
} }
mSoftwareCallback = callback;
synchronized (mLock) {
if (mPerformingSoftwareHotwordDetection) {
Slog.i(TAG, "Hotword validation is already in progress, ignoring.");
return;
}
mPerformingSoftwareHotwordDetection = true;
startListeningFromMicLocked();
}
}
private void startListeningFromMicLocked() {
// TODO: consider making this a non-anonymous class. // TODO: consider making this a non-anonymous class.
IDspHotwordDetectionCallback internalCallback = new IDspHotwordDetectionCallback.Stub() { IDspHotwordDetectionCallback internalCallback = new IDspHotwordDetectionCallback.Stub() {
@Override @Override
@@ -286,15 +279,22 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "onDetected"); Slog.d(TAG, "onDetected");
} }
callback.onDetected(result, null, null); synchronized (mLock) {
if (mPerformingSoftwareHotwordDetection) {
mSoftwareCallback.onDetected(result, null, null);
mPerformingSoftwareHotwordDetection = false;
} else {
Slog.i(TAG, "Hotword detection has already completed");
}
}
} }
@Override @Override
public void onRejected(HotwordRejectedResult result) throws RemoteException { public void onRejected(HotwordRejectedResult result) throws RemoteException {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "onRejected"); Slog.wtf(TAG, "onRejected");
} }
// onRejected isn't allowed here // onRejected isn't allowed here, and we are not expecting it.
} }
}; };
@@ -315,6 +315,7 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "startListeningFromExternalSource"); Slog.d(TAG, "startListeningFromExternalSource");
} }
handleExternalSourceHotwordDetection( handleExternalSourceHotwordDetection(
audioStream, audioStream,
audioFormat, audioFormat,
@@ -326,18 +327,27 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "stopListening"); Slog.d(TAG, "stopListening");
} }
mRemoteHotwordDetectionService.run(service -> service.stopDetection());
synchronized (mLock) { synchronized (mLock) {
if (mCurrentAudioSink != null) { stopListeningLocked();
Slog.i(TAG, "Closing audio stream to hotword detector: stopping requested");
bestEffortClose(mCurrentAudioSink);
}
mCurrentAudioSink = null;
} }
} }
private void stopListeningLocked() {
if (!mPerformingSoftwareHotwordDetection) {
Slog.i(TAG, "Hotword detection is not running");
return;
}
mPerformingSoftwareHotwordDetection = false;
mRemoteHotwordDetectionService.run(IHotwordDetectionService::stopDetection);
if (mCurrentAudioSink != null) {
Slog.i(TAG, "Closing audio stream to hotword detector: stopping requested");
bestEffortClose(mCurrentAudioSink);
}
mCurrentAudioSink = null;
}
void triggerHardwareRecognitionEventForTestLocked( void triggerHardwareRecognitionEventForTestLocked(
SoundTrigger.KeyphraseRecognitionEvent event, SoundTrigger.KeyphraseRecognitionEvent event,
IHotwordRecognitionStatusCallback callback) { IHotwordRecognitionStatusCallback callback) {
@@ -358,7 +368,14 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "onDetected"); Slog.d(TAG, "onDetected");
} }
externalCallback.onKeyphraseDetected(recognitionEvent, result); synchronized (mLock) {
if (mValidatingDspTrigger) {
mValidatingDspTrigger = false;
externalCallback.onKeyphraseDetected(recognitionEvent, result);
} else {
Slog.i(TAG, "Ignored hotword detected since trigger has been handled");
}
}
} }
@Override @Override
@@ -366,16 +383,26 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "onRejected"); Slog.d(TAG, "onRejected");
} }
externalCallback.onRejected(result); synchronized (mLock) {
if (mValidatingDspTrigger) {
mValidatingDspTrigger = false;
externalCallback.onRejected(result);
} else {
Slog.i(TAG, "Ignored hotword rejected since trigger has been handled");
}
}
} }
}; };
mRemoteHotwordDetectionService.run( synchronized (mLock) {
service -> service.detectFromDspSource( mValidatingDspTrigger = true;
recognitionEvent, mRemoteHotwordDetectionService.run(
recognitionEvent.getCaptureFormat(), service -> service.detectFromDspSource(
VALIDATION_TIMEOUT_MILLIS, recognitionEvent,
internalCallback)); recognitionEvent.getCaptureFormat(),
VALIDATION_TIMEOUT_MILLIS,
internalCallback));
}
} }
private void detectFromDspSource(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, private void detectFromDspSource(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent,
@@ -391,7 +418,14 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "onDetected"); Slog.d(TAG, "onDetected");
} }
externalCallback.onKeyphraseDetected(recognitionEvent, result); synchronized (mLock) {
if (!mValidatingDspTrigger) {
Slog.i(TAG, "Ignoring #onDetected due to a process restart");
return;
}
mValidatingDspTrigger = false;
externalCallback.onKeyphraseDetected(recognitionEvent, result);
}
} }
@Override @Override
@@ -399,16 +433,88 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "onRejected"); Slog.d(TAG, "onRejected");
} }
externalCallback.onRejected(result); synchronized (mLock) {
if (!mValidatingDspTrigger) {
Slog.i(TAG, "Ignoring #onRejected due to a process restart");
return;
}
mValidatingDspTrigger = false;
externalCallback.onRejected(result);
}
} }
}; };
mRemoteHotwordDetectionService.run( synchronized (mLock) {
service -> service.detectFromDspSource( mValidatingDspTrigger = true;
recognitionEvent, mRemoteHotwordDetectionService.run(
recognitionEvent.getCaptureFormat(), service -> service.detectFromDspSource(
VALIDATION_TIMEOUT_MILLIS, recognitionEvent,
internalCallback)); recognitionEvent.getCaptureFormat(),
VALIDATION_TIMEOUT_MILLIS,
internalCallback));
}
}
void forceRestart() {
if (DEBUG) {
Slog.i(TAG, "Requested to restart the service internally. Performing the restart");
}
synchronized (mLock) {
restartProcessLocked();
}
}
private void restartProcessLocked() {
if (DEBUG) {
Slog.i(TAG, "Restarting hotword detection process");
}
ServiceConnection oldConnection = mRemoteHotwordDetectionService;
// TODO(volnov): this can be done after connect() has been successful.
if (mValidatingDspTrigger) {
// We're restarting the process while it's processing a DSP trigger, so report a
// rejection. This also allows the Interactor to startReco again
try {
mCallback.onRejected(new HotwordRejectedResult.Builder().build());
} catch (RemoteException e) {
Slog.w(TAG, "Failed to call #rejected");
}
mValidatingDspTrigger = false;
}
mUpdateStateAfterStartFinished.set(false);
mLastRestartInstant = Instant.now();
// Recreate connection to reset the cache.
mRemoteHotwordDetectionService = mServiceConnectionFactory.create();
if (DEBUG) {
Slog.i(TAG, "Started the new process, issuing #onProcessRestarted");
}
try {
mCallback.onProcessRestarted();
} catch (RemoteException e) {
Slog.w(TAG, "Failed to communicate #onProcessRestarted", e);
}
// Restart listening from microphone if the hotword process has been restarted.
if (mPerformingSoftwareHotwordDetection) {
Slog.i(TAG, "Process restarted: calling startRecognition() again");
startListeningFromMicLocked();
}
if (mCurrentAudioSink != null) {
Slog.i(TAG, "Closing external audio stream to hotword detector: process restarted");
bestEffortClose(mCurrentAudioSink);
mCurrentAudioSink = null;
}
if (DEBUG) {
Slog.i(TAG, "#onProcessRestarted called, unbinding from the old process");
}
oldConnection.ignoreConnectionStatusEvents();
oldConnection.unbind();
} }
static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub { static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub {
@@ -462,139 +568,13 @@ final class HotwordDetectionConnection {
} }
} }
// TODO: figure out if we need to let the client configure some of the parameters.
private static AudioRecord createAudioRecord(
@NonNull SoundTrigger.KeyphraseRecognitionEvent recognitionEvent) {
int sampleRate = recognitionEvent.getCaptureFormat().getSampleRate();
return new AudioRecord(
new AudioAttributes.Builder()
.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD).build(),
recognitionEvent.getCaptureFormat(),
getBufferSizeInBytes(
sampleRate,
MAX_STREAMING_SECONDS,
recognitionEvent.getCaptureFormat().getChannelCount()),
recognitionEvent.getCaptureSession());
}
@Nullable
private AudioRecord createMicAudioRecord(AudioFormat audioFormat) {
if (DEBUG) {
Slog.i(TAG, "#createAudioRecord");
}
try {
AudioRecord audioRecord = new AudioRecord(
new AudioAttributes.Builder()
.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD).build(),
audioFormat,
getBufferSizeInBytes(
audioFormat.getSampleRate(),
MICROPHONE_BUFFER_LENGTH_SECONDS,
audioFormat.getChannelCount()),
AudioManager.AUDIO_SESSION_ID_GENERATE);
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Slog.w(TAG, "Failed to initialize AudioRecord");
audioRecord.release();
return null;
}
return audioRecord;
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Failed to create AudioRecord", e);
return null;
}
}
@Nullable
private AudioRecord createFakeAudioRecord() {
if (DEBUG) {
Slog.i(TAG, "#createFakeAudioRecord");
}
try {
AudioRecord audioRecord = new AudioRecord.Builder()
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(32000)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
.setAudioAttributes(new AudioAttributes.Builder()
.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD).build())
.setBufferSizeInBytes(
AudioRecord.getMinBufferSize(32000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT) * 2)
.build();
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Slog.w(TAG, "Failed to initialize AudioRecord");
audioRecord.release();
return null;
}
return audioRecord;
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Failed to create AudioRecord", e);
}
return null;
}
/**
* Returns the number of bytes required to store {@code bufferLengthSeconds} of audio sampled at
* {@code sampleRate} Hz, using the format returned by DSP audio capture.
*/
private static int getBufferSizeInBytes(
int sampleRate, int bufferLengthSeconds, int intChannelCount) {
return BYTES_PER_SAMPLE * sampleRate * bufferLengthSeconds * intChannelCount;
}
private static Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
ParcelFileDescriptor[] fileDescriptors;
try {
fileDescriptors = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
Slog.e(TAG, "Failed to create audio stream pipe", e);
return null;
}
return Pair.create(fileDescriptors[0], fileDescriptors[1]);
}
public void dump(String prefix, PrintWriter pw) { public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("mBound="); pw.println(mBound); pw.print(prefix);
} pw.print("mBound=" + mRemoteHotwordDetectionService.isBound());
pw.print(", mValidatingDspTrigger=" + mValidatingDspTrigger);
private interface AudioReader extends Closeable { pw.print(", mPerformingSoftwareHotwordDetection=" + mPerformingSoftwareHotwordDetection);
int read(byte[] dest, int offset, int length) throws IOException; pw.print(", mRestartCount=" + mServiceConnectionFactory.mRestartCount);
pw.println(", mLastRestartInstant=" + mLastRestartInstant);
static AudioReader createFromInputStream(InputStream is) {
return new AudioReader() {
@Override
public int read(byte[] dest, int offset, int length) throws IOException {
return is.read(dest, offset, length);
}
@Override
public void close() throws IOException {
is.close();
}
};
}
static AudioReader createFromAudioRecord(AudioRecord record) {
record.startRecording();
return new AudioReader() {
@Override
public int read(byte[] dest, int offset, int length) throws IOException {
return record.read(dest, offset, length);
}
@Override
public void close() throws IOException {
record.stop();
record.release();
}
};
}
} }
private void handleExternalSourceHotwordDetection( private void handleExternalSourceHotwordDetection(
@@ -605,8 +585,7 @@ final class HotwordDetectionConnection {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "#handleExternalSourceHotwordDetection"); Slog.d(TAG, "#handleExternalSourceHotwordDetection");
} }
AudioReader audioSource = AudioReader.createFromInputStream( InputStream audioSource = new ParcelFileDescriptor.AutoCloseInputStream(audioStream);
new ParcelFileDescriptor.AutoCloseInputStream(audioStream));
Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe(); Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe();
if (clientPipe == null) { if (clientPipe == null) {
@@ -621,7 +600,7 @@ final class HotwordDetectionConnection {
} }
mAudioCopyExecutor.execute(() -> { mAudioCopyExecutor.execute(() -> {
try (AudioReader source = audioSource; try (InputStream source = audioSource;
OutputStream fos = OutputStream fos =
new ParcelFileDescriptor.AutoCloseOutputStream(serviceAudioSink)) { new ParcelFileDescriptor.AutoCloseOutputStream(serviceAudioSink)) {
@@ -681,6 +660,150 @@ final class HotwordDetectionConnection {
})); }));
} }
private class ServiceConnectionFactory {
private final Intent mIntent;
private final int mBindingFlags;
private int mRestartCount = 0;
ServiceConnectionFactory(@NonNull Intent intent, boolean bindInstantServiceAllowed) {
mIntent = intent;
mBindingFlags = bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0;
}
ServiceConnection create() {
ServiceConnection connection =
new ServiceConnection(mContext, mIntent, mBindingFlags, mUser,
IHotwordDetectionService.Stub::asInterface, ++mRestartCount);
connection.connect();
updateAudioFlinger(connection);
updateContentCaptureManager(connection);
return connection;
}
}
private class ServiceConnection extends ServiceConnector.Impl<IHotwordDetectionService> {
private final Object mLock = new Object();
private final Intent mIntent;
private final int mBindingFlags;
private final int mInstanceNumber;
private boolean mRespectServiceConnectionStatusChanged = true;
private boolean mIsBound = false;
ServiceConnection(@NonNull Context context,
@NonNull Intent intent, int bindingFlags, int userId,
@Nullable Function<IBinder, IHotwordDetectionService> binderAsInterface,
int instanceNumber) {
super(context, intent, bindingFlags, userId, binderAsInterface);
this.mIntent = intent;
this.mBindingFlags = bindingFlags;
this.mInstanceNumber = instanceNumber;
}
@Override // from ServiceConnector.Impl
protected void onServiceConnectionStatusChanged(IHotwordDetectionService service,
boolean connected) {
if (DEBUG) {
Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected);
}
synchronized (mLock) {
if (!mRespectServiceConnectionStatusChanged) {
if (DEBUG) {
Slog.d(TAG, "Ignored onServiceConnectionStatusChanged event");
}
return;
}
mIsBound = connected;
}
}
@Override
protected long getAutoDisconnectTimeoutMs() {
return -1;
}
@Override
public void binderDied() {
super.binderDied();
synchronized (mLock) {
if (!mRespectServiceConnectionStatusChanged) {
if (DEBUG) {
Slog.d(TAG, "Ignored #binderDied event");
}
return;
}
Slog.w(TAG, "binderDied");
try {
mCallback.onError(-1);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to report onError status: " + e);
}
}
}
@Override
protected boolean bindService(
@NonNull android.content.ServiceConnection serviceConnection) {
try {
return mContext.bindIsolatedService(
mIntent,
Context.BIND_AUTO_CREATE | mBindingFlags,
"hotword_detector_" + mInstanceNumber,
mExecutor,
serviceConnection);
} catch (IllegalArgumentException e) {
Slog.wtf(TAG, "Can't bind to the hotword detection service!", e);
return false;
}
}
boolean isBound() {
synchronized (mLock) {
return mIsBound;
}
}
void ignoreConnectionStatusEvents() {
synchronized (mLock) {
mRespectServiceConnectionStatusChanged = false;
}
}
}
private static Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
ParcelFileDescriptor[] fileDescriptors;
try {
fileDescriptors = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
Slog.e(TAG, "Failed to create audio stream pipe", e);
return null;
}
return Pair.create(fileDescriptors[0], fileDescriptors[1]);
}
private static void updateAudioFlinger(ServiceConnection connection) {
// TODO: Consider using a proxy that limits the exposed API surface.
IBinder audioFlinger = ServiceManager.getService("media.audio_flinger");
if (audioFlinger == null) {
throw new IllegalStateException("Service media.audio_flinger wasn't found.");
}
connection.post(service -> service.updateAudioFlinger(audioFlinger));
}
private static void updateContentCaptureManager(ServiceConnection connection) {
IBinder b = ServiceManager
.getService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
IContentCaptureManager binderService = IContentCaptureManager.Stub.asInterface(b);
connection.post(
service -> service.updateContentCaptureManager(binderService,
new ContentCaptureOptions(null)));
}
private static void bestEffortClose(Closeable closeable) { private static void bestEffortClose(Closeable closeable) {
try { try {
closeable.close(); closeable.close();

View File

@@ -795,6 +795,10 @@ public class VoiceInteractionManagerService extends SystemService {
Settings.Secure.ASSISTANT, null, userHandle); Settings.Secure.ASSISTANT, null, userHandle);
} }
void forceRestartHotwordDetector() {
mImpl.forceRestartHotwordDetector();
}
@Override @Override
public void showSession(Bundle args, int flags) { public void showSession(Bundle args, int flags) {
synchronized (this) { synchronized (this) {

View File

@@ -562,6 +562,14 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
&& (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0; && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
} }
void forceRestartHotwordDetector() {
if (mHotwordDetectionConnection == null) {
Slog.w(TAG, "Failed to force-restart hotword detection: no hotword detection active");
return;
}
mHotwordDetectionConnection.forceRestart();
}
public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) { public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!mValid) { if (!mValid) {
pw.print(" NOT VALID: "); pw.print(" NOT VALID: ");

View File

@@ -54,6 +54,8 @@ final class VoiceInteractionManagerServiceShellCommand extends ShellCommand {
return requestHide(pw); return requestHide(pw);
case "disable": case "disable":
return requestDisable(pw); return requestDisable(pw);
case "restart-detection":
return requestRestartDetection(pw);
default: default:
return handleDefaultCommands(cmd); return handleDefaultCommands(cmd);
} }
@@ -74,6 +76,8 @@ final class VoiceInteractionManagerServiceShellCommand extends ShellCommand {
pw.println(""); pw.println("");
pw.println(" disable [true|false]"); pw.println(" disable [true|false]");
pw.println(" Temporarily disable (when true) service"); pw.println(" Temporarily disable (when true) service");
pw.println(" restart-detection");
pw.println(" Force a restart of a hotword detection service");
pw.println(""); pw.println("");
} }
} }
@@ -143,6 +147,16 @@ final class VoiceInteractionManagerServiceShellCommand extends ShellCommand {
return 0; return 0;
} }
private int requestRestartDetection(PrintWriter pw) {
Slog.i(TAG, "requestRestartDetection()");
try {
mService.forceRestartHotwordDetector();
} catch (Exception e) {
return handleError(pw, "requestRestartDetection()", e);
}
return 0;
}
private static int handleError(PrintWriter pw, String message, Exception e) { private static int handleError(PrintWriter pw, String message, Exception e) {
Slog.e(TAG, "error calling " + message, e); Slog.e(TAG, "error calling " + message, e);
pw.printf("Error calling %s: %s\n", message, e); pw.printf("Error calling %s: %s\n", message, e);