1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.assist; 17 18 import android.content.Context; 19 import android.os.Bundle; 20 import android.os.Handler; 21 import android.service.notification.StatusBarNotification; 22 import android.service.voice.VoiceInteractionService; 23 import android.service.voice.VoiceInteractionSession; 24 25 import androidx.annotation.StringDef; 26 import androidx.core.app.NotificationManagerCompat; 27 28 import com.android.car.assist.payloadhandlers.NotificationPayloadHandler; 29 import com.android.car.messenger.common.Conversation; 30 31 /** 32 * An active voice interaction session on the car, providing additional actions which assistant 33 * should act on. Override the {@link #onShow(String, Bundle, int)} to received the action specified 34 * by the voice session initiator. 35 */ 36 public abstract class CarVoiceInteractionSession extends VoiceInteractionSession { 37 /** The key used for the action {@link String} in the payload {@link Bundle}. */ 38 public static final String KEY_ACTION = "KEY_ACTION"; 39 40 /** 41 * The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION} payload 42 * {@link Bundle}. Must map to a {@link ExceptionValue}. 43 */ 44 public static final String KEY_EXCEPTION = "KEY_EXCEPTION"; 45 46 /** 47 * The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION} payload 48 * {@link Bundle}. Must map to a boolean. If value is true, the Fallback Assistant that can 49 * handle the user's request has been disabled. 50 */ 51 public static final String KEY_FALLBACK_ASSISTANT_ENABLED = "KEY_FALLBACK_ASSISTANT_ENABLED"; 52 53 /** 54 * The key used for a substitute package name the Digital Assistant should read out in lieu of 55 * package name associated with the {@link StatusBarNotification}. 56 * 57 * <p>Only system packages which lump together a bunch of unrelated stuff may substitute a 58 * different name to make the purpose of the notification more clear. The correct package label 59 * should always be accessible via SystemUI. 60 */ 61 public static final String EXTRA_SUBSTITUTE_APP_NAME = "android.substName"; 62 63 /** 64 * The key used for the payload {@link Bundle}, if a {@link StatusBarNotification} is used as 65 * the payload. 66 */ 67 public static final String KEY_NOTIFICATION = "KEY_NOTIFICATION"; 68 69 /** 70 * The key used for the payload {@link Bundle}, if a {@link Conversation} is used as the 71 * payload. 72 */ 73 public static final String KEY_CONVERSATION = "KEY_CONVERSATION"; 74 75 /** Indicates to assistant that no action was specified. */ 76 public static final String VOICE_ACTION_NO_ACTION = "VOICE_ACTION_NO_ACTION"; 77 78 /** 79 * Indicates to assistant that a read action is being requested for a given payload. A {@link 80 * StatusBarNotification} object will be provided in the payload 81 */ 82 public static final String VOICE_ACTION_READ_NOTIFICATION = "VOICE_ACTION_READ_NOTIFICATION"; 83 84 /** 85 * Indicates to assistant that a reply action is being requested for a given payload. A {@link 86 * StatusBarNotification} object will be provided in the payload 87 */ 88 public static final String VOICE_ACTION_REPLY_NOTIFICATION = "VOICE_ACTION_REPLY_NOTIFICATION"; 89 90 /** 91 * Indicates to assistant that a read conversation action is being requested. A {@link 92 * Conversation} object will be provided in the payload. 93 */ 94 public static final String VOICE_ACTION_READ_CONVERSATION = "VOICE_ACTION_READ_CONVERSATION"; 95 96 /** 97 * Indicates to assistant that a reply conversation action is being requested. A {@link 98 * Conversation} object will be provided in the payload. 99 */ 100 public static final String VOICE_ACTION_REPLY_CONVERSATION = "VOICE_ACTION_REPLY_CONVERSATION"; 101 102 /** 103 * Indicates to digital assistant that it should capture a SMS message from the user, 104 * potentially finding which contact to send the message to and which device to send the message 105 * from (only if the application does not send the digital assistant this information in the 106 * bundle). Once the digital assistant has gathered the information from the user, it should 107 * send back the PendingIntent (provided in the bundle) with the information so the application 108 * can actually send the SMS. 109 */ 110 public static final String VOICE_ACTION_SEND_SMS = "VOICE_ACTION_SEND_SMS"; 111 112 /* Recipient's phone number. If this and the recipient name are not provided, 113 * by the application, digital assistant must do contact disambiguation 114 * and add the phone number to the pending intent 115 */ 116 public static final String KEY_PHONE_NUMBER = "KEY_PHONE_NUMBER"; 117 118 /** 119 * Recipient's name. If this and the recipient phone number are not provided by the application, 120 * digital assistant must do contact disambiguation but is not required to add the name to the 121 * PendingIntent. 122 */ 123 public static final String KEY_RECIPIENT_NAME = "KEY RECIPIENT NAME"; 124 125 /* Recipient's UID in the Contact Provider database. Optionally provided by the application. 126 * Not required to be sent back by the digital assistant. 127 */ 128 public static final String KEY_RECIPIENT_UID = "KEY_RECIPIENT_UID"; 129 130 /* Friendly name of the device in which to send the message from. If not 131 * provided by the application, digital assistant must do device disambiguation 132 * but is not required to add it to the Pending Intent. 133 */ 134 public static final String KEY_DEVICE_NAME = "KEY DEVICE_NAME"; 135 136 /* Bluetooth device address of the device of which to send the message from. 137 * If not provided by the application, assistant must do device disambiguation 138 * and add this to the Pendingintent. 139 */ 140 public static final String KEY_DEVICE_ADDRESS = "KEY_DEVICE_ADDRESS"; 141 142 /* KEY_SEND_PENDING_INTENT is not null and always be provided by the application. 143 * The application must preload the pending intent with any KEYs, it provides the assistant 144 * that is also needed to send the message. 145 * (i.e. if the application passes in the KEY_PHONE_NUMBER in the Bundle, 146 * the assistant can assume the application has already put this in the 147 * PendingIntent and may not re-add it to the PendingIntent). 148 */ 149 public static final String KEY_SEND_PENDING_INTENT = "KEY_SEND_PENDING INTENT"; 150 151 /** 152 * Indicates to assistant that it should resolve the exception in the given payload (found in 153 * {@link CarVoiceInteractionSession#KEY_EXCEPTION}'s value). 154 */ 155 public static final String VOICE_ACTION_HANDLE_EXCEPTION = "VOICE_ACTION_HANDLE_EXCEPTION"; 156 157 /** The list of exceptions the active voice service must handle. */ 158 @StringDef({EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING}) 159 public @interface ExceptionValue {} 160 161 /** 162 * Indicates to assistant that it is missing the Notification Listener permission, and should 163 * request this permission from the user. 164 */ 165 public static final String EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING = 166 "EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING"; 167 168 private final NotificationPayloadHandler mNotificationPayloadHandler; 169 CarVoiceInteractionSession(Context context)170 public CarVoiceInteractionSession(Context context) { 171 super(context); 172 mNotificationPayloadHandler = new NotificationPayloadHandler(getContext()); 173 } 174 CarVoiceInteractionSession(Context context, Handler handler)175 public CarVoiceInteractionSession(Context context, Handler handler) { 176 super(context, handler); 177 mNotificationPayloadHandler = new NotificationPayloadHandler(getContext()); 178 } 179 180 /** 181 * Returns the notification payload handler, which can be used to handle actions related to 182 * notification payloads. 183 */ getNotificationPayloadHandler()184 public NotificationPayloadHandler getNotificationPayloadHandler() { 185 return mNotificationPayloadHandler; 186 } 187 188 @Override onShow(Bundle args, int showFlags)189 public final void onShow(Bundle args, int showFlags) { 190 super.onShow(args, showFlags); 191 addNotificationAccessExceptionIfNeeded(args); 192 if (args != null) { 193 String action = getRequestedVoiceAction(args); 194 if (!VOICE_ACTION_NO_ACTION.equals(action)) { 195 onShow(action, args, showFlags); 196 return; 197 } 198 } 199 onShow(VOICE_ACTION_NO_ACTION, args, showFlags); 200 } 201 202 /** 203 * Called when the session UI is going to be shown. This is called after {@link 204 * #onCreateContentView} (if the session's content UI needed to be created) and immediately 205 * prior to the window being shown. This may be called while the window is already shown, if a 206 * show request has come in while it is shown, to allow you to update the UI to match the new 207 * show arguments. 208 * 209 * @param action The action that is being requested for this session (e.g. {@link 210 * CarVoiceInteractionSession#VOICE_ACTION_READ_NOTIFICATION}, {@link 211 * CarVoiceInteractionSession#VOICE_ACTION_REPLY_NOTIFICATION}). 212 * @param args The arguments that were supplied to {@link VoiceInteractionService#showSession 213 * VoiceInteractionService.showSession}. 214 * @param flags The show flags originally provided to {@link VoiceInteractionService#showSession 215 * VoiceInteractionService.showSession}. 216 */ onShow(String action, Bundle args, int flags)217 protected abstract void onShow(String action, Bundle args, int flags); 218 219 /** 220 * Transforms bundle to {@link KEY_EXCEPTION} with 221 * value {@link EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING} if 222 * Notification Listener permissions are missing. 223 */ addNotificationAccessExceptionIfNeeded(Bundle args)224 private void addNotificationAccessExceptionIfNeeded(Bundle args) { 225 if (needsNotificationAccess(args)) { 226 args.putString(KEY_ACTION, VOICE_ACTION_HANDLE_EXCEPTION); 227 args.putString(KEY_EXCEPTION, EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING); 228 args.putBoolean(KEY_FALLBACK_ASSISTANT_ENABLED, false); 229 } 230 } 231 needsNotificationAccess(Bundle args)232 private boolean needsNotificationAccess(Bundle args) { 233 return isNotificationAction(args) && !hasNotificationAccess(getContext()); 234 } 235 236 /** 237 * Returns {@code true} if the given {@code args} is a notification action, {@code false} 238 * otherwise 239 */ isNotificationAction(Bundle args)240 private static boolean isNotificationAction(Bundle args) { 241 if (args == null) { 242 return false; 243 } 244 String action = args.getString(KEY_ACTION); 245 return VOICE_ACTION_REPLY_NOTIFICATION.equals(action) 246 || VOICE_ACTION_READ_NOTIFICATION.equals(action); 247 } 248 hasNotificationAccess(Context context)249 private boolean hasNotificationAccess(Context context) { 250 return NotificationManagerCompat 251 .getEnabledListenerPackages(context).contains(context.getPackageName()); 252 } 253 254 /** 255 * Returns the action {@link String} provided in the args {@link Bundle}, or {@link 256 * CarVoiceInteractionSession#VOICE_ACTION_NO_ACTION} if no such string was provided. 257 */ getRequestedVoiceAction(Bundle args)258 protected static String getRequestedVoiceAction(Bundle args) { 259 return args.getString(KEY_ACTION, VOICE_ACTION_NO_ACTION); 260 } 261 }