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