Improve error handling for Trusted Hotword

Add a new callback with an DetectorFailure parameter to let the
assistant know what went wrong during using the hotword detector.

Bug: 256780491
Bug: 261012843
Test: atest CtsVoiceInteractionTestCases

Change-Id: I0b7d9625abfa10f93ad8564e85faedd1db4fa51d
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 6581c42..5f83313 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -13141,7 +13141,8 @@
 
   public static interface HotwordDetector.Callback {
     method public void onDetected(@NonNull android.service.voice.AlwaysOnHotwordDetector.EventPayload);
-    method public void onError();
+    method @Deprecated public void onError();
+    method public default void onFailure(@NonNull android.service.voice.DetectorFailure);
     method public void onHotwordDetectionServiceInitialized(int);
     method public void onHotwordDetectionServiceRestarted();
     method public void onRecognitionPaused();
@@ -13202,7 +13203,7 @@
   }
 
   public static interface VisualQueryDetector.Callback {
-    method public void onError();
+    method public void onFailure(@NonNull android.service.voice.DetectorFailure);
     method public void onQueryDetected(@NonNull String);
     method public void onQueryFinished();
     method public void onQueryRejected();
diff --git a/core/java/android/service/voice/AbstractDetector.java b/core/java/android/service/voice/AbstractDetector.java
index 39c2b98..466bc05 100644
--- a/core/java/android/service/voice/AbstractDetector.java
+++ b/core/java/android/service/voice/AbstractDetector.java
@@ -231,9 +231,12 @@
 
         /** Called when the detection fails due to an error. */
         @Override
-        public void onError() {
-            Slog.v(TAG, "BinderCallback#onError");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> mCallback.onError()));
+        public void onError(DetectorFailure detectorFailure) {
+            Slog.v(TAG, "BinderCallback#onError detectorFailure: " + detectorFailure);
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+                mCallback.onFailure(detectorFailure != null ? detectorFailure
+                        : new UnknownFailure("Error data is null"));
+            }));
         }
 
         @Override
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 48b7a59..bb3f03d 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -732,7 +732,13 @@
          */
         public abstract void onDetected(@NonNull EventPayload eventPayload);
 
-        /** {@inheritDoc} */
+        /**
+         * {@inheritDoc}
+         *
+         * @deprecated Use {@link HotwordDetector.Callback#onError(DetectorFailure)} instead.
+         */
+        @Deprecated
+        @Override
         public abstract void onError();
 
         /** {@inheritDoc} */
@@ -1658,10 +1664,19 @@
         @Override
         public void onError(int status) {
             Slog.i(TAG, "onError: " + status);
-            mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
+            // This is a workaround before the sound trigger uses the onDetectionFailure method.
+            Message.obtain(mHandler, MSG_DETECTION_ERROR,
+                    new SoundTriggerFailure(status, "Sound trigger error")).sendToTarget();
         }
 
         @Override
