blob: 702024bf94ac5048dfa3267bbda8cf3d052220bd [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 Mitra3eb68932019-04-29 16:40:04 -070022import android.app.ActivityManager;
Uriel Sade298f6c02018-12-19 20:34:43 -080023import android.app.Notification;
Uriel Saded81ddc92018-12-21 15:16:56 -080024import android.app.RemoteInput;
Uriel Sade298f6c02018-12-19 20:34:43 -080025import android.content.Context;
26import android.os.Bundle;
27import android.provider.Settings;
28import android.service.notification.StatusBarNotification;
29import android.util.Log;
Uriel Saded81ddc92018-12-21 15:16:56 -080030import android.widget.Toast;
Uriel Sade298f6c02018-12-19 20:34:43 -080031
32import androidx.core.app.NotificationCompat;
33
34import com.android.car.assist.CarVoiceInteractionSession;
Uriel Saded81ddc92018-12-21 15:16:56 -080035import com.android.car.assist.client.tts.TextToSpeechHelper;
Uriel Sade298f6c02018-12-19 20:34:43 -080036import com.android.internal.app.AssistUtils;
Uriel Sade3703dbd2019-01-16 12:55:19 -080037import com.android.internal.app.IVoiceActionCheckCallback;
Uriel Sade298f6c02018-12-19 20:34:43 -080038
39import 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
67 private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() {
68 @Override
69 public void onTextToSpeechStarted() {
70 if (Log.isLoggable(TAG, Log.DEBUG)) {
71 Log.d(TAG, "onTextToSpeechStarted");
72 }
73 }
74
75 @Override
76 public void onTextToSpeechStopped(boolean error) {
77 if (Log.isLoggable(TAG, Log.DEBUG)) {
78 Log.d(TAG, "onTextToSpeechStopped");
79 }
80 if (error) {
81 Toast.makeText(mContext, mErrorMessage, Toast.LENGTH_LONG).show();
82 }
83 }
84 };
Uriel Sade298f6c02018-12-19 20:34:43 -080085
86 private final Context mContext;
87 private final AssistUtils mAssistUtils;
Uriel Saded81ddc92018-12-21 15:16:56 -080088 private final FallbackAssistant mFallbackAssistant;
89 private final String mErrorMessage;
Ritwika Mitra3eb68932019-04-29 16:40:04 -070090 private final ActivityManager mActivityManager;
Uriel Sade298f6c02018-12-19 20:34:43 -080091
Uriel Sade3703dbd2019-01-16 12:55:19 -080092 /** Interface used to receive callbacks from voice action requests. */
93 public interface ActionRequestCallback {
94 /** Callback issued from a voice request on success/error. */
95 void onResult(boolean error);
96 }
97
Uriel Sade298f6c02018-12-19 20:34:43 -080098 public CarAssistUtils(Context context) {
Uriel Sade298f6c02018-12-19 20:34:43 -080099 mContext = context;
Uriel Saded81ddc92018-12-21 15:16:56 -0800100 mAssistUtils = new AssistUtils(context);
101 mFallbackAssistant = new FallbackAssistant(new TextToSpeechHelper(context));
102 mErrorMessage = context.getString(R.string.assist_action_failed_toast);
Ritwika Mitra3eb68932019-04-29 16:40:04 -0700103 mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
Uriel Sade298f6c02018-12-19 20:34:43 -0800104 }
105
106 /**
107 * Returns true if the current active assistant has notification listener permissions.
108 */
109 public boolean assistantIsNotificationListener() {
110 final String activeComponent = mAssistUtils.getActiveServiceComponentName()
111 .flattenToString();
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800112 int slashIndex = activeComponent.indexOf("/");
113 final String activePackage = activeComponent.substring(0, slashIndex);
114
Ritwika Mitra3eb68932019-04-29 16:40:04 -0700115 final String listeners = Settings.Secure.getStringForUser(mContext.getContentResolver(),
116 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, ActivityManager.getCurrentUser());
Uriel Sade298f6c02018-12-19 20:34:43 -0800117
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800118 if (listeners != null) {
119 for (String listener : Arrays.asList(listeners.split(":"))) {
120 if (listener.contains(activePackage)) {
Ritwika Mitra295d91a2019-04-26 09:55:11 -0700121 Log.d(TAG, "Active assistant has notification listener: " + listener);
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800122 return true;
123 }
124 }
125 }
Ritwika Mitra295d91a2019-04-26 09:55:11 -0700126 Log.w(TAG, "No notification listeners found for assistant: " + activeComponent);
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800127 return false;
Uriel Sade298f6c02018-12-19 20:34:43 -0800128 }
129
130 /**
131 * Checks whether the notification is a car-compatible messaging notification.
132 *
133 * @param sbn The notification being checked.
134 * @return true if the notification is a car-compatible messaging notification.
135 */
136 public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
137 return hasMessagingStyle(sbn)
138 && hasRequiredAssistantCallbacks(sbn)
139 && replyCallbackHasRemoteInput(sbn)
140 && assistantCallbacksShowNoUi(sbn);
141 }
142
143 /** Returns true if the semantic action provided can be supported. */
144 public static boolean isSupportedSemanticAction(int semanticAction) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800145 return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
Uriel Sade298f6c02018-12-19 20:34:43 -0800146 }
147
148 /**
149 * Returns true if the notification has a messaging style.
150 * <p/>
151 * This is the case if the notification in question was provided an instance of
152 * {@link Notification.MessagingStyle} (or an instance of
153 * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
154 */
155 private static boolean hasMessagingStyle(StatusBarNotification sbn) {
156 return NotificationCompat.MessagingStyle
157 .extractMessagingStyleFromNotification(sbn.getNotification()) != null;
158 }
159
160 /**
161 * Returns true if the notification has the required Assistant callbacks to be considered
162 * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
163 * is returned if multiple callbacks exist for any semantic action that is supported.
164 */
165 private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
166 List<Integer> semanticActionList = Arrays.stream(sbn.getNotification().actions)
167 .map(Notification.Action::getSemanticAction)
Uriel Saded81ddc92018-12-21 15:16:56 -0800168 .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
Uriel Sade298f6c02018-12-19 20:34:43 -0800169 .collect(Collectors.toList());
170 Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
171
172 return semanticActionList.size() == semanticActionSet.size()
Uriel Saded81ddc92018-12-21 15:16:56 -0800173 && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
Uriel Sade298f6c02018-12-19 20:34:43 -0800174 }
175
176 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800177 * Returns true if the reply callback has at least one {@link RemoteInput}.
Uriel Sade298f6c02018-12-19 20:34:43 -0800178 * <p/>
179 * Precondition: There exists only one reply callback.
180 */
181 private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
182 return Arrays.stream(sbn.getNotification().actions)
Uriel Saded81ddc92018-12-21 15:16:56 -0800183 .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
Uriel Sade298f6c02018-12-19 20:34:43 -0800184 .map(Notification.Action::getRemoteInputs)
Uriel Saded81ddc92018-12-21 15:16:56 -0800185 .filter(Objects::nonNull)
186 .anyMatch(remoteInputs -> remoteInputs.length > 0);
Uriel Sade298f6c02018-12-19 20:34:43 -0800187 }
188
189 /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
190 private static boolean assistantCallbacksShowNoUi(StatusBarNotification sbn) {
191 final Notification notification = sbn.getNotification();
192 return IntStream.range(0, notification.actions.length)
193 .mapToObj(i -> NotificationCompat.getAction(notification, i))
194 .filter(Objects::nonNull)
Uriel Saded81ddc92018-12-21 15:16:56 -0800195 .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
Uriel Sade298f6c02018-12-19 20:34:43 -0800196 .noneMatch(NotificationCompat.Action::getShowsUserInterface);
197 }
198
199 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800200 * Requests a given action from the current active Assistant.
201 *
Uriel Sade298f6c02018-12-19 20:34:43 -0800202 * @param sbn the notification payload to deliver to assistant
Uriel Sade3703dbd2019-01-16 12:55:19 -0800203 * @param semanticAction the semantic action that is being requested
204 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800205 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800206 public void requestAssistantVoiceAction(StatusBarNotification sbn, int semanticAction,
207 ActionRequestCallback callback) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800208 if (!isCarCompatibleMessagingNotification(sbn)) {
209 Log.w(TAG, "Assistant action requested for non-compatible notification.");
Uriel Sade3703dbd2019-01-16 12:55:19 -0800210 callback.onResult(/* error= */ true);
211 return;
212 }
213
214 if (!isSupportedSemanticAction(semanticAction)) {
215 Log.w(TAG, "Requested Assistant action for unsupported semantic action.");
216 callback.onResult(/* error= */ true);
217 return;
Uriel Sade298f6c02018-12-19 20:34:43 -0800218 }
219
Uriel Saded81ddc92018-12-21 15:16:56 -0800220 switch (semanticAction) {
221 case SEMANTIC_ACTION_MARK_AS_READ:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800222 readMessageNotification(sbn, callback);
223 return;
224
Uriel Saded81ddc92018-12-21 15:16:56 -0800225 case SEMANTIC_ACTION_REPLY:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800226 replyMessageNotification(sbn, callback);
227 return;
228
Uriel Saded81ddc92018-12-21 15:16:56 -0800229 default:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800230 callback.onResult(/* error= */ true);
231 return;
Uriel Saded81ddc92018-12-21 15:16:56 -0800232 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800233 }
234
235 /**
236 * Requests a read action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800237 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
238 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800239 *
240 * @param sbn the notification to deliver as the payload
Uriel Sade3703dbd2019-01-16 12:55:19 -0800241 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800242 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800243 private void readMessageNotification(StatusBarNotification sbn,
244 ActionRequestCallback callback) {
245 Bundle args = BundleBuilder.buildAssistantReadBundle(sbn);
246 String action = CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
247
248 requestAction(sbn, args, action, callback);
Uriel Sade298f6c02018-12-19 20:34:43 -0800249 }
250
251 /**
252 * Requests a reply action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800253 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
254 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800255 *
256 * @param sbn the notification to deliver as the payload
Uriel Sade3703dbd2019-01-16 12:55:19 -0800257 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800258 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800259 private void replyMessageNotification(StatusBarNotification sbn,
260 ActionRequestCallback callback) {
261 Bundle args = BundleBuilder.buildAssistantReplyBundle(sbn);
262 String action = CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
263
264 requestAction(sbn, args, action, callback);
Uriel Sade298f6c02018-12-19 20:34:43 -0800265 }
266
Uriel Sade3703dbd2019-01-16 12:55:19 -0800267 private void requestAction(StatusBarNotification sbn, Bundle payloadArguments, String action,
268 ActionRequestCallback callback) {
269
270 if (!assistantIsNotificationListener()) {
271 Log.w(TAG, "Active Assistant does not have Notification Listener permissions.");
272 boolean success = handleFallback(sbn, action);
273 callback.onResult(!success);
274 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);
289 success = handleFallback(sbn, action);
290 }
291 callback.onResult(!success);
292 }
293 };
294
295 Set<String> actionSet = new HashSet<>(Collections.singletonList(action));
296 mAssistUtils.getActiveServiceSupportedActions(actionSet, actionCheckCallback);
297 }
298
299 private boolean handleFallback(StatusBarNotification sbn, String action) {
300 switch (action) {
301 case CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION:
302 return mFallbackAssistant.handleReadAction(sbn, mListener);
303
304 case CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION:
305 return mFallbackAssistant.handleErrorMessage(mErrorMessage, mListener);
306
307 default:
308 Log.w(TAG, "Requested fallback action for unsupported voice action.");
309 return false;
310 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800311 }
312}