blob: 41879855275ff81684213e3c419872950d2e9b2c [file] [log] [blame]
Mady Mellor7f234902019-10-20 12:06:29 -07001/*
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
17package com.android.systemui.bubbles;
18
Mady Mellor89eb3482019-11-13 17:32:01 -080019import static android.app.Notification.EXTRA_MESSAGES;
Mady Mellorb547e5e2019-12-02 10:15:56 -080020import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
21import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST;
22import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED;
23
Mady Mellorff076eb2019-11-13 10:12:06 -080024import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS;
25import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
26import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
Mady Mellorb547e5e2019-12-02 10:15:56 -080027
Mady Mellor7f234902019-10-20 12:06:29 -070028import android.app.Notification;
29import android.app.PendingIntent;
Mady Mellor89eb3482019-11-13 17:32:01 -080030import android.app.Person;
Mady Mellor7f234902019-10-20 12:06:29 -070031import android.content.Context;
Mady Mellorb547e5e2019-12-02 10:15:56 -080032import android.content.Intent;
33import android.content.pm.LauncherApps;
34import android.content.pm.ShortcutInfo;
Mady Mellor7f234902019-10-20 12:06:29 -070035import android.graphics.drawable.Icon;
Mady Mellor89eb3482019-11-13 17:32:01 -080036import android.os.Bundle;
37import android.os.Parcelable;
Mady Mellorb547e5e2019-12-02 10:15:56 -080038import android.os.UserHandle;
Mady Mellor7f234902019-10-20 12:06:29 -070039import android.provider.Settings;
Mady Mellorff076eb2019-11-13 10:12:06 -080040import android.util.Log;
Mady Mellor7f234902019-10-20 12:06:29 -070041
Mady Mellor89eb3482019-11-13 17:32:01 -080042import com.android.internal.util.ArrayUtils;
Mady Mellor7f234902019-10-20 12:06:29 -070043import com.android.systemui.statusbar.notification.collection.NotificationEntry;
44
Mady Mellor89eb3482019-11-13 17:32:01 -080045import java.util.ArrayList;
Mady Mellorb547e5e2019-12-02 10:15:56 -080046import java.util.Arrays;
47import java.util.List;
48
Mady Mellor7f234902019-10-20 12:06:29 -070049/**
50 * Common class for experiments controlled via secure settings.
51 */
52public class BubbleExperimentConfig {
Mady Mellorff076eb2019-11-13 10:12:06 -080053 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
Mady Mellor7f234902019-10-20 12:06:29 -070054
Mady Mellorb547e5e2019-12-02 10:15:56 -080055 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 Mellor7f234902019-10-20 12:06:29 -070060 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 Mellor6ad86282019-12-05 14:33:42 -080064 private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = true;
Mady Mellor7f234902019-10-20 12:06:29 -070065
Mady Mellorb547e5e2019-12-02 10:15:56 -080066 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 Mellorea13b232019-12-05 15:55:46 -080069 private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps";
70
Aran Inkaa4dfa72019-11-18 16:49:07 -050071 private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu";
72 private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false;
73
Mady Mellor7f234902019-10-20 12:06:29 -070074 /**
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 Mellorb547e5e2019-12-02 10:15:56 -080078 * <p>
79 * This does not produce a bubble, only adds the metadata based on the notification info.
Mady Mellor7f234902019-10-20 12:06:29 -070080 */
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 Mellorb547e5e2019-12-02 10:15:56 -080098 * 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 Mellorea13b232019-12-05 15:55:46 -0800111 * 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 Inkaa4dfa72019-11-18 16:49:07 -0500129 * 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 Mellor7f234902019-10-20 12:06:29 -0700139 * 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 Mellor30672942019-12-04 15:43:19 -0800142 *
143 * @return whether an adjustment was made.
Mady Mellor7f234902019-10-20 12:06:29 -0700144 */
Mady Mellor30672942019-12-04 15:43:19 -0800145 static boolean adjustForExperiments(Context context, NotificationEntry entry,
Mady Mellorff076eb2019-11-13 10:12:06 -0800146 boolean previouslyUserCreated) {
Mady Mellorb547e5e2019-12-02 10:15:56 -0800147 Notification.BubbleMetadata metadata = null;
148 boolean addedMetadata = false;
Mady Mellorea13b232019-12-05 15:55:46 -0800149 boolean whiteListedToAutoBubble =
150 isPackageWhitelistedToAutoBubble(context, entry.getSbn().getPackageName());
Mady Mellor7f234902019-10-20 12:06:29 -0700151
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 Mellorb547e5e2019-12-02 10:15:56 -0800158 boolean useShortcutInfo = useShortcutInfoToBubble(context);
159 String shortcutId = entry.getSbn().getNotification().getShortcutId();
160
Mady Mellorff076eb2019-11-13 10:12:06 -0800161 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 Mellorb547e5e2019-12-02 10:15:56 -0800174 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 Mellorff076eb2019-11-13 10:12:06 -0800183 boolean shouldBubble = entry.getBubbleMetadata() != null
184 || bubbleNotifForExperiment
185 || previouslyUserCreated;
Mady Mellorb547e5e2019-12-02 10:15:56 -0800186 if (shouldBubble && metadata != null) {
Mady Mellorff076eb2019-11-13 10:12:06 -0800187 if (DEBUG_EXPERIMENTS) {
188 Log.d(TAG, "Adding experimental shortcut bubble for: " + entry.getKey());
189 }
Mady Mellorb547e5e2019-12-02 10:15:56 -0800190 entry.setBubbleMetadata(metadata);
191 addedMetadata = true;
192 }
Mady Mellor7f234902019-10-20 12:06:29 -0700193 }
194
Mady Mellorb547e5e2019-12-02 10:15:56 -0800195 // Didn't get metadata from a shortcut & we're bubbling for experiment
Mady Mellorff076eb2019-11-13 10:12:06 -0800196 if (entry.getBubbleMetadata() == null
197 && (bubbleNotifForExperiment || previouslyUserCreated)) {
Mady Mellorb547e5e2019-12-02 10:15:56 -0800198 metadata = createFromNotif(context, entry);
199 if (metadata != null) {
Mady Mellorff076eb2019-11-13 10:12:06 -0800200 if (DEBUG_EXPERIMENTS) {
201 Log.d(TAG, "Adding experimental notification bubble for: " + entry.getKey());
202 }
Mady Mellorb547e5e2019-12-02 10:15:56 -0800203 entry.setBubbleMetadata(metadata);
204 addedMetadata = true;
205 }
206 }
207
Mady Mellorea13b232019-12-05 15:55:46 -0800208 boolean bubbleForWhitelist = whiteListedToAutoBubble && (addedMetadata || hasMetadata);
209 if ((previouslyUserCreated && addedMetadata) || bubbleForWhitelist) {
210 // Update to a previous bubble (or new autobubble), set its flag now.
Mady Mellorff076eb2019-11-13 10:12:06 -0800211 if (DEBUG_EXPERIMENTS) {
212 Log.d(TAG, "Setting FLAG_BUBBLE for: " + entry.getKey());
213 }
Mady Mellor7f234902019-10-20 12:06:29 -0700214 entry.setFlagBubble(true);
Mady Mellor30672942019-12-04 15:43:19 -0800215 return true;
Mady Mellor7f234902019-10-20 12:06:29 -0700216 }
Mady Mellor30672942019-12-04 15:43:19 -0800217 return addedMetadata;
Mady Mellor7f234902019-10-20 12:06:29 -0700218 }
Mady Mellorb547e5e2019-12-02 10:15:56 -0800219
220 static Notification.BubbleMetadata createFromNotif(Context context, NotificationEntry entry) {
221 Notification notification = entry.getSbn().getNotification();
222 final PendingIntent intent = notification.contentIntent;
Mady Mellor89eb3482019-11-13 17:32:01 -0800223 Icon icon = null;
224 // Use the icon of the person if available
225 List<Person> personList = getPeopleFromNotification(entry);
226 if (personList.size() > 0) {
Joshua Tsuji22f90a02019-12-11 14:41:10 -0500227 final Person person = personList.get(0);
228
229 if (person != null) {
230 icon = person.getIcon();
231 }
Mady Mellor89eb3482019-11-13 17:32:01 -0800232 }
233 if (icon == null) {
234 icon = notification.getLargeIcon() != null
235 ? notification.getLargeIcon()
236 : notification.getSmallIcon();
237 }
Mady Mellor30672942019-12-04 15:43:19 -0800238 if (intent != null) {
Mady Mellorb547e5e2019-12-02 10:15:56 -0800239 return new Notification.BubbleMetadata.Builder()
240 .setDesiredHeight(BUBBLE_HEIGHT)
Mady Mellor89eb3482019-11-13 17:32:01 -0800241 .setIcon(icon)
Mady Mellorb547e5e2019-12-02 10:15:56 -0800242 .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 Mellor89eb3482019-11-13 17:32:01 -0800288
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 Mellor7f234902019-10-20 12:06:29 -0700315}