+        public void onDetectionFailure(DetectorFailure detectorFailure) {
+            Slog.v(TAG, "onDetectionFailure detectorFailure: " + detectorFailure);
+            Message.obtain(mHandler, MSG_DETECTION_ERROR,
+                    detectorFailure != null ? detectorFailure
+                            : new UnknownFailure("Error data is null")).sendToTarget();
+        }
+        @Override
         public void onRecognitionPaused() {
             Slog.i(TAG, "onRecognitionPaused");
             mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
@@ -1716,7 +1731,7 @@
                         mExternalCallback.onDetected((EventPayload) message.obj);
                         break;
                     case MSG_DETECTION_ERROR:
-                        mExternalCallback.onError();
+                        mExternalCallback.onFailure((DetectorFailure) msg.obj);
                         break;
                     case MSG_DETECTION_PAUSE:
                         mExternalCallback.onRecognitionPaused();
diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java
index 562277e..22d97b7 100644
--- a/core/java/android/service/voice/HotwordDetector.java
+++ b/core/java/android/service/voice/HotwordDetector.java
@@ -231,10 +231,29 @@
 
         /**
          * Called when the detection fails due to an error.
+         *
+         * @deprecated On Android 14 and above, implement {@link #onFailure(DetectorFailure)}
+         * instead.
          */
+        @Deprecated
         void onError();
 
         /**
+         * Called when the detection fails due to an error, the subclasses of
+         * {@link DetectorFailure} will be reported to the detector.
+         *
+         * @see android.service.voice.HotwordDetectionServiceFailure
+         * @see android.service.voice.SoundTriggerFailure
+         * @see android.service.voice.UnknownFailure
+         * @see android.service.voice.VisualQueryDetectionServiceFailure
+         *
+         * @param detectorFailure It provides the error code, error message and suggested action.
+         */
+        default void onFailure(@NonNull DetectorFailure detectorFailure) {
+            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.
diff --git a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
index 61ac68b..f800c1e 100644
--- a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
+++ b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
@@ -17,6 +17,7 @@
 package android.service.voice;
 
 import android.media.AudioFormat;
+import android.service.voice.DetectorFailure;
 import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordRejectedResult;
 
@@ -38,7 +39,7 @@
     /**
      * Called when the detection fails due to an error.
      */
-    void onError();
+    void onError(in DetectorFailure detectorFailure);
 
     /**
      * Called when the detected result was not detected.
diff --git a/core/java/android/service/voice/IVisualQueryDetectionVoiceInteractionCallback.aidl b/core/java/android/service/voice/IVisualQueryDetectionVoiceInteractionCallback.aidl
index 2eb2470..1a935c0 100644
--- a/core/java/android/service/voice/IVisualQueryDetectionVoiceInteractionCallback.aidl
+++ b/core/java/android/service/voice/IVisualQueryDetectionVoiceInteractionCallback.aidl
@@ -16,7 +16,7 @@
 
 package android.service.voice;
 
-import android.media.AudioFormat;
+import android.service.voice.DetectorFailure;
 
 /**
  * Callback for returning the detected result from the VisualQueryDetectionService.
@@ -43,6 +43,5 @@
     /**
      * Called when the detection fails due to an error.
      */
-    void onError();
-
+    void onDetectionFailure(in DetectorFailure detectorFailure);
 }
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index 6e17bd0..d4b6f3b 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -169,9 +169,12 @@
 
         /** Called when the detection fails due to an error. */
         @Override
-        public void onError() {
-            Slog.v(TAG, "BinderCallback#onError");
-            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> mCallback.onError()));
+        public void onError(DetectorFailure detectorFailure) {
+            Slog.v(TAG, "BinderCallback#onError detectorFailure: " + detectorFailure);
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+                mCallback.onFailure(detectorFailure != null ? detectorFailure
+                        : new UnknownFailure("Error data is null"));
+            }));
         }
 
         @Override
@@ -222,6 +225,16 @@
             if (DEBUG) {
                 Slog.i(TAG, "Ignored #onError (" + status + ") event");
             }
+            // TODO: Check if we still need to implement this method with DetectorFailure mechanism.
+        }
+
+        @Override
+        public void onDetectionFailure(DetectorFailure detectorFailure) throws RemoteException {
+            Slog.v(TAG, "onDetectionFailure detectorFailure: " + detectorFailure);
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> {
+                mCallback.onFailure(detectorFailure != null ? detectorFailure
+                        : new UnknownFailure("Error data is null"));
+            }));
         }
 
         @Override
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index e4c47ef..0be3253 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -216,8 +216,7 @@
         /**
          * Called when the detection fails due to an error.
          */
-        //TODO(b/265390855): Replace this callback with the new onError(DetectorError) design.
-        void onError();
+        void onFailure(@NonNull DetectorFailure detectorFailure);
     }
 
     private class VisualQueryDetectorInitializationDelegate extends AbstractDetector {
@@ -294,12 +293,11 @@
 
         /** Called when the detection fails due to an error. */
         @Override
-        public void onError() {
-            Slog.v(TAG, "BinderCallback#onError");
+        public void onDetectionFailure(DetectorFailure detectorFailure) {
+            Slog.v(TAG, "BinderCallback#onDetectionFailure");
             Binder.withCleanCallingIdentity(() -> mExecutor.execute(
-                    () -> mCallback.onError()));
+                    () -> mCallback.onFailure(detectorFailure)));
         }
