blob: ea93193ca7289412a3317328121af9044c6985d0 [file] [log] [blame]
Gustav Senntonb944ce52019-02-25 18:52:43 +00001/*
2 * Copyright (C) 2019 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 */
16
17package com.android.systemui.statusbar.policy;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.Notification;
22import android.app.RemoteInput;
Gustav Sennton5759f872019-02-13 17:25:26 +000023import android.content.Context;
Gustav Sennton5a4fc212019-02-28 16:12:27 +000024import android.content.Intent;
25import android.content.pm.ResolveInfo;
Gustav Senntonb944ce52019-02-25 18:52:43 +000026import android.os.Build;
27import android.util.Log;
28import android.util.Pair;
Gustav Sennton5759f872019-02-13 17:25:26 +000029import android.widget.Button;
Gustav Senntonb944ce52019-02-25 18:52:43 +000030
Gustav Sennton8a52dc32019-04-15 12:48:23 +010031import com.android.internal.annotations.VisibleForTesting;
Gustav Senntonb944ce52019-02-25 18:52:43 +000032import com.android.internal.util.ArrayUtils;
Gustav Sennton5a4fc212019-02-28 16:12:27 +000033import com.android.systemui.Dependency;
34import com.android.systemui.shared.system.ActivityManagerWrapper;
35import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
36import com.android.systemui.shared.system.PackageManagerWrapper;
Gustav Sennton8a52dc32019-04-15 12:48:23 +010037import com.android.systemui.statusbar.NotificationUiAdjustment;
Gustav Sennton5759f872019-02-13 17:25:26 +000038import com.android.systemui.statusbar.SmartReplyController;
Gustav Senntonb944ce52019-02-25 18:52:43 +000039import com.android.systemui.statusbar.notification.collection.NotificationEntry;
40
Gustav Sennton5759f872019-02-13 17:25:26 +000041import java.util.ArrayList;
Gustav Sennton8a52dc32019-04-15 12:48:23 +010042import java.util.Arrays;
43import java.util.Collections;
Gustav Senntonb944ce52019-02-25 18:52:43 +000044import java.util.List;
45
46/**
47 * Holder for inflated smart replies and actions. These objects should be inflated on a background
48 * thread, to later be accessed and modified on the (performance critical) UI thread.
49 */
50public class InflatedSmartReplies {
51 private static final String TAG = "InflatedSmartReplies";
52 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
Gustav Sennton5759f872019-02-13 17:25:26 +000053 @Nullable private final SmartReplyView mSmartReplyView;
54 @Nullable private final List<Button> mSmartSuggestionButtons;
55 @NonNull private final SmartRepliesAndActions mSmartRepliesAndActions;
Gustav Senntonb944ce52019-02-25 18:52:43 +000056
Gustav Sennton5759f872019-02-13 17:25:26 +000057 private InflatedSmartReplies(
58 @Nullable SmartReplyView smartReplyView,
59 @Nullable List<Button> smartSuggestionButtons,
60 @NonNull SmartRepliesAndActions smartRepliesAndActions) {
61 mSmartReplyView = smartReplyView;
62 mSmartSuggestionButtons = smartSuggestionButtons;
63 mSmartRepliesAndActions = smartRepliesAndActions;
64 }
65
66 @Nullable public SmartReplyView getSmartReplyView() {
67 return mSmartReplyView;
68 }
69
70 @Nullable public List<Button> getSmartSuggestionButtons() {
71 return mSmartSuggestionButtons;
72 }
73
74 @NonNull public SmartRepliesAndActions getSmartRepliesAndActions() {
75 return mSmartRepliesAndActions;
76 }
77
78 /**
79 * Inflate a SmartReplyView and its smart suggestions.
80 */
81 public static InflatedSmartReplies inflate(
82 Context context,
Gustav Senntondfa968d2019-09-13 12:00:50 +010083 Context packageContext,
Gustav Sennton5759f872019-02-13 17:25:26 +000084 NotificationEntry entry,
85 SmartReplyConstants smartReplyConstants,
86 SmartReplyController smartReplyController,
Gustav Sennton8a52dc32019-04-15 12:48:23 +010087 HeadsUpManager headsUpManager,
88 SmartRepliesAndActions existingSmartRepliesAndActions) {
89 SmartRepliesAndActions newSmartRepliesAndActions =
Gustav Sennton5759f872019-02-13 17:25:26 +000090 chooseSmartRepliesAndActions(smartReplyConstants, entry);
Gustav Sennton8a52dc32019-04-15 12:48:23 +010091 if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
Gustav Sennton5759f872019-02-13 17:25:26 +000092 return new InflatedSmartReplies(null /* smartReplyView */,
Gustav Sennton8a52dc32019-04-15 12:48:23 +010093 null /* smartSuggestionButtons */, newSmartRepliesAndActions);
Gustav Sennton5759f872019-02-13 17:25:26 +000094 }
95
Gustav Sennton8a52dc32019-04-15 12:48:23 +010096 // Only block clicks if the smart buttons are different from the previous set - to avoid
97 // scenarios where a user incorrectly cannot click smart buttons because the notification is
98 // updated.
99 boolean delayOnClickListener =
100 !areSuggestionsSimilar(existingSmartRepliesAndActions, newSmartRepliesAndActions);
101
Gustav Sennton5759f872019-02-13 17:25:26 +0000102 SmartReplyView smartReplyView = SmartReplyView.inflate(context);
103
104 List<Button> suggestionButtons = new ArrayList<>();
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100105 if (newSmartRepliesAndActions.smartReplies != null) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000106 suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100107 newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
108 delayOnClickListener));
Gustav Sennton5759f872019-02-13 17:25:26 +0000109 }
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100110 if (newSmartRepliesAndActions.smartActions != null) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000111 suggestionButtons.addAll(
Gustav Senntondfa968d2019-09-13 12:00:50 +0100112 smartReplyView.inflateSmartActions(packageContext,
113 newSmartRepliesAndActions.smartActions, smartReplyController, entry,
114 headsUpManager, delayOnClickListener));
Gustav Sennton5759f872019-02-13 17:25:26 +0000115 }
116
117 return new InflatedSmartReplies(smartReplyView, suggestionButtons,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100118 newSmartRepliesAndActions);
119 }
120
121 @VisibleForTesting
122 static boolean areSuggestionsSimilar(
123 SmartRepliesAndActions left, SmartRepliesAndActions right) {
124 if (left == right) return true;
125 if (left == null || right == null) return false;
126
127 if (!Arrays.equals(left.getSmartReplies(), right.getSmartReplies())) {
128 return false;
129 }
130
131 return !NotificationUiAdjustment.areDifferent(
132 left.getSmartActions(), right.getSmartActions());
Gustav Sennton5759f872019-02-13 17:25:26 +0000133 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000134
135 /**
136 * Returns whether we should show the smart reply view and its smart suggestions.
137 */
138 public static boolean shouldShowSmartReplyView(
139 NotificationEntry entry,
140 SmartRepliesAndActions smartRepliesAndActions) {
141 if (smartRepliesAndActions.smartReplies == null
142 && smartRepliesAndActions.smartActions == null) {
143 // There are no smart replies and no smart actions.
144 return false;
145 }
146 // If we are showing the spinner we don't want to add the buttons.
147 boolean showingSpinner = entry.notification.getNotification()
148 .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
149 if (showingSpinner) {
150 return false;
151 }
152 // If we are keeping the notification around while sending we don't want to add the buttons.
153 boolean hideSmartReplies = entry.notification.getNotification()
154 .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
155 if (hideSmartReplies) {
156 return false;
157 }
158 return true;
159 }
160
161 /**
162 * Chose what smart replies and smart actions to display. App generated suggestions take
163 * precedence. So if the app provides any smart replies, we don't show any
164 * replies or actions generated by the NotificationAssistantService (NAS), and if the app
165 * provides any smart actions we also don't show any NAS-generated replies or actions.
166 */
167 @NonNull
168 public static SmartRepliesAndActions chooseSmartRepliesAndActions(
169 SmartReplyConstants smartReplyConstants,
170 final NotificationEntry entry) {
171 Notification notification = entry.notification.getNotification();
172 Pair<RemoteInput, Notification.Action> remoteInputActionPair =
173 notification.findRemoteInputActionPair(false /* freeform */);
174 Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
175 notification.findRemoteInputActionPair(true /* freeform */);
176
177 if (!smartReplyConstants.isEnabled()) {
178 if (DEBUG) {
179 Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
180 + entry.notification.getKey());
181 }
182 return new SmartRepliesAndActions(null, null);
183 }
184 // Only use smart replies from the app if they target P or above. We have this check because
185 // the smart reply API has been used for other things (Wearables) in the past. The API to
186 // add smart actions is new in Q so it doesn't require a target-sdk check.
187 boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
188 || entry.targetSdk >= Build.VERSION_CODES.P);
189
190 boolean appGeneratedSmartRepliesExist =
191 enableAppGeneratedSmartReplies
192 && remoteInputActionPair != null
193 && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
194 && remoteInputActionPair.second.actionIntent != null;
195
196 List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
197 boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();
198
199 SmartReplyView.SmartReplies smartReplies = null;
200 SmartReplyView.SmartActions smartActions = null;
201 if (appGeneratedSmartRepliesExist) {
202 smartReplies = new SmartReplyView.SmartReplies(
203 remoteInputActionPair.first.getChoices(),
204 remoteInputActionPair.first,
205 remoteInputActionPair.second.actionIntent,
206 false /* fromAssistant */);
207 }
208 if (appGeneratedSmartActionsExist) {
209 smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
210 false /* fromAssistant */);
211 }
212 // Apps didn't provide any smart replies / actions, use those from NAS (if any).
213 if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
214 boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.systemGeneratedSmartReplies)
215 && freeformRemoteInputActionPair != null
216 && freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
217 && freeformRemoteInputActionPair.second.actionIntent != null;
218 if (useGeneratedReplies) {
219 smartReplies = new SmartReplyView.SmartReplies(
220 entry.systemGeneratedSmartReplies,
221 freeformRemoteInputActionPair.first,
222 freeformRemoteInputActionPair.second.actionIntent,
223 true /* fromAssistant */);
224 }
225 boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
226 && notification.getAllowSystemGeneratedContextualActions();
227 if (useSmartActions) {
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000228 List<Notification.Action> systemGeneratedActions =
229 entry.systemGeneratedSmartActions;
230 // Filter actions if we're in kiosk-mode - we don't care about screen pinning mode,
231 // since notifications aren't shown there anyway.
232 ActivityManagerWrapper activityManagerWrapper =
233 Dependency.get(ActivityManagerWrapper.class);
234 if (activityManagerWrapper.isLockTaskKioskModeActive()) {
235 systemGeneratedActions = filterWhiteListedLockTaskApps(systemGeneratedActions);
236 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000237 smartActions = new SmartReplyView.SmartActions(
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000238 systemGeneratedActions, true /* fromAssistant */);
Gustav Senntonb944ce52019-02-25 18:52:43 +0000239 }
240 }
241 return new SmartRepliesAndActions(smartReplies, smartActions);
242 }
243
244 /**
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000245 * Filter actions so that only actions pointing to whitelisted apps are allowed.
246 * This filtering is only meaningful when in lock-task mode.
247 */
248 private static List<Notification.Action> filterWhiteListedLockTaskApps(
249 List<Notification.Action> actions) {
250 PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class);
251 DevicePolicyManagerWrapper devicePolicyManagerWrapper =
252 Dependency.get(DevicePolicyManagerWrapper.class);
253 List<Notification.Action> filteredActions = new ArrayList<>();
254 for (Notification.Action action : actions) {
255 if (action.actionIntent == null) continue;
256 Intent intent = action.actionIntent.getIntent();
257 // Only allow actions that are explicit (implicit intents are not handled in lock-task
258 // mode), and link to whitelisted apps.
259 ResolveInfo resolveInfo = packageManagerWrapper.resolveActivity(intent, 0 /* flags */);
260 if (resolveInfo != null && devicePolicyManagerWrapper.isLockTaskPermitted(
261 resolveInfo.activityInfo.packageName)) {
262 filteredActions.add(action);
263 }
264 }
265 return filteredActions;
266 }
267
268 /**
Gustav Senntonb944ce52019-02-25 18:52:43 +0000269 * Returns whether the {@link Notification} represented by entry has a free-form remote input.
270 * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
271 * through the remote input.
272 */
273 public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
274 Notification notification = entry.notification.getNotification();
275 return null != notification.findRemoteInputActionPair(true /* freeform */);
276 }
277
278 /**
279 * A storage for smart replies and smart action.
280 */
281 public static class SmartRepliesAndActions {
282 @Nullable public final SmartReplyView.SmartReplies smartReplies;
283 @Nullable public final SmartReplyView.SmartActions smartActions;
284
285 SmartRepliesAndActions(
286 @Nullable SmartReplyView.SmartReplies smartReplies,
287 @Nullable SmartReplyView.SmartActions smartActions) {
288 this.smartReplies = smartReplies;
289 this.smartActions = smartActions;
290 }
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100291
292 @NonNull public CharSequence[] getSmartReplies() {
293 return smartReplies == null ? new CharSequence[0] : smartReplies.choices;
294 }
295
296 @NonNull public List<Notification.Action> getSmartActions() {
297 return smartActions == null ? Collections.emptyList() : smartActions.actions;
298 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000299 }
300}