/* * 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}. * *
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); } }