-
     }
 
 
@@ -373,5 +371,9 @@
             Slog.v(TAG, "Initialization Error: (" + status + ")");
             // Do nothing
         }
+
+        @Override
+        public void onDetectionFailure(DetectorFailure detectorFailure) throws RemoteException {
+        }
     }
 }
diff --git a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
index d0214e6..813febf 100644
--- a/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
+++ b/core/java/com/android/internal/app/IHotwordRecognitionStatusCallback.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.app;
 
 import android.hardware.soundtrigger.SoundTrigger;
+import android.service.voice.DetectorFailure;
 import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordRejectedResult;
 
@@ -62,6 +63,13 @@
     void onError(int status);
 
     /**
+     * Called when the detection fails due to an error.
+     *
+     * @param detectorFailure It provides the error code, error message and suggested action.
+     */
+    void onDetectionFailure(in DetectorFailure detectorFailure);
+
+    /**
      * Called when the recognition is paused temporarily for some reason.
      */
     void onRecognitionPaused();
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index afee940..ec9bd2f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -70,6 +70,7 @@
 import android.os.SharedMemory;
 import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordDetectionService;
+import android.service.voice.HotwordDetectionServiceFailure;
 import android.service.voice.HotwordDetector;
 import android.service.voice.HotwordRejectedResult;
 import android.service.voice.IDspHotwordDetectionCallback;
@@ -122,10 +123,16 @@
             "Providing hotword detection result to VoiceInteractionService";
 
     // The error codes are used for onError callback
-    static final int HOTWORD_DETECTION_SERVICE_DIED = -1;
-    static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION = -2;
-    static final int CALLBACK_DETECT_TIMEOUT = -3;
-    static final int CALLBACK_ONDETECTED_STREAM_COPY_ERROR = -4;
+    static final int HOTWORD_DETECTION_SERVICE_DIED =
+            HotwordDetectionServiceFailure.ERROR_CODE_BINDING_DIED;
+    static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION =
+            HotwordDetectionServiceFailure.ERROR_CODE_ON_DETECTED_SECURITY_EXCEPTION;
+    static final int CALLBACK_DETECT_TIMEOUT =
+            HotwordDetectionServiceFailure.ERROR_CODE_DETECT_TIMEOUT;
+    static final int CALLBACK_ONDETECTED_STREAM_COPY_ERROR =
+            HotwordDetectionServiceFailure.ERROR_CODE_ON_DETECTED_STREAM_COPY_FAILURE;
+    static final int CALLBACK_COPY_AUDIO_DATA_FAILURE =
+            HotwordDetectionServiceFailure.ERROR_CODE_COPY_AUDIO_DATA_FAILURE;
 
     // TODO: These constants need to be refined.
     private static final long MAX_UPDATE_TIMEOUT_MILLIS = 30000;
@@ -426,7 +433,9 @@
                 Slog.w(TAG, "Failed supplying audio data to validator", e);
 
                 try {
-                    callback.onError();
+                    callback.onError(
+                            new HotwordDetectionServiceFailure(CALLBACK_COPY_AUDIO_DATA_FAILURE,
+                                    "Copy audio data failure for external source detection."));
                 } catch (RemoteException ex) {
                     Slog.w(TAG, "Failed to report onError status: " + ex);
                     if (getDetectorType() != HotwordDetector.DETECTOR_TYPE_VISUAL_QUERY_DETECTOR) {
@@ -505,7 +514,10 @@
                                                     getDetectorType(),
                                                     EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION,
                                                     mVoiceInteractionServiceUid);
-                                            callback.onError();
+                                            callback.onError(new HotwordDetectionServiceFailure(
+                                                    CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION,
+                                                    "Security exception occurs in #onDetected"
+                                                            + " method."));
                                             return;
                                         }
                                         HotwordDetectedResult newResult;
