Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 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.bubbles; |
| 18 | |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 19 | import static android.app.Notification.EXTRA_MESSAGES; |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 20 | import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; |
| 21 | import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; |
| 22 | import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; |
| 23 | |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 24 | import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS; |
| 25 | import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; |
| 26 | import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 27 | |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 28 | import android.app.Notification; |
| 29 | import android.app.PendingIntent; |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 30 | import android.app.Person; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 31 | import android.content.Context; |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 32 | import android.content.Intent; |
| 33 | import android.content.pm.LauncherApps; |
| 34 | import android.content.pm.ShortcutInfo; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 35 | import android.graphics.drawable.Icon; |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 36 | import android.os.Bundle; |
| 37 | import android.os.Parcelable; |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 38 | import android.os.UserHandle; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 39 | import android.provider.Settings; |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 40 | import android.util.Log; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 41 | |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 42 | import com.android.internal.util.ArrayUtils; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 43 | import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| 44 | |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 45 | import java.util.ArrayList; |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 46 | import java.util.Arrays; |
| 47 | import java.util.List; |
| 48 | |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 49 | /** |
| 50 | * Common class for experiments controlled via secure settings. |
| 51 | */ |
| 52 | public class BubbleExperimentConfig { |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 53 | private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 54 | |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 55 | private static final String SHORTCUT_DUMMY_INTENT = "bubble_experiment_shortcut_intent"; |
| 56 | private static PendingIntent sDummyShortcutIntent; |
| 57 | |
| 58 | private static final int BUBBLE_HEIGHT = 10000; |
| 59 | |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 60 | private static final String ALLOW_ANY_NOTIF_TO_BUBBLE = "allow_any_notif_to_bubble"; |
| 61 | private static final boolean ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT = false; |
| 62 | |
| 63 | private static final String ALLOW_MESSAGE_NOTIFS_TO_BUBBLE = "allow_message_notifs_to_bubble"; |
Mady Mellor | 6ad8628 | 2019-12-05 14:33:42 -0800 | [diff] [blame] | 64 | private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = true; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 65 | |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 66 | private static final String ALLOW_SHORTCUTS_TO_BUBBLE = "allow_shortcuts_to_bubble"; |
| 67 | private static final boolean ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT = false; |
| 68 | |
Mady Mellor | ea13b23 | 2019-12-05 15:55:46 -0800 | [diff] [blame] | 69 | private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps"; |
| 70 | |
Aran Ink | aa4dfa7 | 2019-11-18 16:49:07 -0500 | [diff] [blame] | 71 | private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu"; |
| 72 | private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false; |
| 73 | |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 74 | /** |
| 75 | * When true, if a notification has the information necessary to bubble (i.e. valid |
| 76 | * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata} |
| 77 | * object will be created by the system and added to the notification. |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 78 | * <p> |
| 79 | * This does not produce a bubble, only adds the metadata based on the notification info. |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 80 | */ |
| 81 | static boolean allowAnyNotifToBubble(Context context) { |
| 82 | return Settings.Secure.getInt(context.getContentResolver(), |
| 83 | ALLOW_ANY_NOTIF_TO_BUBBLE, |
| 84 | ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Same as {@link #allowAnyNotifToBubble(Context)} except it filters for notifications that |
| 89 | * are using {@link Notification.MessagingStyle} and have remote input. |
| 90 | */ |
| 91 | static boolean allowMessageNotifsToBubble(Context context) { |
| 92 | return Settings.Secure.getInt(context.getContentResolver(), |
| 93 | ALLOW_MESSAGE_NOTIFS_TO_BUBBLE, |
| 94 | ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; |
| 95 | } |
| 96 | |
| 97 | /** |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 98 | * When true, if the notification is able to bubble via {@link #allowAnyNotifToBubble(Context)} |
| 99 | * or {@link #allowMessageNotifsToBubble(Context)} or via normal BubbleMetadata, then a new |
| 100 | * BubbleMetadata object is constructed based on the shortcut info. |
| 101 | * <p> |
| 102 | * This does not produce a bubble, only adds the metadata based on shortcut info. |
| 103 | */ |
| 104 | static boolean useShortcutInfoToBubble(Context context) { |
| 105 | return Settings.Secure.getInt(context.getContentResolver(), |
| 106 | ALLOW_SHORTCUTS_TO_BUBBLE, |
| 107 | ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; |
| 108 | } |
| 109 | |
| 110 | /** |
Mady Mellor | ea13b23 | 2019-12-05 15:55:46 -0800 | [diff] [blame] | 111 | * Returns whether the provided package is whitelisted to bubble. |
| 112 | */ |
| 113 | static boolean isPackageWhitelistedToAutoBubble(Context context, String packageName) { |
| 114 | String unsplitList = Settings.Secure.getString(context.getContentResolver(), |
| 115 | WHITELISTED_AUTO_BUBBLE_APPS); |
| 116 | if (unsplitList != null) { |
| 117 | // We expect the list to be separated by commas and no white space (but we trim in case) |
| 118 | String[] packageList = unsplitList.split(","); |
| 119 | for (int i = 0; i < packageList.length; i++) { |
| 120 | if (packageList[i].trim().equals(packageName)) { |
| 121 | return true; |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | return false; |
| 126 | } |
| 127 | |
| 128 | /** |
Aran Ink | aa4dfa7 | 2019-11-18 16:49:07 -0500 | [diff] [blame] | 129 | * When true, show a menu when a bubble is long-pressed, which will allow the user to take |
| 130 | * actions on that bubble. |
| 131 | */ |
| 132 | static boolean allowBubbleScreenshotMenu(Context context) { |
| 133 | return Settings.Secure.getInt(context.getContentResolver(), |
| 134 | ALLOW_BUBBLE_MENU, |
| 135 | ALLOW_BUBBLE_MENU_DEFAULT ? 1 : 0) != 0; |
| 136 | } |
| 137 | |
| 138 | /** |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 139 | * If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds |
| 140 | * {@link android.app.Notification.BubbleMetadata} to the notification entry as long as |
| 141 | * the notification has necessary info for BubbleMetadata. |
Mady Mellor | 3067294 | 2019-12-04 15:43:19 -0800 | [diff] [blame] | 142 | * |
| 143 | * @return whether an adjustment was made. |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 144 | */ |
Mady Mellor | 3067294 | 2019-12-04 15:43:19 -0800 | [diff] [blame] | 145 | static boolean adjustForExperiments(Context context, NotificationEntry entry, |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 146 | boolean previouslyUserCreated) { |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 147 | Notification.BubbleMetadata metadata = null; |
| 148 | boolean addedMetadata = false; |
Mady Mellor | ea13b23 | 2019-12-05 15:55:46 -0800 | [diff] [blame] | 149 | boolean whiteListedToAutoBubble = |
| 150 | isPackageWhitelistedToAutoBubble(context, entry.getSbn().getPackageName()); |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 151 | |
| 152 | Notification notification = entry.getSbn().getNotification(); |
| 153 | boolean isMessage = Notification.MessagingStyle.class.equals( |
| 154 | notification.getNotificationStyle()); |
| 155 | boolean bubbleNotifForExperiment = (isMessage && allowMessageNotifsToBubble(context)) |
| 156 | || allowAnyNotifToBubble(context); |
| 157 | |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 158 | boolean useShortcutInfo = useShortcutInfoToBubble(context); |
| 159 | String shortcutId = entry.getSbn().getNotification().getShortcutId(); |
| 160 | |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 161 | boolean hasMetadata = entry.getBubbleMetadata() != null; |
| 162 | if ((!hasMetadata && (previouslyUserCreated || bubbleNotifForExperiment)) |
| 163 | || useShortcutInfo) { |
| 164 | if (DEBUG_EXPERIMENTS) { |
| 165 | Log.d(TAG, "Adjusting " + entry.getKey() + " for bubble experiment." |
| 166 | + " allowMessages=" + allowMessageNotifsToBubble(context) |
| 167 | + " isMessage=" + isMessage |
| 168 | + " allowNotifs=" + allowAnyNotifToBubble(context) |
| 169 | + " useShortcutInfo=" + useShortcutInfo |
| 170 | + " previouslyUserCreated=" + previouslyUserCreated); |
| 171 | } |
| 172 | } |
| 173 | |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 174 | if (useShortcutInfo && shortcutId != null) { |
| 175 | // We don't actually get anything useful from ShortcutInfo so just check existence |
| 176 | ShortcutInfo info = getShortcutInfo(context, entry.getSbn().getPackageName(), |
| 177 | entry.getSbn().getUser(), shortcutId); |
| 178 | if (info != null) { |
| 179 | metadata = createForShortcut(context, entry); |
| 180 | } |
| 181 | |
| 182 | // Replace existing metadata with shortcut, or we're bubbling for experiment |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 183 | boolean shouldBubble = entry.getBubbleMetadata() != null |
| 184 | || bubbleNotifForExperiment |
| 185 | || previouslyUserCreated; |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 186 | if (shouldBubble && metadata != null) { |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 187 | if (DEBUG_EXPERIMENTS) { |
| 188 | Log.d(TAG, "Adding experimental shortcut bubble for: " + entry.getKey()); |
| 189 | } |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 190 | entry.setBubbleMetadata(metadata); |
| 191 | addedMetadata = true; |
| 192 | } |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 193 | } |
| 194 | |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 195 | // Didn't get metadata from a shortcut & we're bubbling for experiment |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 196 | if (entry.getBubbleMetadata() == null |
| 197 | && (bubbleNotifForExperiment || previouslyUserCreated)) { |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 198 | metadata = createFromNotif(context, entry); |
| 199 | if (metadata != null) { |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 200 | if (DEBUG_EXPERIMENTS) { |
| 201 | Log.d(TAG, "Adding experimental notification bubble for: " + entry.getKey()); |
| 202 | } |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 203 | entry.setBubbleMetadata(metadata); |
| 204 | addedMetadata = true; |
| 205 | } |
| 206 | } |
| 207 | |
Mady Mellor | ea13b23 | 2019-12-05 15:55:46 -0800 | [diff] [blame] | 208 | boolean bubbleForWhitelist = whiteListedToAutoBubble && (addedMetadata || hasMetadata); |
| 209 | if ((previouslyUserCreated && addedMetadata) || bubbleForWhitelist) { |
| 210 | // Update to a previous bubble (or new autobubble), set its flag now. |
Mady Mellor | ff076eb | 2019-11-13 10:12:06 -0800 | [diff] [blame] | 211 | if (DEBUG_EXPERIMENTS) { |
| 212 | Log.d(TAG, "Setting FLAG_BUBBLE for: " + entry.getKey()); |
| 213 | } |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 214 | entry.setFlagBubble(true); |
Mady Mellor | 3067294 | 2019-12-04 15:43:19 -0800 | [diff] [blame] | 215 | return true; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 216 | } |
Mady Mellor | 3067294 | 2019-12-04 15:43:19 -0800 | [diff] [blame] | 217 | return addedMetadata; |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 218 | } |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 219 | |
| 220 | static Notification.BubbleMetadata createFromNotif(Context context, NotificationEntry entry) { |
| 221 | Notification notification = entry.getSbn().getNotification(); |
| 222 | final PendingIntent intent = notification.contentIntent; |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 223 | Icon icon = null; |
| 224 | // Use the icon of the person if available |
| 225 | List<Person> personList = getPeopleFromNotification(entry); |
| 226 | if (personList.size() > 0) { |
Joshua Tsuji | 22f90a0 | 2019-12-11 14:41:10 -0500 | [diff] [blame] | 227 | final Person person = personList.get(0); |
| 228 | |
| 229 | if (person != null) { |
| 230 | icon = person.getIcon(); |
| 231 | } |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 232 | } |
| 233 | if (icon == null) { |
| 234 | icon = notification.getLargeIcon() != null |
| 235 | ? notification.getLargeIcon() |
| 236 | : notification.getSmallIcon(); |
| 237 | } |
Mady Mellor | 3067294 | 2019-12-04 15:43:19 -0800 | [diff] [blame] | 238 | if (intent != null) { |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 239 | return new Notification.BubbleMetadata.Builder() |
| 240 | .setDesiredHeight(BUBBLE_HEIGHT) |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 241 | .setIcon(icon) |
Mady Mellor | b547e5e | 2019-12-02 10:15:56 -0800 | [diff] [blame] | 242 | .setIntent(intent) |
| 243 | .build(); |
| 244 | } |
| 245 | return null; |
| 246 | } |
| 247 | |
| 248 | static Notification.BubbleMetadata createForShortcut(Context context, NotificationEntry entry) { |
| 249 | // ShortcutInfo does not return an icon, instead a Drawable, lets just use |
| 250 | // notification icon for BubbleMetadata. |
| 251 | Icon icon = entry.getSbn().getNotification().getSmallIcon(); |
| 252 | |
| 253 | // ShortcutInfo does not return the intent, lets make a fake but identifiable |
| 254 | // intent so we can still add bubbleMetadata |
| 255 | if (sDummyShortcutIntent == null) { |
| 256 | Intent i = new Intent(SHORTCUT_DUMMY_INTENT); |
| 257 | sDummyShortcutIntent = PendingIntent.getActivity(context, 0, i, |
| 258 | PendingIntent.FLAG_UPDATE_CURRENT); |
| 259 | } |
| 260 | return new Notification.BubbleMetadata.Builder() |
| 261 | .setDesiredHeight(BUBBLE_HEIGHT) |
| 262 | .setIcon(icon) |
| 263 | .setIntent(sDummyShortcutIntent) |
| 264 | .build(); |
| 265 | } |
| 266 | |
| 267 | static ShortcutInfo getShortcutInfo(Context context, String packageName, UserHandle user, |
| 268 | String shortcutId) { |
| 269 | LauncherApps launcherAppService = |
| 270 | (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); |
| 271 | LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery(); |
| 272 | if (packageName != null) { |
| 273 | query.setPackage(packageName); |
| 274 | } |
| 275 | if (shortcutId != null) { |
| 276 | query.setShortcutIds(Arrays.asList(shortcutId)); |
| 277 | } |
| 278 | query.setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST); |
| 279 | List<ShortcutInfo> shortcuts = launcherAppService.getShortcuts(query, user); |
| 280 | return shortcuts != null && shortcuts.size() > 0 |
| 281 | ? shortcuts.get(0) |
| 282 | : null; |
| 283 | } |
| 284 | |
| 285 | static boolean isShortcutIntent(PendingIntent intent) { |
| 286 | return intent.equals(sDummyShortcutIntent); |
| 287 | } |
Mady Mellor | 89eb348 | 2019-11-13 17:32:01 -0800 | [diff] [blame] | 288 | |
| 289 | static List<Person> getPeopleFromNotification(NotificationEntry entry) { |
| 290 | Bundle extras = entry.getSbn().getNotification().extras; |
| 291 | ArrayList<Person> personList = new ArrayList<>(); |
| 292 | if (extras == null) { |
| 293 | return personList; |
| 294 | } |
| 295 | |
| 296 | List<Person> p = extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST); |
| 297 | |
| 298 | if (p != null) { |
| 299 | personList.addAll(p); |
| 300 | } |
| 301 | |
| 302 | if (Notification.MessagingStyle.class.equals( |
| 303 | entry.getSbn().getNotification().getNotificationStyle())) { |
| 304 | final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES); |
| 305 | if (!ArrayUtils.isEmpty(messages)) { |
| 306 | for (Notification.MessagingStyle.Message message : |
| 307 | Notification.MessagingStyle.Message |
| 308 | .getMessagesFromBundleArray(messages)) { |
| 309 | personList.add(message.getSenderPerson()); |
| 310 | } |
| 311 | } |
| 312 | } |
| 313 | return personList; |
| 314 | } |
Mady Mellor | 7f23490 | 2019-10-20 12:06:29 -0700 | [diff] [blame] | 315 | } |