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