@@ -514,7 +526,9 @@
                                                     .startCopyingAudioStreams(triggerResult);
                                         } catch (IOException e) {
                                             // TODO: Write event
-                                            callback.onError();
+                                            callback.onError(new HotwordDetectionServiceFailure(
+                                                    CALLBACK_ONDETECTED_STREAM_COPY_ERROR,
+                                                    "Copy audio stream failure."));
                                             return;
                                         }
                                         callback.onDetected(newResult, /* audioFormat= */ null,
@@ -569,9 +583,11 @@
         mRemoteDetectionService = remoteDetectionService;
     }
 
-    void reportErrorLocked(int status) {
+    void reportErrorLocked(int errorCode, @NonNull String errorMessage) {
         try {
-            mCallback.onError(status);
+            // TODO: Use instanceof(this) to get different detector to set the right error source.
+            mCallback.onDetectionFailure(
+                    new HotwordDetectionServiceFailure(errorCode, errorMessage));
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to report onError status: " + e);
             if (getDetectorType() != HotwordDetector.DETECTOR_TYPE_VISUAL_QUERY_DETECTOR) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
index cb5b930..63e0f46 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java
@@ -34,6 +34,7 @@
 import android.os.SharedMemory;
 import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordDetectionService;
+import android.service.voice.HotwordDetectionServiceFailure;
 import android.service.voice.HotwordDetector;
 import android.service.voice.HotwordRejectedResult;
 import android.service.voice.IDspHotwordDetectionCallback;
@@ -130,7 +131,9 @@
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
                                 METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION,
                                 mVoiceInteractionServiceUid);
-                        externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
+                        externalCallback.onDetectionFailure(new HotwordDetectionServiceFailure(
+                                CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION,
+                                "Security exception occurs in #onDetected method."));
                         return;
                     }
                     saveProximityValueToBundle(result);
@@ -138,7 +141,9 @@
                     try {
                         newResult = mHotwordAudioStreamCopier.startCopyingAudioStreams(result);
                     } catch (IOException e) {
-                        externalCallback.onError(CALLBACK_ONDETECTED_STREAM_COPY_ERROR);
+                        externalCallback.onDetectionFailure(new HotwordDetectionServiceFailure(
+                                CALLBACK_ONDETECTED_STREAM_COPY_ERROR,
+                                "Copy audio stream failure."));
                         return;
                     }
                     externalCallback.onKeyphraseDetected(recognitionEvent, newResult);
@@ -201,7 +206,9 @@
                                 HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT,
                                 mVoiceInteractionServiceUid);
                         try {
-                            externalCallback.onError(CALLBACK_DETECT_TIMEOUT);
+                            externalCallback.onDetectionFailure(
+                                    new HotwordDetectionServiceFailure(CALLBACK_DETECT_TIMEOUT,
+                                            "Timeout to response to the detection result."));
                         } catch (RemoteException e) {
                             Slog.w(TAG, "Failed to report onError status: ", e);
                             HotwordMetricsLogger.writeDetectorEvent(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index b672b00..1ba3975 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -690,8 +690,8 @@
             //TODO(b265535257): report error to either service only.
             synchronized (HotwordDetectionConnection.this.mLock) {
                 runForEachDetectorSessionLocked((session) -> {
-                    session.reportErrorLocked(
-                            DetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
+                    session.reportErrorLocked(DetectorSession.HOTWORD_DETECTION_SERVICE_DIED,
+                            "Detection service is dead.");
                 });
             }
             // Can improve to log exit reason if needed
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
index 3ad963d..522d832 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
@@ -33,6 +33,7 @@
 import android.os.SharedMemory;
 import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordDetectionService;
+import android.service.voice.HotwordDetectionServiceFailure;
 import android.service.voice.HotwordDetector;
 import android.service.voice.HotwordRejectedResult;
 import android.service.voice.IDspHotwordDetectionCallback;
@@ -121,7 +122,9 @@
                                 HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
                                 METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION,
                                 mVoiceInteractionServiceUid);
-                        mSoftwareCallback.onError();
+                        mSoftwareCallback.onError(new HotwordDetectionServiceFailure(
+                                CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION,
+                                "Security exception occurs in #onDetected method."));
                         return;
                     }
                     saveProximityValueToBundle(result);
@@ -130,7 +133,9 @@
                         newResult = mHotwordAudioStreamCopier.startCopyingAudioStreams(result);
                     } catch (IOException e) {
                         // TODO: Write event
-                        mSoftwareCallback.onError();
+                        mSoftwareCallback.onError(new HotwordDetectionServiceFailure(
+                                CALLBACK_ONDETECTED_STREAM_COPY_ERROR,
+                                "Copy audio stream failure."));
                         return;
                     }
                     mSoftwareCallback.onDetected(newResult, null, null);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index 33150d8..c397812 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
