blob: cb02ee3cf0e3bccefc60de137dee91367b3e433b [file] [log] [blame]
/*
* Copyright (C) 2020 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.messenger.core.util;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_ACTION;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_CONVERSATION;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_DEVICE_ADDRESS;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_DEVICE_NAME;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_NOTIFICATION;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_PHONE_NUMBER;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_SEND_PENDING_INTENT;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_CONVERSATION;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_CONVERSATION;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_SEND_SMS;
import static com.android.car.messenger.core.shared.MessageConstants.ACTION_DIRECT_SEND;
import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MARK_AS_READ;
import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MUTE;
import static com.android.car.messenger.core.shared.MessageConstants.ACTION_REPLY;
import static com.android.car.messenger.core.shared.MessageConstants.EXTRA_ACCOUNT_ID;
import static com.android.car.messenger.core.shared.MessageConstants.EXTRA_CONVERSATION_KEY;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.messenger.R;
import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.common.Conversation.ConversationAction;
import com.android.car.messenger.common.Conversation.ConversationAction.ActionType;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.models.UserAccount;
import com.android.car.messenger.core.service.MessengerService;
import com.android.car.messenger.core.shared.MessageConstants;
import com.android.car.messenger.core.shared.NotificationHandler;
import java.util.ArrayList;
import java.util.List;
/** Voice Util classes for requesting voice interactions and responding to voice actions */
public class VoiceUtil {
/** Represents a null user account id */
private static final int NULL_ACCOUNT_ID = 0;
private VoiceUtil() {}
/** Requests Voice request to read a conversation */
public static void voiceRequestReadConversation(
@NonNull Activity activity,
@NonNull UserAccount userAccount,
@NonNull Conversation conversation) {
if (conversation.getMessages().isEmpty()) {
L.d("No messages to read from Conversation! Returning.");
return;
}
voiceRequestHelper(
activity,
conversation,
userAccount,
VOICE_ACTION_READ_CONVERSATION,
VOICE_ACTION_READ_NOTIFICATION);
}
/** Requests Voice request to reply to a conversation */
public static void voiceRequestReplyConversation(
@NonNull Activity activity,
@NonNull UserAccount userAccount,
@NonNull Conversation conversation) {
voiceRequestHelper(
activity,
conversation,
userAccount,
VOICE_ACTION_REPLY_CONVERSATION,
VOICE_ACTION_REPLY_NOTIFICATION);
}
private static void voiceRequestHelper(
@NonNull Activity activity,
@NonNull Conversation conversation,
@NonNull UserAccount userAccount,
@NonNull String conversationAction,
@NonNull String notificationAction) {
Bundle args = new Bundle();
Conversation tapToReadConversation =
createTapToReadConversation(conversation, userAccount.getId());
// use legacy tap to read by default as support for
// new api using Conversation class is still very limited and very nascent
StatusBarNotification sbn =
NotificationHandler.postNotificationForLegacyTapToRead(tapToReadConversation);
if (sbn != null) {
args.putString(KEY_ACTION, notificationAction);
args.putParcelable(KEY_NOTIFICATION, sbn);
} else {
// New API using generic Conversation class
// is currently limited in support by partner assistants and is currently being phase
// in.
args.putString(KEY_ACTION, conversationAction);
args.putBundle(KEY_CONVERSATION, tapToReadConversation.toBundle());
}
activity.showAssist(args);
}
/** Requests Voice request to start a generic compose voice interaction */
public static void voiceRequestGenericCompose(Activity activity, UserAccount userAccount) {
Bundle bundle = new Bundle();
bundle.putString(KEY_ACTION, VOICE_ACTION_SEND_SMS);
bundle.putString(KEY_DEVICE_ADDRESS, userAccount.getIccId());
bundle.putString(KEY_DEVICE_NAME, userAccount.getName());
PendingIntent sendIntent =
createServiceIntent(
ACTION_DIRECT_SEND, /* conversationKey= */ null, userAccount.getId());
bundle.putParcelable(KEY_SEND_PENDING_INTENT, sendIntent);
activity.showAssist(bundle);
}
/**
* Returns a new conversation containing the tap to read pending intents to be transferred over
* to the Voice Assistant.
*
* <p>The conversation object returned remained unmodified.
*
* <p>This is important to allow the Assistant have a different instance than the one that
* powers our UI. We can create new pending intents without modifying the instance the Assistant
* holds.
*
* @return new conversation instance with the same data and pending intents for tap to read.
*/
public static Conversation createTapToReadConversation(
Conversation conversation, int userAccountId) {
Context context = AppFactory.get().getContext();
String conversationKey = conversation.getId();
Conversation.Builder builder = conversation.toBuilder();
final int replyIcon = R.drawable.car_ui_icon_reply;
final String replyString = context.getString(R.string.action_reply);
PendingIntent replyIntent =
createServiceIntent(ACTION_REPLY, conversationKey, userAccountId);
ConversationAction replyAction =
new ConversationAction(
ActionType.ACTION_TYPE_REPLY,
new RemoteAction(
Icon.createWithResource(context, replyIcon),
replyString,
replyString,
replyIntent),
new RemoteInput.Builder(Intent.EXTRA_TEXT).build());
final int markAsReadIcon = android.R.drawable.ic_media_play;
final String markAsReadString = context.getString(R.string.action_mark_as_read);
PendingIntent markAsReadIntent =
createServiceIntent(ACTION_MARK_AS_READ, conversationKey, userAccountId);
ConversationAction markAsReadAction =
new ConversationAction(
ActionType.ACTION_TYPE_MARK_AS_READ,
new RemoteAction(
Icon.createWithResource(context, markAsReadIcon),
markAsReadString,
markAsReadString,
markAsReadIntent),
null);
final int muteIcon = R.drawable.car_ui_icon_toggle_mute;
final String muteString = context.getString(R.string.action_mute);
PendingIntent muteIntent = createServiceIntent(ACTION_MUTE, conversationKey, userAccountId);
ConversationAction muteAction =
new ConversationAction(
ActionType.ACTION_TYPE_MUTE,
new RemoteAction(
Icon.createWithResource(context, muteIcon),
muteString,
muteString,
muteIntent),
null);
List<ConversationAction> actions = new ArrayList<>();
actions.add(replyAction);
actions.add(markAsReadAction);
actions.add(muteAction);
builder.setActions(actions);
return builder.build();
}
private static PendingIntent createServiceIntent(
@NonNull String action, @Nullable String conversationKey, int userAccountId) {
Context context = AppFactory.get().getContext();
Bundle bundle = new Bundle();
if (conversationKey != null) {
bundle.putString(EXTRA_CONVERSATION_KEY, conversationKey);
}
bundle.putInt(EXTRA_ACCOUNT_ID, userAccountId);
Intent intent =
new Intent(context, MessengerService.class)
.setAction(action)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)
.setClass(context, MessengerService.class)
.putExtras(bundle);
int requestCode =
(conversationKey == null) ? action.hashCode() : conversationKey.hashCode();
return PendingIntent.getForegroundService(
context, requestCode, intent, PendingIntent.FLAG_ONE_SHOT);
}
/** Sends a reply, meant to be used from a caller originating from voice input. */
public static void directSend(Intent intent) {
final CharSequence phoneNumber = intent.getCharSequenceExtra(KEY_PHONE_NUMBER);
final String iccId = intent.getStringExtra(KEY_DEVICE_ADDRESS);
final CharSequence message = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (iccId == null || phoneNumber == null || TextUtils.isEmpty(message)) {
L.e("Dropping voice reply. Received no icc id, phone Number and/or empty message!");
return;
}
L.d("Sending a message to specified phone number");
AppFactory.get()
.getDataModel()
.sendMessage(iccId, phoneNumber.toString(), message.toString());
}
/** Sends a reply, meant to be used from a caller originating from voice input. */
public static void voiceReply(Intent intent) {
final String conversationKey = intent.getStringExtra(EXTRA_CONVERSATION_KEY);
final int accountId =
intent.getIntExtra(MessageConstants.EXTRA_ACCOUNT_ID, NULL_ACCOUNT_ID);
final Bundle bundle = RemoteInput.getResultsFromIntent(intent);
if (bundle == null || accountId == NULL_ACCOUNT_ID) {
L.e("Dropping voice reply. Received null bundle or no user account id in bundle!");
return;
}
final CharSequence message = bundle.getCharSequence(Intent.EXTRA_TEXT);
L.d("voiceReply: " + message);
if (!TextUtils.isEmpty(message)) {
AppFactory.get()
.getDataModel()
.replyConversation(accountId, conversationKey, message.toString());
}
}
/** Mark a conversation associated with a given sender key as read. */
public static void mute(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
final String conversationKey = extras.getString(EXTRA_CONVERSATION_KEY);
L.d("mute");
AppFactory.get().getDataModel().muteConversation(conversationKey, true);
}
}
/** Mark a conversation associated with a given sender key as read. */
public static void markAsRead(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
final String conversationKey = extras.getString(EXTRA_CONVERSATION_KEY);
L.d("marking as read");
AppFactory.get().getDataModel().markAsRead(conversationKey);
}
}
}