Add FallbackAssistant config flag, alert assistant to request perms

Created a config flag to enable or disable the FallbackAssistant.
If there is an active voice service that doesn't have notification listener
permissions, notify it to request permissions from the user.

Bug: 123095811
Test: manual
Change-Id: I069a1c5956d300960a3bb62178d54976e68b238b
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
index 4472fa4..84f4408 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
@@ -19,6 +19,8 @@
 import static android.app.Notification.Action.SEMANTIC_ACTION_REPLY;
 import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_NOTIFICATION;
 
+import static com.android.car.assist.CarVoiceInteractionSession.EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING;
+
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.Notification;
@@ -29,6 +31,7 @@
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 
+import androidx.annotation.StringDef;
 import androidx.core.app.NotificationCompat;
 
 import com.android.car.assist.CarVoiceInteractionSession;
@@ -68,11 +71,36 @@
     private final AssistUtils mAssistUtils;
     private final FallbackAssistant mFallbackAssistant;
     private final String mErrorMessage;
+    private final boolean mIsFallbackAssistantEnabled;
 
     /** Interface used to receive callbacks from voice action requests. */
     public interface ActionRequestCallback {
-        /** Callback issued from a voice request on success/error. */
-        void onResult(boolean hasError);
+        /**
+         * The action was successfully completed either by the active or fallback assistant.
+         **/
+        String RESULT_SUCCESS = "SUCCESS";
+
+        /**
+         * The action was not successfully completed, but the active assistant has been prompted to
+         * alert the user of this error and handle it. The caller of this callback is recommended
+         * to NOT alert the user of this error again.
+         */
+        String RESULT_FAILED_WITH_ERROR_HANDLED = "FAILED_WITH_ERROR_HANDLED";
+
+        /**
+         * The action has not been successfully completed, and the error has not been handled.
+         **/
+        String RESULT_FAILED = "FAILED";
+
+        /**
+         * The list of result states.
+         */
+        @StringDef({RESULT_FAILED, RESULT_FAILED_WITH_ERROR_HANDLED, RESULT_SUCCESS})
+        @interface ResultState {
+        }
+
+        /** Callback containing the result of completing the voice action request. */
+        void onResult(@ResultState String state);
     }
 
     public CarAssistUtils(Context context) {
@@ -80,12 +108,27 @@
         mAssistUtils = new AssistUtils(context);
         mFallbackAssistant = new FallbackAssistant(context);
         mErrorMessage = context.getString(R.string.assist_action_failed_toast);
+        mIsFallbackAssistantEnabled =
+                context.getResources().getBoolean(R.bool.config_enableFallbackAssistant);
+    }
+
+    /**
+     * @return {@code true} if there is an active assistant.
+     */
+    public boolean hasActiveAssistant() {
+        return mAssistUtils.getActiveServiceComponentName() != null;
     }
 
     /**
      * Returns true if the current active assistant has notification listener permissions.
      */
     public boolean assistantIsNotificationListener() {
+        if (!hasActiveAssistant()) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "No active assistant was found.");
+            }
+            return false;
+        }
         final String activeComponent = mAssistUtils.getActiveServiceComponentName()
                 .flattenToString();
         int slashIndex = activeComponent.indexOf("/");
@@ -216,7 +259,7 @@
             ActionRequestCallback callback) {
         if (!isCarCompatibleMessagingNotification(sbn)) {
             Log.w(TAG, "Assistant action requested for non-compatible notification.");
-            callback.onResult(/* hasError= */ true);
+            callback.onResult(ActionRequestCallback.RESULT_FAILED);
             return;
         }
 
@@ -229,7 +272,7 @@
                 return;
             default:
                 Log.w(TAG, "Requested Assistant action for unsupported semantic action.");
-                callback.onResult(/* hasError= */ true);
+                callback.onResult(ActionRequestCallback.RESULT_FAILED);
                 return;
         }
     }
