blob: 8e5fef2da34b112a645f91ba041ee0050ec04bec [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;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.service.notification.StatusBarNotification;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionSession;
import androidx.annotation.StringDef;
import androidx.core.app.NotificationManagerCompat;
import com.android.car.assist.payloadhandlers.NotificationPayloadHandler;
import com.android.car.messenger.common.Conversation;
/**
* An active voice interaction session on the car, providing additional actions which assistant
* should act on. Override the {@link #onShow(String, Bundle, int)} to received the action specified
* by the voice session initiator.
*/
public abstract class CarVoiceInteractionSession extends VoiceInteractionSession {
/** The key used for the action {@link String} in the payload {@link Bundle}. */
public static final String KEY_ACTION = "KEY_ACTION";
/**
* The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION} payload
* {@link Bundle}. Must map to a {@link ExceptionValue}.
*/
public static final String KEY_EXCEPTION = "KEY_EXCEPTION";
/**
* The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION} payload
* {@link Bundle}. Must map to a boolean. If value is true, the Fallback Assistant that can
* handle the user's request has been disabled.
*/
public static final String KEY_FALLBACK_ASSISTANT_ENABLED = "KEY_FALLBACK_ASSISTANT_ENABLED";
/**
* The key used for a substitute package name the Digital Assistant should read out in lieu of
* package name associated with the {@link StatusBarNotification}.
*
* <p>Only system packages which lump together a bunch of unrelated stuff may substitute a
* different name to make the purpose of the notification more clear. The correct package label
* should always be accessible via SystemUI.
*/
public static final String EXTRA_SUBSTITUTE_APP_NAME = "android.substName";
/**
* The key used for the payload {@link Bundle}, if a {@link StatusBarNotification} is used as
* the payload.
*/
public static final String KEY_NOTIFICATION = "KEY_NOTIFICATION";
/**
* The key used for the payload {@link Bundle}, if a {@link Conversation} is used as the
* payload.
*/
public static final String KEY_CONVERSATION = "KEY_CONVERSATION";
/** Indicates to assistant that no action was specified. */
public static final String VOICE_ACTION_NO_ACTION = "VOICE_ACTION_NO_ACTION";
/**
* Indicates to assistant that a read action is being requested for a given payload. A {@link
* StatusBarNotification} object will be provided in the payload
*/
public static final String VOICE_ACTION_READ_NOTIFICATION = "VOICE_ACTION_READ_NOTIFICATION";
/**
* Indicates to assistant that a reply action is being requested for a given payload. A {@link
* StatusBarNotification} object will be provided in the payload
*/
public static final String VOICE_ACTION_REPLY_NOTIFICATION = "VOICE_ACTION_REPLY_NOTIFICATION";
/**
* Indicates to assistant that a read conversation action is being requested. A {@link
* Conversation} object will be provided in the payload.
*/
public static final String VOICE_ACTION_READ_CONVERSATION = "VOICE_ACTION_READ_CONVERSATION";
/**
* Indicates to assistant that a reply conversation action is being requested. A {@link
* Conversation} object will be provided in the payload.
*/
public static final String VOICE_ACTION_REPLY_CONVERSATION = "VOICE_ACTION_REPLY_CONVERSATION";
/**
* Indicates to digital assistant that it should capture a SMS message from the user,
* potentially finding which contact to send the message to and which device to send the message
* from (only if the application does not send the digital assistant this information in the
* bundle). Once the digital assistant has gathered the information from the user, it should
* send back the PendingIntent (provided in the bundle) with the information so the application
* can actually send the SMS.
*/
public static final String VOICE_ACTION_SEND_SMS = "VOICE_ACTION_SEND_SMS";
/* Recipient's phone number. If this and the recipient name are not provided,
* by the application, digital assistant must do contact disambiguation
* and add the phone number to the pending intent
*/
public static final String KEY_PHONE_NUMBER = "KEY_PHONE_NUMBER";
/**
* Recipient's name. If this and the recipient phone number are not provided by the application,
* digital assistant must do contact disambiguation but is not required to add the name to the
* PendingIntent.
*/
public static final String KEY_RECIPIENT_NAME = "KEY RECIPIENT NAME";
/* Recipient's UID in the Contact Provider database. Optionally provided by the application.
* Not required to be sent back by the digital assistant.
*/
public static final String KEY_RECIPIENT_UID = "KEY_RECIPIENT_UID";
/* Friendly name of the device in which to send the message from. If not
* provided by the application, digital assistant must do device disambiguation
* but is not required to add it to the Pending Intent.
*/
public static final String KEY_DEVICE_NAME = "KEY DEVICE_NAME";
/* Bluetooth device address of the device of which to send the message from.
* If not provided by the application, assistant must do device disambiguation
* and add this to the Pendingintent.
*/
public static final String KEY_DEVICE_ADDRESS = "KEY_DEVICE_ADDRESS";
/* KEY_SEND_PENDING_INTENT is not null and always be provided by the application.
* The application must preload the pending intent with any KEYs, it provides the assistant
* that is also needed to send the message.
* (i.e. if the application passes in the KEY_PHONE_NUMBER in the Bundle,
* the assistant can assume the application has already put this in the
* PendingIntent and may not re-add it to the PendingIntent).
*/
public static final String KEY_SEND_PENDING_INTENT = "KEY_SEND_PENDING INTENT";
/**
* Indicates to assistant that it should resolve the exception in the given payload (found in
* {@link CarVoiceInteractionSession#KEY_EXCEPTION}'s value).
*/
public static final String VOICE_ACTION_HANDLE_EXCEPTION = "VOICE_ACTION_HANDLE_EXCEPTION";
/** The list of exceptions the active voice service must handle. */
@StringDef({EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING})
public @interface ExceptionValue {}
/**
* Indicates to assistant that it is missing the Notification Listener permission, and should
* request this permission from the user.
*/
public static final String EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING =
"EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING";
private final NotificationPayloadHandler mNotificationPayloadHandler;
public CarVoiceInteractionSession(Context context) {
super(context);
mNotificationPayloadHandler = new NotificationPayloadHandler(getContext());
}
public CarVoiceInteractionSession(Context context, Handler handler) {
super(context, handler);
mNotificationPayloadHandler = new NotificationPayloadHandler(getContext());
}
/**
* Returns the notification payload handler, which can be used to handle actions related to
* notification payloads.
*/
public NotificationPayloadHandler getNotificationPayloadHandler() {
return mNotificationPayloadHandler;
}
@Override
public final void onShow(Bundle args, int showFlags) {
super.onShow(args, showFlags);
addNotificationAccessExceptionIfNeeded(args);
if (args != null) {
String action = getRequestedVoiceAction(args);
if (!VOICE_ACTION_NO_ACTION.equals(action)) {
onShow(action, args, showFlags);
return;
}
}
onShow(VOICE_ACTION_NO_ACTION, args, showFlags);
}
/**
* Called when the session UI is going to be shown. This is called after {@link
* #onCreateContentView} (if the session's content UI needed to be created) and immediately
* prior to the window being shown. This may be called while the window is already shown, if a
* show request has come in while it is shown, to allow you to update the UI to match the new
* show arguments.
*
* @param action The action that is being requested for this session (e.g. {@link
* CarVoiceInteractionSession#VOICE_ACTION_READ_NOTIFICATION}, {@link
* CarVoiceInteractionSession#VOICE_ACTION_REPLY_NOTIFICATION}).
* @param args The arguments that were supplied to {@link VoiceInteractionService#showSession
* VoiceInteractionService.showSession}.
* @param flags The show flags originally provided to {@link VoiceInteractionService#showSession
* VoiceInteractionService.showSession}.
*/
protected abstract void onShow(String action, Bundle args, int flags);
/**
* Transforms bundle to {@link KEY_EXCEPTION} with
* value {@link EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING} if
* Notification Listener permissions are missing.
*/
private void addNotificationAccessExceptionIfNeeded(Bundle args) {
if (needsNotificationAccess(args)) {
args.putString(KEY_ACTION, VOICE_ACTION_HANDLE_EXCEPTION);
args.putString(KEY_EXCEPTION, EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING);
args.putBoolean(KEY_FALLBACK_ASSISTANT_ENABLED, false);
}
}
private boolean needsNotificationAccess(Bundle args) {
return isNotificationAction(args) && !hasNotificationAccess(getContext());
}
/**
* Returns {@code true} if the given {@code args} is a notification action, {@code false}
* otherwise
*/
private static boolean isNotificationAction(Bundle args) {
if (args == null) {
return false;
}
String action = args.getString(KEY_ACTION);
return VOICE_ACTION_REPLY_NOTIFICATION.equals(action)
|| VOICE_ACTION_READ_NOTIFICATION.equals(action);
}
private boolean hasNotificationAccess(Context context) {
return NotificationManagerCompat
.getEnabledListenerPackages(context).contains(context.getPackageName());
}
/**
* Returns the action {@link String} provided in the args {@link Bundle}, or {@link
* CarVoiceInteractionSession#VOICE_ACTION_NO_ACTION} if no such string was provided.
*/
protected static String getRequestedVoiceAction(Bundle args) {
return args.getString(KEY_ACTION, VOICE_ACTION_NO_ACTION);
}
}