From 9c9f5aa6a1922e46f9f845d19758591694fe19ed Mon Sep 17 00:00:00 2001 From: lpeter Date: Wed, 31 Mar 2021 09:01:35 +0800 Subject: [PATCH] Implement timeout mechanism when HotwordDetectionService initializes Bug: 183684347 Test: atest CtsVoiceInteractionTestCases Test: atest CtsVoiceInteractionTestCases --instant Change-Id: I77d29491b055ee2a45aa088df4c1cd54e604c259 --- .../voice/HotwordDetectionService.java | 15 ++-- .../voice/IHotwordDetectionService.aidl | 5 +- .../HotwordDetectionConnection.java | 81 +++++++++++++++++-- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index 7c14c2e19eb1f..23b2103ce5fd9 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -28,8 +28,10 @@ import android.annotation.SystemApi; import android.app.Service; import android.content.Intent; import android.media.AudioFormat; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; @@ -37,8 +39,6 @@ import android.os.RemoteException; import android.os.SharedMemory; import android.util.Log; -import com.android.internal.app.IHotwordRecognitionStatusCallback; - import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -58,6 +58,8 @@ public abstract class HotwordDetectionService extends Service { private static final boolean DBG = true; private static final long UPDATE_TIMEOUT_MILLIS = 5000; + /** @hide */ + public static final String KEY_INITIALIZATION_STATUS = "initialization_status"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -141,7 +143,7 @@ public abstract class HotwordDetectionService extends Service { @Override public void updateState(PersistableBundle options, SharedMemory sharedMemory, - IHotwordRecognitionStatusCallback callback) throws RemoteException { + IRemoteCallback callback) throws RemoteException { if (DBG) { Log.d(TAG, "#updateState"); } @@ -314,14 +316,15 @@ public abstract class HotwordDetectionService extends Service { } private void onUpdateStateInternal(@Nullable PersistableBundle options, - @Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) { - // TODO (b/183684347): Implement timeout case. + @Nullable SharedMemory sharedMemory, IRemoteCallback callback) { IntConsumer intConsumer = null; if (callback != null) { intConsumer = value -> { try { - callback.onStatusReported(value); + Bundle status = new Bundle(); + status.putInt(KEY_INITIALIZATION_STATUS, value); + callback.sendResult(status); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/service/voice/IHotwordDetectionService.aidl b/core/java/android/service/voice/IHotwordDetectionService.aidl index cac8333a58556..d2421603e5544 100644 --- a/core/java/android/service/voice/IHotwordDetectionService.aidl +++ b/core/java/android/service/voice/IHotwordDetectionService.aidl @@ -17,13 +17,12 @@ package android.service.voice; import android.media.AudioFormat; +import android.os.IRemoteCallback; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.SharedMemory; import android.service.voice.IDspHotwordDetectionCallback; -import com.android.internal.app.IHotwordRecognitionStatusCallback; - /** * Provide the interface to communicate with hotword detection service. * @@ -44,5 +43,5 @@ oneway interface IHotwordDetectionService { in IDspHotwordDetectionCallback callback); void updateState(in PersistableBundle options, in SharedMemory sharedMemory, - in IHotwordRecognitionStatusCallback callback); + in IRemoteCallback callback); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 5d541e9cab5da..2d979ca0a36ab 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -18,6 +18,8 @@ package com.android.server.voiceinteraction; import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL; import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE; +import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_UNKNOWN; +import static android.service.voice.HotwordDetectionService.KEY_INITIALIZATION_STATUS; import android.annotation.NonNull; import android.annotation.Nullable; @@ -31,6 +33,8 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.MediaRecorder; +import android.os.Bundle; +import android.os.IRemoteCallback; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteException; @@ -46,6 +50,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IHotwordRecognitionStatusCallback; +import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import java.io.Closeable; @@ -58,6 +63,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; /** * A class that provides the communication with the HotwordDetectionService. @@ -75,11 +82,13 @@ final class HotwordDetectionConnection { 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 final Executor mAudioCopyExecutor = Executors.newCachedThreadPool(); // TODO: This may need to be a Handler(looper) private final ScheduledExecutorService mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + private final AtomicBoolean mUpdateStateFinish = new AtomicBoolean(false); final Object mLock; final ComponentName mDetectionComponentName; @@ -107,16 +116,11 @@ final class HotwordDetectionConnection { @Override // from ServiceConnector.Impl protected void onServiceConnectionStatusChanged(IHotwordDetectionService service, boolean connected) { + if (DEBUG) { + Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected); + } synchronized (mLock) { mBound = connected; - if (connected) { - try { - service.updateState(options, sharedMemory, callback); - } catch (RemoteException e) { - // TODO: (b/181842909) Report an error to voice interactor - Slog.w(TAG, "Failed to updateState for HotwordDetectionService", e); - } - } } } @@ -126,6 +130,67 @@ final class HotwordDetectionConnection { } }; mRemoteHotwordDetectionService.connect(); + if (callback == null) { + updateStateLocked(options, sharedMemory); + return; + } + updateStateWithCallbackLocked(options, sharedMemory, callback); + } + + private void updateStateWithCallbackLocked(PersistableBundle options, + SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) { + if (DEBUG) { + Slog.d(TAG, "updateStateWithCallbackLocked"); + } + mRemoteHotwordDetectionService.postAsync(service -> { + AndroidFuture future = new AndroidFuture<>(); + IRemoteCallback statusCallback = new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle bundle) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "updateState finish"); + } + future.complete(null); + try { + if (mUpdateStateFinish.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; + callback.onStatusReported(status); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to report initialization status: " + e); + } + } + }; + try { + service.updateState(options, sharedMemory, statusCallback); + } catch (RemoteException e) { + // TODO: (b/181842909) Report an error to voice interactor + Slog.w(TAG, "Failed to updateState for HotwordDetectionService", e); + } + return future; + }).orTimeout(MAX_UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + .whenComplete((res, err) -> { + if (err instanceof TimeoutException) { + Slog.w(TAG, "updateState timed out"); + try { + if (mUpdateStateFinish.getAndSet(true)) { + return; + } + callback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to report initialization status: " + e); + } + } else if (err != null) { + Slog.w(TAG, "Failed to update state: " + err); + } else { + // NOTE: so far we don't need to take any action. + } + }); } private boolean isBound() {