Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d7f42e08 authored by Nicholas Ambur's avatar Nicholas Ambur
Browse files

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
parent 54499ae2
Loading
Loading
Loading
Loading
+20 −17
Original line number Diff line number Diff line
@@ -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();
+19 −11
Original line number Diff line number Diff line
@@ -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.
     * @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");
        }
+224 −73

File changed.

Preview size limit exceeded, changes collapsed.

+62 −10
Original line number Diff line number Diff line
@@ -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.
     *
     * @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.
     */
    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);
        }
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -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");
        }
Loading