1 /* 2 * Copyright (C) 2019 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.client; 18 19 20 import android.annotation.Nullable; 21 import android.app.ActivityManager; 22 import android.app.Notification; 23 import android.app.Notification.MessagingStyle.Message; 24 import android.app.PendingIntent; 25 import android.app.Person; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.os.Parcelable; 29 import android.service.notification.StatusBarNotification; 30 import android.util.Log; 31 import android.widget.Toast; 32 33 import androidx.core.app.NotificationCompat; 34 35 import com.android.car.assist.client.tts.TextToSpeechHelper; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.Map; 42 43 /** 44 * Handles Assistant request fallbacks in the case that Assistant cannot fulfill the request for 45 * any given reason. 46 * <p/> 47 * Simply reads out the notification messages for read requests, and speaks out 48 * an error message for other requests. 49 */ 50 public class FallbackAssistant { 51 52 private static final String TAG = FallbackAssistant.class.getSimpleName(); 53 54 private final Context mContext; 55 private final TextToSpeechHelper mTextToSpeechHelper; 56 private final RequestIdGenerator mRequestIdGenerator; 57 private Map<Long, ActionRequestInfo> mRequestIdToActionRequestInfo = new HashMap<>(); 58 // String that means "says", to be used when reading out a message (i.e. <Sender> says 59 // <Message). 60 private final String mVerbForSays; 61 62 private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() { 63 @Override 64 public void onTextToSpeechStarted(long requestId) { 65 if (Log.isLoggable(TAG, Log.DEBUG)) { 66 Log.d(TAG, "onTextToSpeechStarted"); 67 } 68 } 69 70 @Override 71 public void onTextToSpeechStopped(long requestId, boolean error) { 72 if (Log.isLoggable(TAG, Log.DEBUG)) { 73 Log.d(TAG, "onTextToSpeechStopped"); 74 } 75 76 if (error) { 77 Toast.makeText(mContext, mContext.getString(R.string.assist_action_failed_toast), 78 Toast.LENGTH_LONG).show(); 79 } 80 finishAction(requestId, error); 81 } 82 }; 83 84 /** Listener to allow clients to be alerted when their requested message has been read. **/ 85 public interface Listener { 86 /** 87 * Called after the TTS engine has finished reading aloud the message. 88 */ onMessageRead(boolean hasError)89 void onMessageRead(boolean hasError); 90 } 91 FallbackAssistant(Context context)92 public FallbackAssistant(Context context) { 93 mContext = context; 94 mTextToSpeechHelper = new TextToSpeechHelper(context, mListener); 95 mRequestIdGenerator = new RequestIdGenerator(); 96 mVerbForSays = mContext.getString(R.string.says); 97 } 98 99 /** 100 * Handles a fallback read action by reading all messages in the notification. 101 * 102 * @param sbn the payload notification from which to extract messages from 103 */ handleReadAction(StatusBarNotification sbn, Listener listener)104 public void handleReadAction(StatusBarNotification sbn, Listener listener) { 105 if (mTextToSpeechHelper.isSpeaking()) { 106 mTextToSpeechHelper.requestStop(); 107 } 108 109 Parcelable[] messagesBundle = sbn.getNotification().extras 110 .getParcelableArray(Notification.EXTRA_MESSAGES); 111 112 if (messagesBundle == null || messagesBundle.length == 0) { 113 listener.onMessageRead(/* hasError= */ true); 114 return; 115 } 116 117 List<CharSequence> messages = new ArrayList<>(); 118 List<Message> messageList = Message.getMessagesFromBundleArray(messagesBundle); 119 if (messageList == null || messageList.isEmpty()) { 120 Log.w(TAG, "No messages could be extracted from the bundle"); 121 listener.onMessageRead(/* hasError= */ true); 122 return; 123 } 124 125 Person previousSender = messageList.get(0).getSenderPerson(); 126 if (previousSender != null) { 127 messages.add(previousSender.getName()); 128 messages.add(mVerbForSays); 129 } 130 for (Message message : messageList) { 131 if (!message.getSenderPerson().equals(previousSender)) { 132 messages.add(message.getSenderPerson().getName()); 133 messages.add(mVerbForSays); 134 previousSender = message.getSenderPerson(); 135 } 136 messages.add(message.getText()); 137 } 138 139 long requestId = mRequestIdGenerator.generateRequestId(); 140 141 if (mTextToSpeechHelper.requestPlay(messages, requestId)) { 142 if (Log.isLoggable(TAG, Log.DEBUG)) { 143 Log.d(TAG, "Requesting TTS to read message with requestId: " + requestId); 144 } 145 mRequestIdToActionRequestInfo.put(requestId, new ActionRequestInfo(sbn, listener)); 146 } else { 147 listener.onMessageRead(/* hasError= */ true); 148 } 149 } 150 151 /** 152 * Handles generic (non-read) actions by reading out an error message. 153 * 154 * @param errorMessage the error message to read out 155 */ handleErrorMessage(CharSequence errorMessage, Listener listener)156 public void handleErrorMessage(CharSequence errorMessage, Listener listener) { 157 if (mTextToSpeechHelper.isSpeaking()) { 158 mTextToSpeechHelper.requestStop(); 159 } 160 161 long requestId = mRequestIdGenerator.generateRequestId(); 162 if (mTextToSpeechHelper.requestPlay(Collections.singletonList(errorMessage), 163 requestId)) { 164 if (Log.isLoggable(TAG, Log.DEBUG)) { 165 Log.d(TAG, "Requesting TTS to read error with requestId: " + requestId); 166 } 167 mRequestIdToActionRequestInfo.put(requestId, new ActionRequestInfo( 168 /* statusBarNotification= */ null, 169 listener)); 170 } else { 171 listener.onMessageRead(/* hasError= */ true); 172 } 173 } 174 finishAction(long requestId, boolean hasError)175 private void finishAction(long requestId, boolean hasError) { 176 if (!mRequestIdToActionRequestInfo.containsKey(requestId)) { 177 Log.w(TAG, "No actionRequestInfo found for requestId: " + requestId); 178 return; 179 } 180 181 ActionRequestInfo info = mRequestIdToActionRequestInfo.remove(requestId); 182 183 if (info.getStatusBarNotification() != null && !hasError) { 184 sendMarkAsReadIntent(info.getStatusBarNotification()); 185 } 186 187 info.getListener().onMessageRead(hasError); 188 } 189 sendMarkAsReadIntent(StatusBarNotification sbn)190 private void sendMarkAsReadIntent(StatusBarNotification sbn) { 191 NotificationCompat.Action markAsReadAction = CarAssistUtils.getMarkAsReadAction( 192 sbn.getNotification()); 193 boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG); 194 195 if (markAsReadAction != null) { 196 if (sendPendingIntent(markAsReadAction.getActionIntent(), 197 null /* resultIntent */) != ActivityManager.START_SUCCESS 198 && isDebugLoggable) { 199 Log.d(TAG, "Could not relay mark as read event to the messaging app."); 200 } 201 } else if (isDebugLoggable) { 202 Log.d(TAG, "Car compat message notification has no mark as read action: " 203 + sbn.getKey()); 204 } 205 } 206 sendPendingIntent(PendingIntent pendingIntent, Intent resultIntent)207 private int sendPendingIntent(PendingIntent pendingIntent, Intent resultIntent) { 208 try { 209 return pendingIntent.sendAndReturnResult(/* context= */ mContext, /* code= */ 0, 210 /* intent= */ resultIntent, /* onFinished= */null, 211 /* handler= */ null, /* requiredPermissions= */ null, 212 /* options= */ null); 213 } catch (PendingIntent.CanceledException e) { 214 // Do not take down the app over this 215 Log.w(TAG, "Sending contentIntent failed: " + e); 216 return ActivityManager.START_ABORTED; 217 } 218 } 219 220 /** Helper class that generates unique IDs per TTS request. **/ 221 private class RequestIdGenerator { 222 private long mCounter; 223 RequestIdGenerator()224 RequestIdGenerator() { 225 mCounter = 0; 226 } 227 generateRequestId()228 public long generateRequestId() { 229 return ++mCounter; 230 } 231 } 232 233 /** 234 * Contains all of the information needed to start and finish actions supported by the 235 * FallbackAssistant. 236 **/ 237 private class ActionRequestInfo { 238 private final StatusBarNotification mStatusBarNotification; 239 private final Listener mListener; 240 ActionRequestInfo(@ullable StatusBarNotification statusBarNotification, Listener listener)241 ActionRequestInfo(@Nullable StatusBarNotification statusBarNotification, 242 Listener listener) { 243 mStatusBarNotification = statusBarNotification; 244 mListener = listener; 245 } 246 247 @Nullable getStatusBarNotification()248 StatusBarNotification getStatusBarNotification() { 249 return mStatusBarNotification; 250 } 251 getListener()252 Listener getListener() { 253 return mListener; 254 } 255 } 256 } 257