blob: b0b43f0b3391a6710e016ad5d47e67cb038b5e9e [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;
20
Uriel Sade298f6c02018-12-19 20:34:43 -080021import android.app.Notification;
Uriel Saded81ddc92018-12-21 15:16:56 -080022import android.app.RemoteInput;
Uriel Sade298f6c02018-12-19 20:34:43 -080023import android.content.Context;
24import android.os.Bundle;
25import android.provider.Settings;
26import android.service.notification.StatusBarNotification;
27import android.util.Log;
Uriel Saded81ddc92018-12-21 15:16:56 -080028import android.widget.Toast;
Uriel Sade298f6c02018-12-19 20:34:43 -080029
30import androidx.core.app.NotificationCompat;
31
32import com.android.car.assist.CarVoiceInteractionSession;
Uriel Saded81ddc92018-12-21 15:16:56 -080033import com.android.car.assist.client.tts.TextToSpeechHelper;
Uriel Sade298f6c02018-12-19 20:34:43 -080034import com.android.internal.app.AssistUtils;
35
Priyank Singh128ff292019-04-29 20:07:36 -070036import java.util.ArrayList;
Uriel Sade298f6c02018-12-19 20:34:43 -080037import java.util.Arrays;
38import java.util.Collections;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Objects;
42import java.util.Set;
43import java.util.stream.Collectors;
44import java.util.stream.IntStream;
45
46/**
47 * Util class providing helper methods to interact with the current active voice service,
48 * while ensuring that the active voice service has the required permissions.
49 */
50public class CarAssistUtils {
51 public static final String TAG = "CarAssistUtils";
Uriel Saded81ddc92018-12-21 15:16:56 -080052 private static final List<Integer> REQUIRED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
Uriel Sade298f6c02018-12-19 20:34:43 -080053 Arrays.asList(
Uriel Saded81ddc92018-12-21 15:16:56 -080054 SEMANTIC_ACTION_MARK_AS_READ
Uriel Sade298f6c02018-12-19 20:34:43 -080055 )
56 );
57
Ritwika Mitraaaf0c172019-01-30 08:25:03 -080058 private static final List<Integer> SUPPORTED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
59 Arrays.asList(
60 SEMANTIC_ACTION_MARK_AS_READ,
61 SEMANTIC_ACTION_REPLY
62 )
63 );
Uriel Saded81ddc92018-12-21 15:16:56 -080064
65 private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() {
66 @Override
67 public void onTextToSpeechStarted() {
68 if (Log.isLoggable(TAG, Log.DEBUG)) {
69 Log.d(TAG, "onTextToSpeechStarted");
70 }
71 }
72
73 @Override
74 public void onTextToSpeechStopped(boolean error) {
75 if (Log.isLoggable(TAG, Log.DEBUG)) {
76 Log.d(TAG, "onTextToSpeechStopped");
77 }
78 if (error) {
79 Toast.makeText(mContext, mErrorMessage, Toast.LENGTH_LONG).show();
80 }
81 }
82 };
Uriel Sade298f6c02018-12-19 20:34:43 -080083
84 private final Context mContext;
85 private final AssistUtils mAssistUtils;
Uriel Saded81ddc92018-12-21 15:16:56 -080086 private final FallbackAssistant mFallbackAssistant;
87 private final String mErrorMessage;
Uriel Sade298f6c02018-12-19 20:34:43 -080088
89 public CarAssistUtils(Context context) {
Uriel Sade298f6c02018-12-19 20:34:43 -080090 mContext = context;
Uriel Saded81ddc92018-12-21 15:16:56 -080091 mAssistUtils = new AssistUtils(context);
92 mFallbackAssistant = new FallbackAssistant(new TextToSpeechHelper(context));
93 mErrorMessage = context.getString(R.string.assist_action_failed_toast);
Uriel Sade298f6c02018-12-19 20:34:43 -080094 }
95
96 /**
97 * Returns true if the current active assistant has notification listener permissions.
98 */
99 public boolean assistantIsNotificationListener() {
100 final String activeComponent = mAssistUtils.getActiveServiceComponentName()
101 .flattenToString();
Ritwika Mitraeb040682019-02-01 15:36:23 -0800102 int slashIndex = activeComponent.indexOf("/");
103 final String activePackage = activeComponent.substring(0, slashIndex);
104
Uriel Sade298f6c02018-12-19 20:34:43 -0800105 final String listeners = Settings.Secure.getString(mContext.getContentResolver(),
106 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
107
Ritwika Mitraeb040682019-02-01 15:36:23 -0800108 if (listeners != null) {
109 for (String listener : Arrays.asList(listeners.split(":"))) {
110 if (listener.contains(activePackage)) {
111 return true;
112 }
113 }
114 }
115
116 return false;
Uriel Sade298f6c02018-12-19 20:34:43 -0800117 }
118
119 /**
120 * Checks whether the notification is a car-compatible messaging notification.
121 *
122 * @param sbn The notification being checked.
123 * @return true if the notification is a car-compatible messaging notification.
124 */
125 public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
126 return hasMessagingStyle(sbn)
127 && hasRequiredAssistantCallbacks(sbn)
128 && replyCallbackHasRemoteInput(sbn)
129 && assistantCallbacksShowNoUi(sbn);
130 }
131
132 /** Returns true if the semantic action provided can be supported. */
133 public static boolean isSupportedSemanticAction(int semanticAction) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800134 return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
Uriel Sade298f6c02018-12-19 20:34:43 -0800135 }
136
137 /**
138 * Returns true if the notification has a messaging style.
139 * <p/>
140 * This is the case if the notification in question was provided an instance of
141 * {@link Notification.MessagingStyle} (or an instance of
142 * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
143 */
144 private static boolean hasMessagingStyle(StatusBarNotification sbn) {
145 return NotificationCompat.MessagingStyle
146 .extractMessagingStyleFromNotification(sbn.getNotification()) != null;
147 }
148
149 /**
150 * Returns true if the notification has the required Assistant callbacks to be considered
151 * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
152 * is returned if multiple callbacks exist for any semantic action that is supported.
153 */
154 private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
Priyank Singh128ff292019-04-29 20:07:36 -0700155 List<Integer> semanticActionList = getAllActions(sbn.getNotification())
156 .stream()
157 .map(NotificationCompat.Action::getSemanticAction)
Uriel Saded81ddc92018-12-21 15:16:56 -0800158 .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
Uriel Sade298f6c02018-12-19 20:34:43 -0800159 .collect(Collectors.toList());
160 Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
Uriel Sade298f6c02018-12-19 20:34:43 -0800161 return semanticActionList.size() == semanticActionSet.size()
Uriel Saded81ddc92018-12-21 15:16:56 -0800162 && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
Uriel Sade298f6c02018-12-19 20:34:43 -0800163 }
164
Priyank Singh128ff292019-04-29 20:07:36 -0700165 /** Retrieves all visible and invisible {@link Action}s from the {@link #notification}. */
166 private static List<NotificationCompat.Action> getAllActions(Notification notification) {
167 List<NotificationCompat.Action> actions = new ArrayList<>();
168 actions.addAll(NotificationCompat.getInvisibleActions(notification));
169 for (int i = 0; i < NotificationCompat.getActionCount(notification); i++) {
170 actions.add(NotificationCompat.getAction(notification, i));
171 }
172 return actions;
173 }
174
Uriel Sade298f6c02018-12-19 20:34:43 -0800175 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800176 * Returns true if the reply callback has at least one {@link RemoteInput}.
Uriel Sade298f6c02018-12-19 20:34:43 -0800177 * <p/>
178 * Precondition: There exists only one reply callback.
179 */
180 private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
181 return Arrays.stream(sbn.getNotification().actions)
Uriel Saded81ddc92018-12-21 15:16:56 -0800182 .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
Uriel Sade298f6c02018-12-19 20:34:43 -0800183 .map(Notification.Action::getRemoteInputs)
Uriel Saded81ddc92018-12-21 15:16:56 -0800184 .filter(Objects::nonNull)
185 .anyMatch(remoteInputs -> remoteInputs.length > 0);
Uriel Sade298f6c02018-12-19 20:34:43 -0800186 }
187
188 /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
189 private static boolean assistantCallbacksShowNoUi(StatusBarNotification sbn) {
190 final Notification notification = sbn.getNotification();
191 return IntStream.range(0, notification.actions.length)
192 .mapToObj(i -> NotificationCompat.getAction(notification, i))
193 .filter(Objects::nonNull)
Uriel Saded81ddc92018-12-21 15:16:56 -0800194 .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
Uriel Sade298f6c02018-12-19 20:34:43 -0800195 .noneMatch(NotificationCompat.Action::getShowsUserInterface);
196 }
197
198 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800199 * Requests a given action from the current active Assistant.
200 *
Ritwika Mitraeb040682019-02-01 15:36:23 -0800201 * @param sbn the notification payload to deliver to assistant
Uriel Sade298f6c02018-12-19 20:34:43 -0800202 * @param semanticAction the semantic action that is to be requested
203 * @return true if the request was successful
204 */
Uriel Saded81ddc92018-12-21 15:16:56 -0800205 public boolean requestAssistantVoiceAction(StatusBarNotification sbn, int semanticAction) {
206 if (!isCarCompatibleMessagingNotification(sbn)) {
207 Log.w(TAG, "Assistant action requested for non-compatible notification.");
208 return false;
Uriel Sade298f6c02018-12-19 20:34:43 -0800209 }
210
Uriel Saded81ddc92018-12-21 15:16:56 -0800211 switch (semanticAction) {
212 case SEMANTIC_ACTION_MARK_AS_READ:
213 return readMessageNotification(sbn);
214 case SEMANTIC_ACTION_REPLY:
215 return replyMessageNotification(sbn);
216 default:
217 return false;
218 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800219 }
220
221 /**
222 * Requests a read action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800223 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
224 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800225 *
226 * @param sbn the notification to deliver as the payload
Uriel Saded81ddc92018-12-21 15:16:56 -0800227 * @return true if the read request was handled successfully
Uriel Sade298f6c02018-12-19 20:34:43 -0800228 */
229 private boolean readMessageNotification(StatusBarNotification sbn) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800230 return requestAction(BundleBuilder.buildAssistantReadBundle(sbn))
231 || mFallbackAssistant.handleReadAction(sbn, mListener);
Uriel Sade298f6c02018-12-19 20:34:43 -0800232 }
233
234 /**
235 * Requests a reply action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800236 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
237 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800238 *
239 * @param sbn the notification to deliver as the payload
Uriel Saded81ddc92018-12-21 15:16:56 -0800240 * @return true if the reply request was handled successfully
Uriel Sade298f6c02018-12-19 20:34:43 -0800241 */
242 private boolean replyMessageNotification(StatusBarNotification sbn) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800243 return requestAction(BundleBuilder.buildAssistantReplyBundle(sbn))
244 || mFallbackAssistant.handleErrorMessage(mErrorMessage, mListener);
Uriel Sade298f6c02018-12-19 20:34:43 -0800245 }
246
Uriel Saded81ddc92018-12-21 15:16:56 -0800247 private boolean requestAction(Bundle payloadArguments) {
248 return assistantIsNotificationListener()
Uriel Sade298f6c02018-12-19 20:34:43 -0800249 && mAssistUtils.showSessionForActiveService(payloadArguments,
250 CarVoiceInteractionSession.SHOW_SOURCE_NOTIFICATION, null, null);
251 }
252}