blob: 4876c572eb0b41b0202ea81fa2c37c2af3709749 [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;
Mady Mellor3df7ab02019-12-09 15:07:10 -080036import com.android.systemui.R;
Mark Renouf71a3af62019-04-08 15:02:54 -040037import com.android.systemui.bubbles.BubbleController.DismissReason;
Mady Mellor3dff9e62019-02-05 18:12:53 -080038import com.android.systemui.statusbar.notification.collection.NotificationEntry;
39
Mady Mellor70cba7bb2019-07-02 15:06:07 -070040import java.io.FileDescriptor;
41import java.io.PrintWriter;
Mark Renouf71a3af62019-04-08 15:02:54 -040042import java.util.ArrayList;
Mark Renouf71a3af62019-04-08 15:02:54 -040043import java.util.Collections;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040044import java.util.Comparator;
45import java.util.HashMap;
Mark Renouf3bc5b362019-04-05 14:37:59 -040046import java.util.List;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040047import java.util.Map;
Mark Renouf71a3af62019-04-08 15:02:54 -040048import java.util.Objects;
Mady Mellor3dff9e62019-02-05 18:12:53 -080049
Mady Mellorcfd06c12019-02-13 14:32:12 -080050import javax.inject.Inject;
51import javax.inject.Singleton;
52
Mady Mellor3dff9e62019-02-05 18:12:53 -080053/**
54 * Keeps track of active bubbles.
55 */
Mady Mellorcfd06c12019-02-13 14:32:12 -080056@Singleton
Selim Cinekfdf80332019-03-07 17:29:55 -080057public class BubbleData {
Mady Mellor3dff9e62019-02-05 18:12:53 -080058
Issei Suzukia8d07312019-06-07 12:56:19 +020059 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040060
Mark Renoufba5ab512019-05-02 15:21:01 -040061 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
62 Comparator.comparing(BubbleData::sortKey).reversed();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040063
Mark Renoufba5ab512019-05-02 15:21:01 -040064 private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING =
Mark Renouf9ba6cea2019-04-17 11:53:50 -040065 Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
Mark Renouf71a3af62019-04-08 15:02:54 -040066
Mark Renouf82a40e62019-05-23 16:16:24 -040067 /** Contains information about changes that have been made to the state of bubbles. */
68 static final class Update {
69 boolean expandedChanged;
70 boolean selectionChanged;
71 boolean orderChanged;
72 boolean expanded;
73 @Nullable Bubble selectedBubble;
74 @Nullable Bubble addedBubble;
75 @Nullable Bubble updatedBubble;
76 // Pair with Bubble and @DismissReason Integer
77 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
78
79 // A read-only view of the bubbles list, changes there will be reflected here.
80 final List<Bubble> bubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -080081 final List<Bubble> overflowBubbles;
Mark Renouf82a40e62019-05-23 16:16:24 -040082
Lyn Hanb58c7562020-01-07 14:29:20 -080083 private Update(List<Bubble> row, List<Bubble> overflow) {
84 bubbles = Collections.unmodifiableList(row);
85 overflowBubbles = Collections.unmodifiableList(overflow);
Mark Renouf82a40e62019-05-23 16:16:24 -040086 }
87
88 boolean anythingChanged() {
89 return expandedChanged
90 || selectionChanged
91 || addedBubble != null
92 || updatedBubble != null
93 || !removedBubbles.isEmpty()
94 || orderChanged;
95 }
96
97 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
98 removedBubbles.add(new Pair<>(bubbleToRemove, reason));
99 }
100 }
101
Mark Renouf3bc5b362019-04-05 14:37:59 -0400102 /**
103 * This interface reports changes to the state and appearance of bubbles which should be applied
104 * as necessary to the UI.
Mark Renouf3bc5b362019-04-05 14:37:59 -0400105 */
106 interface Listener {
Mark Renouf82a40e62019-05-23 16:16:24 -0400107 /** Reports changes have have occurred as a result of the most recent operation. */
108 void applyUpdate(Update update);
Mark Renouf3bc5b362019-04-05 14:37:59 -0400109 }
110
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400111 interface TimeSource {
112 long currentTimeMillis();
113 }
114
Mark Renouf71a3af62019-04-08 15:02:54 -0400115 private final Context mContext;
Mady Mellor9ec37962020-01-10 10:21:03 -0800116 /** Bubbles that are actively in the stack. */
Mark Renouf82a40e62019-05-23 16:16:24 -0400117 private final List<Bubble> mBubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -0800118 /** Bubbles that aged out to overflow. */
119 private final List<Bubble> mOverflowBubbles;
Mady Mellor9ec37962020-01-10 10:21:03 -0800120 /** Bubbles that are being loaded but haven't been added to the stack just yet. */
121 private final List<Bubble> mPendingBubbles;
Mark Renouf71a3af62019-04-08 15:02:54 -0400122 private Bubble mSelectedBubble;
123 private boolean mExpanded;
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800124 private final int mMaxBubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -0800125 private final int mMaxOverflowBubbles;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400126
Mark Renoufba5ab512019-05-02 15:21:01 -0400127 // State tracked during an operation -- keeps track of what listener events to dispatch.
Mark Renouf82a40e62019-05-23 16:16:24 -0400128 private Update mStateChange;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400129
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400130 private NotificationListenerService.Ranking mTmpRanking;
131
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400132 private TimeSource mTimeSource = System::currentTimeMillis;
133
134 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400135 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800136
Mady Mellorf44b6832020-01-14 13:26:14 -0800137 @Nullable
138 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
139
Mady Mellore28fe102019-07-09 15:33:32 -0700140 /**
141 * We track groups with summaries that aren't visibly displayed but still kept around because
142 * the bubble(s) associated with the summary still exist.
143 *
144 * The summary must be kept around so that developers can cancel it (and hence the bubbles
145 * associated with it). This list is used to check if the summary should be hidden from the
146 * shade.
147 *
148 * Key: group key of the NotificationEntry
149 * Value: key of the NotificationEntry
150 */
151 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
152
Mady Mellorcfd06c12019-02-13 14:32:12 -0800153 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400154 public BubbleData(Context context) {
155 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400156 mBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800157 mOverflowBubbles = new ArrayList<>();
Mady Mellor9ec37962020-01-10 10:21:03 -0800158 mPendingBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800159 mStateChange = new Update(mBubbles, mOverflowBubbles);
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800160 mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
Lyn Hanb58c7562020-01-07 14:29:20 -0800161 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800162 }
163
Mady Mellorf44b6832020-01-14 13:26:14 -0800164 public void setSuppressionChangedListener(
165 BubbleController.NotificationSuppressionChangedListener listener) {
166 mSuppressionListener = listener;
167 }
168
Mark Renouf71a3af62019-04-08 15:02:54 -0400169 public boolean hasBubbles() {
170 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800171 }
172
Mark Renouf71a3af62019-04-08 15:02:54 -0400173 public boolean isExpanded() {
174 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800175 }
176
Mark Renouf71a3af62019-04-08 15:02:54 -0400177 public boolean hasBubbleWithKey(String key) {
178 return getBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800179 }
180
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400181 @Nullable
182 public Bubble getSelectedBubble() {
183 return mSelectedBubble;
184 }
185
Mark Renouf71a3af62019-04-08 15:02:54 -0400186 public void setExpanded(boolean expanded) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200187 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400188 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800189 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400190 setExpandedInternal(expanded);
191 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800192 }
193
Mark Renouf71a3af62019-04-08 15:02:54 -0400194 public void setSelectedBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200195 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400196 Log.d(TAG, "setSelectedBubble: " + bubble);
197 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400198 setSelectedBubbleInternal(bubble);
199 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400200 }
201
Lyn Han1e19d7f2020-02-05 19:10:58 -0800202 public void promoteBubbleFromOverflow(Bubble bubble, BubbleStackView stack,
203 BubbleIconFactory factory) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800204 if (DEBUG_BUBBLE_DATA) {
205 Log.d(TAG, "promoteBubbleFromOverflow: " + bubble);
206 }
Lyn Han1e19d7f2020-02-05 19:10:58 -0800207
Lyn Hanb58c7562020-01-07 14:29:20 -0800208 // Preserve new order for next repack, which sorts by last updated time.
209 bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
Lyn Han1e19d7f2020-02-05 19:10:58 -0800210 mOverflowBubbles.remove(bubble);
Lyn Han1e19d7f2020-02-05 19:10:58 -0800211 bubble.inflate(
Lyn Han4438d9b2020-03-11 18:04:35 -0700212 b -> {
213 notificationEntryUpdated(bubble, /* suppressFlyout */
214 false, /* showInShade */ true);
215 setSelectedBubbleInternal(bubble);
216 },
Lyn Han1e19d7f2020-02-05 19:10:58 -0800217 mContext, stack, factory);
Lyn Hanb58c7562020-01-07 14:29:20 -0800218 dispatchPendingChanges();
219 }
220
Mady Mellor3df7ab02019-12-09 15:07:10 -0800221 /**
222 * Constructs a new bubble or returns an existing one. Does not add new bubbles to
223 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
224 * for that.
225 */
226 Bubble getOrCreateBubble(NotificationEntry entry) {
227 Bubble bubble = getBubbleWithKey(entry.getKey());
228 if (bubble == null) {
Mady Mellor9ec37962020-01-10 10:21:03 -0800229 // Check for it in pending
230 for (int i = 0; i < mPendingBubbles.size(); i++) {
231 Bubble b = mPendingBubbles.get(i);
232 if (b.getKey().equals(entry.getKey())) {
233 return b;
234 }
235 }
Mady Mellorf44b6832020-01-14 13:26:14 -0800236 bubble = new Bubble(entry, mSuppressionListener);
Mady Mellor9ec37962020-01-10 10:21:03 -0800237 mPendingBubbles.add(bubble);
Mady Mellor3df7ab02019-12-09 15:07:10 -0800238 } else {
239 bubble.setEntry(entry);
240 }
241 return bubble;
242 }
243
244 /**
245 * When this method is called it is expected that all info in the bubble has completed loading.
246 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
247 * BubbleStackView, BubbleIconFactory).
248 */
249 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200250 if (DEBUG_BUBBLE_DATA) {
Mady Mellor3df7ab02019-12-09 15:07:10 -0800251 Log.d(TAG, "notificationEntryUpdated: " + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400252 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800253 mPendingBubbles.remove(bubble); // No longer pending once we're here
Mady Mellor3df7ab02019-12-09 15:07:10 -0800254 Bubble prevBubble = getBubbleWithKey(bubble.getKey());
Mady Mellor8e97a962020-01-21 16:20:15 -0800255 suppressFlyout |= !bubble.getEntry().getRanking().visuallyInterruptive();
Lyn Han405e0b72019-08-13 16:07:55 -0700256
Mady Mellor3df7ab02019-12-09 15:07:10 -0800257 if (prevBubble == null) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400258 // Create a new bubble
Mark Renoufc19b4732019-06-26 12:08:33 -0400259 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400260 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400261 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400262 } else {
263 // Updates an existing bubble
Lyn Han405e0b72019-08-13 16:07:55 -0700264 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400265 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400266 }
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700267 if (bubble.shouldAutoExpand()) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400268 setSelectedBubbleInternal(bubble);
269 if (!mExpanded) {
270 setExpandedInternal(true);
271 }
272 } else if (mSelectedBubble == null) {
273 setSelectedBubbleInternal(bubble);
274 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800275
Mady Mellorf44b6832020-01-14 13:26:14 -0800276 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
277 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
278 bubble.setSuppressNotification(suppress);
279 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
280
281 dispatchPendingChanges();
Mark Renoufba5ab512019-05-02 15:21:01 -0400282 }
283
284 public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200285 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400286 Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
287 }
Ned Burns00b4b2d2019-10-17 22:09:27 -0400288 doRemove(entry.getKey(), reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400289 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400290 }
291
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400292 /**
293 * Called when NotificationListener has received adjusted notification rank and reapplied
294 * filtering and sorting. This is used to dismiss any bubbles which should no longer be shown
295 * due to changes in permissions on the notification channel or the global setting.
296 *
297 * @param rankingMap the updated ranking map from NotificationListenerService
298 */
299 public void notificationRankingUpdated(RankingMap rankingMap) {
300 if (mTmpRanking == null) {
301 mTmpRanking = new NotificationListenerService.Ranking();
302 }
303
304 String[] orderedKeys = rankingMap.getOrderedKeys();
305 for (int i = 0; i < orderedKeys.length; i++) {
306 String key = orderedKeys[i];
307 if (hasBubbleWithKey(key)) {
308 rankingMap.getRanking(key, mTmpRanking);
309 if (!mTmpRanking.canBubble()) {
310 doRemove(key, BubbleController.DISMISS_BLOCKED);
311 }
312 }
313 }
314 dispatchPendingChanges();
315 }
316
Mady Mellor22f2f072019-04-18 13:26:18 -0700317 /**
Mady Mellore28fe102019-07-09 15:33:32 -0700318 * Adds a group key indicating that the summary for this group should be suppressed.
319 *
320 * @param groupKey the group key of the group whose summary should be suppressed.
321 * @param notifKey the notification entry key of that summary.
322 */
323 void addSummaryToSuppress(String groupKey, String notifKey) {
324 mSuppressedGroupKeys.put(groupKey, notifKey);
325 }
326
327 /**
328 * Retrieves the notif entry key of the summary associated with the provided group key.
329 *
330 * @param groupKey the group to look up
331 * @return the key for the {@link NotificationEntry} that is the summary of this group.
332 */
333 String getSummaryKey(String groupKey) {
334 return mSuppressedGroupKeys.get(groupKey);
335 }
336
337 /**
338 * Removes a group key indicating that summary for this group should no longer be suppressed.
339 */
340 void removeSuppressedSummary(String groupKey) {
341 mSuppressedGroupKeys.remove(groupKey);
342 }
343
344 /**
345 * Whether the summary for the provided group key is suppressed.
346 */
347 boolean isSummarySuppressed(String groupKey) {
348 return mSuppressedGroupKeys.containsKey(groupKey);
349 }
350
351 /**
Mady Mellor22f2f072019-04-18 13:26:18 -0700352 * Retrieves any bubbles that are part of the notification group represented by the provided
353 * group key.
354 */
355 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
356 ArrayList<Bubble> bubbleChildren = new ArrayList<>();
357 if (groupKey == null) {
358 return bubbleChildren;
359 }
360 for (Bubble b : mBubbles) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400361 if (groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
Mady Mellor22f2f072019-04-18 13:26:18 -0700362 bubbleChildren.add(b);
363 }
364 }
365 return bubbleChildren;
366 }
367
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400368 private void doAdd(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200369 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400370 Log.d(TAG, "doAdd: " + bubble);
371 }
372 int minInsertPoint = 0;
373 boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId());
374 if (isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400375 // first bubble of a group goes to the beginning, otherwise within the existing group
376 minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400377 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400378 if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400379 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400380 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400381 mStateChange.addedBubble = bubble;
Lyn Hanb58c7562020-01-07 14:29:20 -0800382
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400383 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400384 mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
Mark Renoufba5ab512019-05-02 15:21:01 -0400385 // Top bubble becomes selected.
386 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400387 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400388 }
389
390 private void trim() {
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800391 if (mBubbles.size() > mMaxBubbles) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400392 mBubbles.stream()
393 // sort oldest first (ascending lastActivity)
394 .sorted(Comparator.comparingLong(Bubble::getLastActivity))
395 // skip the selected bubble
396 .filter((b) -> !b.equals(mSelectedBubble))
397 .findFirst()
Mark Renoufba5ab512019-05-02 15:21:01 -0400398 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400399 }
400 }
401
402 private void doUpdate(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200403 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400404 Log.d(TAG, "doUpdate: " + bubble);
405 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400406 mStateChange.updatedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400407 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400408 // while collapsed, update causes re-pack
409 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400410 mBubbles.remove(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400411 int newPos = insertBubble(0, bubble);
412 if (prevPos != newPos) {
413 packGroup(newPos);
Mark Renouf82a40e62019-05-23 16:16:24 -0400414 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400415 }
416 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400417 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400418 }
419
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400420 private void doRemove(String key, @DismissReason int reason) {
Mady Mellor9ec37962020-01-10 10:21:03 -0800421 // If it was pending remove it
422 for (int i = 0; i < mPendingBubbles.size(); i++) {
423 if (mPendingBubbles.get(i).getKey().equals(key)) {
424 mPendingBubbles.remove(mPendingBubbles.get(i));
425 }
426 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400427 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400428 if (indexToRemove == -1) {
429 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400430 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400431 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
432 if (mBubbles.size() == 1) {
433 // Going to become empty, handle specially.
434 setExpandedInternal(false);
435 setSelectedBubbleInternal(null);
436 }
437 if (indexToRemove < mBubbles.size() - 1) {
438 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400439 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400440 }
441 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400442 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400443 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400444 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400445 }
446
Lyn Hand6981862020-02-18 18:01:43 -0800447 overflowBubble(reason, bubbleToRemove);
Lyn Hanb58c7562020-01-07 14:29:20 -0800448
Mark Renoufba5ab512019-05-02 15:21:01 -0400449 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
450 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
451 // Move selection to the new bubble at the same position.
452 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
453 Bubble newSelected = mBubbles.get(newIndex);
454 setSelectedBubbleInternal(newSelected);
455 }
Mady Mellored99c272019-06-13 15:58:30 -0700456 maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
Mark Renouf71a3af62019-04-08 15:02:54 -0400457 }
458
Lyn Hand6981862020-02-18 18:01:43 -0800459 void overflowBubble(@DismissReason int reason, Bubble bubble) {
460 if (reason == BubbleController.DISMISS_AGED
461 || reason == BubbleController.DISMISS_USER_GESTURE) {
462 if (DEBUG_BUBBLE_DATA) {
463 Log.d(TAG, "overflowing bubble: " + bubble);
464 }
465 mOverflowBubbles.add(0, bubble);
466
467 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
468 // Remove oldest bubble.
469 if (DEBUG_BUBBLE_DATA) {
470 Log.d(TAG, "Overflow full. Remove bubble: " + mOverflowBubbles.get(
471 mOverflowBubbles.size() - 1));
472 }
473 mOverflowBubbles.remove(mOverflowBubbles.size() - 1);
474 }
475 }
476 }
477
Mark Renouf71a3af62019-04-08 15:02:54 -0400478 public void dismissAll(@DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200479 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400480 Log.d(TAG, "dismissAll: reason=" + reason);
481 }
482 if (mBubbles.isEmpty()) {
483 return;
484 }
485 setExpandedInternal(false);
486 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400487 while (!mBubbles.isEmpty()) {
Lyn Hand6981862020-02-18 18:01:43 -0800488 doRemove(mBubbles.get(0).getKey(), reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400489 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400490 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400491 }
492
Mady Mellorca184aae2019-09-17 16:07:12 -0700493 /**
494 * Indicates that the provided display is no longer in use and should be cleaned up.
495 *
496 * @param displayId the id of the display to clean up.
497 */
498 void notifyDisplayEmpty(int displayId) {
499 for (Bubble b : mBubbles) {
500 if (b.getDisplayId() == displayId) {
501 if (b.getExpandedView() != null) {
502 b.getExpandedView().notifyDisplayEmpty();
503 }
504 return;
505 }
506 }
507 }
508
Mark Renoufba5ab512019-05-02 15:21:01 -0400509 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400510 if (mListener != null && mStateChange.anythingChanged()) {
511 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400512 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800513 mStateChange = new Update(mBubbles, mOverflowBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400514 }
515
516 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400517 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400518 *
519 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400520 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400521 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200522 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400523 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
524 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400525 if (Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400526 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400527 }
Lyn Han1e19d7f2020-02-05 19:10:58 -0800528 if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400529 Log.e(TAG, "Cannot select bubble which doesn't exist!"
530 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400531 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400532 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400533 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700534 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400535 }
536 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400537 mStateChange.selectedBubble = bubble;
538 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400539 }
540
Mark Renouf71a3af62019-04-08 15:02:54 -0400541 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400542 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400543 *
544 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400545 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400546 private void setExpandedInternal(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200547 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400548 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
549 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400550 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400551 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400552 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400553 if (shouldExpand) {
554 if (mBubbles.isEmpty()) {
555 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400556 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400557 }
558 if (mSelectedBubble == null) {
559 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400560 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400561 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400562 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400563 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400564 } else if (!mBubbles.isEmpty()) {
565 // Apply ordering and grouping rules from expanded -> collapsed, then save
566 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400567 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400568 // Save the state which should be returned to when expanded (with no other changes)
569
570 if (mBubbles.indexOf(mSelectedBubble) > 0) {
571 // Move the selected bubble to the top while collapsed.
572 if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) {
573 // The selected bubble cannot be raised to the first position because
574 // there is an ongoing bubble there. Instead, force the top ongoing bubble
575 // to become selected.
576 setSelectedBubbleInternal(mBubbles.get(0));
577 } else {
578 // Raise the selected bubble (and it's group) up to the front so the selected
579 // bubble remains on top.
580 mBubbles.remove(mSelectedBubble);
581 mBubbles.add(0, mSelectedBubble);
Mark Renouf6e4ddd72019-07-16 13:28:43 -0400582 mStateChange.orderChanged |= packGroup(0);
Mark Renoufba5ab512019-05-02 15:21:01 -0400583 }
584 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400585 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400586 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400587 mStateChange.expanded = shouldExpand;
588 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400589 }
590
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400591 private static long sortKey(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400592 long key = bubble.getLastUpdateTime();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400593 if (bubble.isOngoing()) {
594 // Set 2nd highest bit (signed long int), to partition between ongoing and regular
595 key |= 0x4000000000000000L;
596 }
597 return key;
598 }
599
600 /**
601 * Locates and inserts the bubble into a sorted position. The is inserted
602 * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
603 * required to keep grouping intact.
604 *
605 * @param minPosition the first insert point to consider
Lyn Han72f50902019-10-25 15:55:49 -0700606 * @param newBubble the bubble to insert
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400607 * @return the position where the bubble was inserted
608 */
609 private int insertBubble(int minPosition, Bubble newBubble) {
610 long newBubbleSortKey = sortKey(newBubble);
611 String previousGroupId = null;
612
613 for (int pos = minPosition; pos < mBubbles.size(); pos++) {
614 Bubble bubbleAtPos = mBubbles.get(pos);
615 String groupIdAtPos = bubbleAtPos.getGroupId();
616 boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
617
618 if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
619 // Insert before the start of first group which has older bubbles.
620 mBubbles.add(pos, newBubble);
621 return pos;
622 }
623 previousGroupId = groupIdAtPos;
624 }
625 mBubbles.add(newBubble);
626 return mBubbles.size() - 1;
627 }
628
629 private boolean hasBubbleWithGroupId(String groupId) {
630 return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
631 }
632
633 private int findFirstIndexForGroup(String appId) {
634 for (int i = 0; i < mBubbles.size(); i++) {
635 Bubble bubbleAtPos = mBubbles.get(i);
636 if (bubbleAtPos.getGroupId().equals(appId)) {
637 return i;
638 }
639 }
640 return 0;
641 }
642
643 /**
644 * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
645 * at positions lower than {@code position} are unchanged. Relative order within the group
646 * unchanged. Relative order of any other bubbles are also unchanged.
647 *
648 * @param position the position of the first bubble for the group
Mark Renoufba5ab512019-05-02 15:21:01 -0400649 * @return true if the position of any bubbles has changed as a result
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400650 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400651 private boolean packGroup(int position) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200652 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400653 Log.d(TAG, "packGroup: position=" + position);
654 }
655 Bubble groupStart = mBubbles.get(position);
656 final String groupAppId = groupStart.getGroupId();
657 List<Bubble> moving = new ArrayList<>();
658
659 // Walk backward, collect bubbles within the group
660 for (int i = mBubbles.size() - 1; i > position; i--) {
661 if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
662 moving.add(0, mBubbles.get(i));
663 }
664 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400665 if (moving.isEmpty()) {
666 return false;
667 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400668 mBubbles.removeAll(moving);
669 mBubbles.addAll(position + 1, moving);
Mark Renoufba5ab512019-05-02 15:21:01 -0400670 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400671 }
672
Mark Renoufba5ab512019-05-02 15:21:01 -0400673 /**
674 * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped
675 * by groupId. Each group is then sorted by the max(lastUpdated) time of it's bubbles. Bubbles
676 * within each group are then sorted by lastUpdated descending.
677 *
678 * @return true if the position of any bubbles changed as a result
679 */
680 private boolean repackAll() {
Issei Suzukia8d07312019-06-07 12:56:19 +0200681 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400682 Log.d(TAG, "repackAll()");
683 }
684 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400685 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400686 }
687 Map<String, Long> groupLastActivity = new HashMap<>();
688 for (Bubble bubble : mBubbles) {
689 long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
690 long sortKeyForBubble = sortKey(bubble);
691 if (sortKeyForBubble > maxSortKeyForGroup) {
692 groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
693 }
694 }
695
696 // Sort groups by their most recently active bubble
697 List<String> groupsByMostRecentActivity =
698 groupLastActivity.entrySet().stream()
Mark Renoufba5ab512019-05-02 15:21:01 -0400699 .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400700 .map(Map.Entry::getKey)
701 .collect(toList());
702
703 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
704
705 // For each group, add bubbles, freshest to oldest
706 for (String appId : groupsByMostRecentActivity) {
707 mBubbles.stream()
708 .filter((b) -> b.getGroupId().equals(appId))
Mark Renoufba5ab512019-05-02 15:21:01 -0400709 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400710 .forEachOrdered(repacked::add);
711 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400712 if (repacked.equals(mBubbles)) {
713 return false;
714 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400715 mBubbles.clear();
716 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400717 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400718 }
719
Mark Renouf71a3af62019-04-08 15:02:54 -0400720 private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
721 if (reason == BubbleController.DISMISS_USER_GESTURE) {
722 Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
723 PendingIntent deleteIntent = bubbleMetadata != null
724 ? bubbleMetadata.getDeleteIntent()
725 : null;
726 if (deleteIntent != null) {
727 try {
728 deleteIntent.send();
729 } catch (PendingIntent.CanceledException e) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400730 Log.w(TAG, "Failed to send delete intent for bubble with key: "
731 + entry.getKey());
Mark Renouf71a3af62019-04-08 15:02:54 -0400732 }
733 }
734 }
735 }
736
Mark Renouf71a3af62019-04-08 15:02:54 -0400737 private int indexForKey(String key) {
738 for (int i = 0; i < mBubbles.size(); i++) {
739 Bubble bubble = mBubbles.get(i);
740 if (bubble.getKey().equals(key)) {
741 return i;
742 }
743 }
744 return -1;
745 }
746
Mark Renouf71a3af62019-04-08 15:02:54 -0400747 /**
Lyn Hanb58c7562020-01-07 14:29:20 -0800748 * The set of bubbles in row.
Mark Renouf71a3af62019-04-08 15:02:54 -0400749 */
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400750 @VisibleForTesting(visibility = PRIVATE)
751 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400752 return Collections.unmodifiableList(mBubbles);
753 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800754 /**
755 * The set of bubbles in overflow.
756 */
757 @VisibleForTesting(visibility = PRIVATE)
758 public List<Bubble> getOverflowBubbles() {
759 return Collections.unmodifiableList(mOverflowBubbles);
760 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400761
762 @VisibleForTesting(visibility = PRIVATE)
763 Bubble getBubbleWithKey(String key) {
764 for (int i = 0; i < mBubbles.size(); i++) {
765 Bubble bubble = mBubbles.get(i);
766 if (bubble.getKey().equals(key)) {
767 return bubble;
768 }
769 }
770 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800771 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400772
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400773 @VisibleForTesting(visibility = PRIVATE)
774 void setTimeSource(TimeSource timeSource) {
775 mTimeSource = timeSource;
776 }
777
Mark Renouf3bc5b362019-04-05 14:37:59 -0400778 public void setListener(Listener listener) {
779 mListener = listener;
780 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400781
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700782 /**
783 * Description of current bubble data state.
784 */
785 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
Lyn Han72f50902019-10-25 15:55:49 -0700786 pw.print("selected: ");
787 pw.println(mSelectedBubble != null
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700788 ? mSelectedBubble.getKey()
789 : "null");
Lyn Han72f50902019-10-25 15:55:49 -0700790 pw.print("expanded: ");
791 pw.println(mExpanded);
792 pw.print("count: ");
793 pw.println(mBubbles.size());
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700794 for (Bubble bubble : mBubbles) {
795 bubble.dump(fd, pw, args);
796 }
Lyn Han72f50902019-10-25 15:55:49 -0700797 pw.print("summaryKeys: ");
798 pw.println(mSuppressedGroupKeys.size());
Mady Mellore28fe102019-07-09 15:33:32 -0700799 for (String key : mSuppressedGroupKeys.keySet()) {
800 pw.println(" suppressing: " + key);
801 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400802 }
Mark Renoufc19b4732019-06-26 12:08:33 -0400803}