blob: fe8c5ae092ef63bc8f4810585ac96390cb483465 [file] [log] [blame]
/*
* 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;
}
}
}