update HotwordDetector exception throws

A few updates to throwing exceptions in AOHD:
- overrideAvailability throws exception when overriding to ENROLLED
  state without an enrolled model
- APIs no longer throw runtime exceptions when it could be due to
  system server modifying the detector state without the caller's
  knowledge. In cases where a RuntimeException was thrown, a checked
  exception is thrown instead.

Bug: 226355112
Test: atest AlwaysOnHotwordDetectorTest
Test: atest AlwaysOnHotwordDetectorSystemApiTest
Change-Id: Id6884a325a1cd79f1cc880f370a0a97e4ab0ec04
This commit is contained in:
Nicholas Ambur
2022-03-23 13:03:48 -07:00
parent 54499ae255
commit d7f42e085f
6 changed files with 343 additions and 120 deletions

View File

@@ -11903,20 +11903,20 @@ package android.service.trust {
package android.service.voice {
public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector {
method @Nullable public android.content.Intent createEnrollIntent();
method @Nullable public android.content.Intent createReEnrollIntent();
method @Nullable public android.content.Intent createUnEnrollIntent();
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int getParameter(int);
method @Nullable public android.content.Intent createEnrollIntent() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @Nullable public android.content.Intent createReEnrollIntent() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @Nullable public android.content.Intent createUnEnrollIntent() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int getParameter(int) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method public int getSupportedAudioCapabilities();
method public int getSupportedRecognitionModes();
method @Nullable @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public android.service.voice.AlwaysOnHotwordDetector.ModelParamRange queryParameter(int);
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int setParameter(int, int);
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(int, @NonNull byte[]);
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(int);
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 @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean stopRecognition();
method public final void updateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory);
method public int getSupportedRecognitionModes() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @Nullable @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public android.service.voice.AlwaysOnHotwordDetector.ModelParamRange queryParameter(int) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public int setParameter(int, int) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(int, @NonNull byte[]) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition(int) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method public boolean startRecognition(@NonNull android.os.ParcelFileDescriptor, @NonNull android.media.AudioFormat, @Nullable android.os.PersistableBundle) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean stopRecognition() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method public final void updateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
field public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION = 1; // 0x1
field public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION = 2; // 0x2
field public static final int MODEL_PARAM_THRESHOLD_FACTOR = 0; // 0x0
@@ -12023,10 +12023,10 @@ 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();
method public void updateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory);
method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public boolean startRecognition() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method public boolean startRecognition(@NonNull android.os.ParcelFileDescriptor, @NonNull android.media.AudioFormat, @Nullable android.os.PersistableBundle) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method public boolean stopRecognition() throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
method public void updateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory) throws android.service.voice.HotwordDetector.IllegalDetectorStateException;
}
public static interface HotwordDetector.Callback {
@@ -12039,6 +12039,9 @@ package android.service.voice {
method public void onRejected(@NonNull android.service.voice.HotwordRejectedResult);
}
public static class HotwordDetector.IllegalDetectorStateException extends android.util.AndroidException {
}
public final class HotwordRejectedResult implements android.os.Parcelable {
method public int describeContents();
method public int getConfidenceLevel();

View File

@@ -22,6 +22,7 @@ import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.compat.CompatChanges;
import android.media.AudioFormat;
import android.media.permission.Identity;
import android.os.Handler;
@@ -80,7 +81,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector {
public boolean startRecognition(
@NonNull ParcelFileDescriptor audioStream,
@NonNull AudioFormat audioFormat,
@Nullable PersistableBundle options) {
@Nullable PersistableBundle options) throws IllegalDetectorStateException {
if (DEBUG) {
Slog.i(TAG, "#recognizeHotword");
}
@@ -105,19 +106,22 @@ abstract class AbstractHotwordDetector implements HotwordDetector {
* Set configuration and pass read-only data to hotword detection service.
*
* @param options Application configuration data to provide to the
* {@link HotwordDetectionService}. PersistableBundle does not allow any remotable objects or
* other contents that can be used to communicate with other processes.
* {@link HotwordDetectionService}. PersistableBundle does not allow any remotable
* objects or other contents that can be used to communicate with other processes.
* @param sharedMemory The unrestricted data blob to provide to the
* {@link HotwordDetectionService}. Use this to provide the hotword models data or other
* such data to the trusted process.
*
* @throws IllegalStateException if this AlwaysOnHotwordDetector wasn't specified to use a
* {@link HotwordDetectionService} when it was created. In addition, if this
* AlwaysOnHotwordDetector is in an invalid or error state.
* {@link HotwordDetectionService}. Use this to provide the hotword models data or other
* such data to the trusted process.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of
* Android Tiramisu or above and attempts to start a recognition when the detector is
* not able based on the state. Because the caller receives updates via an asynchronous
* callback and the state of the detector can change without caller's knowledge, a
* checked exception is thrown.
* @throws IllegalStateException if this HotwordDetector wasn't specified to use a
* {@link HotwordDetectionService} when it was created.
*/
@Override
public void updateState(@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory) {
@Nullable SharedMemory sharedMemory) throws IllegalDetectorStateException {
if (DEBUG) {
Slog.d(TAG, "updateState()");
}
@@ -163,9 +167,13 @@ abstract class AbstractHotwordDetector implements HotwordDetector {
}
}
protected void throwIfDetectorIsNoLongerActive() {
protected void throwIfDetectorIsNoLongerActive() throws IllegalDetectorStateException {
if (!mIsDetectorActive.get()) {
Slog.e(TAG, "attempting to use a destroyed detector which is no longer active");
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"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");
}

View File

@@ -27,6 +27,7 @@ import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.ActivityThread;
import android.app.compat.CompatChanges;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.Intent;
@@ -834,19 +835,28 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
/**
* {@inheritDoc}
*
* @throws IllegalStateException if this AlwaysOnHotwordDetector wasn't specified to use a
* {@link HotwordDetectionService} when it was created. In addition, if this
* AlwaysOnHotwordDetector is in an invalid or error state.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and this AlwaysOnHotwordDetector wasn't specified to use a
* {@link HotwordDetectionService} when it was created. In addition, the exception can
* be thrown if this AlwaysOnHotwordDetector is in an invalid or error state.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if
* this AlwaysOnHotwordDetector wasn't specified to use a
* {@link HotwordDetectionService} when it was created. In addition, the exception can
* be thrown if this AlwaysOnHotwordDetector is in an invalid or error state.
*/
@Override
public final void updateState(@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory) {
@Nullable SharedMemory sharedMemory) throws IllegalDetectorStateException {
synchronized (mLock) {
if (!mSupportHotwordDetectionService) {
throw new IllegalStateException(
"updateState called, but it doesn't support hotword detection service");
}
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"updateState called on an invalid detector or error state");
}
throw new IllegalStateException(
"updateState called on an invalid detector or error state");
}
@@ -867,16 +877,17 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
@TestApi
public void overrideAvailability(int availability) {
synchronized (mLock) {
mAvailability = availability;
mIsAvailabilityOverriddenByTestApi = true;
// ENROLLED state requires there to be metadata about the sound model so a fake one
// is created.
if (mKeyphraseMetadata == null && mAvailability == STATE_KEYPHRASE_ENROLLED) {
if (mKeyphraseMetadata == null && availability == STATE_KEYPHRASE_ENROLLED) {
Set<Locale> fakeSupportedLocales = new HashSet<>();
fakeSupportedLocales.add(mLocale);
mKeyphraseMetadata = new KeyphraseMetadata(1, mText, fakeSupportedLocales,
AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER);
}
mAvailability = availability;
mIsAvailabilityOverriddenByTestApi = true;
notifyStateChangedLocked();
}
}
@@ -932,14 +943,23 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* @see #RECOGNITION_MODE_USER_IDENTIFICATION
* @see #RECOGNITION_MODE_VOICE_TRIGGER
*
* @throws UnsupportedOperationException if the keyphrase itself isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above. Because the caller receives availability updates via an asynchronous
* callback, it may be due to the availability changing while this call is performed.
* - Throws if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 Android if the recognition isn't supported. Callers should only call this method
* after a supported state callback on {@link Callback#onAvailabilityChanged(int)} to
* avoid this exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below Android API level
* 33 if the detector is in an invalid or error state. This may happen if another
* detector has been instantiated or the {@link VoiceInteractionService} hosting this
* detector has been shut down.
*/
public @RecognitionModes int getSupportedRecognitionModes() {
public @RecognitionModes
int getSupportedRecognitionModes() throws IllegalDetectorStateException {
if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()");
synchronized (mLock) {
return getSupportedRecognitionModesLocked();
@@ -947,14 +967,22 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
}
@GuardedBy("mLock")
private int getSupportedRecognitionModesLocked() {
private int getSupportedRecognitionModesLocked() throws IllegalDetectorStateException {
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException("getSupportedRecognitionModes called on an"
+ " invalid detector or error state");
}
throw new IllegalStateException(
"getSupportedRecognitionModes called on an invalid detector or error state");
}
// This method only makes sense if we can actually support a recognition.
if (mAvailability != STATE_KEYPHRASE_ENROLLED || mKeyphraseMetadata == null) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException("Getting supported recognition modes for"
+ " the keyphrase is not supported");
}
throw new UnsupportedOperationException(
"Getting supported recognition modes for the keyphrase is not supported");
}
@@ -1009,15 +1037,30 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* startRecognition request. This data is intended to provide additional parameters
* when starting the opaque sound model.
* @return Indicates whether the call succeeded or not.
* @throws UnsupportedOperationException if the recognition isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and attempts to start a recognition when the detector is not able based on
* the availability state. This can be thrown even if the state has been checked before
* calling this method because the caller receives availability updates via an
* asynchronous callback, it may be due to the availability changing while this call is
* performed.
* - Throws if the recognition isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* - Also throws if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 Android if the recognition isn't supported. Callers should only call this method
* after a supported state callback on {@link Callback#onAvailabilityChanged(int)} to
* avoid this exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below Android API level
* 33 if the detector is in an invalid or error state. This may happen if another
* detector has been instantiated or the {@link VoiceInteractionService} hosting this
* detector has been shut down.
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public boolean startRecognition(@RecognitionFlags int recognitionFlags, @NonNull byte[] data) {
public boolean startRecognition(@RecognitionFlags int recognitionFlags, @NonNull byte[] data)
throws IllegalDetectorStateException {
synchronized (mLock) {
return startRecognitionLocked(recognitionFlags, data)
== STATUS_OK;
@@ -1034,15 +1077,30 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
*
* @param recognitionFlags The flags to control the recognition properties.
* @return Indicates whether the call succeeded or not.
* @throws UnsupportedOperationException if the recognition isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and attempts to start a recognition when the detector is not able based on
* the availability state. This can be thrown even if the state has been checked before
* calling this method because the caller receives availability updates via an
* asynchronous callback, it may be due to the availability changing while this call is
* performed.
* - Throws if the recognition isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* - Also throws if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 if the recognition isn't supported. Callers should only call this method after a
* supported state callback on {@link Callback#onAvailabilityChanged(int)} to avoid this
* exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if the
* detector is in an invalid or error state. This may happen if another detector has
* been instantiated or the {@link VoiceInteractionService} hosting this detector has
* been shut down.
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
public boolean startRecognition(@RecognitionFlags int recognitionFlags)
throws IllegalDetectorStateException {
if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
synchronized (mLock) {
return startRecognitionLocked(recognitionFlags, null /* data */) == STATUS_OK;
@@ -1056,7 +1114,8 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
@Override
public boolean startRecognition() {
public boolean startRecognition()
throws IllegalDetectorStateException {
return startRecognition(0);
}
@@ -1066,28 +1125,44 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* Settings.Secure.VOICE_INTERACTION_SERVICE.
*
* @return Indicates whether the call succeeded or not.
* @throws UnsupportedOperationException if the recognition isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of
* API level 33 or above and attempts to stop a recognition when the detector is
* not able based on the state. This can be thrown even if the state has been checked
* before calling this method because the caller receives availability updates via an
* asynchronous callback, it may be due to the availability changing while this call is
* performed.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 if the recognition isn't supported. Callers should only call this method after a
* supported state callback on {@link Callback#onAvailabilityChanged(int)} to avoid this
* exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if the
* detector is in an invalid or error state. This may happen if another detector has
* been instantiated or the {@link VoiceInteractionService} hosting this detector has
* been shut down.
*/
// TODO: Remove this RequiresPermission since it isn't actually enforced. Also fix the javadoc
// about permissions enforcement (when it throws vs when it just returns false) for other
// methods in this class.
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
@Override
public boolean stopRecognition() {
public boolean stopRecognition() throws IllegalDetectorStateException {
if (DBG) Slog.d(TAG, "stopRecognition()");
synchronized (mLock) {
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"stopRecognition called on an invalid detector or error state");
}
throw new IllegalStateException(
"stopRecognition called on an invalid detector or error state");
}
// Check if we can start/stop a recognition.
if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"Recognition for the given keyphrase is not supported");
}
throw new UnsupportedOperationException(
"Recognition for the given keyphrase is not supported");
}
@@ -1112,18 +1187,28 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
* - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
* if API is not supported by HAL
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* if the detector is in an invalid or error state. This may happen if another detector
* has been instantiated or the {@link VoiceInteractionService} hosting this detector
* has been shut down.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if the
* detector is in an invalid or error state. This may happen if another detector has
* been instantiated or the {@link VoiceInteractionService} hosting this detector has
* been shut down.
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public int setParameter(@ModelParams int modelParam, int value) {
public int setParameter(@ModelParams int modelParam, int value)
throws IllegalDetectorStateException {
if (DBG) {
Slog.d(TAG, "setParameter(" + modelParam + ", " + value + ")");
}
synchronized (mLock) {
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"setParameter called on an invalid detector or error state");
}
throw new IllegalStateException(
"setParameter called on an invalid detector or error state");
}
@@ -1144,18 +1229,27 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
*
* @param modelParam {@link ModelParams}
* @return value of parameter
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* if the detector is in an invalid or error state. This may happen if another detector
* has been instantiated or the {@link VoiceInteractionService} hosting this detector
* has been shut down.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if
* the detector is in an invalid or error state. This may happen if another detector has
* been instantiated or the {@link VoiceInteractionService} hosting this detector has
* been shut down.
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public int getParameter(@ModelParams int modelParam) {
public int getParameter(@ModelParams int modelParam) throws IllegalDetectorStateException {
if (DBG) {
Slog.d(TAG, "getParameter(" + modelParam + ")");
}
synchronized (mLock) {
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"getParameter called on an invalid detector or error state");
}
throw new IllegalStateException(
"getParameter called on an invalid detector or error state");
}
@@ -1173,19 +1267,29 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
*
* @param modelParam {@link ModelParams}
* @return supported range of parameter, null if not supported
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* if the detector is in an invalid or error state. This may happen if another detector
* has been instantiated or the {@link VoiceInteractionService} hosting this detector
* has been shut down.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if
* the detector is in an invalid or error state. This may happen if another detector has
* been instantiated or the {@link VoiceInteractionService} hosting this detector has
* been shut down.
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
@Nullable
public ModelParamRange queryParameter(@ModelParams int modelParam) {
public ModelParamRange queryParameter(@ModelParams int modelParam)
throws IllegalDetectorStateException {
if (DBG) {
Slog.d(TAG, "queryParameter(" + modelParam + ")");
}
synchronized (mLock) {
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"queryParameter called on an invalid detector or error state");
}
throw new IllegalStateException(
"queryParameter called on an invalid detector or error state");
}
@@ -1202,15 +1306,25 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* otherwise {@link #createReEnrollIntent()} should be preferred.
*
* @return An {@link Intent} to start enrollment for the given keyphrase.
* @throws UnsupportedOperationException if managing they keyphrase isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above.
* - Thrown if managing they keyphrase isn't supported. Callers should only call this
* method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* - Thrown if the detector is in an invalid state. This may happen if another detector
* has been instantiated or the {@link VoiceInteractionService} hosting this detector
* has been shut down.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 if managing they keyphrase isn't supported. Callers should only call this method
* after a supported state callback on {@link Callback#onAvailabilityChanged(int)} to
* avoid this exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if the
* detector is in an invalid state. This may happen if another detector has been
* instantiated or the {@link VoiceInteractionService} hosting this detector has been
* shut down.
*/
@Nullable
public Intent createEnrollIntent() {
public Intent createEnrollIntent() throws IllegalDetectorStateException {
if (DBG) Slog.d(TAG, "createEnrollIntent");
synchronized (mLock) {
return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_ENROLL);
@@ -1224,15 +1338,25 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
*
* @return An {@link Intent} to start un-enrollment for the given keyphrase.
* @throws UnsupportedOperationException if managing they keyphrase isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above.
* - Thrown if managing they keyphrase isn't supported. Callers should only call this
* method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* - Thrown if the detector is in an invalid state. This may happen if another detector
* has been instantiated or the {@link VoiceInteractionService} hosting this detector
* has been shut down.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 if managing they keyphrase isn't supported. Callers should only call this method
* after a supported state callback on {@link Callback#onAvailabilityChanged(int)} to
* avoid this exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if the
* detector is in an invalid state. This may happen if another detector has been
* instantiated or the {@link VoiceInteractionService} hosting this detector has been
* shut down.
*/
@Nullable
public Intent createUnEnrollIntent() {
public Intent createUnEnrollIntent() throws IllegalDetectorStateException {
if (DBG) Slog.d(TAG, "createUnEnrollIntent");
synchronized (mLock) {
return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_UN_ENROLL);
@@ -1246,15 +1370,25 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
* i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
*
* @return An {@link Intent} to start re-enrollment for the given keyphrase.
* @throws UnsupportedOperationException if managing they keyphrase isn't supported.
* Callers should only call this method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* @throws IllegalStateException if the detector is in an invalid or error state.
* This may happen if another detector has been instantiated or the
* {@link VoiceInteractionService} hosting this detector has been shut down.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above.
* - Thrown if managing they keyphrase isn't supported. Callers should only call this
* method after a supported state callback on
* {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
* - Thrown if the detector is in an invalid state. This may happen if another detector
* has been instantiated or the {@link VoiceInteractionService} hosting this detector
* has been shut down.
* @throws UnsupportedOperationException Thrown when a caller has a target SDK below API level
* 33 if managing they keyphrase isn't supported. Callers should only call this method
* after a supported state callback on {@link Callback#onAvailabilityChanged(int)} to
* avoid this exception.
* @throws IllegalStateException Thrown when a caller has a target SDK below API level 33 if the
* detector is in an invalid state. This may happen if another detector has been
* instantiated or the {@link VoiceInteractionService} hosting this detector has been
* shut down.
*/
@Nullable
public Intent createReEnrollIntent() {
public Intent createReEnrollIntent() throws IllegalDetectorStateException {
if (DBG) Slog.d(TAG, "createReEnrollIntent");
synchronized (mLock) {
return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_RE_ENROLL);
@@ -1262,15 +1396,24 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
}
@GuardedBy("mLock")
private Intent getManageIntentLocked(@KeyphraseEnrollmentInfo.ManageActions int action) {
private Intent getManageIntentLocked(@KeyphraseEnrollmentInfo.ManageActions int action)
throws IllegalDetectorStateException {
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"getManageIntent called on an invalid detector or error state");
}
throw new IllegalStateException(
"getManageIntent called on an invalid detector or error state");
"getManageIntent called on an invalid detector or error state");
}
// This method only makes sense if we can actually support a recognition.
if (mAvailability != STATE_KEYPHRASE_ENROLLED
&& mAvailability != STATE_KEYPHRASE_UNENROLLED) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"Managing the given keyphrase is not supported");
}
throw new UnsupportedOperationException(
"Managing the given keyphrase is not supported");
}
@@ -1357,19 +1500,27 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
@GuardedBy("mLock")
private int startRecognitionLocked(int recognitionFlags,
@Nullable byte[] data) {
@Nullable byte[] data) throws IllegalDetectorStateException {
if (DBG) {
Slog.d(TAG, "startRecognition("
+ recognitionFlags
+ ", " + Arrays.toString(data) + ")");
}
if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"startRecognition called on an invalid detector or error state");
}
throw new IllegalStateException(
"startRecognition called on an invalid detector or error state");
}
// Check if we can start/stop a recognition.
if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
if (CompatChanges.isChangeEnabled(HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION)) {
throw new IllegalDetectorStateException(
"Recognition for the given keyphrase is not supported");
}
throw new UnsupportedOperationException(
"Recognition for the given keyphrase is not supported");
}

View File

@@ -23,10 +23,14 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.media.AudioFormat;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.SharedMemory;
import android.util.AndroidException;
/**
* Basic functionality for hotword detectors.
@@ -36,6 +40,23 @@ import android.os.SharedMemory;
@SystemApi
public interface HotwordDetector {
/**
* Prior to API level 33, API calls of {@link android.service.voice.HotwordDetector} could
* return both {@link java.lang.IllegalStateException} or
* {@link java.lang.UnsupportedOperationException} depending on the detector's underlying state.
* This lead to confusing behavior as the underlying state of the detector can be modified
* without the knowledge of the caller via system service layer updates.
*
* This change ID, when enabled, changes the API calls to only throw checked exception
* {@link android.service.voice.HotwordDetector.IllegalDetectorStateException} when checking
* against state information modified by both the caller and the system services.
*
* @hide
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
long HOTWORD_DETECTOR_THROW_CHECKED_EXCEPTION = 226355112L;
/**
* Indicates that it is a non-trusted hotword detector.
*
@@ -74,16 +95,26 @@ public interface HotwordDetector {
* Calling this again while recognition is active does nothing.
*
* @return true if the request to start recognition succeeded
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and attempts to start a recognition when the detector is not able based on
* the state. This can be thrown even if the state has been checked before calling this
* method because the caller receives updates via an asynchronous callback, and the
* state of the detector can change concurrently to the caller calling this method.
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
boolean startRecognition();
boolean startRecognition() throws IllegalDetectorStateException;
/**
* Stops hotword recognition.
*
* @return true if the request to stop recognition succeeded
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and attempts to stop a recognition when the detector is not able based on
* the state. This can be thrown even if the state has been checked before calling this
* method because the caller receives updates via an asynchronous callback, and the
* state of the detector can change concurrently to the caller calling this method.
*/
boolean stopRecognition();
boolean stopRecognition() throws IllegalDetectorStateException;
/**
* Starts hotword recognition on audio coming from an external connected microphone.
@@ -97,26 +128,37 @@ public interface HotwordDetector {
* PersistableBundle does not allow any remotable objects or other contents that can be
* used to communicate with other processes.
* @return true if the request to start recognition succeeded
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and attempts to start a recognition when the detector is not able based on
* the state. This can be thrown even if the state has been checked before calling this
* method because the caller receives updates via an asynchronous callback, and the
* state of the detector can change concurrently to the caller calling this method.
*/
boolean startRecognition(
@NonNull ParcelFileDescriptor audioStream,
@NonNull AudioFormat audioFormat,
@Nullable PersistableBundle options);
@Nullable PersistableBundle options) throws IllegalDetectorStateException;
/**
* Set configuration and pass read-only data to hotword detection service.
*
* @param options Application configuration data to provide to the
* {@link HotwordDetectionService}. PersistableBundle does not allow any remotable objects or
* other contents that can be used to communicate with other processes.
* {@link HotwordDetectionService}. PersistableBundle does not allow any remotable
* objects or other contents that can be used to communicate with other processes.
* @param sharedMemory The unrestricted data blob to provide to the
* {@link HotwordDetectionService}. Use this to provide the hotword models data or other
* such data to the trusted process.
*
* {@link HotwordDetectionService}. Use this to provide the hotword models data or other
* such data to the trusted process.
* @throws IllegalDetectorStateException Thrown when a caller has a target SDK of API level 33
* or above and the detector is not able to perform the operation based on the
* underlying state. This can be thrown even if the state has been checked before
* calling this method because the caller receives updates via an asynchronous callback,
* and the state of the detector can change concurrently to the caller calling this
* method.
* @throws IllegalStateException if this HotwordDetector wasn't specified to use a
* {@link HotwordDetectionService} when it was created.
* {@link HotwordDetectionService} when it was created.
*/
void updateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory);
void updateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory)
throws IllegalDetectorStateException;
/**
* Invalidates this hotword detector so that any future calls to this result
@@ -205,4 +247,14 @@ public interface HotwordDetector {
*/
void onHotwordDetectionServiceRestarted();
}
/**
* {@link HotwordDetector} specific exception thrown when the underlying state of the detector
* is invalid for the given action.
*/
class IllegalDetectorStateException extends AndroidException {
IllegalDetectorStateException(String message) {
super(message);
}
}
}

View File

@@ -78,7 +78,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
@RequiresPermission(RECORD_AUDIO)
@Override
public boolean startRecognition() {
public boolean startRecognition() throws IllegalDetectorStateException {
if (DEBUG) {
Slog.i(TAG, "#startRecognition");
}
@@ -101,7 +101,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
/** TODO: stopRecognition */
@RequiresPermission(RECORD_AUDIO)
@Override
public boolean stopRecognition() {
public boolean stopRecognition() throws IllegalDetectorStateException {
if (DEBUG) {
Slog.i(TAG, "#stopRecognition");
}

View File

@@ -20,6 +20,7 @@ import android.content.Intent;
import android.service.voice.AlwaysOnHotwordDetector;
import android.service.voice.AlwaysOnHotwordDetector.Callback;
import android.service.voice.AlwaysOnHotwordDetector.EventPayload;
import android.service.voice.HotwordDetector;
import android.service.voice.VoiceInteractionService;
import android.util.Log;
@@ -83,16 +84,24 @@ public class MainInteractionService extends VoiceInteractionService {
break;
case AlwaysOnHotwordDetector.STATE_KEYPHRASE_UNENROLLED:
Log.i(TAG, "STATE_KEYPHRASE_UNENROLLED");
Intent enroll = mHotwordDetector.createEnrollIntent();
Log.i(TAG, "Need to enroll with " + enroll);
try {
Intent enroll = mHotwordDetector.createEnrollIntent();
Log.i(TAG, "Need to enroll with " + enroll);
} catch (HotwordDetector.IllegalDetectorStateException e) {
Log.e(TAG, "createEnrollIntent failed", e);
}
break;
case AlwaysOnHotwordDetector.STATE_KEYPHRASE_ENROLLED:
Log.i(TAG, "STATE_KEYPHRASE_ENROLLED - starting recognition");
if (mHotwordDetector.startRecognition(
AlwaysOnHotwordDetector.RECOGNITION_FLAG_NONE)) {
Log.i(TAG, "startRecognition succeeded");
} else {
Log.i(TAG, "startRecognition failed");
try {
if (mHotwordDetector.startRecognition(
AlwaysOnHotwordDetector.RECOGNITION_FLAG_NONE)) {
Log.i(TAG, "startRecognition succeeded");
} else {
Log.i(TAG, "startRecognition failed");
}
} catch (HotwordDetector.IllegalDetectorStateException e) {
Log.e(TAG, "startRecognition failed", e);
}
break;
}