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