From 935e33f41671847f5bf117dad78d2f6f759819f6 Mon Sep 17 00:00:00 2001 From: Nicholas Ambur Date: Tue, 8 Feb 2022 19:58:14 -0800 Subject: [PATCH] API to destroy active detectors The active AlwaysOnHotwordDetector and SoftwareHotwordDetector is always maintained in memory regardless if the client is using it or not. This added API allows an active VoiceInteractionService to indicate that they will no longer use the detector, and it can be cleaned up. Test: atest HotwordDetectionServiceBasicTest Bug: 193232191 Change-Id: I47c6c64c5c85c01e75ddc6bc504664883a57730b --- core/api/system-current.txt | 1 + .../voice/AbstractHotwordDetector.java | 38 ++++++++++ .../voice/AlwaysOnHotwordDetector.java | 10 ++- .../service/voice/HotwordDetector.java | 11 +++ .../voice/SoftwareHotwordDetector.java | 16 ++++- .../voice/VoiceInteractionService.java | 70 ++++++++----------- 6 files changed, 102 insertions(+), 44 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 452cbe0b8e690..38bed01228bcc 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11841,6 +11841,7 @@ package android.service.voice { } public interface HotwordDetector { + method public default void destroy(); method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(); method public boolean startRecognition(@NonNull android.os.ParcelFileDescriptor, @NonNull android.media.AudioFormat, @Nullable android.os.PersistableBundle); method public boolean stopRecognition(); diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java index c3bf5694dbffc..01d5638461af8 100644 --- a/core/java/android/service/voice/AbstractHotwordDetector.java +++ b/core/java/android/service/voice/AbstractHotwordDetector.java @@ -18,6 +18,7 @@ package android.service.voice; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread; @@ -34,6 +35,9 @@ import android.util.Slog; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVoiceInteractionManagerService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + /** Base implementation of {@link HotwordDetector}. */ abstract class AbstractHotwordDetector implements HotwordDetector { private static final String TAG = AbstractHotwordDetector.class.getSimpleName(); @@ -45,6 +49,8 @@ abstract class AbstractHotwordDetector implements HotwordDetector { private final Handler mHandler; private final HotwordDetector.Callback mCallback; private final int mDetectorType; + private Consumer mOnDestroyListener; + private final AtomicBoolean mIsDetectorActive; AbstractHotwordDetector( IVoiceInteractionManagerService managerService, @@ -55,6 +61,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { mHandler = new Handler(Looper.getMainLooper()); mCallback = callback; mDetectorType = detectorType; + mIsDetectorActive = new AtomicBoolean(true); } /** @@ -70,6 +77,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { if (DEBUG) { Slog.i(TAG, "#recognizeHotword"); } + throwIfDetectorIsNoLongerActive(); // TODO: consider closing existing session. @@ -106,6 +114,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { if (DEBUG) { Slog.d(TAG, "updateState()"); } + throwIfDetectorIsNoLongerActive(); synchronized (mLock) { updateStateLocked(options, sharedMemory, null /* callback */, mDetectorType); } @@ -126,6 +135,35 @@ abstract class AbstractHotwordDetector implements HotwordDetector { } } + void registerOnDestroyListener(Consumer onDestroyListener) { + synchronized (mLock) { + if (mOnDestroyListener != null) { + throw new IllegalStateException("only one destroy listener can be registered"); + } + mOnDestroyListener = onDestroyListener; + } + } + + @CallSuper + @Override + public void destroy() { + if (!mIsDetectorActive.get()) { + return; + } + mIsDetectorActive.set(false); + synchronized (mLock) { + mOnDestroyListener.accept(this); + } + } + + protected void throwIfDetectorIsNoLongerActive() { + if (!mIsDetectorActive.get()) { + Slog.e(TAG, "attempting to use a destroyed detector which is no longer active"); + throw new IllegalStateException( + "attempting to use a destroyed detector which is no longer active"); + } + } + private static class BinderCallback extends IMicrophoneHotwordDetectionVoiceInteractionCallback.Stub { private final Handler mHandler; diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index b90f27b986c11..bec5d1be57fd1 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -1205,11 +1205,14 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { /** * Invalidates this hotword detector so that any future calls to this result * in an IllegalStateException. - * - * @hide */ - void invalidate() { + @Override + public void destroy() { synchronized (mLock) { + if (mAvailability == STATE_KEYPHRASE_ENROLLED) { + stopRecognition(); + } + mAvailability = STATE_INVALID; notifyStateChangedLocked(); @@ -1221,6 +1224,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { } } } + super.destroy(); } /** diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java index 969ec22beb978..96fd8bbda0160 100644 --- a/core/java/android/service/voice/HotwordDetector.java +++ b/core/java/android/service/voice/HotwordDetector.java @@ -118,6 +118,17 @@ public interface HotwordDetector { */ void updateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory); + /** + * Invalidates this hotword detector so that any future calls to this result + * in an {@link IllegalStateException}. + * + *

If there are no other {@link HotwordDetector} instances linked to the + * {@link HotwordDetectionService}, the service will be shutdown. + */ + default void destroy() { + throw new UnsupportedOperationException("Not implemented. Must override in a subclass."); + } + /** * @hide */ diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java index 426f312b2e86a..2d662eaf0a4f0 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -77,7 +77,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { if (DEBUG) { Slog.i(TAG, "#startRecognition"); } - + throwIfDetectorIsNoLongerActive(); maybeCloseExistingSession(); try { @@ -100,6 +100,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { if (DEBUG) { Slog.i(TAG, "#stopRecognition"); } + throwIfDetectorIsNoLongerActive(); try { mManagerService.stopListeningFromMic(); @@ -110,6 +111,19 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { return true; } + @Override + public void destroy() { + stopRecognition(); + maybeCloseExistingSession(); + + try { + mManagerService.shutdownHotwordDetectionService(); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + super.destroy(); + } + private void maybeCloseExistingSession() { // TODO: needs to be synchronized. // TODO: implement this diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index f52c9ff210d65..bf0cfbe49f31d 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -271,7 +271,7 @@ public class VoiceInteractionService extends Service { // It's still guaranteed to have been stopped. // This helps with cases where the voice interaction implementation is changed // by the user. - safelyShutdownHotwordDetector(); + safelyShutdownAllHotwordDetectors(); } /** @@ -380,11 +380,13 @@ public class VoiceInteractionService extends Service { } synchronized (mLock) { // Allow only one concurrent recognition via the APIs. - safelyShutdownHotwordDetector(); + safelyShutdownAllHotwordDetectors(); mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback, mKeyphraseEnrollmentInfo, mSystemService, getApplicationContext().getApplicationInfo().targetSdkVersion, supportHotwordDetectionService, options, sharedMemory); + mHotwordDetector.registerOnDestroyListener((detector) -> onDspHotwordDetectorDestroyed( + (AlwaysOnHotwordDetector) detector)); } return mHotwordDetector; } @@ -433,10 +435,13 @@ public class VoiceInteractionService extends Service { } synchronized (mLock) { // Allow only one concurrent recognition via the APIs. - safelyShutdownHotwordDetector(); + safelyShutdownAllHotwordDetectors(); mSoftwareHotwordDetector = new SoftwareHotwordDetector( mSystemService, null, options, sharedMemory, callback); + mSoftwareHotwordDetector.registerOnDestroyListener( + (detector) -> onMicrophoneHotwordDetectorDestroyed( + (SoftwareHotwordDetector) detector)); } return mSoftwareHotwordDetector; } @@ -482,51 +487,36 @@ public class VoiceInteractionService extends Service { return mKeyphraseEnrollmentInfo.getKeyphraseMetadata(keyphrase, locale) != null; } - private void safelyShutdownHotwordDetector() { + private void safelyShutdownAllHotwordDetectors() { synchronized (mLock) { - shutdownDspHotwordDetectorLocked(); - shutdownMicrophoneHotwordDetectorLocked(); + if (mHotwordDetector != null) { + try { + mHotwordDetector.destroy(); + } catch (Exception ex) { + Log.i(TAG, "exception destroying AlwaysOnHotwordDetector", ex); + } + } + + if (mSoftwareHotwordDetector != null) { + try { + mSoftwareHotwordDetector.destroy(); + } catch (Exception ex) { + Log.i(TAG, "exception destroying SoftwareHotwordDetector", ex); + } + } } } - private void shutdownDspHotwordDetectorLocked() { - if (mHotwordDetector == null) { - return; + private void onDspHotwordDetectorDestroyed(@NonNull AlwaysOnHotwordDetector detector) { + synchronized (mLock) { + mHotwordDetector = null; } - - try { - mHotwordDetector.stopRecognition(); - } catch (Exception ex) { - // Ignore. - } - - try { - mHotwordDetector.invalidate(); - } catch (Exception ex) { - // Ignore. - } - - mHotwordDetector = null; } - private void shutdownMicrophoneHotwordDetectorLocked() { - if (mSoftwareHotwordDetector == null) { - return; + private void onMicrophoneHotwordDetectorDestroyed(@NonNull SoftwareHotwordDetector detector) { + synchronized (mLock) { + mSoftwareHotwordDetector = null; } - - try { - mSoftwareHotwordDetector.stopRecognition(); - } catch (Exception ex) { - // Ignore. - } - - try { - mSystemService.shutdownHotwordDetectionService(); - } catch (Exception ex) { - // Ignore. - } - - mSoftwareHotwordDetector = null; } /**