Car Assist Tap To Read with Conversation Update
Adds support for tap to read api using a Conversation class
Bug: 161144808
Test: Tested locally and works with code logic
Change-Id: I025569c058e0fa4f18281970439cd957db174849
diff --git a/car-assist-lib/Android.bp b/car-assist-lib/Android.bp
index 4f8f578..a500b36 100644
--- a/car-assist-lib/Android.bp
+++ b/car-assist-lib/Android.bp
@@ -26,6 +26,8 @@
sdk_version: "current",
static_libs: [
+ // ensure unbundled
+ "car-messaging-models",
"androidx.legacy_legacy-support-v4",
"androidx.annotation_annotation",
],
diff --git a/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java b/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java
index 6c16fdc..b66f5eb 100644
--- a/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java
+++ b/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java
@@ -25,6 +25,7 @@
import androidx.annotation.StringDef;
import com.android.car.assist.payloadhandlers.NotificationPayloadHandler;
+import com.android.car.messenger.common.Conversation;
/**
* An active voice interaction session on the car, providing additional actions which assistant
@@ -42,47 +43,124 @@
public static final String KEY_EXCEPTION = "KEY_EXCEPTION";
/**
- * The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION} payload
+ * The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION}
+ * payload
* {@link Bundle}. Must map to a boolean. If value is true, the Fallback Assistant that can
- * handle the user's request has been disabled.
+ * handle
+ * the user's request has been disabled.
*/
public static final String KEY_FALLBACK_ASSISTANT_ENABLED = "KEY_FALLBACK_ASSISTANT_ENABLED";
/**
* The key used for the payload {@link Bundle}, if a {@link StatusBarNotification} is used as
- * the payload.
+ * the
+ * payload.
*/
public static final String KEY_NOTIFICATION = "KEY_NOTIFICATION";
+ /**
+ * The key used for the payload {@link Bundle}, if a {@link Conversation} is used as the
+ * payload.
+ */
+ public static final String KEY_CONVERSATION = "KEY_CONVERSATION";
+
/** Indicates to assistant that no action was specified. */
public static final String VOICE_ACTION_NO_ACTION = "VOICE_ACTION_NO_ACTION";
- /** Indicates to assistant that a read action is being requested for a given payload. */
+ /**
+ * Indicates to assistant that a read action is being requested for a given payload.
+ * A {@link StatusBarNotification} object will be provided in the payload
+ */
public static final String VOICE_ACTION_READ_NOTIFICATION = "VOICE_ACTION_READ_NOTIFICATION";
- /** Indicates to assistant that a reply action is being requested for a given payload. */
+ /**
+ * Indicates to assistant that a reply action is being requested for a given payload.
+ * A {@link StatusBarNotification} object will be provided in the payload
+ */
public static final String VOICE_ACTION_REPLY_NOTIFICATION = "VOICE_ACTION_REPLY_NOTIFICATION";
/**
+ * Indicates to assistant that a read conversation action is being requested.
+ * A {@link Conversation} object will be provided in the payload.
+ */
+ public static final String VOICE_ACTION_READ_CONVERSATION = "VOICE_ACTION_READ_CONVERSATION";
+
+ /**
+ * Indicates to assistant that a reply conversation action is being requested.
+ * A {@link Conversation} object will be provided in the payload.
+ */
+ public static final String VOICE_ACTION_REPLY_CONVERSATION = "VOICE_ACTION_REPLY_CONVERSATION";
+
+ /**
+ * Indicates to digital assistant that it should capture a SMS message from the user,
+ * potentially
+ * finding which contact to send the message to and which device to send the message from
+ * (only if the application does not send the digital assistant this information in the
+ * bundle).
+ * Once the digital assistant has gathered the information from the user, it should send back
+ * the PendingIntent (provided in the bundle) with the information so the application can
+ * actually
+ * send the SMS.
+ */
+ public static final String VOICE_ACTION_SEND_SMS = "VOICE_ACTION_SEND_SMS";
+
+ /* Recipient's phone number. If this and the recipient name are not provided,
+ * by the application, digital assistant must do contact disambiguation
+ * and add the phone number to the pending intent
+ */
+ public static final String KEY_PHONE_NUMBER = "KEY_PHONE_NUMBER";
+
+ /**
+ * Recipient's name. If this and the recipient phone number are not provided by the application,
+ * digital assistant must do contact disambiguation but is not required to add the name
+ * to the PendingIntent.
+ */
+ public static final String KEY_RECIPIENT_NAME = "KEY RECIPIENT NAME";
+
+ /* Recipient's UID in the Contact Provider database. Optionally provided by the application.
+ * Not required to be sent back by the digital assistant.
+ */
+ public static final String KEY_RECIPIENT_UID = "KEY_RECIPIENT_UID";
+
+ /* Friendly name of the device in which to send the message from. If not
+ * provided by the application, digital assistant must do device disambiguation
+ * but is not required to add it to the Pending Intent.
+ */
+ public static final String KEY_DEVICE_NAME = "KEY DEVICE_NAME";
+
+ /* Bluetooth device address of the device of which to send the message from.
+ * If not provided by the application, assistant must do device disambiguation
+ * and add this to the Pendingintent.
+ */
+ public static final String KEY_DEVICE_ADDRESS = "KEY_DEVICE_ADDRESS";
+
+ /* KEY_SEND_PENDING_INTENT is not null and always be provided by the application.
+ * The application must preload the pending intent with any KEYs, it provides the assistant
+ * that is also needed to send the message.
+ * (i.e. if the application passes in the KEY_PHONE_NUMBER in the Bundle,
+ * the assistant can assume the application has already put this in the
+ * PendingIntent and may not re-add it to the PendingIntent).
+ */
+ public static final String KEY_SEND_PENDING_INTENT = "KEY_SEND_PENDING INTENT";
+
+ /**
* Indicates to assistant that it should resolve the exception in the given payload (found in
* {@link CarVoiceInteractionSession#KEY_EXCEPTION}'s value).
*/
public static final String VOICE_ACTION_HANDLE_EXCEPTION = "VOICE_ACTION_HANDLE_EXCEPTION";
- /**
- * The list of exceptions the active voice service must handle.
- */
+ /** The list of exceptions the active voice service must handle. */
@StringDef({EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING})
- public @interface ExceptionValue {}
+ public @interface ExceptionValue {
+ }
/**
* Indicates to assistant that it is missing the Notification Listener permission, and should
* request this permission from the user.
- **/
+ */
public static final String EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING =
"EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING";
-
private final NotificationPayloadHandler mNotificationPayloadHandler;
public CarVoiceInteractionSession(Context context) {
@@ -117,34 +195,34 @@
}
/**
- * Called when the session UI is going to be shown. This is called after
- * {@link #onCreateContentView} (if the session's content UI needed to be created) and
- * immediately prior to the window being shown. This may be called while the window
- * is already shown, if a show request has come in while it is shown, to allow you to
- * update the UI to match the new show arguments.
+ * Called when the session UI is going to be shown. This is called after {@link
+ * #onCreateContentView} (if the session's content UI needed to be created) and immediately
+ * prior
+ * to the window being shown. This may be called while the window is already shown, if a show
+ * request has come in while it is shown, to allow you to update the UI to match the new show
+ * arguments.
*
- * @param action The action that is being requested for this session
- * (e.g. {@link CarVoiceInteractionSession#VOICE_ACTION_READ_NOTIFICATION},
- * {@link CarVoiceInteractionSession#VOICE_ACTION_REPLY_NOTIFICATION}).
- * @param args The arguments that were supplied to
- * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
- * @param flags The show flags originally provided to
- * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
+ * @param action The action that is being requested for this session (e.g. {@link
+ * CarVoiceInteractionSession#VOICE_ACTION_READ_NOTIFICATION}, {@link
+ * CarVoiceInteractionSession#VOICE_ACTION_REPLY_NOTIFICATION}).
+ * @param args The arguments that were supplied to {@link VoiceInteractionService#showSession
+ * VoiceInteractionService.showSession}.
+ * @param flags The show flags originally provided to
+ * {@link VoiceInteractionService#showSession
+ * VoiceInteractionService.showSession}.
*/
protected abstract void onShow(String action, Bundle args, int flags);
- /**
- * Returns true if the request was initiated for a car notification.
- */
+ /** Returns true if the request was initiated for a car notification. */
private static boolean isCarNotificationSource(int flags) {
return (flags & SHOW_SOURCE_NOTIFICATION) != 0;
}
/**
- * Returns the action {@link String} provided in the args {@Bundle},
- * or {@link CarVoiceInteractionSession#VOICE_ACTION_NO_ACTION} if no such string was provided.
+ * Returns the action {@link String} provided in the args {@link Bundle}, or {@link
+ * CarVoiceInteractionSession#VOICE_ACTION_NO_ACTION} if no such string was provided.
*/
protected static String getRequestedVoiceAction(Bundle args) {
return args.getString(KEY_ACTION, VOICE_ACTION_NO_ACTION);
}
-}
+}
\ No newline at end of file
diff --git a/car-assist-lib/src/com/android/car/assist/payloadhandlers/ConversationPayloadHandler.java b/car-assist-lib/src/com/android/car/assist/payloadhandlers/ConversationPayloadHandler.java
new file mode 100644
index 0000000..36bebb4
--- /dev/null
+++ b/car-assist-lib/src/com/android/car/assist/payloadhandlers/ConversationPayloadHandler.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.assist.payloadhandlers;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationCompat.Action;
+import androidx.core.app.NotificationCompat.Action.SemanticAction;
+import androidx.core.app.NotificationCompat.MessagingStyle;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.android.car.messenger.common.Conversation;
+import com.android.car.messenger.common.Conversation.ConversationAction.ActionType;
+
+/**
+ * Util class that provides a conversion between {@link Conversation} and {@link
+ * Notification}
+ */
+public class ConversationPayloadHandler {
+
+ private ConversationPayloadHandler() {
+ }
+
+ /**
+ * Returns the package name from the pending intent
+ */
+ @SuppressWarnings("PendingIntentCreator")
+ @NonNull
+ public static String getPackageName(@NonNull PendingIntent pendingIntent) {
+ return pendingIntent.getCreatorPackage();
+ }
+
+ /**
+ * Creates a notification from {@link Conversation}
+ */
+ @Nullable
+ public static Notification createNotificationFromConversation(
+ @NonNull Context context,
+ @NonNull String channelId,
+ @NonNull Conversation conversation,
+ @NonNull @DrawableRes int iconRes) {
+ MessagingStyle messagingStyle = getMessagingStyle(conversation);
+ Action muteAction = getNotificationAction(context, conversation,
+ ActionType.ACTION_TYPE_MUTE);
+ Action markAsReadAction = getNotificationAction(context, conversation,
+ ActionType.ACTION_TYPE_MARK_AS_READ);
+ Action replyAction = getNotificationAction(context, conversation,
+ ActionType.ACTION_TYPE_REPLY);
+
+ return new NotificationCompat.Builder(context, channelId)
+ .setStyle(messagingStyle)
+ .setSmallIcon(iconRes)
+ .setLargeIcon(getBitmap(conversation.getConversationIcon(), context))
+ .addAction(replyAction)
+ .addAction(markAsReadAction)
+ .addAction(muteAction)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .build();
+ }
+
+ @Nullable
+ private static Action getNotificationAction(
+ @NonNull Context context,
+ @NonNull Conversation conversation,
+ @ActionType int actionType) {
+ Conversation.ConversationAction conversationAction =
+ getConversationAction(conversation, actionType);
+ Action notificationAction = null;
+ if (conversationAction != null) {
+ RemoteAction remoteAction = conversationAction.getRemoteAction();
+ IconCompat icon =
+ IconCompat.createFromIcon(context, remoteAction.getIcon());
+ Action.Builder builder =
+ new Action.Builder(
+ /* icon= */ icon,
+ /* charSequence= */ remoteAction.getTitle(),
+ remoteAction.getActionIntent())
+ .setShowsUserInterface(false)
+ .setSemanticAction(getSemanticAction(actionType));
+ if (conversationAction.getRemoteInput() != null) {
+ builder.addRemoteInput(toCompat(conversationAction.getRemoteInput()));
+ }
+ notificationAction = builder.build();
+ }
+ return notificationAction;
+ }
+
+ @SemanticAction
+ private static int getSemanticAction(@ActionType int actionType) {
+ switch (actionType) {
+ case ActionType.ACTION_TYPE_MUTE:
+ return Action.SEMANTIC_ACTION_MUTE;
+ case ActionType.ACTION_TYPE_MARK_AS_READ:
+ return Action.SEMANTIC_ACTION_MARK_AS_READ;
+ case ActionType.ACTION_TYPE_REPLY:
+ return Action.SEMANTIC_ACTION_REPLY;
+ default:
+ return Action.SEMANTIC_ACTION_NONE;
+ }
+ }
+
+ private static MessagingStyle getMessagingStyle(
+ @NonNull Conversation conversation) {
+ MessagingStyle messagingStyle =
+ new MessagingStyle(conversation.getUser());
+ messagingStyle.setGroupConversation(isGroupConversation(conversation));
+ messagingStyle.setConversationTitle(conversation.getConversationTitle());
+ for (Conversation.Message message : conversation.getMessages()) {
+ messagingStyle.addMessage(
+ new MessagingStyle.Message(
+ message.getText(), message.getTimestamp(), message.getPerson()));
+ }
+ return messagingStyle;
+ }
+
+ private static Bitmap getBitmap(@Nullable IconCompat icon, Context context) {
+ if (icon == null) {
+ return null;
+ }
+ Drawable drawable = icon.loadDrawable(context);
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ return null;
+ }
+ }
+
+ private static boolean isGroupConversation(Conversation conversation) {
+ if (conversation.getParticipants().contains(conversation.getUser())) {
+ return conversation.getParticipants().size() > 2;
+ } else {
+ return conversation.getParticipants().size() > 1;
+ }
+ }
+
+ @Nullable
+ private static Conversation.ConversationAction getConversationAction(
+ Conversation conversation, @ActionType int action) {
+ if (conversation.getActions() == null) {
+ return null;
+ }
+ return conversation.getActions().stream()
+ .filter(it -> it.getActionType() == action)
+ .findFirst()
+ .orElse(null);
+ }
+
+ // Conversation classes use android.app.RemoteInput
+ // instead of androidx.core.app.RemoteInput in order to support
+ // being parcelable. While Notification uses androidx.core.app.RemoteInput.
+ // We used these methods to convert between the two.
+ private static androidx.core.app.RemoteInput toCompat(RemoteInput src) {
+ androidx.core.app.RemoteInput.Builder builder =
+ new androidx.core.app.RemoteInput.Builder(src.getResultKey())
+ .setLabel(src.getLabel())
+ .addExtras(src.getExtras());
+ return builder.build();
+ }
+}