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 package com.android.car.messenger.core.util; 17 18 import static com.android.car.assist.CarVoiceInteractionSession.KEY_ACTION; 19 import static com.android.car.assist.CarVoiceInteractionSession.KEY_CONVERSATION; 20 import static com.android.car.assist.CarVoiceInteractionSession.KEY_DEVICE_ADDRESS; 21 import static com.android.car.assist.CarVoiceInteractionSession.KEY_DEVICE_NAME; 22 import static com.android.car.assist.CarVoiceInteractionSession.KEY_NOTIFICATION; 23 import static com.android.car.assist.CarVoiceInteractionSession.KEY_PHONE_NUMBER; 24 import static com.android.car.assist.CarVoiceInteractionSession.KEY_SEND_PENDING_INTENT; 25 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_CONVERSATION; 26 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION; 27 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_CONVERSATION; 28 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION; 29 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_SEND_SMS; 30 import static com.android.car.messenger.core.shared.MessageConstants.ACTION_DIRECT_SEND; 31 import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MARK_AS_READ; 32 import static com.android.car.messenger.core.shared.MessageConstants.ACTION_REPLY; 33 import static com.android.car.messenger.core.shared.MessageConstants.EXTRA_ACCOUNT_ID; 34 import static com.android.car.messenger.core.shared.MessageConstants.EXTRA_CONVERSATION_KEY; 35 36 import android.app.Activity; 37 import android.app.PendingIntent; 38 import android.app.RemoteAction; 39 import android.app.RemoteInput; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.graphics.drawable.Icon; 43 import android.os.Bundle; 44 import android.service.notification.StatusBarNotification; 45 import android.text.TextUtils; 46 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 50 import com.android.car.messenger.R; 51 import com.android.car.messenger.common.Conversation; 52 import com.android.car.messenger.common.Conversation.ConversationAction; 53 import com.android.car.messenger.common.Conversation.ConversationAction.ActionType; 54 import com.android.car.messenger.core.interfaces.AppFactory; 55 import com.android.car.messenger.core.models.UserAccount; 56 import com.android.car.messenger.core.service.MessengerService; 57 import com.android.car.messenger.core.shared.MessageConstants; 58 import com.android.car.messenger.core.shared.NotificationHandler; 59 60 import java.util.ArrayList; 61 import java.util.List; 62 63 /** Voice Util classes for requesting voice interactions and responding to voice actions */ 64 public class VoiceUtil { 65 66 /** Represents a null user account id */ 67 private static final int NULL_ACCOUNT_ID = 0; 68 VoiceUtil()69 private VoiceUtil() {} 70 71 /** Requests Voice request to read a conversation */ voiceRequestReadConversation( @onNull Activity activity, @NonNull UserAccount userAccount, @NonNull Conversation conversation)72 public static void voiceRequestReadConversation( 73 @NonNull Activity activity, 74 @NonNull UserAccount userAccount, 75 @NonNull Conversation conversation) { 76 if (conversation.getMessages().isEmpty()) { 77 L.d("No messages to read from Conversation! Returning."); 78 return; 79 } 80 voiceRequestHelper( 81 activity, 82 conversation, 83 userAccount, 84 VOICE_ACTION_READ_CONVERSATION, 85 VOICE_ACTION_READ_NOTIFICATION); 86 } 87 88 /** Requests Voice request to reply to a conversation */ voiceRequestReplyConversation( @onNull Activity activity, @NonNull UserAccount userAccount, @NonNull Conversation conversation)89 public static void voiceRequestReplyConversation( 90 @NonNull Activity activity, 91 @NonNull UserAccount userAccount, 92 @NonNull Conversation conversation) { 93 voiceRequestHelper( 94 activity, 95 conversation, 96 userAccount, 97 VOICE_ACTION_REPLY_CONVERSATION, 98 VOICE_ACTION_REPLY_NOTIFICATION); 99 } 100 voiceRequestHelper( @onNull Activity activity, @NonNull Conversation conversation, @NonNull UserAccount userAccount, @NonNull String conversationAction, @NonNull String notificationAction)101 private static void voiceRequestHelper( 102 @NonNull Activity activity, 103 @NonNull Conversation conversation, 104 @NonNull UserAccount userAccount, 105 @NonNull String conversationAction, 106 @NonNull String notificationAction) { 107 Bundle args = new Bundle(); 108 Conversation tapToReadConversation = 109 createTapToReadConversation(conversation, userAccount.getId()); 110 boolean isConversationSupported = 111 activity.getResources().getBoolean(R.bool.ttr_conversation_supported); 112 if (isConversationSupported) { 113 // New API using generic Conversation class 114 // is currently limited in support by partner assistants and is being phased in. 115 args.putString(KEY_ACTION, conversationAction); 116 args.putBundle(KEY_CONVERSATION, tapToReadConversation.toBundle()); 117 } else { 118 // Continue using legacy SBN 119 StatusBarNotification sbn = 120 NotificationHandler.postNotificationForLegacyTapToRead(tapToReadConversation); 121 if (sbn == null) { 122 L.e("Failed to convert Conversation to SBN for Legacy Tap To Read."); 123 return; 124 } 125 args.putString(KEY_ACTION, notificationAction); 126 args.putParcelable(KEY_NOTIFICATION, sbn); 127 } 128 129 activity.showAssist(args); 130 } 131 132 /** Requests Voice request to start a generic compose voice interaction */ voiceRequestGenericCompose(Activity activity, UserAccount userAccount)133 public static void voiceRequestGenericCompose(Activity activity, UserAccount userAccount) { 134 Bundle bundle = new Bundle(); 135 bundle.putString(KEY_ACTION, VOICE_ACTION_SEND_SMS); 136 bundle.putString(KEY_DEVICE_ADDRESS, userAccount.getIccId()); 137 bundle.putString(KEY_DEVICE_NAME, userAccount.getName()); 138 PendingIntent sendIntent = 139 createServiceIntent( 140 ACTION_DIRECT_SEND, /* conversationKey= */ null, userAccount.getId()); 141 bundle.putParcelable(KEY_SEND_PENDING_INTENT, sendIntent); 142 activity.showAssist(bundle); 143 } 144 145 /** 146 * Returns a new conversation containing the tap to read pending intents to be transferred over 147 * to the Voice Assistant. 148 * 149 * <p>The conversation object returned remained unmodified. 150 * 151 * <p>This is important to allow the Assistant have a different instance than the one that 152 * powers our UI. We can create new pending intents without modifying the instance the Assistant 153 * holds. 154 * 155 * @return new conversation instance with the same data and pending intents for tap to read. 156 */ createTapToReadConversation( Conversation conversation, int userAccountId)157 public static Conversation createTapToReadConversation( 158 Conversation conversation, int userAccountId) { 159 Context context = AppFactory.get().getContext(); 160 String conversationKey = conversation.getId(); 161 Conversation.Builder builder = conversation.toBuilder(); 162 163 final int replyIcon = R.drawable.car_ui_icon_reply; 164 final String replyString = context.getString(R.string.action_reply); 165 PendingIntent replyIntent = 166 createServiceIntent(ACTION_REPLY, conversationKey, userAccountId); 167 ConversationAction replyAction = 168 new ConversationAction( 169 ActionType.ACTION_TYPE_REPLY, 170 new RemoteAction( 171 Icon.createWithResource(context, replyIcon), 172 replyString, 173 replyString, 174 replyIntent), 175 new RemoteInput.Builder(Intent.EXTRA_TEXT).build()); 176 177 final int markAsReadIcon = android.R.drawable.ic_media_play; 178 final String markAsReadString = context.getString(R.string.action_mark_as_read); 179 PendingIntent markAsReadIntent = 180 createServiceIntent(ACTION_MARK_AS_READ, conversationKey, userAccountId); 181 ConversationAction markAsReadAction = 182 new ConversationAction( 183 ActionType.ACTION_TYPE_MARK_AS_READ, 184 new RemoteAction( 185 Icon.createWithResource(context, markAsReadIcon), 186 markAsReadString, 187 markAsReadString, 188 markAsReadIntent), 189 null); 190 191 List<ConversationAction> actions = new ArrayList<>(); 192 actions.add(replyAction); 193 actions.add(markAsReadAction); 194 builder.setActions(actions); 195 return builder.build(); 196 } 197 createServiceIntent( @onNull String action, @Nullable String conversationKey, int userAccountId)198 private static PendingIntent createServiceIntent( 199 @NonNull String action, @Nullable String conversationKey, int userAccountId) { 200 Context context = AppFactory.get().getContext(); 201 Bundle bundle = new Bundle(); 202 if (conversationKey != null) { 203 bundle.putString(EXTRA_CONVERSATION_KEY, conversationKey); 204 } 205 bundle.putInt(EXTRA_ACCOUNT_ID, userAccountId); 206 Intent intent = 207 new Intent(context, MessengerService.class) 208 .setAction(action) 209 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP) 210 .setClass(context, MessengerService.class) 211 .putExtras(bundle); 212 213 int requestCode = 214 (conversationKey == null) ? action.hashCode() : conversationKey.hashCode(); 215 return PendingIntent.getForegroundService( 216 context, requestCode, intent, PendingIntent.FLAG_MUTABLE); 217 } 218 219 /** Sends a reply, meant to be used from a caller originating from voice input. */ directSend(Intent intent)220 public static void directSend(Intent intent) { 221 final CharSequence phoneNumber = intent.getCharSequenceExtra(KEY_PHONE_NUMBER); 222 final String iccId = intent.getStringExtra(KEY_DEVICE_ADDRESS); 223 final CharSequence message = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); 224 if (iccId == null || phoneNumber == null || TextUtils.isEmpty(message)) { 225 L.e("Dropping voice reply. Received no icc id, phone Number and/or empty message!"); 226 return; 227 } 228 L.d("Sending a message to specified phone number"); 229 AppFactory.get() 230 .getDataModel() 231 .sendMessage(iccId, phoneNumber.toString(), message.toString()); 232 } 233 234 /** Sends a reply, meant to be used from a caller originating from voice input. */ voiceReply(Intent intent)235 public static void voiceReply(Intent intent) { 236 final String conversationKey = intent.getStringExtra(EXTRA_CONVERSATION_KEY); 237 final int accountId = 238 intent.getIntExtra(MessageConstants.EXTRA_ACCOUNT_ID, NULL_ACCOUNT_ID); 239 final Bundle bundle = RemoteInput.getResultsFromIntent(intent); 240 if (bundle == null || accountId == NULL_ACCOUNT_ID) { 241 L.e("Dropping voice reply. Received null bundle or no user account id in bundle!"); 242 return; 243 } 244 final CharSequence message = bundle.getCharSequence(Intent.EXTRA_TEXT); 245 L.d("voiceReply: " + message); 246 if (!TextUtils.isEmpty(message)) { 247 AppFactory.get() 248 .getDataModel() 249 .replyConversation(accountId, conversationKey, message.toString()); 250 } 251 } 252 253 /** Mark a conversation associated with a given sender key as read. */ mute(Intent intent)254 public static void mute(Intent intent) { 255 Bundle extras = intent.getExtras(); 256 if (extras != null) { 257 final String conversationKey = extras.getString(EXTRA_CONVERSATION_KEY); 258 L.d("mute"); 259 AppFactory.get().getDataModel().muteConversation(conversationKey, true); 260 } 261 } 262 263 /** Mark a conversation associated with a given sender key as read. */ markAsRead(Intent intent)264 public static void markAsRead(Intent intent) { 265 Bundle extras = intent.getExtras(); 266 if (extras != null) { 267 final String conversationKey = extras.getString(EXTRA_CONVERSATION_KEY); 268 L.d("marking as read"); 269 AppFactory.get().getDataModel().markAsRead(conversationKey); 270 } 271 } 272 } 273