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

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

send onFailure callback for asyncronous exceptions

A ChangeId is added and only applied for clients with target SDK UDC and
above.

If the ChangeId is enabled, we will still mark the internal availability
state as STATE_ERROR, but we will send onFailure callback instead. This
allows for the client to get the full exception message detail.

If the ChangeId is not enabled, then STATE_ERROR is still propagated to
the client via onAvailabilityChanged.

One other update here is that we are catching all exceptions in the
async task.  This resolves the case where uncaught exceptions would
crash the client's VoiceInteractionService process.

Test: Manually throw an exception from system service to verify
callback
Test: atest CtsVoiceInteractionTestCases
Fixes: 280471513
Bug: 272147641

Change-Id: I316e5f545eed88e490bab2e0a052c051cc0cc94e
parent 619bfd92
Loading
Loading
Loading
Loading
+47 −11
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.service.voice;

import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN;
import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS;

import android.annotation.ElapsedRealtimeLong;
@@ -269,6 +270,15 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    static final long THROW_ON_INITIALIZE_IF_NO_DSP = 269165460L;

    /**
     * Gates returning {@link Callback#onFailure} and {@link Callback#onUnknownFailure}
     * when asynchronous exceptions are propagated to the client. If the change is not enabled,
     * the existing behavior of delivering {@link #STATE_ERROR} is retained.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    static final long SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS = 280471513L;

    /**
     * Controls the sensitivity threshold adjustment factor for a given model.
     * Negative value corresponds to less sensitive model (high threshold) and
@@ -1409,12 +1419,16 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
            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;
                    }
                } catch (Exception e) {
                    Slog.w(TAG, "Failed to stop recognition after enrollment update", e);
                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
                        sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN,
                                "Failed to stop recognition after enrollment update: "
                                        + Log.getStackTraceString(e),
                                FailureSuggestedAction.RECREATE_DETECTOR));
                    } else {
                        updateAndNotifyStateChangedLocked(STATE_ERROR);
                    }
                    return;
                }
            }
@@ -1538,6 +1552,12 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {

    @GuardedBy("mLock")
    private void updateAndNotifyStateChangedLocked(int availability) {
        updateAvailabilityLocked(availability);
        notifyStateChangedLocked();
    }

    @GuardedBy("mLock")
    private void updateAvailabilityLocked(int availability) {
        if (DBG) {
            Slog.d(TAG, "Hotword availability changed from " + mAvailability
                    + " -> " + availability);
@@ -1545,7 +1565,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
        if (!mIsAvailabilityOverriddenByTestApi) {
            mAvailability = availability;
        }
        notifyStateChangedLocked();
    }

    @GuardedBy("mLock")
@@ -1555,6 +1574,18 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
        message.sendToTarget();
    }

    @GuardedBy("mLock")
    private void sendUnknownFailure(String failureMessage) {
        // update but do not call onAvailabilityChanged callback for STATE_ERROR
        updateAvailabilityLocked(STATE_ERROR);
        Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget();
    }

    private void sendSoundTriggerFailure(@NonNull SoundTriggerFailure soundTriggerFailure) {
        Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE, soundTriggerFailure)
                .sendToTarget();
    }

    /** @hide */
    static final class SoundTriggerListener extends IHotwordRecognitionStatusCallback.Stub {
        private final Handler mHandler;
@@ -1726,6 +1757,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
        }
    }

    // TODO(b/267681692): remove the AsyncTask usage
    class RefreshAvailabilityTask extends AsyncTask<Void, Void, Void> {

        @Override
@@ -1744,15 +1776,19 @@ public class AlwaysOnHotwordDetector extends AbstractDetector {
                    }
                    updateAndNotifyStateChangedLocked(availability);
                }
            } catch (SecurityException e) {
            } catch (Exception e) {
                // Any exception here not caught will crash the process because AsyncTask does not
                // bubble up the exceptions to the client app, so we must propagate it to the app.
                Slog.w(TAG, "Failed to refresh availability", e);
                if (mTargetSdkVersion <= Build.VERSION_CODES.R) {
                    throw e;
                }
                synchronized (mLock) {
                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
                        sendUnknownFailure(
                                "Failed to refresh availability: " + Log.getStackTraceString(e));
                    } else {
                        updateAndNotifyStateChangedLocked(STATE_ERROR);
                    }
                }
            }

            return null;
        }
+26 −5
Original line number Diff line number Diff line
@@ -74,14 +74,22 @@ public final class SoundTriggerFailure implements Parcelable {
    public @interface SoundTriggerErrorCode {}

    private final int mErrorCode;
    private final int mSuggestedAction;
    private final String mErrorMessage;

    /**
     * @hide
     */
    @TestApi
    public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode,
            @NonNull String errorMessage) {
    public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage) {
        this(errorCode, errorMessage, getSuggestedActionBasedOnErrorCode(errorCode));
    }

    /**
     * @hide
     */
    public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage,
            @FailureSuggestedAction.FailureSuggestedActionDef int suggestedAction) {
        if (TextUtils.isEmpty(errorMessage)) {
            throw new IllegalArgumentException("errorMessage is empty or null.");
        }
@@ -95,7 +103,13 @@ public final class SoundTriggerFailure implements Parcelable {
            default:
                throw new IllegalArgumentException("Invalid ErrorCode: " + errorCode);
        }
        if (suggestedAction != getSuggestedActionBasedOnErrorCode(errorCode)
                && errorCode != ERROR_CODE_UNKNOWN) {
            throw new IllegalArgumentException("Invalid suggested next action: "
                    + "errorCode=" + errorCode + ", suggestedAction=" + suggestedAction);
        }
        mErrorMessage = errorMessage;
        mSuggestedAction = suggestedAction;
    }

    /**
@@ -119,7 +133,11 @@ public final class SoundTriggerFailure implements Parcelable {
     */
    @FailureSuggestedAction.FailureSuggestedActionDef
    public int getSuggestedAction() {
        switch (mErrorCode) {
        return mSuggestedAction;
    }

    private static int getSuggestedActionBasedOnErrorCode(@SoundTriggerErrorCode int errorCode) {
        switch (errorCode) {
            case ERROR_CODE_UNKNOWN:
            case ERROR_CODE_MODULE_DIED:
            case ERROR_CODE_UNEXPECTED_PREEMPTION:
@@ -144,8 +162,11 @@ public final class SoundTriggerFailure implements Parcelable {

    @Override
    public String toString() {
        return "SoundTriggerFailure { errorCode = " + mErrorCode + ", errorMessage = "
                + mErrorMessage + " }";
        return "SoundTriggerFailure {"
                + " errorCode = " + mErrorCode
                + ", errorMessage = " + mErrorMessage
                + ", suggestedNextAction = " + mSuggestedAction
                + " }";
    }

    public static final @NonNull Parcelable.Creator<SoundTriggerFailure> CREATOR =