| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.bubbles; |
| |
| import static android.app.Notification.EXTRA_MESSAGES; |
| import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; |
| import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; |
| import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; |
| |
| import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS; |
| import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; |
| import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; |
| |
| import android.app.Notification; |
| import android.app.PendingIntent; |
| import android.app.Person; |
| import android.content.Context; |
| import android.content.pm.LauncherApps; |
| import android.content.pm.ShortcutInfo; |
| import android.graphics.Color; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.Icon; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.internal.graphics.ColorUtils; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.ContrastColorUtil; |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| import com.android.systemui.statusbar.notification.people.PeopleHubNotificationListenerKt; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Common class for experiments controlled via secure settings. |
| */ |
| public class BubbleExperimentConfig { |
| private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; |
| |
| private static final String SHORTCUT_DUMMY_INTENT = "bubble_experiment_shortcut_intent"; |
| private static PendingIntent sDummyShortcutIntent; |
| |
| private static final int BUBBLE_HEIGHT = 10000; |
| |
| private static final String ALLOW_ANY_NOTIF_TO_BUBBLE = "allow_any_notif_to_bubble"; |
| private static final boolean ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT = false; |
| |
| private static final String ALLOW_MESSAGE_NOTIFS_TO_BUBBLE = "allow_message_notifs_to_bubble"; |
| private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = true; |
| |
| private static final String ALLOW_SHORTCUTS_TO_BUBBLE = "allow_shortcuts_to_bubble"; |
| private static final boolean ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT = false; |
| |
| private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps"; |
| |
| private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu"; |
| private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false; |
| |
| private static final String ALLOW_BUBBLE_OVERFLOW = "allow_bubble_overflow"; |
| private static final boolean ALLOW_BUBBLE_OVERFLOW_DEFAULT = false; |
| |
| /** |
| * When true, if a notification has the information necessary to bubble (i.e. valid |
| * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata} |
| * object will be created by the system and added to the notification. |
| * <p> |
| * This does not produce a bubble, only adds the metadata based on the notification info. |
| */ |
| static boolean allowAnyNotifToBubble(Context context) { |
| return Settings.Secure.getInt(context.getContentResolver(), |
| ALLOW_ANY_NOTIF_TO_BUBBLE, |
| ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; |
| } |
| |
| /** |
| * Same as {@link #allowAnyNotifToBubble(Context)} except it filters for notifications that |
| * are using {@link Notification.MessagingStyle} and have remote input. |
| */ |
| static boolean allowMessageNotifsToBubble(Context context) { |
| return Settings.Secure.getInt(context.getContentResolver(), |
| ALLOW_MESSAGE_NOTIFS_TO_BUBBLE, |
| ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; |
| } |
| |
| /** |
| * When true, if the notification is able to bubble via {@link #allowAnyNotifToBubble(Context)} |
| * or {@link #allowMessageNotifsToBubble(Context)} or via normal BubbleMetadata, then a new |
| * BubbleMetadata object is constructed based on the shortcut info. |
| * <p> |
| * This does not produce a bubble, only adds the metadata based on shortcut info. |
| */ |
| static boolean useShortcutInfoToBubble(Context context) { |
| return Settings.Secure.getInt(context.getContentResolver(), |
| ALLOW_SHORTCUTS_TO_BUBBLE, |
| ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT ? 1 : 0) != 0; |
| } |
| |
| /** |
| * Returns whether the provided package is whitelisted to bubble. |
| */ |
| static boolean isPackageWhitelistedToAutoBubble(Context context, String packageName) { |
| String unsplitList = Settings.Secure.getString(context.getContentResolver(), |
| WHITELISTED_AUTO_BUBBLE_APPS); |
| if (unsplitList != null) { |
| // We expect the list to be separated by commas and no white space (but we trim in case) |
| String[] packageList = unsplitList.split(","); |
| for (int i = 0; i < packageList.length; i++) { |
| if (packageList[i].trim().equals(packageName)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * When true, show a menu when a bubble is long-pressed, which will allow the user to take |
| * actions on that bubble. |
| */ |
| static boolean allowBubbleScreenshotMenu(Context context) { |
| return Settings.Secure.getInt(context.getContentResolver(), |
| ALLOW_BUBBLE_MENU, |
| ALLOW_BUBBLE_MENU_DEFAULT ? 1 : 0) != 0; |
| } |
| |
| /** |
| * When true, show a menu when a bubble is long-pressed, which will allow the user to take |
| * actions on that bubble. |
| */ |
| static boolean allowBubbleOverflow(Context context) { |
| return Settings.Secure.getInt(context.getContentResolver(), |
| ALLOW_BUBBLE_OVERFLOW, |
| ALLOW_BUBBLE_OVERFLOW_DEFAULT ? 1 : 0) != 0; |
| } |
| |
| /** |
| * If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds |
| * {@link android.app.Notification.BubbleMetadata} to the notification entry as long as |
| * the notification has necessary info for BubbleMetadata. |
| * |
| * @return whether an adjustment was made. |
| */ |
| static boolean adjustForExperiments(Context context, NotificationEntry entry, |
| boolean previouslyUserCreated, boolean userBlocked) { |
| Notification.BubbleMetadata metadata = null; |
| boolean addedMetadata = false; |
| boolean whiteListedToAutoBubble = |
| isPackageWhitelistedToAutoBubble(context, entry.getSbn().getPackageName()); |
| |
| Notification notification = entry.getSbn().getNotification(); |
| boolean isMessage = Notification.MessagingStyle.class.equals( |
| notification.getNotificationStyle()); |
| boolean bubbleNotifForExperiment = (isMessage && allowMessageNotifsToBubble(context)) |
| || allowAnyNotifToBubble(context); |
| |
| boolean useShortcutInfo = useShortcutInfoToBubble(context); |
| String shortcutId = entry.getSbn().getNotification().getShortcutId(); |
| |
| boolean hasMetadata = entry.getBubbleMetadata() != null; |
| if ((!hasMetadata && (previouslyUserCreated || bubbleNotifForExperiment)) |
| || useShortcutInfo) { |
| if (DEBUG_EXPERIMENTS) { |
| Log.d(TAG, "Adjusting " + entry.getKey() + " for bubble experiment." |
| + " allowMessages=" + allowMessageNotifsToBubble(context) |
| + " isMessage=" + isMessage |
| + " allowNotifs=" + allowAnyNotifToBubble(context) |
| + " useShortcutInfo=" + useShortcutInfo |
| + " previouslyUserCreated=" + previouslyUserCreated); |
| } |
| } |
| |
| if (useShortcutInfo && shortcutId != null) { |
| // We don't actually get anything useful from ShortcutInfo so just check existence |
| ShortcutInfo info = getShortcutInfo(context, entry.getSbn().getPackageName(), |
| entry.getSbn().getUser(), shortcutId); |
| if (info != null) { |
| metadata = createForShortcut(shortcutId); |
| } |
| |
| // Replace existing metadata with shortcut, or we're bubbling for experiment |
| boolean shouldBubble = entry.getBubbleMetadata() != null |
| || bubbleNotifForExperiment |
| || previouslyUserCreated; |
| if (shouldBubble && metadata != null) { |
| if (DEBUG_EXPERIMENTS) { |
| Log.d(TAG, "Adding experimental shortcut bubble for: " + entry.getKey()); |
| } |
| entry.setBubbleMetadata(metadata); |
| addedMetadata = true; |
| } |
| } |
| |
| // Didn't get metadata from a shortcut & we're bubbling for experiment |
| if (entry.getBubbleMetadata() == null |
| && (bubbleNotifForExperiment || previouslyUserCreated)) { |
| metadata = createFromNotif(context, entry); |
| if (metadata != null) { |
| if (DEBUG_EXPERIMENTS) { |
| Log.d(TAG, "Adding experimental notification bubble for: " + entry.getKey()); |
| } |
| entry.setBubbleMetadata(metadata); |
| addedMetadata = true; |
| } |
| } |
| |
| boolean bubbleForWhitelist = !userBlocked |
| && whiteListedToAutoBubble |
| && (addedMetadata || hasMetadata); |
| if ((previouslyUserCreated && addedMetadata) || bubbleForWhitelist) { |
| // Update to a previous bubble (or new autobubble), set its flag now. |
| if (DEBUG_EXPERIMENTS) { |
| Log.d(TAG, "Setting FLAG_BUBBLE for: " + entry.getKey()); |
| } |
| entry.setFlagBubble(true); |
| return true; |
| } |
| return addedMetadata; |
| } |
| |
| static Notification.BubbleMetadata createFromNotif(Context context, NotificationEntry entry) { |
| Notification notification = entry.getSbn().getNotification(); |
| final PendingIntent intent = notification.contentIntent; |
| Icon icon = null; |
| // Use the icon of the person if available |
| List<Person> personList = getPeopleFromNotification(entry); |
| if (personList.size() > 0) { |
| final Person person = personList.get(0); |
| if (person != null) { |
| icon = person.getIcon(); |
| if (icon == null) { |
| // Lets try and grab the icon constructed by the layout |
| Drawable d = PeopleHubNotificationListenerKt.extractAvatarFromRow(entry); |
| if (d instanceof BitmapDrawable) { |
| icon = Icon.createWithBitmap(((BitmapDrawable) d).getBitmap()); |
| } |
| } |
| } |
| } |
| if (icon == null) { |
| boolean shouldTint = notification.getLargeIcon() == null; |
| icon = shouldTint |
| ? notification.getSmallIcon() |
| : notification.getLargeIcon(); |
| if (shouldTint) { |
| int notifColor = entry.getSbn().getNotification().color; |
| notifColor = ColorUtils.setAlphaComponent(notifColor, 255); |
| notifColor = ContrastColorUtil.findContrastColor(notifColor, Color.WHITE, |
| true /* findFg */, 3f); |
| icon.setTint(notifColor); |
| } |
| } |
| if (intent != null) { |
| return new Notification.BubbleMetadata.Builder() |
| .createIntentBubble(intent, icon) |
| .setDesiredHeight(BUBBLE_HEIGHT) |
| .build(); |
| } |
| return null; |
| } |
| |
| static Notification.BubbleMetadata createForShortcut(String shortcutId) { |
| return new Notification.BubbleMetadata.Builder() |
| .setDesiredHeight(BUBBLE_HEIGHT) |
| .createShortcutBubble(shortcutId) |
| .build(); |
| } |
| |
| static ShortcutInfo getShortcutInfo(Context context, String packageName, UserHandle user, |
| String shortcutId) { |
| LauncherApps launcherAppService = |
| (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE); |
| LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery(); |
| if (packageName != null) { |
| query.setPackage(packageName); |
| } |
| if (shortcutId != null) { |
| query.setShortcutIds(Arrays.asList(shortcutId)); |
| } |
| query.setQueryFlags(FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST); |
| List<ShortcutInfo> shortcuts = launcherAppService.getShortcuts(query, user); |
| return shortcuts != null && shortcuts.size() > 0 |
| ? shortcuts.get(0) |
| : null; |
| } |
| |
| static List<Person> getPeopleFromNotification(NotificationEntry entry) { |
| Bundle extras = entry.getSbn().getNotification().extras; |
| ArrayList<Person> personList = new ArrayList<>(); |
| if (extras == null) { |
| return personList; |
| } |
| |
| List<Person> p = extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST); |
| |
| if (p != null) { |
| personList.addAll(p); |
| } |
| |
| if (Notification.MessagingStyle.class.equals( |
| entry.getSbn().getNotification().getNotificationStyle())) { |
| final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES); |
| if (!ArrayUtils.isEmpty(messages)) { |
| for (Notification.MessagingStyle.Message message : |
| Notification.MessagingStyle.Message |
| .getMessagesFromBundleArray(messages)) { |
| personList.add(message.getSenderPerson()); |
| } |
| } |
| } |
| return personList; |
| } |
| } |