blob: 4cfd22d77f8443bff373ae29e92a8ee993402d93 [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
36import java.util.Arrays;
37import java.util.Collections;
38import java.util.HashSet;
39import java.util.List;
40import java.util.Objects;
41import java.util.Set;
42import java.util.stream.Collectors;
43import java.util.stream.IntStream;
44
45/**
46 * Util class providing helper methods to interact with the current active voice service,
47 * while ensuring that the active voice service has the required permissions.
48 */
49public class CarAssistUtils {
50 public static final String TAG = "CarAssistUtils";
Uriel Saded81ddc92018-12-21 15:16:56 -080051 private static final List<Integer> REQUIRED_SEMANTIC_ACTIONS = Collections.unmodifiableList(
Uriel Sade298f6c02018-12-19 20:34:43 -080052 Arrays.asList(
Uriel Saded81ddc92018-12-21 15:16:56 -080053 SEMANTIC_ACTION_REPLY,
54 SEMANTIC_ACTION_MARK_AS_READ
Uriel Sade298f6c02018-12-19 20:34:43 -080055 )
56 );
57
58 // Currently, all supported semantic actions are required.
Uriel Saded81ddc92018-12-21 15:16:56 -080059 private static final List<Integer> SUPPORTED_SEMANTIC_ACTIONS = REQUIRED_SEMANTIC_ACTIONS;
60
61 private final TextToSpeechHelper.Listener mListener = new TextToSpeechHelper.Listener() {
62 @Override
63 public void onTextToSpeechStarted() {
64 if (Log.isLoggable(TAG, Log.DEBUG)) {
65 Log.d(TAG, "onTextToSpeechStarted");
66 }
67 }
68
69 @Override
70 public void onTextToSpeechStopped(boolean error) {
71 if (Log.isLoggable(TAG, Log.DEBUG)) {
72 Log.d(TAG, "onTextToSpeechStopped");
73 }
74 if (error) {
75 Toast.makeText(mContext, mErrorMessage, Toast.LENGTH_LONG).show();
76 }
77 }
78 };
Uriel Sade298f6c02018-12-19 20:34:43 -080079
80 private final Context mContext;
81 private final AssistUtils mAssistUtils;
Uriel Saded81ddc92018-12-21 15:16:56 -080082 private final FallbackAssistant mFallbackAssistant;
83 private final String mErrorMessage;
Uriel Sade298f6c02018-12-19 20:34:43 -080084
85 public CarAssistUtils(Context context) {
Uriel Sade298f6c02018-12-19 20:34:43 -080086 mContext = context;
Uriel Saded81ddc92018-12-21 15:16:56 -080087 mAssistUtils = new AssistUtils(context);
88 mFallbackAssistant = new FallbackAssistant(new TextToSpeechHelper(context));
89 mErrorMessage = context.getString(R.string.assist_action_failed_toast);
Uriel Sade298f6c02018-12-19 20:34:43 -080090 }
91
92 /**
93 * Returns true if the current active assistant has notification listener permissions.
94 */
95 public boolean assistantIsNotificationListener() {
96 final String activeComponent = mAssistUtils.getActiveServiceComponentName()
97 .flattenToString();
98 final String listeners = Settings.Secure.getString(mContext.getContentResolver(),
99 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
100
101 return listeners != null
102 && Arrays.asList(listeners.split(":")).contains(activeComponent);
103 }
104
105 /**
106 * Checks whether the notification is a car-compatible messaging notification.
107 *
108 * @param sbn The notification being checked.
109 * @return true if the notification is a car-compatible messaging notification.
110 */
111 public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
112 return hasMessagingStyle(sbn)
113 && hasRequiredAssistantCallbacks(sbn)
114 && replyCallbackHasRemoteInput(sbn)
115 && assistantCallbacksShowNoUi(sbn);
116 }
117
118 /** Returns true if the semantic action provided can be supported. */
119 public static boolean isSupportedSemanticAction(int semanticAction) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800120 return SUPPORTED_SEMANTIC_ACTIONS.contains(semanticAction);
Uriel Sade298f6c02018-12-19 20:34:43 -0800121 }
122
123 /**
124 * Returns true if the notification has a messaging style.
125 * <p/>
126 * This is the case if the notification in question was provided an instance of
127 * {@link Notification.MessagingStyle} (or an instance of
128 * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
129 */
130 private static boolean hasMessagingStyle(StatusBarNotification sbn) {
131 return NotificationCompat.MessagingStyle
132 .extractMessagingStyleFromNotification(sbn.getNotification()) != null;
133 }
134
135 /**
136 * Returns true if the notification has the required Assistant callbacks to be considered
137 * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
138 * is returned if multiple callbacks exist for any semantic action that is supported.
139 */
140 private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
141 List<Integer> semanticActionList = Arrays.stream(sbn.getNotification().actions)
142 .map(Notification.Action::getSemanticAction)
Uriel Saded81ddc92018-12-21 15:16:56 -0800143 .filter(REQUIRED_SEMANTIC_ACTIONS::contains)
Uriel Sade298f6c02018-12-19 20:34:43 -0800144 .collect(Collectors.toList());
145 Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
146
147 return semanticActionList.size() == semanticActionSet.size()
Uriel Saded81ddc92018-12-21 15:16:56 -0800148 && semanticActionSet.containsAll(REQUIRED_SEMANTIC_ACTIONS);
Uriel Sade298f6c02018-12-19 20:34:43 -0800149 }
150
151 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800152 * Returns true if the reply callback has at least one {@link RemoteInput}.
Uriel Sade298f6c02018-12-19 20:34:43 -0800153 * <p/>
154 * Precondition: There exists only one reply callback.
155 */
156 private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
157 return Arrays.stream(sbn.getNotification().actions)
Uriel Saded81ddc92018-12-21 15:16:56 -0800158 .filter(action -> action.getSemanticAction() == SEMANTIC_ACTION_REPLY)
Uriel Sade298f6c02018-12-19 20:34:43 -0800159 .map(Notification.Action::getRemoteInputs)
Uriel Saded81ddc92018-12-21 15:16:56 -0800160 .filter(Objects::nonNull)
161 .anyMatch(remoteInputs -> remoteInputs.length > 0);
Uriel Sade298f6c02018-12-19 20:34:43 -0800162 }
163
164 /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
165 private static boolean assistantCallbacksShowNoUi(StatusBarNotification sbn) {
166 final Notification notification = sbn.getNotification();
167 return IntStream.range(0, notification.actions.length)
168 .mapToObj(i -> NotificationCompat.getAction(notification, i))
169 .filter(Objects::nonNull)
Uriel Saded81ddc92018-12-21 15:16:56 -0800170 .filter(action -> SUPPORTED_SEMANTIC_ACTIONS.contains(action.getSemanticAction()))
Uriel Sade298f6c02018-12-19 20:34:43 -0800171 .noneMatch(NotificationCompat.Action::getShowsUserInterface);
172 }
173
174 /**
Uriel Saded81ddc92018-12-21 15:16:56 -0800175 * Requests a given action from the current active Assistant.
176 *
Uriel Sade298f6c02018-12-19 20:34:43 -0800177 *
178 * @param sbn the notification payload to deliver to assistant
179 * @param semanticAction the semantic action that is to be requested
180 * @return true if the request was successful
181 */
Uriel Saded81ddc92018-12-21 15:16:56 -0800182 public boolean requestAssistantVoiceAction(StatusBarNotification sbn, int semanticAction) {
183 if (!isCarCompatibleMessagingNotification(sbn)) {
184 Log.w(TAG, "Assistant action requested for non-compatible notification.");
185 return false;
Uriel Sade298f6c02018-12-19 20:34:43 -0800186 }
187
Uriel Saded81ddc92018-12-21 15:16:56 -0800188 switch (semanticAction) {
189 case SEMANTIC_ACTION_MARK_AS_READ:
190 return readMessageNotification(sbn);
191 case SEMANTIC_ACTION_REPLY:
192 return replyMessageNotification(sbn);
193 default:
194 return false;
195 }
Uriel Sade298f6c02018-12-19 20:34:43 -0800196 }
197
198 /**
199 * Requests a read action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800200 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
201 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800202 *
203 * @param sbn the notification to deliver as the payload
Uriel Saded81ddc92018-12-21 15:16:56 -0800204 * @return true if the read request was handled successfully
Uriel Sade298f6c02018-12-19 20:34:43 -0800205 */
206 private boolean readMessageNotification(StatusBarNotification sbn) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800207 return requestAction(BundleBuilder.buildAssistantReadBundle(sbn))
208 || mFallbackAssistant.handleReadAction(sbn, mListener);
Uriel Sade298f6c02018-12-19 20:34:43 -0800209 }
210
211 /**
212 * Requests a reply action for the notification from the current active Assistant.
Uriel Saded81ddc92018-12-21 15:16:56 -0800213 * If the Assistant is cannot handle the request, a fallback implementation will attempt to
214 * handle it.
Uriel Sade298f6c02018-12-19 20:34:43 -0800215 *
216 * @param sbn the notification to deliver as the payload
Uriel Saded81ddc92018-12-21 15:16:56 -0800217 * @return true if the reply request was handled successfully
Uriel Sade298f6c02018-12-19 20:34:43 -0800218 */
219 private boolean replyMessageNotification(StatusBarNotification sbn) {
Uriel Saded81ddc92018-12-21 15:16:56 -0800220 return requestAction(BundleBuilder.buildAssistantReplyBundle(sbn))
221 || mFallbackAssistant.handleErrorMessage(mErrorMessage, mListener);
Uriel Sade298f6c02018-12-19 20:34:43 -0800222 }
223
Uriel Saded81ddc92018-12-21 15:16:56 -0800224 private boolean requestAction(Bundle payloadArguments) {
225 return assistantIsNotificationListener()
Uriel Sade298f6c02018-12-19 20:34:43 -0800226 && mAssistUtils.showSessionForActiveService(payloadArguments,
227 CarVoiceInteractionSession.SHOW_SOURCE_NOTIFICATION, null, null);
228 }
229}