blob: ee78a723a49c63b4560c5354f9b6761e97ddfd1a [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,
83 NotificationEntry entry,
84 SmartReplyConstants smartReplyConstants,
85 SmartReplyController smartReplyController,
Gustav Sennton8a52dc32019-04-15 12:48:23 +010086 HeadsUpManager headsUpManager,
87 SmartRepliesAndActions existingSmartRepliesAndActions) {
88 SmartRepliesAndActions newSmartRepliesAndActions =
Gustav Sennton5759f872019-02-13 17:25:26 +000089 chooseSmartRepliesAndActions(smartReplyConstants, entry);
Gustav Sennton8a52dc32019-04-15 12:48:23 +010090 if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
Gustav Sennton5759f872019-02-13 17:25:26 +000091 return new InflatedSmartReplies(null /* smartReplyView */,
Gustav Sennton8a52dc32019-04-15 12:48:23 +010092 null /* smartSuggestionButtons */, newSmartRepliesAndActions);
Gustav Sennton5759f872019-02-13 17:25:26 +000093 }
94
Gustav Sennton8a52dc32019-04-15 12:48:23 +010095 // Only block clicks if the smart buttons are different from the previous set - to avoid
96 // scenarios where a user incorrectly cannot click smart buttons because the notification is
97 // updated.
98 boolean delayOnClickListener =
99 !areSuggestionsSimilar(existingSmartRepliesAndActions, newSmartRepliesAndActions);
100
Gustav Sennton5759f872019-02-13 17:25:26 +0000101 SmartReplyView smartReplyView = SmartReplyView.inflate(context);
102
103 List<Button> suggestionButtons = new ArrayList<>();
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100104 if (newSmartRepliesAndActions.smartReplies != null) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000105 suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100106 newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
107 delayOnClickListener));
Gustav Sennton5759f872019-02-13 17:25:26 +0000108 }
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100109 if (newSmartRepliesAndActions.smartActions != null) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000110 suggestionButtons.addAll(
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100111 smartReplyView.inflateSmartActions(newSmartRepliesAndActions.smartActions,
112 smartReplyController, entry, headsUpManager,
113 delayOnClickListener));
Gustav Sennton5759f872019-02-13 17:25:26 +0000114 }
115
116 return new InflatedSmartReplies(smartReplyView, suggestionButtons,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100117 newSmartRepliesAndActions);
118 }
119
120 @VisibleForTesting
121 static boolean areSuggestionsSimilar(
122 SmartRepliesAndActions left, SmartRepliesAndActions right) {
123 if (left == right) return true;
124 if (left == null || right == null) return false;
125
126 if (!Arrays.equals(left.getSmartReplies(), right.getSmartReplies())) {
127 return false;
128 }
129
130 return !NotificationUiAdjustment.areDifferent(
131 left.getSmartActions(), right.getSmartActions());
Gustav Sennton5759f872019-02-13 17:25:26 +0000132 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000133
134 /**
135 * Returns whether we should show the smart reply view and its smart suggestions.
136 */
137 public static boolean shouldShowSmartReplyView(
138 NotificationEntry entry,
139 SmartRepliesAndActions smartRepliesAndActions) {
140 if (smartRepliesAndActions.smartReplies == null
141 && smartRepliesAndActions.smartActions == null) {
142 // There are no smart replies and no smart actions.
143 return false;
144 }
145 // If we are showing the spinner we don't want to add the buttons.
146 boolean showingSpinner = entry.notification.getNotification()
147 .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
148 if (showingSpinner) {
149 return false;
150 }
151 // If we are keeping the notification around while sending we don't want to add the buttons.
152 boolean hideSmartReplies = entry.notification.getNotification()
153 .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
154 if (hideSmartReplies) {
155 return false;
156 }
157 return true;
158 }
159
160 /**
161 * Chose what smart replies and smart actions to display. App generated suggestions take
162 * precedence. So if the app provides any smart replies, we don't show any
163 * replies or actions generated by the NotificationAssistantService (NAS), and if the app
164 * provides any smart actions we also don't show any NAS-generated replies or actions.
165 */
166 @NonNull
167 public static SmartRepliesAndActions chooseSmartRepliesAndActions(
168 SmartReplyConstants smartReplyConstants,
169 final NotificationEntry entry) {
170 Notification notification = entry.notification.getNotification();
171 Pair<RemoteInput, Notification.Action> remoteInputActionPair =
172 notification.findRemoteInputActionPair(false /* freeform */);
173 Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
174 notification.findRemoteInputActionPair(true /* freeform */);
175
176 if (!smartReplyConstants.isEnabled()) {
177 if (DEBUG) {
178 Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
179 + entry.notification.getKey());
180 }
181 return new SmartRepliesAndActions(null, null);
182 }
183 // Only use smart replies from the app if they target P or above. We have this check because
184 // the smart reply API has been used for other things (Wearables) in the past. The API to
185 // add smart actions is new in Q so it doesn't require a target-sdk check.
186 boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
187 || entry.targetSdk >= Build.VERSION_CODES.P);
188
189 boolean appGeneratedSmartRepliesExist =
190 enableAppGeneratedSmartReplies
191 && remoteInputActionPair != null
192 && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
193 && remoteInputActionPair.second.actionIntent != null;
194
195 List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
196 boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();
197
198 SmartReplyView.SmartReplies smartReplies = null;
199 SmartReplyView.SmartActions smartActions = null;
200 if (appGeneratedSmartRepliesExist) {
201 smartReplies = new SmartReplyView.SmartReplies(
202 remoteInputActionPair.first.getChoices(),
203 remoteInputActionPair.first,
204 remoteInputActionPair.second.actionIntent,
205 false /* fromAssistant */);
206 }
207 if (appGeneratedSmartActionsExist) {
208 smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
209 false /* fromAssistant */);
210 }
211 // Apps didn't provide any smart replies / actions, use those from NAS (if any).
212 if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
213 boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.systemGeneratedSmartReplies)
214 && freeformRemoteInputActionPair != null
215 && freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
216 && freeformRemoteInputActionPair.second.actionIntent != null;
217 if (useGeneratedReplies) {
218 smartReplies = new SmartReplyView.SmartReplies(
219 entry.systemGeneratedSmartReplies,
220 freeformRemoteInputActionPair.first,
221 freeformRemoteInputActionPair.second.actionIntent,
222 true /* fromAssistant */);
223 }
224 boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
225 && notification.getAllowSystemGeneratedContextualActions();
226 if (useSmartActions) {
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000227 List<Notification.Action> systemGeneratedActions =
228 entry.systemGeneratedSmartActions;
229 // Filter actions if we're in kiosk-mode - we don't care about screen pinning mode,
230 // since notifications aren't shown there anyway.
231 ActivityManagerWrapper activityManagerWrapper =
232 Dependency.get(ActivityManagerWrapper.class);
233 if (activityManagerWrapper.isLockTaskKioskModeActive()) {
234 systemGeneratedActions = filterWhiteListedLockTaskApps(systemGeneratedActions);
235 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000236 smartActions = new SmartReplyView.SmartActions(
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000237 systemGeneratedActions, true /* fromAssistant */);
Gustav Senntonb944ce52019-02-25 18:52:43 +0000238 }
239 }
240 return new SmartRepliesAndActions(smartReplies, smartActions);
241 }
242
243 /**
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000244 * Filter actions so that only actions pointing to whitelisted apps are allowed.
245 * This filtering is only meaningful when in lock-task mode.
246 */
247 private static List<Notification.Action> filterWhiteListedLockTaskApps(
248 List<Notification.Action> actions) {
249 PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class);
250 DevicePolicyManagerWrapper devicePolicyManagerWrapper =
251 Dependency.get(DevicePolicyManagerWrapper.class);
252 List<Notification.Action> filteredActions = new ArrayList<>();
253 for (Notification.Action action : actions) {
254 if (action.actionIntent == null) continue;
255 Intent intent = action.actionIntent.getIntent();
256 // Only allow actions that are explicit (implicit intents are not handled in lock-task
257 // mode), and link to whitelisted apps.
258 ResolveInfo resolveInfo = packageManagerWrapper.resolveActivity(intent, 0 /* flags */);
259 if (resolveInfo != null && devicePolicyManagerWrapper.isLockTaskPermitted(
260 resolveInfo.activityInfo.packageName)) {
261 filteredActions.add(action);
262 }
263 }
264 return filteredActions;
265 }
266
267 /**
Gustav Senntonb944ce52019-02-25 18:52:43 +0000268 * Returns whether the {@link Notification} represented by entry has a free-form remote input.
269 * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
270 * through the remote input.
271 */
272 public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
273 Notification notification = entry.notification.getNotification();
274 return null != notification.findRemoteInputActionPair(true /* freeform */);
275 }
276
277 /**
278 * A storage for smart replies and smart action.
279 */
280 public static class SmartRepliesAndActions {
281 @Nullable public final SmartReplyView.SmartReplies smartReplies;
282 @Nullable public final SmartReplyView.SmartActions smartActions;
283
284 SmartRepliesAndActions(
285 @Nullable SmartReplyView.SmartReplies smartReplies,
286 @Nullable SmartReplyView.SmartActions smartActions) {
287 this.smartReplies = smartReplies;
288 this.smartActions = smartActions;
289 }
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100290
291 @NonNull public CharSequence[] getSmartReplies() {
292 return smartReplies == null ? new CharSequence[0] : smartReplies.choices;
293 }
294
295 @NonNull public List<Notification.Action> getSmartActions() {
296 return smartActions == null ? Collections.emptyList() : smartActions.actions;
297 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000298 }
299}