diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 917d70032d714..c08242fca68dd 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -1439,18 +1439,18 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { mAvailability = STATE_INVALID; mIsAvailabilityOverriddenByTestApi = false; notifyStateChangedLocked(); - - if (mSupportHotwordDetectionService) { - try { - mModelManagementService.shutdownHotwordDetectionService(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } } super.destroy(); } + /** + * @hide + */ + @Override + public boolean isUsingHotwordDetectionService() { + return mSupportHotwordDetectionService; + } + /** * Reloads the sound models from the service. * @@ -1821,6 +1821,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { } /** @hide */ + @Override public void dump(String prefix, PrintWriter pw) { synchronized (mLock) { pw.print(prefix); pw.print("Text="); pw.println(mText); diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java index 38d9589333e12..1a0dc8945b631 100644 --- a/core/java/android/service/voice/HotwordDetector.java +++ b/core/java/android/service/voice/HotwordDetector.java @@ -32,6 +32,8 @@ import android.os.PersistableBundle; import android.os.SharedMemory; import android.util.AndroidException; +import java.io.PrintWriter; + /** * Basic functionality for hotword detectors. * @@ -171,6 +173,13 @@ public interface HotwordDetector { throw new UnsupportedOperationException("Not implemented. Must override in a subclass."); } + /** + * @hide + */ + default boolean isUsingHotwordDetectionService() { + throw new UnsupportedOperationException("Not implemented. Must override in a subclass."); + } + /** * @hide */ @@ -187,6 +196,11 @@ public interface HotwordDetector { } } + /** @hide */ + default void dump(String prefix, PrintWriter pw) { + throw new UnsupportedOperationException("Not implemented. Must override in a subclass."); + } + /** * The callback to notify of detection events. */ diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java index 2ab7898059580..36ea91e4edb57 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -124,15 +124,17 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { Log.i(TAG, "failed to stopRecognition in destroy", e); } maybeCloseExistingSession(); - - try { - mManagerService.shutdownHotwordDetectionService(); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); - } super.destroy(); } + /** + * @hide + */ + @Override + public boolean isUsingHotwordDetectionService() { + return true; + } + private void maybeCloseExistingSession() { // TODO: needs to be synchronized. // TODO: implement this @@ -240,6 +242,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { } /** @hide */ + @Override public void dump(String prefix, PrintWriter pw) { // TODO: implement this } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 88b7a58a57098..1170237a346eb 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -24,12 +24,16 @@ import android.annotation.SdkConstant; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.Service; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; import android.media.voice.KeyphraseModelManager; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -89,6 +93,37 @@ public class VoiceInteractionService extends Service { */ public static final String SERVICE_META_DATA = "android.voice_interaction"; + /** + * For apps targeting Build.VERSION_CODES.TRAMISU and above, implementors of this + * service can create multiple AlwaysOnHotwordDetector instances in parallel. They will + * also e ale to create a single SoftwareHotwordDetector in parallel with any other + * active AlwaysOnHotwordDetector instances. + * + *

Requirements when this change is enabled: + *

+ * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT) + static final long MULTIPLE_ACTIVE_HOTWORD_DETECTORS = 193232191L; + IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() { @Override public void ready() { @@ -133,8 +168,7 @@ public class VoiceInteractionService extends Service { private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; - private AlwaysOnHotwordDetector mHotwordDetector; - private SoftwareHotwordDetector mSoftwareHotwordDetector; + private final Set mActiveHotwordDetectors = new ArraySet<>(); /** * Called when a user has activated an affordance to launch voice assist from the Keyguard. @@ -284,10 +318,12 @@ public class VoiceInteractionService extends Service { private void onSoundModelsChangedInternal() { synchronized (this) { - if (mHotwordDetector != null) { - // TODO: Stop recognition if a sound model that was being recognized gets deleted. - mHotwordDetector.onSoundModelsChanged(); - } + // TODO: Stop recognition if a sound model that was being recognized gets deleted. + mActiveHotwordDetectors.forEach(detector -> { + if (detector instanceof AlwaysOnHotwordDetector) { + ((AlwaysOnHotwordDetector) detector).onSoundModelsChanged(); + } + }); } } @@ -379,19 +415,31 @@ public class VoiceInteractionService extends Service { throw new IllegalStateException("Not available until onReady() is called"); } synchronized (mLock) { - // Allow only one concurrent recognition via the APIs. - safelyShutdownAllHotwordDetectors(); + if (!CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) { + // Allow only one concurrent recognition via the APIs. + safelyShutdownAllHotwordDetectors(); + } - mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, - callback, - mKeyphraseEnrollmentInfo, mSystemService, + AlwaysOnHotwordDetector dspDetector = new AlwaysOnHotwordDetector(keyphrase, locale, + callback, mKeyphraseEnrollmentInfo, mSystemService, getApplicationContext().getApplicationInfo().targetSdkVersion, supportHotwordDetectionService); - mHotwordDetector.registerOnDestroyListener((detector) -> onDspHotwordDetectorDestroyed( - (AlwaysOnHotwordDetector) detector)); - mHotwordDetector.initialize(options, sharedMemory); + if (!mActiveHotwordDetectors.add(dspDetector)) { + throw new IllegalArgumentException( + "the keyphrase=" + keyphrase + " and locale=" + locale + + " are already used by another always-on detector"); + } + + try { + dspDetector.registerOnDestroyListener(this::onHotwordDetectorDestroyed); + dspDetector.initialize(options, sharedMemory); + } catch (Exception e) { + mActiveHotwordDetectors.remove(dspDetector); + dspDetector.destroy(); + throw e; + } + return dspDetector; } - return mHotwordDetector; } /** @@ -437,18 +485,34 @@ public class VoiceInteractionService extends Service { throw new IllegalStateException("Not available until onReady() is called"); } synchronized (mLock) { - // Allow only one concurrent recognition via the APIs. - safelyShutdownAllHotwordDetectors(); + if (!CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) { + // Allow only one concurrent recognition via the APIs. + safelyShutdownAllHotwordDetectors(); + } else { + for (HotwordDetector detector : mActiveHotwordDetectors) { + if (detector instanceof SoftwareHotwordDetector) { + throw new IllegalArgumentException( + "There is already an active SoftwareHotwordDetector. " + + "It must be destroyed to create a new one."); + } + } + } - mSoftwareHotwordDetector = + SoftwareHotwordDetector softwareHotwordDetector = new SoftwareHotwordDetector( mSystemService, null, callback); - mSoftwareHotwordDetector.registerOnDestroyListener( - (detector) -> onMicrophoneHotwordDetectorDestroyed( - (SoftwareHotwordDetector) detector)); - mSoftwareHotwordDetector.initialize(options, sharedMemory); + + try { + softwareHotwordDetector.registerOnDestroyListener( + this::onHotwordDetectorDestroyed); + softwareHotwordDetector.initialize(options, sharedMemory); + } catch (Exception e) { + mActiveHotwordDetectors.remove(softwareHotwordDetector); + softwareHotwordDetector.destroy(); + throw e; + } + return softwareHotwordDetector; } - return mSoftwareHotwordDetector; } /** @@ -494,33 +558,34 @@ public class VoiceInteractionService extends Service { private void safelyShutdownAllHotwordDetectors() { synchronized (mLock) { - if (mHotwordDetector != null) { + mActiveHotwordDetectors.forEach(detector -> { try { - mHotwordDetector.destroy(); + detector.destroy(); } catch (Exception ex) { - Log.i(TAG, "exception destroying AlwaysOnHotwordDetector", ex); + Log.i(TAG, "exception destroying HotwordDetector", ex); } - } - - if (mSoftwareHotwordDetector != null) { - try { - mSoftwareHotwordDetector.destroy(); - } catch (Exception ex) { - Log.i(TAG, "exception destroying SoftwareHotwordDetector", ex); - } - } + }); } } - private void onDspHotwordDetectorDestroyed(@NonNull AlwaysOnHotwordDetector detector) { + private void onHotwordDetectorDestroyed(@NonNull HotwordDetector detector) { synchronized (mLock) { - mHotwordDetector = null; + mActiveHotwordDetectors.remove(detector); + shutdownHotwordDetectionServiceIfRequiredLocked(); } } - private void onMicrophoneHotwordDetectorDestroyed(@NonNull SoftwareHotwordDetector detector) { - synchronized (mLock) { - mSoftwareHotwordDetector = null; + private void shutdownHotwordDetectionServiceIfRequiredLocked() { + for (HotwordDetector detector : mActiveHotwordDetectors) { + if (detector.isUsingHotwordDetectionService()) { + return; + } + } + + try { + mSystemService.shutdownHotwordDetectionService(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } } @@ -545,18 +610,14 @@ public class VoiceInteractionService extends Service { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("VOICE INTERACTION"); synchronized (mLock) { - pw.println(" AlwaysOnHotwordDetector"); - if (mHotwordDetector == null) { + pw.println(" HotwordDetector(s)"); + if (mActiveHotwordDetectors.size() == 0) { pw.println(" NULL"); } else { - mHotwordDetector.dump(" ", pw); - } - - pw.println(" MicrophoneHotwordDetector"); - if (mSoftwareHotwordDetector == null) { - pw.println(" NULL"); - } else { - mSoftwareHotwordDetector.dump(" ", pw); + mActiveHotwordDetectors.forEach(detector -> { + detector.dump(" ", pw); + pw.println(); + }); } } }