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:
+ *
+ * -
+ * Any number of AlwaysOnHotwordDetector instances can be created in parallel
+ * as long as they are unique to any other active AlwaysOnHotwordDetector.
+ *
+ * -
+ * Only a single instance of SoftwareHotwordDetector can be active at a given
+ * time. It can be active at the same time as any number of
+ * AlwaysOnHotwordDetector instances.
+ *
+ * -
+ * To release that reference and any resources associated with that reference,
+ * HotwordDetector#destroy() must be called. An attempt to create an
+ * HotwordDetector equal to an active HotwordDetector will be rejected
+ * until HotwordDetector#destroy() is called on the active instance.
+ *
+ *
+ *
+ * @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();
+ });
}
}
}