blob: eb826e54d340f91d7ca95c85c303ed0714a68d9c [file] [log] [blame]
Mady Mellor3dff9e62019-02-05 18:12:53 -08001/*
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 */
16package com.android.systemui.bubbles;
17
Mark Renouf71a3af62019-04-08 15:02:54 -040018import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
Issei Suzukia8d07312019-06-07 12:56:19 +020019import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
20import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
21import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
Mark Renouf71a3af62019-04-08 15:02:54 -040022
Mark Renouf9ba6cea2019-04-17 11:53:50 -040023import static java.util.stream.Collectors.toList;
24
Mark Renouf71a3af62019-04-08 15:02:54 -040025import android.app.Notification;
26import android.app.PendingIntent;
27import android.content.Context;
Mark Renoufbbcf07f2019-05-09 10:42:43 -040028import android.service.notification.NotificationListenerService;
29import android.service.notification.NotificationListenerService.RankingMap;
Mark Renouf71a3af62019-04-08 15:02:54 -040030import android.util.Log;
Mark Renoufba5ab512019-05-02 15:21:01 -040031import android.util.Pair;
Mady Mellor3dff9e62019-02-05 18:12:53 -080032
Mark Renouf9ba6cea2019-04-17 11:53:50 -040033import androidx.annotation.Nullable;
34
Selim Cinekfdf80332019-03-07 17:29:55 -080035import com.android.internal.annotations.VisibleForTesting;
Mark Renouf71a3af62019-04-08 15:02:54 -040036import com.android.systemui.bubbles.BubbleController.DismissReason;
Mady Mellor3dff9e62019-02-05 18:12:53 -080037import com.android.systemui.statusbar.notification.collection.NotificationEntry;
38
Mady Mellor70cba7bb2019-07-02 15:06:07 -070039import java.io.FileDescriptor;
40import java.io.PrintWriter;
Mark Renouf71a3af62019-04-08 15:02:54 -040041import java.util.ArrayList;
Mark Renouf71a3af62019-04-08 15:02:54 -040042import java.util.Collections;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040043import java.util.Comparator;
44import java.util.HashMap;
Mark Renouf3bc5b362019-04-05 14:37:59 -040045import java.util.List;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040046import java.util.Map;
Mark Renouf71a3af62019-04-08 15:02:54 -040047import java.util.Objects;
Mady Mellor3dff9e62019-02-05 18:12:53 -080048
Mady Mellorcfd06c12019-02-13 14:32:12 -080049import javax.inject.Inject;
50import javax.inject.Singleton;
51
Mady Mellor3dff9e62019-02-05 18:12:53 -080052/**
53 * Keeps track of active bubbles.
54 */
Mady Mellorcfd06c12019-02-13 14:32:12 -080055@Singleton
Selim Cinekfdf80332019-03-07 17:29:55 -080056public class BubbleData {
Mady Mellor3dff9e62019-02-05 18:12:53 -080057
Issei Suzukia8d07312019-06-07 12:56:19 +020058 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040059
60 private static final int MAX_BUBBLES = 5;
61
Mark Renoufba5ab512019-05-02 15:21:01 -040062 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
63 Comparator.comparing(BubbleData::sortKey).reversed();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040064
Mark Renoufba5ab512019-05-02 15:21:01 -040065 private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING =
Mark Renouf9ba6cea2019-04-17 11:53:50 -040066 Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
Mark Renouf71a3af62019-04-08 15:02:54 -040067
Mark Renouf82a40e62019-05-23 16:16:24 -040068 /** Contains information about changes that have been made to the state of bubbles. */
69 static final class Update {
70 boolean expandedChanged;
71 boolean selectionChanged;
72 boolean orderChanged;
73 boolean expanded;
74 @Nullable Bubble selectedBubble;
75 @Nullable Bubble addedBubble;
76 @Nullable Bubble updatedBubble;
77 // Pair with Bubble and @DismissReason Integer
78 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
79
80 // A read-only view of the bubbles list, changes there will be reflected here.
81 final List<Bubble> bubbles;
82
83 private Update(List<Bubble> bubbleOrder) {
84 bubbles = Collections.unmodifiableList(bubbleOrder);
85 }
86
87 boolean anythingChanged() {
88 return expandedChanged
89 || selectionChanged
90 || addedBubble != null
91 || updatedBubble != null
92 || !removedBubbles.isEmpty()
93 || orderChanged;
94 }
95
96 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
97 removedBubbles.add(new Pair<>(bubbleToRemove, reason));
98 }
99 }
100
Mark Renouf3bc5b362019-04-05 14:37:59 -0400101 /**
102 * This interface reports changes to the state and appearance of bubbles which should be applied
103 * as necessary to the UI.
Mark Renouf3bc5b362019-04-05 14:37:59 -0400104 */
105 interface Listener {
Mark Renouf82a40e62019-05-23 16:16:24 -0400106 /** Reports changes have have occurred as a result of the most recent operation. */
107 void applyUpdate(Update update);
Mark Renouf3bc5b362019-04-05 14:37:59 -0400108 }
109
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400110 interface TimeSource {
111 long currentTimeMillis();
112 }
113
Mark Renouf71a3af62019-04-08 15:02:54 -0400114 private final Context mContext;
Mark Renouf82a40e62019-05-23 16:16:24 -0400115 private final List<Bubble> mBubbles;
Mark Renouf71a3af62019-04-08 15:02:54 -0400116 private Bubble mSelectedBubble;
117 private boolean mExpanded;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400118
Mark Renoufba5ab512019-05-02 15:21:01 -0400119 // State tracked during an operation -- keeps track of what listener events to dispatch.
Mark Renouf82a40e62019-05-23 16:16:24 -0400120 private Update mStateChange;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400121
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400122 private NotificationListenerService.Ranking mTmpRanking;
123
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400124 private TimeSource mTimeSource = System::currentTimeMillis;
125
126 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400127 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800128
Mady Mellore28fe102019-07-09 15:33:32 -0700129 /**
130 * We track groups with summaries that aren't visibly displayed but still kept around because
131 * the bubble(s) associated with the summary still exist.
132 *
133 * The summary must be kept around so that developers can cancel it (and hence the bubbles
134 * associated with it). This list is used to check if the summary should be hidden from the
135 * shade.
136 *
137 * Key: group key of the NotificationEntry
138 * Value: key of the NotificationEntry
139 */
140 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
141
Mady Mellorcfd06c12019-02-13 14:32:12 -0800142 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400143 public BubbleData(Context context) {
144 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400145 mBubbles = new ArrayList<>();
Mark Renouf82a40e62019-05-23 16:16:24 -0400146 mStateChange = new Update(mBubbles);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800147 }
148
Mark Renouf71a3af62019-04-08 15:02:54 -0400149 public boolean hasBubbles() {
150 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800151 }
152
Mark Renouf71a3af62019-04-08 15:02:54 -0400153 public boolean isExpanded() {
154 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800155 }
156
Mark Renouf71a3af62019-04-08 15:02:54 -0400157 public boolean hasBubbleWithKey(String key) {
158 return getBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800159 }
160
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400161 @Nullable
162 public Bubble getSelectedBubble() {
163 return mSelectedBubble;
164 }
165
Mark Renouf71a3af62019-04-08 15:02:54 -0400166 public void setExpanded(boolean expanded) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200167 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400168 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800169 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400170 setExpandedInternal(expanded);
171 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800172 }
173
Mark Renouf71a3af62019-04-08 15:02:54 -0400174 public void setSelectedBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200175 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400176 Log.d(TAG, "setSelectedBubble: " + bubble);
177 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400178 setSelectedBubbleInternal(bubble);
179 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400180 }
181
Mark Renoufc19b4732019-06-26 12:08:33 -0400182 void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200183 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400184 Log.d(TAG, "notificationEntryUpdated: " + entry);
185 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400186 Bubble bubble = getBubbleWithKey(entry.key);
187 if (bubble == null) {
188 // Create a new bubble
Issei Suzukia91f3962019-06-07 11:48:23 +0200189 bubble = new Bubble(mContext, entry);
Mark Renoufc19b4732019-06-26 12:08:33 -0400190 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400191 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400192 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400193 } else {
194 // Updates an existing bubble
Mady Mellorce23c462019-06-17 17:30:07 -0700195 bubble.updateEntry(entry);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400196 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400197 }
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700198 if (bubble.shouldAutoExpand()) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400199 setSelectedBubbleInternal(bubble);
200 if (!mExpanded) {
201 setExpandedInternal(true);
202 }
203 } else if (mSelectedBubble == null) {
204 setSelectedBubbleInternal(bubble);
205 }
Mady Melloraea895f02019-07-10 14:37:48 -0700206 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
207 bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected);
208 bubble.setShowBubbleDot(!isBubbleExpandedAndSelected);
Mark Renoufba5ab512019-05-02 15:21:01 -0400209 dispatchPendingChanges();
210 }
211
212 public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200213 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400214 Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
215 }
216 doRemove(entry.key, reason);
217 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400218 }
219
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400220 /**
221 * Called when NotificationListener has received adjusted notification rank and reapplied
222 * filtering and sorting. This is used to dismiss any bubbles which should no longer be shown
223 * due to changes in permissions on the notification channel or the global setting.
224 *
225 * @param rankingMap the updated ranking map from NotificationListenerService
226 */
227 public void notificationRankingUpdated(RankingMap rankingMap) {
228 if (mTmpRanking == null) {
229 mTmpRanking = new NotificationListenerService.Ranking();
230 }
231
232 String[] orderedKeys = rankingMap.getOrderedKeys();
233 for (int i = 0; i < orderedKeys.length; i++) {
234 String key = orderedKeys[i];
235 if (hasBubbleWithKey(key)) {
236 rankingMap.getRanking(key, mTmpRanking);
237 if (!mTmpRanking.canBubble()) {
238 doRemove(key, BubbleController.DISMISS_BLOCKED);
239 }
240 }
241 }
242 dispatchPendingChanges();
243 }
244
Mady Mellor22f2f072019-04-18 13:26:18 -0700245 /**
Mady Mellore28fe102019-07-09 15:33:32 -0700246 * Adds a group key indicating that the summary for this group should be suppressed.
247 *
248 * @param groupKey the group key of the group whose summary should be suppressed.
249 * @param notifKey the notification entry key of that summary.
250 */
251 void addSummaryToSuppress(String groupKey, String notifKey) {
252 mSuppressedGroupKeys.put(groupKey, notifKey);
253 }
254
255 /**
256 * Retrieves the notif entry key of the summary associated with the provided group key.
257 *
258 * @param groupKey the group to look up
259 * @return the key for the {@link NotificationEntry} that is the summary of this group.
260 */
261 String getSummaryKey(String groupKey) {
262 return mSuppressedGroupKeys.get(groupKey);
263 }
264
265 /**
266 * Removes a group key indicating that summary for this group should no longer be suppressed.
267 */
268 void removeSuppressedSummary(String groupKey) {
269 mSuppressedGroupKeys.remove(groupKey);
270 }
271
272 /**
273 * Whether the summary for the provided group key is suppressed.
274 */
275 boolean isSummarySuppressed(String groupKey) {
276 return mSuppressedGroupKeys.containsKey(groupKey);
277 }
278
279 /**
Mady Mellor22f2f072019-04-18 13:26:18 -0700280 * Retrieves any bubbles that are part of the notification group represented by the provided
281 * group key.
282 */
283 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
284 ArrayList<Bubble> bubbleChildren = new ArrayList<>();
285 if (groupKey == null) {
286 return bubbleChildren;
287 }
288 for (Bubble b : mBubbles) {
289 if (groupKey.equals(b.getEntry().notification.getGroupKey())) {
290 bubbleChildren.add(b);
291 }
292 }
293 return bubbleChildren;
294 }
295
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400296 private void doAdd(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200297 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400298 Log.d(TAG, "doAdd: " + bubble);
299 }
300 int minInsertPoint = 0;
301 boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId());
302 if (isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400303 // first bubble of a group goes to the beginning, otherwise within the existing group
304 minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400305 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400306 if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400307 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400308 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400309 mStateChange.addedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400310 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400311 mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
Mark Renoufba5ab512019-05-02 15:21:01 -0400312 // Top bubble becomes selected.
313 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400314 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400315 }
316
317 private void trim() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400318 if (mBubbles.size() > MAX_BUBBLES) {
319 mBubbles.stream()
320 // sort oldest first (ascending lastActivity)
321 .sorted(Comparator.comparingLong(Bubble::getLastActivity))
322 // skip the selected bubble
323 .filter((b) -> !b.equals(mSelectedBubble))
324 .findFirst()
Mark Renoufba5ab512019-05-02 15:21:01 -0400325 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400326 }
327 }
328
329 private void doUpdate(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200330 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400331 Log.d(TAG, "doUpdate: " + bubble);
332 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400333 mStateChange.updatedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400334 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400335 // while collapsed, update causes re-pack
336 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400337 mBubbles.remove(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400338 int newPos = insertBubble(0, bubble);
339 if (prevPos != newPos) {
340 packGroup(newPos);
Mark Renouf82a40e62019-05-23 16:16:24 -0400341 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400342 }
343 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400344 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400345 }
346
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400347 private void doRemove(String key, @DismissReason int reason) {
348 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400349 if (indexToRemove == -1) {
350 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400351 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400352 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
353 if (mBubbles.size() == 1) {
354 // Going to become empty, handle specially.
355 setExpandedInternal(false);
356 setSelectedBubbleInternal(null);
357 }
358 if (indexToRemove < mBubbles.size() - 1) {
359 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400360 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400361 }
362 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400363 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400364 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400365 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400366 }
367
368 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
369 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
370 // Move selection to the new bubble at the same position.
371 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
372 Bubble newSelected = mBubbles.get(newIndex);
373 setSelectedBubbleInternal(newSelected);
374 }
Mady Mellored99c272019-06-13 15:58:30 -0700375 maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
Mark Renouf71a3af62019-04-08 15:02:54 -0400376 }
377
378 public void dismissAll(@DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200379 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400380 Log.d(TAG, "dismissAll: reason=" + reason);
381 }
382 if (mBubbles.isEmpty()) {
383 return;
384 }
385 setExpandedInternal(false);
386 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400387 while (!mBubbles.isEmpty()) {
388 Bubble bubble = mBubbles.remove(0);
Mady Mellored99c272019-06-13 15:58:30 -0700389 maybeSendDeleteIntent(reason, bubble.getEntry());
Mark Renouf82a40e62019-05-23 16:16:24 -0400390 mStateChange.bubbleRemoved(bubble, reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400391 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400392 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400393 }
394
Mady Mellorca184aae2019-09-17 16:07:12 -0700395 /**
396 * Indicates that the provided display is no longer in use and should be cleaned up.
397 *
398 * @param displayId the id of the display to clean up.
399 */
400 void notifyDisplayEmpty(int displayId) {
401 for (Bubble b : mBubbles) {
402 if (b.getDisplayId() == displayId) {
403 if (b.getExpandedView() != null) {
404 b.getExpandedView().notifyDisplayEmpty();
405 }
406 return;
407 }
408 }
409 }
410
Mark Renoufba5ab512019-05-02 15:21:01 -0400411 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400412 if (mListener != null && mStateChange.anythingChanged()) {
413 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400414 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400415 mStateChange = new Update(mBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400416 }
417
418 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400419 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400420 *
421 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400422 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400423 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200424 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400425 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
426 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400427 if (Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400428 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400429 }
430 if (bubble != null && !mBubbles.contains(bubble)) {
431 Log.e(TAG, "Cannot select bubble which doesn't exist!"
432 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400433 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400434 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400435 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700436 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400437 }
438 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400439 mStateChange.selectedBubble = bubble;
440 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400441 }
442
Mark Renouf71a3af62019-04-08 15:02:54 -0400443 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400444 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400445 *
446 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400447 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400448 private void setExpandedInternal(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200449 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400450 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
451 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400452 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400453 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400454 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400455 if (shouldExpand) {
456 if (mBubbles.isEmpty()) {
457 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400458 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400459 }
460 if (mSelectedBubble == null) {
461 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400462 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400463 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400464 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400465 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400466 } else if (!mBubbles.isEmpty()) {
467 // Apply ordering and grouping rules from expanded -> collapsed, then save
468 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400469 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400470 // Save the state which should be returned to when expanded (with no other changes)
471
472 if (mBubbles.indexOf(mSelectedBubble) > 0) {
473 // Move the selected bubble to the top while collapsed.
474 if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) {
475 // The selected bubble cannot be raised to the first position because
476 // there is an ongoing bubble there. Instead, force the top ongoing bubble
477 // to become selected.
478 setSelectedBubbleInternal(mBubbles.get(0));
479 } else {
480 // Raise the selected bubble (and it's group) up to the front so the selected
481 // bubble remains on top.
482 mBubbles.remove(mSelectedBubble);
483 mBubbles.add(0, mSelectedBubble);
Mark Renouf6e4ddd72019-07-16 13:28:43 -0400484 mStateChange.orderChanged |= packGroup(0);
Mark Renoufba5ab512019-05-02 15:21:01 -0400485 }
486 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400487 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400488 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400489 mStateChange.expanded = shouldExpand;
490 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400491 }
492
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400493 private static long sortKey(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400494 long key = bubble.getLastUpdateTime();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400495 if (bubble.isOngoing()) {
496 // Set 2nd highest bit (signed long int), to partition between ongoing and regular
497 key |= 0x4000000000000000L;
498 }
499 return key;
500 }
501
502 /**
503 * Locates and inserts the bubble into a sorted position. The is inserted
504 * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
505 * required to keep grouping intact.
506 *
507 * @param minPosition the first insert point to consider
508 * @param newBubble the bubble to insert
509 * @return the position where the bubble was inserted
510 */
511 private int insertBubble(int minPosition, Bubble newBubble) {
512 long newBubbleSortKey = sortKey(newBubble);
513 String previousGroupId = null;
514
515 for (int pos = minPosition; pos < mBubbles.size(); pos++) {
516 Bubble bubbleAtPos = mBubbles.get(pos);
517 String groupIdAtPos = bubbleAtPos.getGroupId();
518 boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
519
520 if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
521 // Insert before the start of first group which has older bubbles.
522 mBubbles.add(pos, newBubble);
523 return pos;
524 }
525 previousGroupId = groupIdAtPos;
526 }
527 mBubbles.add(newBubble);
528 return mBubbles.size() - 1;
529 }
530
531 private boolean hasBubbleWithGroupId(String groupId) {
532 return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
533 }
534
535 private int findFirstIndexForGroup(String appId) {
536 for (int i = 0; i < mBubbles.size(); i++) {
537 Bubble bubbleAtPos = mBubbles.get(i);
538 if (bubbleAtPos.getGroupId().equals(appId)) {
539 return i;
540 }
541 }
542 return 0;
543 }
544
545 /**
546 * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
547 * at positions lower than {@code position} are unchanged. Relative order within the group
548 * unchanged. Relative order of any other bubbles are also unchanged.
549 *
550 * @param position the position of the first bubble for the group
Mark Renoufba5ab512019-05-02 15:21:01 -0400551 * @return true if the position of any bubbles has changed as a result
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400552 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400553 private boolean packGroup(int position) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200554 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400555 Log.d(TAG, "packGroup: position=" + position);
556 }
557 Bubble groupStart = mBubbles.get(position);
558 final String groupAppId = groupStart.getGroupId();
559 List<Bubble> moving = new ArrayList<>();
560
561 // Walk backward, collect bubbles within the group
562 for (int i = mBubbles.size() - 1; i > position; i--) {
563 if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
564 moving.add(0, mBubbles.get(i));
565 }
566 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400567 if (moving.isEmpty()) {
568 return false;
569 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400570 mBubbles.removeAll(moving);
571 mBubbles.addAll(position + 1, moving);
Mark Renoufba5ab512019-05-02 15:21:01 -0400572 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400573 }
574
Mark Renoufba5ab512019-05-02 15:21:01 -0400575 /**
576 * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped
577 * by groupId. Each group is then sorted by the max(lastUpdated) time of it's bubbles. Bubbles
578 * within each group are then sorted by lastUpdated descending.
579 *
580 * @return true if the position of any bubbles changed as a result
581 */
582 private boolean repackAll() {
Issei Suzukia8d07312019-06-07 12:56:19 +0200583 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400584 Log.d(TAG, "repackAll()");
585 }
586 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400587 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400588 }
589 Map<String, Long> groupLastActivity = new HashMap<>();
590 for (Bubble bubble : mBubbles) {
591 long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
592 long sortKeyForBubble = sortKey(bubble);
593 if (sortKeyForBubble > maxSortKeyForGroup) {
594 groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
595 }
596 }
597
598 // Sort groups by their most recently active bubble
599 List<String> groupsByMostRecentActivity =
600 groupLastActivity.entrySet().stream()
Mark Renoufba5ab512019-05-02 15:21:01 -0400601 .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400602 .map(Map.Entry::getKey)
603 .collect(toList());
604
605 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
606
607 // For each group, add bubbles, freshest to oldest
608 for (String appId : groupsByMostRecentActivity) {
609 mBubbles.stream()
610 .filter((b) -> b.getGroupId().equals(appId))
Mark Renoufba5ab512019-05-02 15:21:01 -0400611 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400612 .forEachOrdered(repacked::add);
613 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400614 if (repacked.equals(mBubbles)) {
615 return false;
616 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400617 mBubbles.clear();
618 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400619 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400620 }
621
Mark Renouf71a3af62019-04-08 15:02:54 -0400622 private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
623 if (reason == BubbleController.DISMISS_USER_GESTURE) {
624 Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
625 PendingIntent deleteIntent = bubbleMetadata != null
626 ? bubbleMetadata.getDeleteIntent()
627 : null;
628 if (deleteIntent != null) {
629 try {
630 deleteIntent.send();
631 } catch (PendingIntent.CanceledException e) {
632 Log.w(TAG, "Failed to send delete intent for bubble with key: " + entry.key);
633 }
634 }
635 }
636 }
637
Mark Renouf71a3af62019-04-08 15:02:54 -0400638 private int indexForKey(String key) {
639 for (int i = 0; i < mBubbles.size(); i++) {
640 Bubble bubble = mBubbles.get(i);
641 if (bubble.getKey().equals(key)) {
642 return i;
643 }
644 }
645 return -1;
646 }
647
Mark Renouf71a3af62019-04-08 15:02:54 -0400648 /**
649 * The set of bubbles.
Mark Renouf71a3af62019-04-08 15:02:54 -0400650 */
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400651 @VisibleForTesting(visibility = PRIVATE)
652 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400653 return Collections.unmodifiableList(mBubbles);
654 }
655
656 @VisibleForTesting(visibility = PRIVATE)
657 Bubble getBubbleWithKey(String key) {
658 for (int i = 0; i < mBubbles.size(); i++) {
659 Bubble bubble = mBubbles.get(i);
660 if (bubble.getKey().equals(key)) {
661 return bubble;
662 }
663 }
664 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800665 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400666
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400667 @VisibleForTesting(visibility = PRIVATE)
668 void setTimeSource(TimeSource timeSource) {
669 mTimeSource = timeSource;
670 }
671
Mark Renouf3bc5b362019-04-05 14:37:59 -0400672 public void setListener(Listener listener) {
673 mListener = listener;
674 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400675
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700676 /**
677 * Description of current bubble data state.
678 */
679 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
680 pw.print("selected: "); pw.println(mSelectedBubble != null
681 ? mSelectedBubble.getKey()
682 : "null");
683 pw.print("expanded: "); pw.println(mExpanded);
684 pw.print("count: "); pw.println(mBubbles.size());
685 for (Bubble bubble : mBubbles) {
686 bubble.dump(fd, pw, args);
687 }
Mady Mellore28fe102019-07-09 15:33:32 -0700688 pw.print("summaryKeys: "); pw.println(mSuppressedGroupKeys.size());
689 for (String key : mSuppressedGroupKeys.keySet()) {
690 pw.println(" suppressing: " + key);
691 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400692 }
Mark Renoufc19b4732019-06-26 12:08:33 -0400693}