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:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user