1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.assist.payloadhandlers; 18 19 import android.app.Notification; 20 import android.app.PendingIntent; 21 import android.app.RemoteAction; 22 import android.app.RemoteInput; 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.drawable.BitmapDrawable; 26 import android.graphics.drawable.Drawable; 27 28 import androidx.annotation.DrawableRes; 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 import androidx.core.app.NotificationCompat; 32 import androidx.core.app.NotificationCompat.Action; 33 import androidx.core.app.NotificationCompat.Action.SemanticAction; 34 import androidx.core.app.NotificationCompat.MessagingStyle; 35 import androidx.core.graphics.drawable.IconCompat; 36 37 import com.android.car.messenger.common.Conversation; 38 import com.android.car.messenger.common.Conversation.ConversationAction.ActionType; 39 40 /** 41 * Util class that provides a conversion between {@link Conversation} and {@link 42 * Notification} 43 */ 44 public class ConversationPayloadHandler { 45 ConversationPayloadHandler()46 private ConversationPayloadHandler() { 47 } 48 49 /** 50 * Returns the package name from the pending intent 51 */ 52 @SuppressWarnings("PendingIntentCreator") 53 @NonNull getPackageName(@onNull PendingIntent pendingIntent)54 public static String getPackageName(@NonNull PendingIntent pendingIntent) { 55 return pendingIntent.getCreatorPackage(); 56 } 57 58 /** 59 * Creates a notification from {@link Conversation} 60 */ 61 @NonNull createNotificationFromConversation( @onNull Context context, @NonNull String channelId, @NonNull Conversation conversation, @DrawableRes int iconRes, @Nullable String group)62 public static Notification createNotificationFromConversation( 63 @NonNull Context context, 64 @NonNull String channelId, 65 @NonNull Conversation conversation, 66 @DrawableRes int iconRes, 67 @Nullable String group) { 68 MessagingStyle messagingStyle = getMessagingStyle(conversation); 69 Action muteAction = getNotificationAction(context, conversation, 70 ActionType.ACTION_TYPE_MUTE); 71 Action markAsReadAction = getNotificationAction(context, conversation, 72 ActionType.ACTION_TYPE_MARK_AS_READ); 73 Action replyAction = getNotificationAction(context, conversation, 74 ActionType.ACTION_TYPE_REPLY); 75 76 return new NotificationCompat.Builder(context, channelId) 77 .setStyle(messagingStyle) 78 .setSmallIcon(iconRes) 79 .setLargeIcon(getBitmap(conversation.getConversationIcon(), context)) 80 .addAction(replyAction) 81 .addAction(markAsReadAction) 82 .addAction(muteAction) 83 .setCategory(NotificationCompat.CATEGORY_MESSAGE) 84 .setGroup(group) 85 .build(); 86 } 87 88 @Nullable getNotificationAction( @onNull Context context, @NonNull Conversation conversation, @ActionType int actionType)89 private static Action getNotificationAction( 90 @NonNull Context context, 91 @NonNull Conversation conversation, 92 @ActionType int actionType) { 93 Conversation.ConversationAction conversationAction = 94 getConversationAction(conversation, actionType); 95 Action notificationAction = null; 96 if (conversationAction != null) { 97 RemoteAction remoteAction = conversationAction.getRemoteAction(); 98 IconCompat icon = 99 IconCompat.createFromIcon(context, remoteAction.getIcon()); 100 Action.Builder builder = 101 new Action.Builder( 102 /* icon= */ icon, 103 /* charSequence= */ remoteAction.getTitle(), 104 remoteAction.getActionIntent()) 105 .setShowsUserInterface(false) 106 .setSemanticAction(getSemanticAction(actionType)); 107 if (conversationAction.getRemoteInput() != null) { 108 builder.addRemoteInput(toCompat(conversationAction.getRemoteInput())); 109 } 110 notificationAction = builder.build(); 111 } 112 return notificationAction; 113 } 114 115 @SemanticAction getSemanticAction(@ctionType int actionType)116 private static int getSemanticAction(@ActionType int actionType) { 117 switch (actionType) { 118 case ActionType.ACTION_TYPE_MUTE: 119 return Action.SEMANTIC_ACTION_MUTE; 120 case ActionType.ACTION_TYPE_MARK_AS_READ: 121 return Action.SEMANTIC_ACTION_MARK_AS_READ; 122 case ActionType.ACTION_TYPE_REPLY: 123 return Action.SEMANTIC_ACTION_REPLY; 124 default: 125 return Action.SEMANTIC_ACTION_NONE; 126 } 127 } 128 getMessagingStyle( @onNull Conversation conversation)129 private static MessagingStyle getMessagingStyle( 130 @NonNull Conversation conversation) { 131 MessagingStyle messagingStyle = 132 new MessagingStyle(conversation.getUser()); 133 messagingStyle.setGroupConversation(isGroupConversation(conversation)); 134 messagingStyle.setConversationTitle(conversation.getConversationTitle()); 135 for (Conversation.Message message : conversation.getMessages()) { 136 messagingStyle.addMessage( 137 new MessagingStyle.Message( 138 message.getText(), message.getTimestamp(), message.getPerson())); 139 } 140 return messagingStyle; 141 } 142 143 @Nullable getBitmap(@ullable IconCompat icon, Context context)144 private static Bitmap getBitmap(@Nullable IconCompat icon, Context context) { 145 if (icon == null) { 146 return null; 147 } 148 Drawable drawable = icon.loadDrawable(context); 149 if (drawable instanceof BitmapDrawable) { 150 return ((BitmapDrawable) drawable).getBitmap(); 151 } else { 152 return null; 153 } 154 } 155 isGroupConversation(Conversation conversation)156 private static boolean isGroupConversation(Conversation conversation) { 157 if (conversation.getParticipants().contains(conversation.getUser())) { 158 return conversation.getParticipants().size() > 2; 159 } else { 160 return conversation.getParticipants().size() > 1; 161 } 162 } 163 164 @Nullable getConversationAction( Conversation conversation, @ActionType int action)165 private static Conversation.ConversationAction getConversationAction( 166 Conversation conversation, @ActionType int action) { 167 if (conversation.getActions() == null) { 168 return null; 169 } 170 return conversation.getActions().stream() 171 .filter(it -> it.getActionType() == action) 172 .findFirst() 173 .orElse(null); 174 } 175 176 // Conversation classes use android.app.RemoteInput 177 // instead of androidx.core.app.RemoteInput in order to support 178 // being parcelable. While Notification uses androidx.core.app.RemoteInput. 179 // We used these methods to convert between the two. toCompat(RemoteInput src)180 private static androidx.core.app.RemoteInput toCompat(RemoteInput src) { 181 androidx.core.app.RemoteInput.Builder builder = 182 new androidx.core.app.RemoteInput.Builder(src.getResultKey()) 183 .setLabel(src.getLabel()) 184 .addExtras(src.getExtras()); 185 return builder.build(); 186 } 187 } 188