blob: 39b66a36c1730aa1837f1c556213af5bfe7bc4ad [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
Uriel Sade298f6c02018-12-19 20:34:43 -080022import android.app.Notification;
Uriel Saded81ddc92018-12-21 15:16:56 -080023import android.app.RemoteInput;
Uriel Sade298f6c02018-12-19 20:34:43 -080024import android.content.Context;
25import android.os.Bundle;
26import android.provider.Settings;
27import android.service.notification.StatusBarNotification;
28import android.util.Log;
Uriel Saded81ddc92018-12-21 15:16:56 -080029import android.widget.Toast;
Uriel Sade298f6c02018-12-19 20:34:43 -080030
31import androidx.core.app.NotificationCompat;
32
33import com.android.car.assist.CarVoiceInteractionSession;
Uriel Saded81ddc92018-12-21 15:16:56 -080034import com.android.car.assist.client.tts.TextToSpeechHelper;
Uriel Sade298f6c02018-12-19 20:34:43 -080035import 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
38import java.util.Arrays;
39import java.util.Collections;
40import java.util.HashSet;
41import java.util.List;
42import java.util.Objects;
43import java.util.Set;
44import java.util.stream.Collectors;
45import java.util.stream.IntStream;
46
47/**
48 * Util class providing helper methods to interact with the current active voice service,
49 * while ensuring that the active voice service has the required permissions.
50 */
51public class CarAssistUtils {
52 public static final String TAG = "CarAssistUtils";
Uriel Saded81ddc92018-12-21 15:16:56 -080053 private static final List<Integer> REQUIRED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
Uriel Sade298f6c02018-12-19 20:34:43 -080054 Arrays.asList(
Uriel Saded81ddc92018-12-21 15:16:56 -080055 SEMANTIC_ACTION_REPLY,
56 SEMANTIC_ACTION_MARK_AS_READ
Uriel Sade298f6c02018-12-19 20:34:43 -080057 )
58 );
59
60 // Currently, all supported semantic actions are required.
Uriel Saded81ddc92018-12-21 15:16:56 -080061 private static final List<Integer> SUPPORTED_SEMANTIC_ACTIONS = REQUIRED_SEMANTIC_ACTIONS;
62
63 private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() {
64 @Override
65 public void onTextToSpeechStarted() {
66 if (Log.isLoggable(TAG, Log.DEBUG)) {
67 Log.d(TAG, "onTextToSpeechStarted");
68 }
69 }
70
71 @Override
72 public void onTextToSpeechStopped(boolean error) {
73 if (Log.isLoggable(TAG, Log.DEBUG)) {
74 Log.d(TAG, "onTextToSpeechStopped");
75 }
76 if (error) {
77 Toast.makeText(mContext, mErrorMessage, Toast.LENGTH_LONG).show();
78 }
79 }
80 };
Uriel Sade298f6c02018-12-19 20:34:43 -080081
82 private final Context mContext;
83 private final AssistUtils mAssistUtils;
Uriel Saded81ddc92018-12-21 15:16:56 -080084 private final FallbackAssistant mFallbackAssistant;
85 private final String mErrorMessage;
Uriel Sade298f6c02018-12-19 20:34:43 -080086
Uriel Sade3703dbd2019-01-16 12:55:19 -080087 /** Interface used to receive callbacks from voice action requests. */
88 public interface ActionRequestCallback {
89 /** Callback issued from a voice request on success/error. */
90 void onResult(boolean error);
91 }
92
Uriel Sade298f6c02018-12-19 20:34:43 -080093 public CarAssistUtils(Context context) {
Uriel Sade298f6c02018-12-19 20:34:43 -080094 mContext = context;
Uriel Saded81ddc92018-12-21 15:16:56 -080095 mAssistUtils = new AssistUtils(context);
96 mFallbackAssistant = new FallbackAssistant(new TextToSpeechHelper(context));
97 mErrorMessage = context.getString(R.string.assist_action_failed_toast);
Uriel Sade298f6c02018-12-19 20:34:43 -080098 }
99
100 /**
101 * Returns true if the current active assistant has notification listener permissions.
102 */
103 public boolean assistantIsNotificationListener() {
104 final String activeComponent = mAssistUtils.getActiveServiceComponentName()
105 .flattenToString();
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800106 int slashIndex = activeComponent.indexOf("/");
107 final String activePackage = activeComponent.substring(0, slashIndex);
108
Uriel Sade298f6c02018-12-19 20:34:43 -0800109 final String listeners = Settings.Secure.getString(mContext.getContentResolver(),
110 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
111
Ritwika Mitraac5f1512019-02-04 10:21:14 -0800112 if (listeners != null) {
113 for (String listener : Arrays.asList(listeners.split(":"))) {
114 if (listener.contains(activePackage)) {
115 return true;
116 }
117 }
118 }
119
120 return false;
Uriel Sade298f6c02018-12-19 20:34:43 -0800121 }
122
123 /**
124 * Checks whether the notification is a car-compatible messaging notification.
125 *
126 * @param sbn The notification being checked.
127 * @return true if the notification is a car-compatible messaging notification.
128 */
129 public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
130 return hasMessagingStyle(sbn)
131 && hasRequiredAssistantCallbacks(sbn)
132 && replyCallbackHasRemoteInput(sbn)
133 && assistantCallbacksShowNoUi(sbn);
134 }
135
136 /** Returns true if the semantic action provided can be supported. */
137 public static boolean isSupportedSemanticAction(int semanticAction) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800138 return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
Uriel Sade298f6c02018-12-19 20:34:43 -0800139 }
140
141 /**
142 * Returns true if the notification has a messaging style.
143 * <p/>
144 * This is the case if the notification in question was provided an instance of
145 * {@link Notification.MessagingStyle} (or an instance of
146 * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
147 */
148 private static boolean hasMessagingStyle(StatusBarNotification sbn) {
149 return NotificationCompat.MessagingStyle
150 .extractMessagingStyleFromNotification(sbn.getNotification()) != null;
151 }
152
153 /**
154 * Returns true if the notification has the required Assistant callbacks to be considered
155 * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
156 * is returned if multiple callbacks exist for any semantic action that is supported.
157 */
158 private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
159 List<Integer> semanticActionList = Arrays.stream(sbn.getNotification().actions)
160 .map(Notification.Action::getSemanticAction)
Uriel Saded81ddc92018-12-21 15:16:56 -0800161 .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
Uriel Sade298f6c02018-12-19 20:34:43 -0800162 .collect(Collectors.toList());
163 Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
164
165 return semanticActionList.size() == semanticActionSet.size()
Uriel Saded81ddc92018-12-21 15:16:56 -0800166 && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
Uriel Sade298f6c02018-12-19 20:34:43 -0800167 }
168
169 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800170 * Returns true if the reply callback has at least one {@link RemoteInput}.
Uriel Sade298f6c02018-12-19 20:34:43 -0800171 * <p/>
172 * Precondition: There exists only one reply callback.
173 */
174 private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
175 return Arrays.stream(sbn.getNotification().actions)
Uriel Saded81ddc92018-12-21 15:16:56 -0800176 .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
Uriel Sade298f6c02018-12-19 20:34:43 -0800177 .map(Notification.Action::getRemoteInputs)
Uriel Saded81ddc92018-12-21 15:16:56 -0800178 .filter(Objects::nonNull)
179 .anyMatch(remoteInputs -> remoteInputs.length > 0);
Uriel Sade298f6c02018-12-19 20:34:43 -0800180 }
181
182 /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
183 private static boolean assistantCallbacksShowNoUi(StatusBarNotification sbn) {
184 final Notification notification = sbn.getNotification();
185 return IntStream.range(0, notification.actions.length)
186 .mapToObj(i -> NotificationCompat.getAction(notification, i))
187 .filter(Objects::nonNull)
Uriel Saded81ddc92018-12-21 15:16:56 -0800188 .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
Uriel Sade298f6c02018-12-19 20:34:43 -0800189 .noneMatch(NotificationCompat.Action::getShowsUserInterface);
190 }
191
192 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800193 * Requests a given action from the current active Assistant.
194 *
Uriel Sade298f6c02018-12-19 20:34:43 -0800195 * @param sbn the notification payload to deliver to assistant
Uriel Sade3703dbd2019-01-16 12:55:19 -0800196 * @param semanticAction the semantic action that is being requested
197 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800198 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800199 public void requestAssistantVoiceAction(StatusBarNotification sbn, int semanticAction,
200 ActionRequestCallback callback) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800201 if (!isCarCompatibleMessagingNotification(sbn)) {
202 Log.w(TAG, "Assistant action requested for non-compatible notification.");
Uriel Sade3703dbd2019-01-16 12:55:19 -0800203 callback.onResult(/* error= */ true);
204 return;
205 }
206
207 if (!isSupportedSemanticAction(semanticAction)) {
208 Log.w(TAG, "Requested Assistant action for unsupported semantic action.");
209 callback.onResult(/* error= */ true);
210 return;
Uriel Sade298f6c02018-12-19 20:34:43 -0800211 }
212
Uriel Saded81ddc92018-12-21 15:16:56 -0800213 switch (semanticAction) {
214 case SEMANTIC_ACTION_MARK_AS_READ:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800215 readMessageNotification(sbn, callback);
216 return;
217
Uriel Saded81ddc92018-12-21 15:16:56 -0800218 case SEMANTIC_ACTION_REPLY:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800219 replyMessageNotification(sbn, callback);
220 return;
221
Uriel Saded81ddc92018-12-21 15:16:56 -0800222 default:
Uriel Sade3703dbd2019-01-16 12:55:19 -0800223 callback.onResult(/* error= */ true);
224 return;
Uriel Saded81ddc92018-12-21 15:16:56 -0800225 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800226 }
227
228 /**
229 * Requests a read action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800230 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
231 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800232 *
233 * @param sbn the notification to deliver as the payload
Uriel Sade3703dbd2019-01-16 12:55:19 -0800234 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800235 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800236 private void readMessageNotification(StatusBarNotification sbn,
237 ActionRequestCallback callback) {
238 Bundle args = BundleBuilder.buildAssistantReadBundle(sbn);
239 String action = CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
240
241 requestAction(sbn, args, action, callback);
Uriel Sade298f6c02018-12-19 20:34:43 -0800242 }
243
244 /**
245 * Requests a reply action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800246 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
247 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800248 *
249 * @param sbn the notification to deliver as the payload
Uriel Sade3703dbd2019-01-16 12:55:19 -0800250 * @param callback the callback to issue on success/error
Uriel Sade298f6c02018-12-19 20:34:43 -0800251 */
Uriel Sade3703dbd2019-01-16 12:55:19 -0800252 private void replyMessageNotification(StatusBarNotification sbn,
253 ActionRequestCallback callback) {
254 Bundle args = BundleBuilder.buildAssistantReplyBundle(sbn);
255 String action = CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
256
257 requestAction(sbn, args, action, callback);
Uriel Sade298f6c02018-12-19 20:34:43 -0800258 }
259
Uriel Sade3703dbd2019-01-16 12:55:19 -0800260 private void requestAction(StatusBarNotification sbn, Bundle payloadArguments, String action,
261 ActionRequestCallback callback) {
262
263 if (!assistantIsNotificationListener()) {
264 Log.w(TAG, "Active Assistant does not have Notification Listener permissions.");
265 boolean success = handleFallback(sbn, action);
266 callback.onResult(!success);
267 return;
268 }
269
270 IVoiceActionCheckCallback actionCheckCallback = new IVoiceActionCheckCallback.Stub() {
271 @Override
272 public void onComplete(List<String> supportedActions) {
273 boolean success;
274 if (supportedActions != null && supportedActions.contains(action)) {
275 if (Log.isLoggable(TAG, Log.DEBUG)) {
276 Log.d(TAG, "Launching active Assistant for action: " + action);
277 }
278 success = mAssistUtils.showSessionForActiveService(payloadArguments,
279 SHOW_SOURCE_NOTIFICATION, null, null);
280 } else {
281 Log.w(TAG, "Active Assistant does not support voice action: " + action);
282 success = handleFallback(sbn, action);
283 }
284 callback.onResult(!success);
285 }
286 };
287
288 Set<String> actionSet = new HashSet<>(Collections.singletonList(action));
289 mAssistUtils.getActiveServiceSupportedActions(actionSet, actionCheckCallback);
290 }
291
292 private boolean handleFallback(StatusBarNotification sbn, String action) {
293 switch (action) {
294 case CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION:
295 return mFallbackAssistant.handleReadAction(sbn, mListener);
296
297 case CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION:
298 return mFallbackAssistant.handleErrorMessage(mErrorMessage, mListener);
299
300 default:
301 Log.w(TAG, "Requested fallback action for unsupported voice action.");
302 return false;
303 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800304 }
305}