| /* |
| * Copyright (C) 2019 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.client; |
| |
| |
| import android.annotation.Nullable; |
| import android.app.ActivityManager; |
| import android.app.Notification; |
| import android.app.Notification.MessagingStyle.Message; |
| import android.app.PendingIntent; |
| import android.app.Person; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.Parcelable; |
| import android.service.notification.StatusBarNotification; |
| import android.util.Log; |
| import android.widget.Toast; |
| |
| import androidx.core.app.NotificationCompat; |
| |
| import com.android.car.assist.client.tts.TextToSpeechHelper; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Handles Assistant request fallbacks in the case that Assistant cannot fulfill the request for |
| * any given reason. |
| * <p/> |
| * Simply reads out the notification messages for read requests, and speaks out |
| * an error message for other requests. |
| */ |
| public class FallbackAssistant { |
| |
| private static final String TAG = FallbackAssistant.class.getSimpleName(); |
| |
| private final Context mContext; |
| private final TextToSpeechHelper mTextToSpeechHelper; |
| private final RequestIdGenerator mRequestIdGenerator; |
| private Map<Long, ActionRequestInfo> mRequestIdToActionRequestInfo = new HashMap<>(); |
| // String that means "says", to be used when reading out a message (i.e. <Sender> says |
| // <Message). |
| private final String mVerbForSays; |
| |
| private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() { |
| @Override |
| public void onTextToSpeechStarted(long requestId) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onTextToSpeechStarted"); |
| } |
| } |
| |
| @Override |
| public void onTextToSpeechStopped(long requestId, boolean error) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "onTextToSpeechStopped"); |
| } |
| |
| if (error) { |
| Toast.makeText(mContext, mContext.getString(R.string.assist_action_failed_toast), |
| Toast.LENGTH_LONG).show(); |
| } |
| finishAction(requestId, error); |
| } |
| }; |
| |
| /** Listener to allow clients to be alerted when their requested message has been read. **/ |
| public interface Listener { |
| /** |
| * Called after the TTS engine has finished reading aloud the message. |
| */ |
| void onMessageRead(boolean hasError); |
| } |
| |
| public FallbackAssistant(Context context) { |
| mContext = context; |
| mTextToSpeechHelper = new TextToSpeechHelper(context, mListener); |
| mRequestIdGenerator = new RequestIdGenerator(); |
| mVerbForSays = mContext.getString(R.string.says); |
| } |
| |
| /** |
| * Handles a fallback read action by reading all messages in the notification. |
| * |
| * @param sbn the payload notification from which to extract messages from |
| */ |
| public void handleReadAction(StatusBarNotification sbn, Listener listener) { |
| if (mTextToSpeechHelper.isSpeaking()) { |
| mTextToSpeechHelper.requestStop(); |
| } |
| |
| Parcelable[] messagesBundle = sbn.getNotification().extras |
| .getParcelableArray(Notification.EXTRA_MESSAGES); |
| |
| if (messagesBundle == null || messagesBundle.length == 0) { |
| listener.onMessageRead(/* hasError= */ true); |
| return; |
| } |
| |
| List<CharSequence> messages = new ArrayList<>(); |
| List<Message> messageList = Message.getMessagesFromBundleArray(messagesBundle); |
| if (messageList == null || messageList.isEmpty()) { |
| Log.w(TAG, "No messages could be extracted from the bundle"); |
| listener.onMessageRead(/* hasError= */ true); |
| return; |
| } |
| |
| Person previousSender = messageList.get(0).getSenderPerson(); |
| if (previousSender != null) { |
| messages.add(previousSender.getName()); |
| messages.add(mVerbForSays); |
| } |
| for (Message message : messageList) { |
| if (!message.getSenderPerson().equals(previousSender)) { |
| messages.add(message.getSenderPerson().getName()); |
| messages.add(mVerbForSays); |
| previousSender = message.getSenderPerson(); |
| } |
| messages.add(message.getText()); |
| } |
| |
| long requestId = mRequestIdGenerator.generateRequestId(); |
| |
| if (mTextToSpeechHelper.requestPlay(messages, requestId)) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Requesting TTS to read message with requestId: " + requestId); |
| } |
| mRequestIdToActionRequestInfo.put(requestId, new ActionRequestInfo(sbn, listener)); |
| } else { |
| listener.onMessageRead(/* hasError= */ true); |
| } |
| } |
| |
| /** |
| * Handles generic (non-read) actions by reading out an error message. |
| * |
| * @param errorMessage the error message to read out |
| */ |
| public void handleErrorMessage(CharSequence errorMessage, Listener listener) { |
| if (mTextToSpeechHelper.isSpeaking()) { |
| mTextToSpeechHelper.requestStop(); |
| } |
| |
| long requestId = mRequestIdGenerator.generateRequestId(); |
| if (mTextToSpeechHelper.requestPlay(Collections.singletonList(errorMessage), |
| requestId)) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Requesting TTS to read error with requestId: " + requestId); |
| } |
| mRequestIdToActionRequestInfo.put(requestId, new ActionRequestInfo( |
| /* statusBarNotification= */ null, |
| listener)); |
| } else { |
| listener.onMessageRead(/* hasError= */ true); |
| } |
| } |
| |
| private void finishAction(long requestId, boolean hasError) { |
| if (!mRequestIdToActionRequestInfo.containsKey(requestId)) { |
| Log.w(TAG, "No actionRequestInfo found for requestId: " + requestId); |
| return; |
| } |
| |
| ActionRequestInfo info = mRequestIdToActionRequestInfo.remove(requestId); |
| |
| if (info.getStatusBarNotification() != null && !hasError) { |
| sendMarkAsReadIntent(info.getStatusBarNotification()); |
| } |
| |
| info.getListener().onMessageRead(hasError); |
| } |
| |
| private void sendMarkAsReadIntent(StatusBarNotification sbn) { |
| NotificationCompat.Action markAsReadAction = CarAssistUtils.getMarkAsReadAction( |
| sbn.getNotification()); |
| boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG); |
| |
| if (markAsReadAction != null) { |
| if (sendPendingIntent(markAsReadAction.getActionIntent(), |
| null /* resultIntent */) != ActivityManager.START_SUCCESS |
| && isDebugLoggable) { |
| Log.d(TAG, "Could not relay mark as read event to the messaging app."); |
| } |
| } else if (isDebugLoggable) { |
| Log.d(TAG, "Car compat message notification has no mark as read action: " |
| + sbn.getKey()); |
| } |
| } |
| |
| private int sendPendingIntent(PendingIntent pendingIntent, Intent resultIntent) { |
| try { |
| return pendingIntent.sendAndReturnResult(/* context= */ mContext, /* code= */ 0, |
| /* intent= */ resultIntent, /* onFinished= */null, |
| /* handler= */ null, /* requiredPermissions= */ null, |
| /* options= */ null); |
| } catch (PendingIntent.CanceledException e) { |
| // Do not take down the app over this |
| Log.w(TAG, "Sending contentIntent failed: " + e); |
| return ActivityManager.START_ABORTED; |
| } |
| } |
| |
| /** Helper class that generates unique IDs per TTS request. **/ |
| private class RequestIdGenerator { |
| private long mCounter; |
| |
| RequestIdGenerator() { |
| mCounter = 0; |
| } |
| |
| public long generateRequestId() { |
| return ++mCounter; |
| } |
| } |
| |
| /** |
| * Contains all of the information needed to start and finish actions supported by the |
| * FallbackAssistant. |
| **/ |
| private class ActionRequestInfo { |
| private final StatusBarNotification mStatusBarNotification; |
| private final Listener mListener; |
| |
| ActionRequestInfo(@Nullable StatusBarNotification statusBarNotification, |
| Listener listener) { |
| mStatusBarNotification = statusBarNotification; |
| mListener = listener; |
| } |
| |
| @Nullable |
| StatusBarNotification getStatusBarNotification() { |
| return mStatusBarNotification; |
| } |
| |
| Listener getListener() { |
| return mListener; |
| } |
| } |
| } |