blob: 4472fa4f26763cc453b1e32028ba72e45d89cbdc [file] [log] [blame]
Uriel Sade298f6c02018-12-19 20:34:43 -08001/*
Uriel Saded81ddc92018-12-21 15:16:56 -08002 * Copyright (C) 2019 The Android Open Source Project
Uriel Sade298f6c02018-12-19 20:34:43 -08003 *
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 */
16package com.android.car.assist.client;
17
Uriel Saded81ddc92018-12-21 15:16:56 -080018import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_AS_READ;
19import static android.app.Notification.Action.SEMANTIC_ACTION_REPLY;
Uriel Sade3703dbd2019-01-16 12:55:19 -080020import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_NOTIFICATION;
Uriel Saded81ddc92018-12-21 15:16:56 -080021
Ritwika Mitra146ed852019-05-01 10:33:06 -070022import android.annotation.Nullable;
Ritwika Mitra3eb68932019-04-29 16:40:04 -070023import android.app.ActivityManager;
Uriel Sade298f6c02018-12-19 20:34:43 -080024import android.app.Notification;
Uriel Saded81ddc92018-12-21 15:16:56 -080025import android.app.RemoteInput;
Uriel Sade298f6c02018-12-19 20:34:43 -080026import android.content.Context;
27import android.os.Bundle;
28import android.provider.Settings;
29import android.service.notification.StatusBarNotification;
30import android.util.Log;
31
32import androidx.core.app.NotificationCompat;
33
34import com.android.car.assist.CarVoiceInteractionSession;
35import com.android.internal.app.AssistUtils;
Uriel Sade3703dbd2019-01-16 12:55:19 -080036import com.android.internal.app.IVoiceActionCheckCallback;
Uriel Sade298f6c02018-12-19 20:34:43 -080037
Priyank Singh128ff292019-04-29 20:07:36 -070038import java.util.ArrayList;
Uriel Sade298f6c02018-12-19 20:34:43 -080039import java.util.Arrays;
40import java.util.Collections;
41import java.util.HashSet;
42import java.util.List;
43import java.util.Objects;
44import java.util.Set;
45import java.util.stream.Collectors;
46import java.util.stream.IntStream;
47
48/**
49 * Util class providing helper methods to interact with the current active voice service,
50 * while ensuring that the active voice service has the required permissions.
51 */
52public class CarAssistUtils {
53 public static final String TAG = "CarAssistUtils";
Uriel Saded81ddc92018-12-21 15:16:56 -080054 private static final List<Integer> REQUIRED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
Uriel Sade298f6c02018-12-19 20:34:43 -080055 Arrays.asList(
Uriel Saded81ddc92018-12-21 15:16:56 -080056 SEMANTIC_ACTION_MARK_AS_READ
Uriel Sade298f6c02018-12-19 20:34:43 -080057 )
58 );
59
Ritwika Mitraaaf0c172019-01-30 08:25:03 -080060 private static final List<Integer> SUPPORTED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
61 Arrays.asList(
62 SEMANTIC_ACTION_MARK_AS_READ,
63 SEMANTIC_ACTION_REPLY
64 )
65 );
Uriel Saded81ddc92018-12-21 15:16:56 -080066
Uriel Sade298f6c02018-12-19 20:34:43 -080067 private final Context mContext;
68 private final AssistUtils mAssistUtils;
Uriel Saded81ddc92018-12-21 15:16:56 -080069 private final FallbackAssistant mFallbackAssistant;
70 private final String mErrorMessage;
Uriel Sade298f6c02018-12-19 20:34:43 -080071
Uriel Sade3703dbd2019-01-16 12:55:19 -080072 /** Interface used to receive callbacks from voice action requests. */
73 public interface ActionRequestCallback {
74 /** Callback issued from a voice request on success/error. */
Ritwika Mitraeeb910c2019-05-23 12:32:06 -070075 void onResult(boolean hasError);
Uriel Sade3703dbd2019-01-16 12:55:19 -080076 }
77
Uriel Sade298f6c02018-12-19 20:34:43 -080078 public CarAssistUtils(Context context) {
Uriel Sade298f6c02018-12-19 20:34:43 -080079 mContext = context;
Uriel Saded81ddc92018-12-21 15:16:56 -080080 mAssistUtils = new AssistUtils(context);
Ritwika Mitra146ed852019-05-01 10:33:06 -070081 mFallbackAssistant = new FallbackAssistant(context);
Uriel Saded81ddc92018-12-21 15:16:56 -080082 mErrorMessage = context.getString(R.string.assist_action_failed_toast);
Uriel Sade298f6c02018-12-19 20:34:43 -080083 }
84
85 /**
86 * Returns true if the current active assistant has notification listener permissions.
87 */
88 public boolean assistantIsNotificationListener() {
89 final String activeComponent = mAssistUtils.getActiveServiceComponentName()
90 .flattenToString();
Ritwika Mitraac5f1512019-02-04 10:21:14 -080091 int slashIndex = activeComponent.indexOf("/");
92 final String activePackage = activeComponent.substring(0, slashIndex);
93
Ritwika Mitra3eb68932019-04-29 16:40:04 -070094 final String listeners = Settings.Secure.getStringForUser(mContext.getContentResolver(),
95 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, ActivityManager.getCurrentUser());
Uriel Sade298f6c02018-12-19 20:34:43 -080096
Ritwika Mitraf89855b2019-05-21 11:56:20 -070097 if (Log.isLoggable(TAG, Log.DEBUG)) {
98 Log.d(TAG, "Current user: " + ActivityManager.getCurrentUser()
99 + " has active voice service: " + activePackage + " and enabled notification "
100 + " listeners: " + listeners);
101 }
102
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800103 if (listeners != null) {
104 for (String listener : Arrays.asList(listeners.split(":"))) {
105 if (listener.contains(activePackage)) {
106 return true;
107 }
108 }
109 }
Ritwika Mitra295d91a2019-04-26 09:55:11 -0700110 Log.w(TAG, "No notification listeners found for assistant: " + activeComponent);
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800111 return false;
Uriel Sade298f6c02018-12-19 20:34:43 -0800112 }
113
114 /**
115 * Checks whether the notification is a car-compatible messaging notification.
116 *
117 * @param sbn The notification being checked.
118 * @return true if the notification is a car-compatible messaging notification.
119 */
120 public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
121 return hasMessagingStyle(sbn)
122 && hasRequiredAssistantCallbacks(sbn)
123 && replyCallbackHasRemoteInput(sbn)
124 && assistantCallbacksShowNoUi(sbn);
125 }
126
127 /** Returns true if the semantic action provided can be supported. */
128 public static boolean isSupportedSemanticAction(int semanticAction) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800129 return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
Uriel Sade298f6c02018-12-19 20:34:43 -0800130 }
131
132 /**
133 * Returns true if the notification has a messaging style.
134 * <p/>
135 * This is the case if the notification in question was provided an instance of
136 * {@link Notification.MessagingStyle} (or an instance of
137 * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
138 */
139 private static boolean hasMessagingStyle(StatusBarNotification sbn) {
140 return NotificationCompat.MessagingStyle
141 .extractMessagingStyleFromNotification(sbn.getNotification()) != null;
142 }
143
144 /**
145 * Returns true if the notification has the required Assistant callbacks to be considered
146 * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
147 * is returned if multiple callbacks exist for any semantic action that is supported.
148 */
149 private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
Priyank Singh128ff292019-04-29 20:07:36 -0700150 List<Integer> semanticActionList = getAllActions(sbn.getNotification())
151 .stream()
152 .map(NotificationCompat.Action::getSemanticAction)
Uriel Saded81ddc92018-12-21 15:16:56 -0800153 .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
Uriel Sade298f6c02018-12-19 20:34:43 -0800154 .collect(Collectors.toList());
155 Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
Uriel Sade298f6c02018-12-19 20:34:43 -0800156 return semanticActionList.size() == semanticActionSet.size()
Uriel Saded81ddc92018-12-21 15:16:56 -0800157 && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
Uriel Sade298f6c02018-12-19 20:34:43 -0800158 }
159
Priyank Singh128ff292019-04-29 20:07:36 -0700160 /** Retrieves all visible and invisible {@link Action}s from the {@link #notification}. */
Ritwika Mitra146ed852019-05-01 10:33:06 -0700161 public static List<NotificationCompat.Action> getAllActions(Notification notification) {
Priyank Singh128ff292019-04-29 20:07:36 -0700162 List<NotificationCompat.Action> actions = new ArrayList<>();
163 actions.addAll(NotificationCompat.getInvisibleActions(notification));
164 for (int i = 0; i < NotificationCompat.getActionCount(notification); i++) {
165 actions.add(NotificationCompat.getAction(notification, i));
166 }
167 return actions;
168 }
169
Uriel Sade298f6c02018-12-19 20:34:43 -0800170 /**
Ritwika Mitra146ed852019-05-01 10:33:06 -0700171 * Retrieves the {@link NotificationCompat.Action} containing the
172 * {@link NotificationCompat.Action#SEMANTIC_ACTION_MARK_AS_READ} semantic action.
173 */
174 @Nullable
175 public static NotificationCompat.Action getMarkAsReadAction(Notification notification) {
176 for (NotificationCompat.Action action : getAllActions(notification)) {
177 if (action.getSemanticAction()
178 == NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) {
179 return action;
180 }
181 }
182 return null;
183 }
184
185 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800186 * Returns true if the reply callback has at least one {@link RemoteInput}.
Uriel Sade298f6c02018-12-19 20:34:43 -0800187 * <p/>
188 * Precondition: There exists only one reply callback.
189 */
190 private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
191 return Arrays.stream(sbn.getNotification().actions)
Uriel Saded81ddc92018-12-21 15:16:56 -0800192 .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
Uriel Sade298f6c02018-12-19 20:34:43 -0800193 .map(Notification.Action::getRemoteInputs)
Uriel Saded81ddc92018-12-21 15:16:56 -0800194 .filter(Objects::nonNull)
195 .anyMatch(remoteInputs -> remoteInputs.length > 0);
Uriel Sade298f6c02018-12-19 20:34:43 -0800196 }
197
198 /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
199 private static boolean assistantCallbacksShowNoUi(StatusBarNotification sbn) {
200 final Notification notification = sbn.getNotification();
201 return IntStream.range(0, notification.actions.length)
202 .mapToObj(i -> NotificationCompat.getAction(notification, i))
203 .filter(Objects::nonNull)
Uriel Saded81ddc92018-12-21 15:16:56 -0800204 .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
Uriel Sade298f6c02018-12-19 20:34:43 -0800205 .noneMatch(NotificationCompat.Action::getShowsUserInterface);
206 }
207
208 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800209 * Requests a given action from the current active Assistant.
210 *
Ritwika Mitra146ed852019-05-01 10:33:06 -0700211 * @param sbn the notification payload to deliver to assistant
212 * @param voiceAction must be a valid {@link CarVoiceInteractionSession} VOICE_ACTION
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700213 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800214 */
Ritwika Mitra146ed852019-05-01 10:33:06 -0700215 public void requestAssistantVoiceAction(StatusBarNotification sbn, String voiceAction,
Uriel Sade3703dbd2019-01-16 12:55:19 -0800216 ActionRequestCallback callback) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800217 if (!isCarCompatibleMessagingNotification(sbn)) {
218 Log.w(TAG, "Assistant action requested for non-compatible notification.");
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700219 callback.onResult(/* hasError= */ true);
Uriel Sade3703dbd2019-01-16 12:55:19 -0800220 return;
221 }
222
Ritwika Mitra146ed852019-05-01 10:33:06 -0700223 switch (voiceAction) {
224 case CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800225 readMessageNotification(sbn, callback);
226 return;
Ritwika Mitra146ed852019-05-01 10:33:06 -0700227 case CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800228 replyMessageNotification(sbn, callback);
229 return;
Uriel Saded81ddc92018-12-21 15:16:56 -0800230 default:
Ritwika Mitra146ed852019-05-01 10:33:06 -0700231 Log.w(TAG, "Requested Assistant action for unsupported semantic action.");
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700232 callback.onResult(/* hasError= */ true);
Uriel Sade3703dbd2019-01-16 12:55:19 -0800233 return;
Uriel Saded81ddc92018-12-21 15:16:56 -0800234 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800235 }
236
237 /**
238 * Requests a read action for the notification from the current active Assistant.
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700239 * If the Assistant cannot handle the request, a fallback implementation will attempt to
Uriel Saded81ddc92018-12-21 15:16:56 -0800240 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800241 *
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700242 * @param sbn the notification to deliver as the payload
Uriel Sade3703dbd2019-01-16 12:55:19 -0800243 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800244 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800245 private void readMessageNotification(StatusBarNotification sbn,
246 ActionRequestCallback callback) {
247 Bundle args = BundleBuilder.buildAssistantReadBundle(sbn);
248 String action = CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
249
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700250 requestAction(action, sbn, args, callback);
Uriel Sade298f6c02018-12-19 20:34:43 -0800251 }
252
253 /**
254 * Requests a reply action for the notification from the current active Assistant.
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700255 * If the Assistant cannot handle the request, a fallback implementation will attempt to
Uriel Saded81ddc92018-12-21 15:16:56 -0800256 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800257 *
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700258 * @param sbn the notification to deliver as the payload
Uriel Sade3703dbd2019-01-16 12:55:19 -0800259 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800260 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800261 private void replyMessageNotification(StatusBarNotification sbn,
262 ActionRequestCallback callback) {
263 Bundle args = BundleBuilder.buildAssistantReplyBundle(sbn);
264 String action = CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
265
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700266 requestAction(action, sbn, args, callback);
Uriel Sade298f6c02018-12-19 20:34:43 -0800267 }
268
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700269 private void requestAction(String action, StatusBarNotification sbn, Bundle payloadArguments,
Uriel Sade3703dbd2019-01-16 12:55:19 -0800270 ActionRequestCallback callback) {
271
272 if (!assistantIsNotificationListener()) {
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700273 handleFallback(sbn, action, callback);
Uriel Sade3703dbd2019-01-16 12:55:19 -0800274 return;
275 }
276
277 IVoiceActionCheckCallback actionCheckCallback = new IVoiceActionCheckCallback.Stub() {
278 @Override
279 public void onComplete(List<String> supportedActions) {
280 boolean success;
281 if (supportedActions != null && supportedActions.contains(action)) {
282 if (Log.isLoggable(TAG, Log.DEBUG)) {
283 Log.d(TAG, "Launching active Assistant for action: " + action);
284 }
285 success = mAssistUtils.showSessionForActiveService(payloadArguments,
286 SHOW_SOURCE_NOTIFICATION, null, null);
287 } else {
288 Log.w(TAG, "Active Assistant does not support voice action: " + action);
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700289 success = false;
Uriel Sade3703dbd2019-01-16 12:55:19 -0800290 }
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700291 callback.onResult(/* hasError= */ !success);
Uriel Sade3703dbd2019-01-16 12:55:19 -0800292 }
293 };
294
295 Set<String> actionSet = new HashSet<>(Collections.singletonList(action));
296 mAssistUtils.getActiveServiceSupportedActions(actionSet, actionCheckCallback);
297 }
298
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700299 private void handleFallback(StatusBarNotification sbn, String action,
300 ActionRequestCallback callback) {
301 FallbackAssistant.Listener listener = new FallbackAssistant.Listener() {
302 @Override
303 public void onMessageRead(boolean error) {
304 callback.onResult(error);
305 }
306 };
307
Uriel Sade3703dbd2019-01-16 12:55:19 -0800308 switch (action) {
309 case CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION:
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700310 mFallbackAssistant.handleReadAction(sbn, listener);
311 break;
Uriel Sade3703dbd2019-01-16 12:55:19 -0800312 case CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION:
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700313 mFallbackAssistant.handleErrorMessage(mErrorMessage, listener);
314 break;
Uriel Sade3703dbd2019-01-16 12:55:19 -0800315 default:
Ritwika Mitraeeb910c2019-05-23 12:32:06 -0700316 Log.w(TAG, "Requested unsupported FallbackAssistant action.");
317 callback.onResult(/* hasError= */ true);
318 return;
Uriel Sade3703dbd2019-01-16 12:55:19 -0800319 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800320 }
321}