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();
+    }
+}