Add implementation of fallback voice actions.
This commit adds a simple implementation of a fallback voice service,
which is used when a request to Assistant cannot be completed or fails.
For read actions, it reads out notification messages; for any other
action, it reads out an error message for.
The TTS package was adopted from the Car Messaging System App and
subsequently refactored.
Fix: 110587280
Test: manual
Change-Id: I2897e837a42154bcaab85f0b9a2b73185f599cfb
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 8c6dceb..4cfd22d 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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,16 +15,22 @@
*/
package com.android.car.assist.client;
+import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_AS_READ;
+import static android.app.Notification.Action.SEMANTIC_ACTION_REPLY;
+
import android.app.Notification;
+import android.app.RemoteInput;
import android.content.Context;
import android.os.Bundle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.Log;
+import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import com.android.car.assist.CarVoiceInteractionSession;
+import com.android.car.assist.client.tts.TextToSpeechHelper;
import com.android.internal.app.AssistUtils;
import java.util.Arrays;
@@ -42,22 +48,45 @@
*/
public class CarAssistUtils {
public static final String TAG = "CarAssistUtils";
- private static final List<Integer> sRequiredSemanticActions = Collections.unmodifiableList(
+ private static final List<Integer> REQUIRED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
Arrays.asList(
- Notification.Action.SEMANTIC_ACTION_REPLY,
- Notification.Action.SEMANTIC_ACTION_MARK_AS_READ
+ SEMANTIC_ACTION_REPLY,
+ SEMANTIC_ACTION_MARK_AS_READ
)
);
// Currently, all supported semantic actions are required.
- private static final List<Integer> sSupportedSemanticActions = sRequiredSemanticActions;
+ private static final List<Integer> SUPPORTED_SEMANTIC_ACTIONS = REQUIRED_SEMANTIC_ACTIONS;
+
+ private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() {
+ @Override
+ public void onTextToSpeechStarted() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTextToSpeechStarted");
+ }
+ }
+
+ @Override
+ public void onTextToSpeechStopped(boolean error) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTextToSpeechStopped");
+ }
+ if (error) {
+ Toast.makeText(mContext, mErrorMessage, Toast.LENGTH_LONG).show();
+ }
+ }
+ };
private final Context mContext;
private final AssistUtils mAssistUtils;
+ private final FallbackAssistant mFallbackAssistant;
+ private final String mErrorMessage;
public CarAssistUtils(Context context) {
- mAssistUtils = new AssistUtils(context);
mContext = context;
+ mAssistUtils = new AssistUtils(context);
+ mFallbackAssistant = new FallbackAssistant(new TextToSpeechHelper(context));
+ mErrorMessage = context.getString(R.string.assist_action_failed_toast);
}
/**
@@ -88,7 +117,7 @@
/** Returns true if the semantic action provided can be supported. */
public static boolean isSupportedSemanticAction(int semanticAction) {
- return sSupportedSemanticActions.contains(semanticAction);
+ return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
}
/**
@@ -111,25 +140,25 @@
private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
List<Integer> semanticActionList = Arrays.stream(sbn.getNotification().actions)
.map(Notification.Action::getSemanticAction)
- .filter(sRequiredSemanticActions::contains)
+ .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
.collect(Collectors.toList());
Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
return semanticActionList.size() == semanticActionSet.size()
- && semanticActionSet.containsAll(sRequiredSemanticActions);
+ && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
}
/**
- * Returns true if the reply callback has exactly one RemoteInput.
+ * Returns true if the reply callback has at least one {@link RemoteInput}.
* <p/>
* Precondition: There exists only one reply callback.
*/
private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
return Arrays.stream(sbn.getNotification().actions)
- .filter(action ->
- action.getSemanticAction() == Notification.Action.SEMANTIC_ACTION_REPLY)
+ .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
.map(Notification.Action::getRemoteInputs)
- .anyMatch(remoteInputs -> remoteInputs != null && remoteInputs.length == 1);
+ .filter(Objects::nonNull)
+ .anyMatch(remoteInputs -> remoteInputs.length > 0);
}
/** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
@@ -138,53 +167,62 @@
return IntStream.range(0, notification.actions.length)
.mapToObj(i -> NotificationCompat.getAction(notification, i))
.filter(Objects::nonNull)
- .filter(action -> sRequiredSemanticActions.contains(action.getSemanticAction()))
+ .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
.noneMatch(NotificationCompat.Action::getShowsUserInterface);
}
/**
- * Requests a given action from the current active assistant.
+ * Requests a given action from the current active Assistant.
+ *
*
* @param sbn the notification payload to deliver to assistant
* @param semanticAction the semantic action that is to be requested
* @return true if the request was successful
*/
- public boolean requestAssistantAction(StatusBarNotification sbn, int semanticAction) {
- switch (semanticAction) {
- case Notification.Action.SEMANTIC_ACTION_MARK_AS_READ:
- return readMessageNotification(sbn);
- case Notification.Action.SEMANTIC_ACTION_REPLY:
- return replyMessageNotification(sbn);
- default:
- Log.w(TAG, "Unhanded semanticAction");
+ public boolean requestAssistantVoiceAction(StatusBarNotification sbn, int semanticAction) {
+ if (!isCarCompatibleMessagingNotification(sbn)) {
+ Log.w(TAG, "Assistant action requested for non-compatible notification.");
+ return false;
}
- return false;
+ switch (semanticAction) {
+ case SEMANTIC_ACTION_MARK_AS_READ:
+ return readMessageNotification(sbn);
+ case SEMANTIC_ACTION_REPLY:
+ return replyMessageNotification(sbn);
+ default:
+ return false;
+ }
}
/**
* Requests a read action for the notification from the current active Assistant.
+ * If the Assistant is cannot handle the request, a fallback implementation will attempt to
+ * handle it.
*
* @param sbn the notification to deliver as the payload
- * @return true if the read request to Assistant was successful
+ * @return true if the read request was handled successfully
*/
private boolean readMessageNotification(StatusBarNotification sbn) {
- return requestAction(sbn, BundleBuilder.buildAssistantReadBundle(sbn));
+ return requestAction(BundleBuilder.buildAssistantReadBundle(sbn))
+ || mFallbackAssistant.handleReadAction(sbn, mListener);
}
/**
* Requests a reply action for the notification from the current active Assistant.
+ * If the Assistant is cannot handle the request, a fallback implementation will attempt to
+ * handle it.
*
* @param sbn the notification to deliver as the payload
- * @return true if the reply request to Assistant was successful
+ * @return true if the reply request was handled successfully
*/
private boolean replyMessageNotification(StatusBarNotification sbn) {
- return requestAction(sbn, BundleBuilder.buildAssistantReplyBundle(sbn));
+ return requestAction(BundleBuilder.buildAssistantReplyBundle(sbn))
+ || mFallbackAssistant.handleErrorMessage(mErrorMessage, mListener);
}
- private boolean requestAction(StatusBarNotification sbn, Bundle payloadArguments) {
- return isCarCompatibleMessagingNotification(sbn)
- && assistantIsNotificationListener()
+ private boolean requestAction(Bundle payloadArguments) {
+ return assistantIsNotificationListener()
&& mAssistUtils.showSessionForActiveService(payloadArguments,
CarVoiceInteractionSession.SHOW_SOURCE_NOTIFICATION, null, null);
}