blob: 8c6dcebcb18753283b08f3e8384dde2561d23217 [file] [log] [blame]
Uriel Sade298f6c02018-12-19 20:34:43 -08001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
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
18import android.app.Notification;
19import android.content.Context;
20import android.os.Bundle;
21import android.provider.Settings;
22import android.service.notification.StatusBarNotification;
23import android.util.Log;
24
25import androidx.core.app.NotificationCompat;
26
27import com.android.car.assist.CarVoiceInteractionSession;
28import com.android.internal.app.AssistUtils;
29
30import java.util.Arrays;
31import java.util.Collections;
32import java.util.HashSet;
33import java.util.List;
34import java.util.Objects;
35import java.util.Set;
36import java.util.stream.Collectors;
37import java.util.stream.IntStream;
38
39/**
40 * Util class providing helper methods to interact with the current active voice service,
41 * while ensuring that the active voice service has the required permissions.
42 */
43public class CarAssistUtils {
44 public static final String TAG = "CarAssistUtils";
45 private static final List<Integer> sRequiredSemanticActions = Collections.unmodifiableList(
46 Arrays.asList(
47 Notification.Action.SEMANTIC_ACTION_REPLY,
48 Notification.Action.SEMANTIC_ACTION_MARK_AS_READ
49 )
50 );
51
52 // Currently, all supported semantic actions are required.
53 private static final List<Integer> sSupportedSemanticActions = sRequiredSemanticActions;
54
55 private final Context mContext;
56 private final AssistUtils mAssistUtils;
57
58 public CarAssistUtils(Context context) {
59 mAssistUtils = new AssistUtils(context);
60 mContext = context;
61 }
62
63 /**
64 * Returns true if the current active assistant has notification listener permissions.
65 */
66 public boolean assistantIsNotificationListener() {
67 final String activeComponent = mAssistUtils.getActiveServiceComponentName()
68 .flattenToString();
69 final String listeners = Settings.Secure.getString(mContext.getContentResolver(),
70 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
71
72 return listeners != null
73 && Arrays.asList(listeners.split(":")).contains(activeComponent);
74 }
75
76 /**
77 * Checks whether the notification is a car-compatible messaging notification.
78 *
79 * @param sbn The notification being checked.
80 * @return true if the notification is a car-compatible messaging notification.
81 */
82 public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
83 return hasMessagingStyle(sbn)
84 && hasRequiredAssistantCallbacks(sbn)
85 && replyCallbackHasRemoteInput(sbn)
86 && assistantCallbacksShowNoUi(sbn);
87 }
88
89 /** Returns true if the semantic action provided can be supported. */
90 public static boolean isSupportedSemanticAction(int semanticAction) {
91 return sSupportedSemanticActions.contains(semanticAction);
92 }
93
94 /**
95 * Returns true if the notification has a messaging style.
96 * <p/>
97 * This is the case if the notification in question was provided an instance of
98 * {@link Notification.MessagingStyle} (or an instance of
99 * {@link NotificationCompat.MessagingStyle} if {@link NotificationCompat} was used).
100 */
101 private static boolean hasMessagingStyle(StatusBarNotification sbn) {
102 return NotificationCompat.MessagingStyle
103 .extractMessagingStyleFromNotification(sbn.getNotification()) != null;
104 }
105
106 /**
107 * Returns true if the notification has the required Assistant callbacks to be considered
108 * a car-compatible messaging notification. The callbacks must be unambiguous, therefore false
109 * is returned if multiple callbacks exist for any semantic action that is supported.
110 */
111 private static boolean hasRequiredAssistantCallbacks(StatusBarNotification sbn) {
112 List<Integer> semanticActionList = Arrays.stream(sbn.getNotification().actions)
113 .map(Notification.Action::getSemanticAction)
114 .filter(sRequiredSemanticActions::contains)
115 .collect(Collectors.toList());
116 Set<Integer> semanticActionSet = new HashSet<>(semanticActionList);
117
118 return semanticActionList.size() == semanticActionSet.size()
119 && semanticActionSet.containsAll(sRequiredSemanticActions);
120 }
121
122 /**
123 * Returns true if the reply callback has exactly one RemoteInput.
124 * <p/>
125 * Precondition: There exists only one reply callback.
126 */
127 private static boolean replyCallbackHasRemoteInput(StatusBarNotification sbn) {
128 return Arrays.stream(sbn.getNotification().actions)
129 .filter(action ->
130 action.getSemanticAction() == Notification.Action.SEMANTIC_ACTION_REPLY)
131 .map(Notification.Action::getRemoteInputs)
132 .anyMatch(remoteInputs -> remoteInputs != null && remoteInputs.length == 1);
133 }
134
135 /** Returns true if all Assistant callbacks indicate that they show no UI, false otherwise. */
136 private static boolean assistantCallbacksShowNoUi(StatusBarNotification sbn) {
137 final Notification notification = sbn.getNotification();
138 return IntStream.range(0, notification.actions.length)
139 .mapToObj(i -> NotificationCompat.getAction(notification, i))
140 .filter(Objects::nonNull)
141 .filter(action -> sRequiredSemanticActions.contains(action.getSemanticAction()))
142 .noneMatch(NotificationCompat.Action::getShowsUserInterface);
143 }
144
145 /**
146 * Requests a given action from the current active assistant.
147 *
148 * @param sbn the notification payload to deliver to assistant
149 * @param semanticAction the semantic action that is to be requested
150 * @return true if the request was successful
151 */
152 public boolean requestAssistantAction(StatusBarNotification sbn, int semanticAction) {
153 switch (semanticAction) {
154 case Notification.Action.SEMANTIC_ACTION_MARK_AS_READ:
155 return readMessageNotification(sbn);
156 case Notification.Action.SEMANTIC_ACTION_REPLY:
157 return replyMessageNotification(sbn);
158 default:
159 Log.w(TAG, "Unhanded semanticAction");
160 }
161
162 return false;
163 }
164
165 /**
166 * Requests a read action for the notification from the current active Assistant.
167 *
168 * @param sbn the notification to deliver as the payload
169 * @return true if the read request to Assistant was successful
170 */
171 private boolean readMessageNotification(StatusBarNotification sbn) {
172 return requestAction(sbn, BundleBuilder.buildAssistantReadBundle(sbn));
173 }
174
175 /**
176 * Requests a reply action for the notification from the current active Assistant.
177 *
178 * @param sbn the notification to deliver as the payload
179 * @return true if the reply request to Assistant was successful
180 */
181 private boolean replyMessageNotification(StatusBarNotification sbn) {
182 return requestAction(sbn, BundleBuilder.buildAssistantReplyBundle(sbn));
183 }
184
185 private boolean requestAction(StatusBarNotification sbn, Bundle payloadArguments) {
186 return isCarCompatibleMessagingNotification(sbn)
187 && assistantIsNotificationListener()
188 && mAssistUtils.showSessionForActiveService(payloadArguments,
189 CarVoiceInteractionSession.SHOW_SOURCE_NOTIFICATION, null, null);
190 }
191}