• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui.statusbar.policy;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Notification;
22 import android.app.RemoteInput;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ResolveInfo;
26 import android.os.Build;
27 import android.util.Log;
28 import android.util.Pair;
29 import android.widget.Button;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.util.ArrayUtils;
33 import com.android.systemui.Dependency;
34 import com.android.systemui.shared.system.ActivityManagerWrapper;
35 import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
36 import com.android.systemui.shared.system.PackageManagerWrapper;
37 import com.android.systemui.statusbar.NotificationUiAdjustment;
38 import com.android.systemui.statusbar.SmartReplyController;
39 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
40 
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Collections;
44 import 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  */
50 public class InflatedSmartReplies {
51     private static final String TAG = "InflatedSmartReplies";
52     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
53     @Nullable private final SmartReplyView mSmartReplyView;
54     @Nullable private final List<Button> mSmartSuggestionButtons;
55     @NonNull private final SmartRepliesAndActions mSmartRepliesAndActions;
56 
InflatedSmartReplies( @ullable SmartReplyView smartReplyView, @Nullable List<Button> smartSuggestionButtons, @NonNull SmartRepliesAndActions smartRepliesAndActions)57     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 
getSmartReplyView()66     @Nullable public SmartReplyView getSmartReplyView() {
67         return mSmartReplyView;
68     }
69 
getSmartSuggestionButtons()70     @Nullable public List<Button> getSmartSuggestionButtons() {
71         return mSmartSuggestionButtons;
72     }
73 
getSmartRepliesAndActions()74     @NonNull public SmartRepliesAndActions getSmartRepliesAndActions() {
75         return mSmartRepliesAndActions;
76     }
77 
78     /**
79      * Inflate a SmartReplyView and its smart suggestions.
80      */
inflate( Context context, NotificationEntry entry, SmartReplyConstants smartReplyConstants, SmartReplyController smartReplyController, HeadsUpManager headsUpManager, SmartRepliesAndActions existingSmartRepliesAndActions)81     public static InflatedSmartReplies inflate(
82             Context context,
83             NotificationEntry entry,
84             SmartReplyConstants smartReplyConstants,
85             SmartReplyController smartReplyController,
86             HeadsUpManager headsUpManager,
87             SmartRepliesAndActions existingSmartRepliesAndActions) {
88         SmartRepliesAndActions newSmartRepliesAndActions =
89                 chooseSmartRepliesAndActions(smartReplyConstants, entry);
90         if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
91             return new InflatedSmartReplies(null /* smartReplyView */,
92                     null /* smartSuggestionButtons */, newSmartRepliesAndActions);
93         }
94 
95         // 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 
101         SmartReplyView smartReplyView = SmartReplyView.inflate(context);
102 
103         List<Button> suggestionButtons = new ArrayList<>();
104         if (newSmartRepliesAndActions.smartReplies != null) {
105             suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
106                     newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
107                     delayOnClickListener));
108         }
109         if (newSmartRepliesAndActions.smartActions != null) {
110             suggestionButtons.addAll(
111                     smartReplyView.inflateSmartActions(newSmartRepliesAndActions.smartActions,
112                             smartReplyController, entry, headsUpManager,
113                             delayOnClickListener));
114         }
115 
116         return new InflatedSmartReplies(smartReplyView, suggestionButtons,
117                 newSmartRepliesAndActions);
118     }
119 
120     @VisibleForTesting
areSuggestionsSimilar( SmartRepliesAndActions left, SmartRepliesAndActions right)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());
132     }
133 
134     /**
135      * Returns whether we should show the smart reply view and its smart suggestions.
136      */
shouldShowSmartReplyView( NotificationEntry entry, SmartRepliesAndActions smartRepliesAndActions)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
chooseSmartRepliesAndActions( SmartReplyConstants smartReplyConstants, final NotificationEntry entry)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) {
227                 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                 }
236                 smartActions = new SmartReplyView.SmartActions(
237                         systemGeneratedActions, true /* fromAssistant */);
238             }
239         }
240         return new SmartRepliesAndActions(smartReplies, smartActions);
241     }
242 
243     /**
244      * Filter actions so that only actions pointing to whitelisted apps are allowed.
245      * This filtering is only meaningful when in lock-task mode.
246      */
filterWhiteListedLockTaskApps( List<Notification.Action> actions)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     /**
268      * 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      */
hasFreeformRemoteInput(NotificationEntry entry)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 
SmartRepliesAndActions( @ullable SmartReplyView.SmartReplies smartReplies, @Nullable SmartReplyView.SmartActions smartActions)284         SmartRepliesAndActions(
285                 @Nullable SmartReplyView.SmartReplies smartReplies,
286                 @Nullable SmartReplyView.SmartActions smartActions) {
287             this.smartReplies = smartReplies;
288             this.smartActions = smartActions;
289         }
290 
getSmartReplies()291         @NonNull public CharSequence[] getSmartReplies() {
292             return smartReplies == null ? new CharSequence[0] : smartReplies.choices;
293         }
294 
getSmartActions()295         @NonNull public List<Notification.Action> getSmartActions() {
296             return smartActions == null ? Collections.emptyList() : smartActions.actions;
297         }
298     }
299 }
300