diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 14f050df2b49b..00fe3ca99384e 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -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); } - 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 { @Override @@ -1744,13 +1776,17 @@ 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) { - updateAndNotifyStateChangedLocked(STATE_ERROR); + if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { + sendUnknownFailure( + "Failed to refresh availability: " + Log.getStackTraceString(e)); + } else { + updateAndNotifyStateChangedLocked(STATE_ERROR); + } } } diff --git a/core/java/android/service/voice/SoundTriggerFailure.java b/core/java/android/service/voice/SoundTriggerFailure.java index 2ce5e5da47242..b4b0ffaf7c2a1 100644 --- a/core/java/android/service/voice/SoundTriggerFailure.java +++ b/core/java/android/service/voice/SoundTriggerFailure.java @@ -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 CREATOR =