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; } /**