add support for multiple keyphrase detectors

This change allows for the active VoiceInteractionService to create any
number of unique DSP-based AlwaysOnHotwordDetectors in parallel. This
change also allows for a SoftwareHotwordDetector to be created alongside
the DSP-based detector(s).

When a implementation of VoiceInteractionService has a target SDK lower
than Android T SDK version, there is no behavior change, and the
application will only be able to use a single HotwordDetector at a given
time.

Test: atest VoiceInteractionSystemApiTest
Bug: 193232191
Change-Id: I4bc7211884d2d63dc7fcda059384f3a8233a41a8
This commit is contained in:
Nicholas Ambur
2022-02-14 13:04:09 -08:00
parent ee2e992398
commit d4c79f5f8e
4 changed files with 143 additions and 64 deletions

View File

@@ -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);

View File

@@ -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.
*/

View File

@@ -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
}

View File

@@ -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.
*
* <p>Requirements when this change is enabled:
* <ul>
* <li>
* Any number of AlwaysOnHotwordDetector instances can be created in parallel
* as long as they are unique to any other active AlwaysOnHotwordDetector.
* </li>
* <li>
* 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.
* </li>
* <li>
* 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.
* </li>
* </ul>
*
* @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<HotwordDetector> 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();
});
}
}
}