@@ -269,26 +312,46 @@
     private void requestAction(String action, StatusBarNotification sbn, Bundle payloadArguments,
             ActionRequestCallback callback) {
 
+        if (!hasActiveAssistant()) {
+            if (mIsFallbackAssistantEnabled) {
+                handleFallback(sbn, action, callback);
+            } else {
+                // If there is no active assistant, and fallback assistant is not enabled, then
+                // there is nothing for us to do.
+                callback.onResult(ActionRequestCallback.RESULT_FAILED);
+            }
+            return;
+        }
+
         if (!assistantIsNotificationListener()) {
-            handleFallback(sbn, action, callback);
+            if (mIsFallbackAssistantEnabled) {
+                handleFallback(sbn, action, callback);
+            } else {
+                // If there is an active assistant, alert them to request permissions.
+                callback.onResult(requestHandleMissingPermissions()
+                        ? ActionRequestCallback.RESULT_FAILED_WITH_ERROR_HANDLED
+                        : ActionRequestCallback.RESULT_FAILED);
+            }
             return;
         }
 
         IVoiceActionCheckCallback actionCheckCallback = new IVoiceActionCheckCallback.Stub() {
             @Override
             public void onComplete(List<String> supportedActions) {
+                String resultState = ActionRequestCallback.RESULT_FAILED;
                 boolean success;
                 if (supportedActions != null && supportedActions.contains(action)) {
                     if (Log.isLoggable(TAG, Log.DEBUG)) {
                         Log.d(TAG, "Launching active Assistant for action: " + action);
                     }
-                    success = mAssistUtils.showSessionForActiveService(payloadArguments,
-                            SHOW_SOURCE_NOTIFICATION, null, null);
+                    if (mAssistUtils.showSessionForActiveService(payloadArguments,
+                            SHOW_SOURCE_NOTIFICATION, null, null)) {
+                        resultState = ActionRequestCallback.RESULT_SUCCESS;
+                    }
                 } else {
                     Log.w(TAG, "Active Assistant does not support voice action: " + action);
-                    success = false;
                 }
-                callback.onResult(/* hasError= */ !success);
+                callback.onResult(resultState);
             }
         };
 
@@ -300,8 +363,16 @@
             ActionRequestCallback callback) {
         FallbackAssistant.Listener listener = new FallbackAssistant.Listener() {
             @Override
-            public void onMessageRead(boolean error) {
-                callback.onResult(error);
+            public void onMessageRead(boolean hasError) {
+                String resultState = hasError ? ActionRequestCallback.RESULT_FAILED
+                        : ActionRequestCallback.RESULT_SUCCESS;
+                // Only change the resultState if fallback failed, and assistant successfully
+                // alerted to prompt user for permissions.
+                if (hasActiveAssistant() && requestHandleMissingPermissions()
+                        && resultState.equals(ActionRequestCallback.RESULT_FAILED)) {
+                    resultState = ActionRequestCallback.RESULT_FAILED_WITH_ERROR_HANDLED;
+                }
+                callback.onResult(resultState);
             }
         };
 
@@ -314,8 +385,25 @@
                 break;
             default:
                 Log.w(TAG, "Requested unsupported FallbackAssistant action.");
-                callback.onResult(/* hasError= */ true);
+                callback.onResult(ActionRequestCallback.RESULT_FAILED);
                 return;
         }
     }
+
+    /**
+     * Requests the active voice service to handle the permissions missing error.
+     *
+     * @return {@code true} if active assistant was successfully alerted.
+     **/
+    private boolean requestHandleMissingPermissions() {
+        Bundle payloadArguments = BundleBuilder
+                .buildAssistantHandleExceptionBundle(
+                        EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING);
+        boolean requestedSuccessfully = mAssistUtils.showSessionForActiveService(payloadArguments,
+                SHOW_SOURCE_NOTIFICATION, null, null);
+        if (!requestedSuccessfully) {
+            Log.w(TAG, "Failed to alert assistant to request permissions from user");
+        }
+        return requestedSuccessfully;
+    }
 }