blob: 5b2e398b66e14d505d4d541b9f0e9f79ccc1034a [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
31import com.android.internal.util.ArrayUtils;
Gustav Sennton5a4fc212019-02-28 16:12:27 +000032import com.android.systemui.Dependency;
33import com.android.systemui.shared.system.ActivityManagerWrapper;
34import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
35import com.android.systemui.shared.system.PackageManagerWrapper;
Gustav Sennton5759f872019-02-13 17:25:26 +000036import com.android.systemui.statusbar.SmartReplyController;
Gustav Senntonb944ce52019-02-25 18:52:43 +000037import com.android.systemui.statusbar.notification.collection.NotificationEntry;
38
Gustav Sennton5759f872019-02-13 17:25:26 +000039import java.util.ArrayList;
Gustav Senntonb944ce52019-02-25 18:52:43 +000040import java.util.List;
41
42/**
43 * Holder for inflated smart replies and actions. These objects should be inflated on a background
44 * thread, to later be accessed and modified on the (performance critical) UI thread.
45 */
46public class InflatedSmartReplies {
47 private static final String TAG = "InflatedSmartReplies";
48 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
Gustav Sennton5759f872019-02-13 17:25:26 +000049 @Nullable private final SmartReplyView mSmartReplyView;
50 @Nullable private final List<Button> mSmartSuggestionButtons;
51 @NonNull private final SmartRepliesAndActions mSmartRepliesAndActions;
Gustav Senntonb944ce52019-02-25 18:52:43 +000052
Gustav Sennton5759f872019-02-13 17:25:26 +000053 private InflatedSmartReplies(
54 @Nullable SmartReplyView smartReplyView,
55 @Nullable List<Button> smartSuggestionButtons,
56 @NonNull SmartRepliesAndActions smartRepliesAndActions) {
57 mSmartReplyView = smartReplyView;
58 mSmartSuggestionButtons = smartSuggestionButtons;
59 mSmartRepliesAndActions = smartRepliesAndActions;
60 }
61
62 @Nullable public SmartReplyView getSmartReplyView() {
63 return mSmartReplyView;
64 }
65
66 @Nullable public List<Button> getSmartSuggestionButtons() {
67 return mSmartSuggestionButtons;
68 }
69
70 @NonNull public SmartRepliesAndActions getSmartRepliesAndActions() {
71 return mSmartRepliesAndActions;
72 }
73
74 /**
75 * Inflate a SmartReplyView and its smart suggestions.
76 */
77 public static InflatedSmartReplies inflate(
78 Context context,
79 NotificationEntry entry,
80 SmartReplyConstants smartReplyConstants,
81 SmartReplyController smartReplyController,
82 HeadsUpManager headsUpManager) {
83 SmartRepliesAndActions smartRepliesAndActions =
84 chooseSmartRepliesAndActions(smartReplyConstants, entry);
85 if (!shouldShowSmartReplyView(entry, smartRepliesAndActions)) {
86 return new InflatedSmartReplies(null /* smartReplyView */,
87 null /* smartSuggestionButtons */, smartRepliesAndActions);
88 }
89
90 SmartReplyView smartReplyView = SmartReplyView.inflate(context);
91
92 List<Button> suggestionButtons = new ArrayList<>();
93 if (smartRepliesAndActions.smartReplies != null) {
94 suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
95 smartRepliesAndActions.smartReplies, smartReplyController, entry));
96 }
97 if (smartRepliesAndActions.smartActions != null) {
98 suggestionButtons.addAll(
99 smartReplyView.inflateSmartActions(smartRepliesAndActions.smartActions,
100 smartReplyController, entry, headsUpManager));
101 }
102
103 return new InflatedSmartReplies(smartReplyView, suggestionButtons,
104 smartRepliesAndActions);
105 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000106
107 /**
108 * Returns whether we should show the smart reply view and its smart suggestions.
109 */
110 public static boolean shouldShowSmartReplyView(
111 NotificationEntry entry,
112 SmartRepliesAndActions smartRepliesAndActions) {
113 if (smartRepliesAndActions.smartReplies == null
114 && smartRepliesAndActions.smartActions == null) {
115 // There are no smart replies and no smart actions.
116 return false;
117 }
118 // If we are showing the spinner we don't want to add the buttons.
119 boolean showingSpinner = entry.notification.getNotification()
120 .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
121 if (showingSpinner) {
122 return false;
123 }
124 // If we are keeping the notification around while sending we don't want to add the buttons.
125 boolean hideSmartReplies = entry.notification.getNotification()
126 .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
127 if (hideSmartReplies) {
128 return false;
129 }
130 return true;
131 }
132
133 /**
134 * Chose what smart replies and smart actions to display. App generated suggestions take
135 * precedence. So if the app provides any smart replies, we don't show any
136 * replies or actions generated by the NotificationAssistantService (NAS), and if the app
137 * provides any smart actions we also don't show any NAS-generated replies or actions.
138 */
139 @NonNull
140 public static SmartRepliesAndActions chooseSmartRepliesAndActions(
141 SmartReplyConstants smartReplyConstants,
142 final NotificationEntry entry) {
143 Notification notification = entry.notification.getNotification();
144 Pair<RemoteInput, Notification.Action> remoteInputActionPair =
145 notification.findRemoteInputActionPair(false /* freeform */);
146 Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
147 notification.findRemoteInputActionPair(true /* freeform */);
148
149 if (!smartReplyConstants.isEnabled()) {
150 if (DEBUG) {
151 Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
152 + entry.notification.getKey());
153 }
154 return new SmartRepliesAndActions(null, null);
155 }
156 // Only use smart replies from the app if they target P or above. We have this check because
157 // the smart reply API has been used for other things (Wearables) in the past. The API to
158 // add smart actions is new in Q so it doesn't require a target-sdk check.
159 boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
160 || entry.targetSdk >= Build.VERSION_CODES.P);
161
162 boolean appGeneratedSmartRepliesExist =
163 enableAppGeneratedSmartReplies
164 && remoteInputActionPair != null
165 && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
166 && remoteInputActionPair.second.actionIntent != null;
167
168 List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
169 boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();
170
171 SmartReplyView.SmartReplies smartReplies = null;
172 SmartReplyView.SmartActions smartActions = null;
173 if (appGeneratedSmartRepliesExist) {
174 smartReplies = new SmartReplyView.SmartReplies(
175 remoteInputActionPair.first.getChoices(),
176 remoteInputActionPair.first,
177 remoteInputActionPair.second.actionIntent,
178 false /* fromAssistant */);
179 }
180 if (appGeneratedSmartActionsExist) {
181 smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
182 false /* fromAssistant */);
183 }
184 // Apps didn't provide any smart replies / actions, use those from NAS (if any).
185 if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
186 boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.systemGeneratedSmartReplies)
187 && freeformRemoteInputActionPair != null
188 && freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
189 && freeformRemoteInputActionPair.second.actionIntent != null;
190 if (useGeneratedReplies) {
191 smartReplies = new SmartReplyView.SmartReplies(
192 entry.systemGeneratedSmartReplies,
193 freeformRemoteInputActionPair.first,
194 freeformRemoteInputActionPair.second.actionIntent,
195 true /* fromAssistant */);
196 }
197 boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
198 && notification.getAllowSystemGeneratedContextualActions();
199 if (useSmartActions) {
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000200 List<Notification.Action> systemGeneratedActions =
201 entry.systemGeneratedSmartActions;
202 // Filter actions if we're in kiosk-mode - we don't care about screen pinning mode,
203 // since notifications aren't shown there anyway.
204 ActivityManagerWrapper activityManagerWrapper =
205 Dependency.get(ActivityManagerWrapper.class);
206 if (activityManagerWrapper.isLockTaskKioskModeActive()) {
207 systemGeneratedActions = filterWhiteListedLockTaskApps(systemGeneratedActions);
208 }
Gustav Senntonb944ce52019-02-25 18:52:43 +0000209 smartActions = new SmartReplyView.SmartActions(
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000210 systemGeneratedActions, true /* fromAssistant */);
Gustav Senntonb944ce52019-02-25 18:52:43 +0000211 }
212 }
213 return new SmartRepliesAndActions(smartReplies, smartActions);
214 }
215
216 /**
Gustav Sennton5a4fc212019-02-28 16:12:27 +0000217 * Filter actions so that only actions pointing to whitelisted apps are allowed.
218 * This filtering is only meaningful when in lock-task mode.
219 */
220 private static List<Notification.Action> filterWhiteListedLockTaskApps(
221 List<Notification.Action> actions) {
222 PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class);
223 DevicePolicyManagerWrapper devicePolicyManagerWrapper =
224 Dependency.get(DevicePolicyManagerWrapper.class);
225 List<Notification.Action> filteredActions = new ArrayList<>();
226 for (Notification.Action action : actions) {
227 if (action.actionIntent == null) continue;
228 Intent intent = action.actionIntent.getIntent();
229 // Only allow actions that are explicit (implicit intents are not handled in lock-task
230 // mode), and link to whitelisted apps.
231 ResolveInfo resolveInfo = packageManagerWrapper.resolveActivity(intent, 0 /* flags */);
232 if (resolveInfo != null && devicePolicyManagerWrapper.isLockTaskPermitted(
233 resolveInfo.activityInfo.packageName)) {
234 filteredActions.add(action);
235 }
236 }
237 return filteredActions;
238 }
239
240 /**
Gustav Senntonb944ce52019-02-25 18:52:43 +0000241 * Returns whether the {@link Notification} represented by entry has a free-form remote input.
242 * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
243 * through the remote input.
244 */
245 public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
246 Notification notification = entry.notification.getNotification();
247 return null != notification.findRemoteInputActionPair(true /* freeform */);
248 }
249
250 /**
251 * A storage for smart replies and smart action.
252 */
253 public static class SmartRepliesAndActions {
254 @Nullable public final SmartReplyView.SmartReplies smartReplies;
255 @Nullable public final SmartReplyView.SmartActions smartActions;
256
257 SmartRepliesAndActions(
258 @Nullable SmartReplyView.SmartReplies smartReplies,
259 @Nullable SmartReplyView.SmartActions smartActions) {
260 this.smartReplies = smartReplies;
261 this.smartActions = smartActions;
262 }
263 }
264}