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

Commit 3492b92b authored by lpeter's avatar lpeter Committed by Peter Li
Browse files

Catch the SecurityException in the APIs of SoundTriggerSession to avoid crashing.

Currently when switching the user, it will be possible to meet this
kind of issue due to timing factor that the API of SoundTriggerSession
was called by previous user and caused the exception "Caller is not
the current voice interaction service.". It seems that the flow between
switching user and voice interaction needs to be improved to avoid this
race condition issue, but the impact will be high.

It would be better to catch the exception and return the fail status
to caller instead of crashing as a short term solution.

Four approaches are considered below:
(1) Change state to STATE_INVALID
When AlwaysOnHotwordDetector is shutdown, the state will be set to
STATE_INVALID. Then all called methods from it will fail with an
IllegalStateException. This is a normal behavior that has been defined
in the javadoc. But the STATE_INVALID does not clearly indicate what
happended in the AlwaysOnHotwordDetector for this issue and what to do
next. So it is not the best approach to change the state to
STATE_INVALID directly.

(2) Use a new state to inform the caller (apply this)
Define a new state STATE_ERROR to indicate the unknown active
keyphrase availability due to an error.
public static final int STATE_ERROR = 3;

When the exception occurs, it will pass STATE_ERROR to the caller by
the callback API "onAvailabilityChanged(int status)".
Then all called methods from it should fail with an IllegalStateException
due to AlwaysOnHotwordDetector can not be used currently.
The caller should create a new instance after receiving this state.

In order to avoid unintended behavior in the application, it would be
better to use this approach when the target SDK version is greater
than R.

(3) Use onError() callback to inform the caller
Currently when some errors occurred during Recognition, the onError()
callback will be called. But it doesn't mean the caller should stop
to using the AlwaysOnHotwordDetector. It is different from what we
want that the caller should not use the AlwaysOnHotwordDetector and
should create a new instance. So it is not suitable to use the onError()
callback to inform the caller due to no error reason in the onError()
callback. The caller will not know what happened and what to do next.

(4) Create a new onError(int reason) callback to inform the caller
In this approach, it will need to create a new onError(int reason)
callback to inform the caller. It also needs to change the state of
AlwaysOnHotwordDetector, then all called methods from it should fail
with an IllegalStateException due to AlwaysOnHotwordDetector can not
be used currently. This approach is also fine, but it is very similar
to approach 2. Finaly we use the approach 2.