@@ -16,6 +16,9 @@
 
 package com.android.server.voiceinteraction;
 
+import static android.service.voice.VisualQueryDetectionServiceFailure.ERROR_CODE_ILLEGAL_ATTENTION_STATE;
+import static android.service.voice.VisualQueryDetectionServiceFailure.ERROR_CODE_ILLEGAL_STREAMING_STATE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -30,6 +33,7 @@
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
 import android.service.voice.ISandboxedDetectionService;
 import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback;
+import android.service.voice.VisualQueryDetectionServiceFailure;
 import android.util.Slog;
 
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
@@ -102,6 +106,13 @@
                     mAttentionListener.onAttentionGained();
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error delivering attention gained event.", e);
+                    try {
+                        callback.onDetectionFailure(new VisualQueryDetectionServiceFailure(
+                                ERROR_CODE_ILLEGAL_ATTENTION_STATE,
+                                "Attention listener failed to switch to GAINED state."));
+                    } catch (RemoteException ex) {
+                        Slog.v(TAG, "Fail to call onDetectionFailure");
+                    }
                     return;
                 }
             }
@@ -117,6 +128,13 @@
                     mAttentionListener.onAttentionLost();
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error delivering attention lost event.", e);
+                    try {
+                        callback.onDetectionFailure(new VisualQueryDetectionServiceFailure(
+                                ERROR_CODE_ILLEGAL_ATTENTION_STATE,
+                                "Attention listener failed to switch to LOST state."));
+                    } catch (RemoteException ex) {
+                        Slog.v(TAG, "Fail to call onDetectionFailure");
+                    }
                     return;
                 }
             }
@@ -127,6 +145,9 @@
                 Slog.v(TAG, "BinderCallback#onQueryDetected");
                 if (!mEgressingData) {
                     Slog.v(TAG, "Query should not be egressed within the unattention state.");
+                    callback.onDetectionFailure(new VisualQueryDetectionServiceFailure(
+                            ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                            "Cannot stream queries without attention signals."));
                     return;
                 }
                 mQueryStreaming = true;
@@ -140,6 +161,9 @@
                 if (!mQueryStreaming) {
                     Slog.v(TAG, "Query streaming state signal FINISHED is block since there is"
                             + " no active query being streamed.");
+                    callback.onDetectionFailure(new VisualQueryDetectionServiceFailure(
+                            ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                            "Cannot send FINISHED signal with no query streamed."));
                     return;
                 }
                 callback.onQueryFinished();
@@ -152,6 +176,9 @@
                 if (!mQueryStreaming) {
                     Slog.v(TAG, "Query streaming state signal REJECTED is block since there is"
                             + " no active query being streamed.");
+                    callback.onDetectionFailure(new VisualQueryDetectionServiceFailure(
+                            ERROR_CODE_ILLEGAL_STREAMING_STATE,
+                            "Cannot send REJECTED signal with no query streamed."));
                     return;
                 }
                 callback.onQueryRejected();