Bug: 148136382
Test: atest CtsVoiceInteractionTestCases
Test: atest CtsSoundTriggerTestCases
Change-Id: I4697a2e6e26d412cf5c985467c652d30586adbe7
parent 596a4ea4
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -9852,6 +9852,7 @@ package android.service.voice {
    field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
    field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
    field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
    field public static final int STATE_ERROR = 3; // 0x3
    field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
    field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
    field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
+101 −38
Original line number Diff line number Diff line
@@ -22,9 +22,9 @@ import static android.Manifest.permission.RECORD_AUDIO;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.Intent;
@@ -41,6 +41,7 @@ import android.media.AudioFormat;
import android.media.permission.Identity;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -78,6 +79,7 @@ public class AlwaysOnHotwordDetector {
     * No further interaction should be performed with the detector that returns this availability.
     */
    public static final int STATE_HARDWARE_UNAVAILABLE = -2;

    /**
     * Indicates that recognition for the given keyphrase is not supported.
     * No further interaction should be performed with the detector that returns this availability.
@@ -88,17 +90,27 @@ public class AlwaysOnHotwordDetector {
     */
    @Deprecated
    public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;

    /**
     * Indicates that the given keyphrase is not enrolled.
     * The caller may choose to begin an enrollment flow for the keyphrase.
     */
    public static final int STATE_KEYPHRASE_UNENROLLED = 1;

    /**
     * Indicates that the given keyphrase is currently enrolled and it's possible to start
     * recognition for it.
     */
    public static final int STATE_KEYPHRASE_ENROLLED = 2;

    /**
     * Indicates that the availability state of the active keyphrase can't be known due to an error.
     *
     * <p>NOTE: No further interaction should be performed with the detector that returns this
     * state, it would be better to create {@link AlwaysOnHotwordDetector} again.
     */
    public static final int STATE_ERROR = 3;

    /**
     * Indicates that the detector isn't ready currently.
     */
@@ -122,11 +134,13 @@ public class AlwaysOnHotwordDetector {
     * @hide
     */
    public static final int RECOGNITION_FLAG_NONE = 0;

    /**
     * Recognition flag for {@link #startRecognition(int)} that indicates
     * whether the trigger audio for hotword needs to be captured.
     */
    public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;

    /**
     * Recognition flag for {@link #startRecognition(int)} that indicates
     * whether the recognition should keep going on even after the keyphrase triggers.
@@ -174,6 +188,7 @@ public class AlwaysOnHotwordDetector {
     */
    public static final int RECOGNITION_MODE_VOICE_TRIGGER
            = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;

    /**
     * User identification performed with the keyphrase recognition.
     * Returned by {@link #getSupportedRecognitionModes()}
@@ -249,6 +264,7 @@ public class AlwaysOnHotwordDetector {
    private final Object mLock = new Object();
    private final Handler mHandler;
    private final IBinder mBinder = new Binder();
    private final int mTargetSdkVersion;

    private int mAvailability = STATE_NOT_READY;

@@ -401,8 +417,10 @@ public class AlwaysOnHotwordDetector {
         * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE
         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED
         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED
         * @see AlwaysOnHotwordDetector#STATE_ERROR
         */
        public abstract void onAvailabilityChanged(int status);

        /**
         * Called when the keyphrase is spoken.
         * This implicitly stops listening for the keyphrase once it's detected.
@@ -414,16 +432,19 @@ public class AlwaysOnHotwordDetector {
         *        {@link AlwaysOnHotwordDetector#startRecognition(int)}.
         */
        public abstract void onDetected(@NonNull EventPayload eventPayload);

        /**
         * Called when the detection fails due to an error.
         */
        public abstract void onError();

        /**
         * Called when the recognition is paused temporarily for some reason.
         * This is an informational callback, and the clients shouldn't be doing anything here
         * except showing an indication on their UI if they have to.
         */
        public abstract void onRecognitionPaused();

        /**
         * Called when the recognition is resumed after it was temporarily paused.
         * This is an informational callback, and the clients shouldn't be doing anything here
@@ -437,11 +458,12 @@ public class AlwaysOnHotwordDetector {
     * @param locale The java locale for the detector.
     * @param callback A non-null Callback for receiving the recognition events.
     * @param modelManagementService A service that allows management of sound models.
     * @param targetSdkVersion The target SDK version.
     * @hide
     */
    public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback,
            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
            IVoiceInteractionManagerService modelManagementService) {
            IVoiceInteractionManagerService modelManagementService, int targetSdkVersion) {
        mText = text;
        mLocale = locale;
        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
@@ -449,6 +471,7 @@ public class AlwaysOnHotwordDetector {
        mHandler = new MyHandler();
        mInternalCallback = new SoundTriggerListener(mHandler);
        mModelManagementService = modelManagementService;
        mTargetSdkVersion = targetSdkVersion;
        try {
            Identity identity = new Identity();
            identity.packageName = ActivityThread.currentOpPackageName();
@@ -469,7 +492,7 @@ public class AlwaysOnHotwordDetector {
     * @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 state.
     * @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.
     */
@@ -481,9 +504,9 @@ public class AlwaysOnHotwordDetector {
    }

    private int getSupportedRecognitionModesLocked() {
        if (mAvailability == STATE_INVALID) {
        if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
            throw new IllegalStateException(
                    "getSupportedRecognitionModes called on an invalid detector");
                    "getSupportedRecognitionModes called on an invalid detector or error state");
        }

        // This method only makes sense if we can actually support a recognition.
@@ -541,7 +564,7 @@ public class AlwaysOnHotwordDetector {
     * @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 state.
     * @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.
     */
@@ -549,8 +572,9 @@ public class AlwaysOnHotwordDetector {
    public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
        if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
        synchronized (mLock) {
            if (mAvailability == STATE_INVALID) {
                throw new IllegalStateException("startRecognition called on an invalid detector");
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "startRecognition called on an invalid detector or error state");
            }

            // Check if we can start/stop a recognition.
@@ -572,7 +596,7 @@ public class AlwaysOnHotwordDetector {
     * @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 state.
     * @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.
     */
@@ -580,8 +604,9 @@ public class AlwaysOnHotwordDetector {
    public boolean stopRecognition() {
        if (DBG) Slog.d(TAG, "stopRecognition()");
        synchronized (mLock) {
            if (mAvailability == STATE_INVALID) {
                throw new IllegalStateException("stopRecognition called on an invalid detector");
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "stopRecognition called on an invalid detector or error state");
            }

            // Check if we can start/stop a recognition.
@@ -610,6 +635,9 @@ public class AlwaysOnHotwordDetector {
     *         - {@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.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public int setParameter(@ModelParams int modelParam, int value) {
@@ -618,8 +646,9 @@ public class AlwaysOnHotwordDetector {
        }

        synchronized (mLock) {
            if (mAvailability == STATE_INVALID) {
                throw new IllegalStateException("setParameter called on an invalid detector");
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "setParameter called on an invalid detector or error state");
            }

            return setParameterLocked(modelParam, value);
@@ -638,6 +667,9 @@ public class AlwaysOnHotwordDetector {
     *
     * @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.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public int getParameter(@ModelParams int modelParam) {
@@ -646,8 +678,9 @@ public class AlwaysOnHotwordDetector {
        }

        synchronized (mLock) {
            if (mAvailability == STATE_INVALID) {
                throw new IllegalStateException("getParameter called on an invalid detector");
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "getParameter called on an invalid detector or error state");
            }

            return getParameterLocked(modelParam);
@@ -663,6 +696,9 @@ public class AlwaysOnHotwordDetector {
     *
     * @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.
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    @Nullable
@@ -672,8 +708,9 @@ public class AlwaysOnHotwordDetector {
        }

        synchronized (mLock) {
            if (mAvailability == STATE_INVALID) {
                throw new IllegalStateException("queryParameter called on an invalid detector");
            if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
                throw new IllegalStateException(
                        "queryParameter called on an invalid detector or error state");
            }

            return queryParameterLocked(modelParam);
@@ -735,7 +772,7 @@ public class AlwaysOnHotwordDetector {
     * @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.
     * @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.
     */
@@ -748,8 +785,9 @@ public class AlwaysOnHotwordDetector {
    }

    private Intent getManageIntentLocked(@KeyphraseEnrollmentInfo.ManageActions int action) {
        if (mAvailability == STATE_INVALID) {
            throw new IllegalStateException("getManageIntent called on an invalid detector");
        if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) {
            throw new IllegalStateException(
                    "getManageIntent called on an invalid detector or error state");
        }

        // This method only makes sense if we can actually support a recognition.
@@ -783,8 +821,10 @@ public class AlwaysOnHotwordDetector {
    void onSoundModelsChanged() {
        synchronized (mLock) {
            if (mAvailability == STATE_INVALID
                    || mAvailability == STATE_HARDWARE_UNAVAILABLE) {
                Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config");
                    || mAvailability == STATE_HARDWARE_UNAVAILABLE
                    || mAvailability == STATE_ERROR) {
                Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config"
                        + " or in the error state");
                return;
            }

@@ -794,7 +834,16 @@ public class AlwaysOnHotwordDetector {
            // The availability change callback should ensure that the client starts recognition
            // again if needed.
            if (mAvailability == STATE_KEYPHRASE_ENROLLED) {
                try {
                    stopRecognitionLocked();
                } catch (SecurityException e) {
                    Slog.w(TAG, "Failed to Stop the recognition", e);
                    if (mTargetSdkVersion <= Build.VERSION_CODES.R) {
                        throw e;
                    }
                    updateAndNotifyStateChangedLocked(STATE_ERROR);
                    return;
                }
            }

            // Execute a refresh availability task - which should then notify of a change.
@@ -890,6 +939,15 @@ public class AlwaysOnHotwordDetector {
        }
    }

    private void updateAndNotifyStateChangedLocked(int availability) {
        if (DBG) {
            Slog.d(TAG, "Hotword availability changed from " + mAvailability
                    + " -> " + availability);
        }
        mAvailability = availability;
        notifyStateChangedLocked();
    }

    private void notifyStateChangedLocked() {
        Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
        message.arg1 = mAvailability;
@@ -976,6 +1034,7 @@ public class AlwaysOnHotwordDetector {

        @Override
        public Void doInBackground(Void... params) {
            try {
                int availability = internalGetInitialAvailability();

                synchronized (mLock) {
@@ -987,14 +1046,18 @@ public class AlwaysOnHotwordDetector {
                            availability = STATE_KEYPHRASE_UNENROLLED;
                        }
                    }

                if (DBG) {
                    Slog.d(TAG, "Hotword availability changed from " + mAvailability
                            + " -> " + availability);
                    updateAndNotifyStateChangedLocked(availability);
                }
                mAvailability = availability;
                notifyStateChangedLocked();
            } catch (SecurityException e) {
                Slog.w(TAG, "Failed to refresh availability", e);
                if (mTargetSdkVersion <= Build.VERSION_CODES.R) {
                    throw e;
                }
                synchronized (mLock) {
                    updateAndNotifyStateChangedLocked(STATE_ERROR);
                }
            }

            return null;
        }

+2 −1
Original line number Diff line number Diff line
@@ -325,7 +325,8 @@ public class VoiceInteractionService extends Service {
            // Allow only one concurrent recognition via the APIs.
            safelyShutdownHotwordDetector();
            mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback,
                    mKeyphraseEnrollmentInfo, mSystemService);
                    mKeyphraseEnrollmentInfo, mSystemService,
                    getApplicationContext().getApplicationInfo().targetSdkVersion);
        }
        return mHotwordDetector;
    }