blob: 1c69594469c1170eb6a578e6b93195192c067473 [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;
Mark Renouf71a3af62019-04-08 15:02:54 -040029import android.util.Log;
Mark Renoufba5ab512019-05-02 15:21:01 -040030import android.util.Pair;
Mady Mellor3dff9e62019-02-05 18:12:53 -080031
Mark Renouf9ba6cea2019-04-17 11:53:50 -040032import androidx.annotation.Nullable;
33
Selim Cinekfdf80332019-03-07 17:29:55 -080034import com.android.internal.annotations.VisibleForTesting;
Mady Mellor3df7ab02019-12-09 15:07:10 -080035import com.android.systemui.R;
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
Mark Renoufba5ab512019-05-02 15:21:01 -040060 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
61 Comparator.comparing(BubbleData::sortKey).reversed();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040062
Mark Renoufba5ab512019-05-02 15:21:01 -040063 private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING =
Mark Renouf9ba6cea2019-04-17 11:53:50 -040064 Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
Mark Renouf71a3af62019-04-08 15:02:54 -040065
Mark Renouf82a40e62019-05-23 16:16:24 -040066 /** Contains information about changes that have been made to the state of bubbles. */
67 static final class Update {
68 boolean expandedChanged;
69 boolean selectionChanged;
70 boolean orderChanged;
71 boolean expanded;
72 @Nullable Bubble selectedBubble;
73 @Nullable Bubble addedBubble;
74 @Nullable Bubble updatedBubble;
75 // Pair with Bubble and @DismissReason Integer
76 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
77
78 // A read-only view of the bubbles list, changes there will be reflected here.
79 final List<Bubble> bubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -080080 final List<Bubble> overflowBubbles;
Mark Renouf82a40e62019-05-23 16:16:24 -040081
Lyn Hanb58c7562020-01-07 14:29:20 -080082 private Update(List<Bubble> row, List<Bubble> overflow) {
83 bubbles = Collections.unmodifiableList(row);
84 overflowBubbles = Collections.unmodifiableList(overflow);
Mark Renouf82a40e62019-05-23 16:16:24 -040085 }
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;
Mady Mellor9ec37962020-01-10 10:21:03 -0800115 /** Bubbles that are actively in the stack. */
Mark Renouf82a40e62019-05-23 16:16:24 -0400116 private final List<Bubble> mBubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -0800117 /** Bubbles that aged out to overflow. */
118 private final List<Bubble> mOverflowBubbles;
Mady Mellor9ec37962020-01-10 10:21:03 -0800119 /** Bubbles that are being loaded but haven't been added to the stack just yet. */
120 private final List<Bubble> mPendingBubbles;
Mark Renouf71a3af62019-04-08 15:02:54 -0400121 private Bubble mSelectedBubble;
122 private boolean mExpanded;
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800123 private final int mMaxBubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -0800124 private final int mMaxOverflowBubbles;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400125
Mark Renoufba5ab512019-05-02 15:21:01 -0400126 // State tracked during an operation -- keeps track of what listener events to dispatch.
Mark Renouf82a40e62019-05-23 16:16:24 -0400127 private Update mStateChange;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400128
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400129 private NotificationListenerService.Ranking mTmpRanking;
130
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400131 private TimeSource mTimeSource = System::currentTimeMillis;
132
133 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400134 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800135
Mady Mellorf44b6832020-01-14 13:26:14 -0800136 @Nullable
137 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
138
Mady Mellore28fe102019-07-09 15:33:32 -0700139 /**
140 * We track groups with summaries that aren't visibly displayed but still kept around because
141 * the bubble(s) associated with the summary still exist.
142 *
143 * The summary must be kept around so that developers can cancel it (and hence the bubbles
144 * associated with it). This list is used to check if the summary should be hidden from the
145 * shade.
146 *
147 * Key: group key of the NotificationEntry
148 * Value: key of the NotificationEntry
149 */
150 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
151
Mady Mellorcfd06c12019-02-13 14:32:12 -0800152 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400153 public BubbleData(Context context) {
154 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400155 mBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800156 mOverflowBubbles = new ArrayList<>();
Mady Mellor9ec37962020-01-10 10:21:03 -0800157 mPendingBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800158 mStateChange = new Update(mBubbles, mOverflowBubbles);
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800159 mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
Lyn Hanb58c7562020-01-07 14:29:20 -0800160 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800161 }
162
Mady Mellorf44b6832020-01-14 13:26:14 -0800163 public void setSuppressionChangedListener(
164 BubbleController.NotificationSuppressionChangedListener listener) {
165 mSuppressionListener = listener;
166 }
167
Mark Renouf71a3af62019-04-08 15:02:54 -0400168 public boolean hasBubbles() {
169 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800170 }
171
Mark Renouf71a3af62019-04-08 15:02:54 -0400172 public boolean isExpanded() {
173 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800174 }
175
Mark Renouf71a3af62019-04-08 15:02:54 -0400176 public boolean hasBubbleWithKey(String key) {
177 return getBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800178 }
179
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400180 @Nullable
181 public Bubble getSelectedBubble() {
182 return mSelectedBubble;
183 }
184
Mark Renouf71a3af62019-04-08 15:02:54 -0400185 public void setExpanded(boolean expanded) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200186 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400187 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800188 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400189 setExpandedInternal(expanded);
190 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800191 }
192
Mark Renouf71a3af62019-04-08 15:02:54 -0400193 public void setSelectedBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200194 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400195 Log.d(TAG, "setSelectedBubble: " + bubble);
196 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400197 setSelectedBubbleInternal(bubble);
198 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400199 }
200
Lyn Han1e19d7f2020-02-05 19:10:58 -0800201 public void promoteBubbleFromOverflow(Bubble bubble, BubbleStackView stack,
202 BubbleIconFactory factory) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800203 if (DEBUG_BUBBLE_DATA) {
204 Log.d(TAG, "promoteBubbleFromOverflow: " + bubble);
205 }
Lyn Han18cdc1c2020-03-18 19:12:42 -0700206 moveOverflowBubbleToPending(bubble);
Lyn Han1e19d7f2020-02-05 19:10:58 -0800207 bubble.inflate(
Lyn Han4438d9b2020-03-11 18:04:35 -0700208 b -> {
209 notificationEntryUpdated(bubble, /* suppressFlyout */
210 false, /* showInShade */ true);
Lyn Han89274b42020-03-25 00:56:26 -0700211 setSelectedBubble(bubble);
Lyn Han4438d9b2020-03-11 18:04:35 -0700212 },
Lyn Han1e19d7f2020-02-05 19:10:58 -0800213 mContext, stack, factory);
Lyn Hanb58c7562020-01-07 14:29:20 -0800214 dispatchPendingChanges();
215 }
216
Lyn Han18cdc1c2020-03-18 19:12:42 -0700217 private void moveOverflowBubbleToPending(Bubble b) {
218 // Preserve new order for next repack, which sorts by last updated time.
219 b.markUpdatedAt(mTimeSource.currentTimeMillis());
220 mOverflowBubbles.remove(b);
221 mPendingBubbles.add(b);
222 }
223
Mady Mellor3df7ab02019-12-09 15:07:10 -0800224 /**
225 * Constructs a new bubble or returns an existing one. Does not add new bubbles to
226 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
227 * for that.
228 */
229 Bubble getOrCreateBubble(NotificationEntry entry) {
230 Bubble bubble = getBubbleWithKey(entry.getKey());
231 if (bubble == null) {
Lyn Hanbf1b3d62020-03-12 10:31:19 -0700232 for (int i = 0; i < mOverflowBubbles.size(); i++) {
233 Bubble b = mOverflowBubbles.get(i);
234 if (b.getKey().equals(entry.getKey())) {
Lyn Han18cdc1c2020-03-18 19:12:42 -0700235 moveOverflowBubbleToPending(b);
Mady Mellore45ff862020-03-24 15:54:50 -0700236 b.setEntry(entry);
Lyn Hanbf1b3d62020-03-12 10:31:19 -0700237 return b;
238 }
239 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800240 // Check for it in pending
241 for (int i = 0; i < mPendingBubbles.size(); i++) {
242 Bubble b = mPendingBubbles.get(i);
243 if (b.getKey().equals(entry.getKey())) {
Mady Mellore45ff862020-03-24 15:54:50 -0700244 b.setEntry(entry);
Mady Mellor9ec37962020-01-10 10:21:03 -0800245 return b;
246 }
247 }
Mady Mellorf44b6832020-01-14 13:26:14 -0800248 bubble = new Bubble(entry, mSuppressionListener);
Mady Mellor9ec37962020-01-10 10:21:03 -0800249 mPendingBubbles.add(bubble);
Mady Mellor3df7ab02019-12-09 15:07:10 -0800250 } else {
251 bubble.setEntry(entry);
252 }
253 return bubble;
254 }
255
256 /**
257 * When this method is called it is expected that all info in the bubble has completed loading.
258 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
259 * BubbleStackView, BubbleIconFactory).
260 */
261 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200262 if (DEBUG_BUBBLE_DATA) {
Mady Mellor3df7ab02019-12-09 15:07:10 -0800263 Log.d(TAG, "notificationEntryUpdated: " + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400264 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800265 mPendingBubbles.remove(bubble); // No longer pending once we're here
Mady Mellor3df7ab02019-12-09 15:07:10 -0800266 Bubble prevBubble = getBubbleWithKey(bubble.getKey());
Mady Mellor8e97a962020-01-21 16:20:15 -0800267 suppressFlyout |= !bubble.getEntry().getRanking().visuallyInterruptive();
Lyn Han405e0b72019-08-13 16:07:55 -0700268
Mady Mellor3df7ab02019-12-09 15:07:10 -0800269 if (prevBubble == null) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400270 // Create a new bubble
Mark Renoufc19b4732019-06-26 12:08:33 -0400271 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400272 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400273 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400274 } else {
275 // Updates an existing bubble
Lyn Han405e0b72019-08-13 16:07:55 -0700276 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400277 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400278 }
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700279 if (bubble.shouldAutoExpand()) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400280 setSelectedBubbleInternal(bubble);
281 if (!mExpanded) {
282 setExpandedInternal(true);
283 }
284 } else if (mSelectedBubble == null) {
285 setSelectedBubbleInternal(bubble);
286 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800287
Mady Mellorf44b6832020-01-14 13:26:14 -0800288 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
289 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
290 bubble.setSuppressNotification(suppress);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400291 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
Mady Mellorf44b6832020-01-14 13:26:14 -0800292
293 dispatchPendingChanges();
Mark Renoufba5ab512019-05-02 15:21:01 -0400294 }
295
296 public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200297 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400298 Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
299 }
Ned Burns00b4b2d2019-10-17 22:09:27 -0400300 doRemove(entry.getKey(), reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400301 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400302 }
303
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400304 /**
Mady Mellore28fe102019-07-09 15:33:32 -0700305 * Adds a group key indicating that the summary for this group should be suppressed.
306 *
307 * @param groupKey the group key of the group whose summary should be suppressed.
308 * @param notifKey the notification entry key of that summary.
309 */
310 void addSummaryToSuppress(String groupKey, String notifKey) {
311 mSuppressedGroupKeys.put(groupKey, notifKey);
312 }
313
314 /**
315 * Retrieves the notif entry key of the summary associated with the provided group key.
316 *
317 * @param groupKey the group to look up
318 * @return the key for the {@link NotificationEntry} that is the summary of this group.
319 */
320 String getSummaryKey(String groupKey) {
321 return mSuppressedGroupKeys.get(groupKey);
322 }
323
324 /**
325 * Removes a group key indicating that summary for this group should no longer be suppressed.
326 */
327 void removeSuppressedSummary(String groupKey) {
328 mSuppressedGroupKeys.remove(groupKey);
329 }
330
331 /**
332 * Whether the summary for the provided group key is suppressed.
333 */
334 boolean isSummarySuppressed(String groupKey) {
335 return mSuppressedGroupKeys.containsKey(groupKey);
336 }
337
338 /**
Mady Mellor22f2f072019-04-18 13:26:18 -0700339 * Retrieves any bubbles that are part of the notification group represented by the provided
340 * group key.
341 */
342 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
343 ArrayList<Bubble> bubbleChildren = new ArrayList<>();
344 if (groupKey == null) {
345 return bubbleChildren;
346 }
347 for (Bubble b : mBubbles) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400348 if (groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
Mady Mellor22f2f072019-04-18 13:26:18 -0700349 bubbleChildren.add(b);
350 }
351 }
352 return bubbleChildren;
353 }
354
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400355 private void doAdd(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200356 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400357 Log.d(TAG, "doAdd: " + bubble);
358 }
359 int minInsertPoint = 0;
360 boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId());
361 if (isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400362 // first bubble of a group goes to the beginning, otherwise within the existing group
363 minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400364 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400365 if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400366 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400367 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400368 mStateChange.addedBubble = bubble;
Lyn Hanb58c7562020-01-07 14:29:20 -0800369
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400370 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400371 mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
Mark Renoufba5ab512019-05-02 15:21:01 -0400372 // Top bubble becomes selected.
373 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400374 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400375 }
376
377 private void trim() {
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800378 if (mBubbles.size() > mMaxBubbles) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400379 mBubbles.stream()
380 // sort oldest first (ascending lastActivity)
381 .sorted(Comparator.comparingLong(Bubble::getLastActivity))
382 // skip the selected bubble
383 .filter((b) -> !b.equals(mSelectedBubble))
384 .findFirst()
Mark Renoufba5ab512019-05-02 15:21:01 -0400385 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400386 }
387 }
388
389 private void doUpdate(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200390 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400391 Log.d(TAG, "doUpdate: " + bubble);
392 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400393 mStateChange.updatedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400394 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400395 // while collapsed, update causes re-pack
396 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400397 mBubbles.remove(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400398 int newPos = insertBubble(0, bubble);
399 if (prevPos != newPos) {
400 packGroup(newPos);
Mark Renouf82a40e62019-05-23 16:16:24 -0400401 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400402 }
403 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400404 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400405 }
406
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400407 private void doRemove(String key, @DismissReason int reason) {
Lyn Hane1395572020-03-23 15:48:54 -0700408 if (DEBUG_BUBBLE_DATA) {
409 Log.d(TAG, "doRemove: " + key);
410 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800411 // If it was pending remove it
412 for (int i = 0; i < mPendingBubbles.size(); i++) {
413 if (mPendingBubbles.get(i).getKey().equals(key)) {
414 mPendingBubbles.remove(mPendingBubbles.get(i));
415 }
416 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400417 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400418 if (indexToRemove == -1) {
419 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400420 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400421 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
422 if (mBubbles.size() == 1) {
423 // Going to become empty, handle specially.
424 setExpandedInternal(false);
425 setSelectedBubbleInternal(null);
426 }
427 if (indexToRemove < mBubbles.size() - 1) {
428 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400429 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400430 }
431 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400432 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400433 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400434 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400435 }
436
Lyn Hand6981862020-02-18 18:01:43 -0800437 overflowBubble(reason, bubbleToRemove);
Lyn Hanb58c7562020-01-07 14:29:20 -0800438
Mark Renoufba5ab512019-05-02 15:21:01 -0400439 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
440 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
441 // Move selection to the new bubble at the same position.
442 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
443 Bubble newSelected = mBubbles.get(newIndex);
444 setSelectedBubbleInternal(newSelected);
445 }
Mady Mellored99c272019-06-13 15:58:30 -0700446 maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
Mark Renouf71a3af62019-04-08 15:02:54 -0400447 }
448
Lyn Hand6981862020-02-18 18:01:43 -0800449 void overflowBubble(@DismissReason int reason, Bubble bubble) {
450 if (reason == BubbleController.DISMISS_AGED
451 || reason == BubbleController.DISMISS_USER_GESTURE) {
452 if (DEBUG_BUBBLE_DATA) {
Lyn Hane1395572020-03-23 15:48:54 -0700453 Log.d(TAG, "Overflowing: " + bubble);
Lyn Hand6981862020-02-18 18:01:43 -0800454 }
455 mOverflowBubbles.add(0, bubble);
Lyn Han18cdc1c2020-03-18 19:12:42 -0700456 bubble.stopInflation();
Lyn Hand6981862020-02-18 18:01:43 -0800457 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
458 // Remove oldest bubble.
459 if (DEBUG_BUBBLE_DATA) {
Lyn Hane1395572020-03-23 15:48:54 -0700460 Log.d(TAG, "Overflow full. Remove: " + mOverflowBubbles.get(
Lyn Hand6981862020-02-18 18:01:43 -0800461 mOverflowBubbles.size() - 1));
462 }
463 mOverflowBubbles.remove(mOverflowBubbles.size() - 1);
464 }
465 }
466 }
467
Mark Renouf71a3af62019-04-08 15:02:54 -0400468 public void dismissAll(@DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200469 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400470 Log.d(TAG, "dismissAll: reason=" + reason);
471 }
472 if (mBubbles.isEmpty()) {
473 return;
474 }
475 setExpandedInternal(false);
476 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400477 while (!mBubbles.isEmpty()) {
Lyn Hand6981862020-02-18 18:01:43 -0800478 doRemove(mBubbles.get(0).getKey(), reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400479 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400480 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400481 }
482
Mady Mellorca184aae2019-09-17 16:07:12 -0700483 /**
484 * Indicates that the provided display is no longer in use and should be cleaned up.
485 *
486 * @param displayId the id of the display to clean up.
487 */
488 void notifyDisplayEmpty(int displayId) {
489 for (Bubble b : mBubbles) {
490 if (b.getDisplayId() == displayId) {
491 if (b.getExpandedView() != null) {
492 b.getExpandedView().notifyDisplayEmpty();
493 }
494 return;
495 }
496 }
497 }
498
Mark Renoufba5ab512019-05-02 15:21:01 -0400499 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400500 if (mListener != null && mStateChange.anythingChanged()) {
501 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400502 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800503 mStateChange = new Update(mBubbles, mOverflowBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400504 }
505
506 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400507 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400508 *
509 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400510 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400511 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200512 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400513 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
514 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400515 if (Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400516 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400517 }
Lyn Han1e19d7f2020-02-05 19:10:58 -0800518 if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400519 Log.e(TAG, "Cannot select bubble which doesn't exist!"
520 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400521 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400522 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400523 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700524 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400525 }
526 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400527 mStateChange.selectedBubble = bubble;
528 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400529 }
530
Mark Renouf71a3af62019-04-08 15:02:54 -0400531 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400532 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400533 *
534 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400535 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400536 private void setExpandedInternal(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200537 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400538 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
539 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400540 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400541 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400542 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400543 if (shouldExpand) {
544 if (mBubbles.isEmpty()) {
545 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400546 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400547 }
548 if (mSelectedBubble == null) {
549 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400550 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400551 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400552 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400553 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400554 } else if (!mBubbles.isEmpty()) {
555 // Apply ordering and grouping rules from expanded -> collapsed, then save
556 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400557 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400558 // Save the state which should be returned to when expanded (with no other changes)
559
560 if (mBubbles.indexOf(mSelectedBubble) > 0) {
561 // Move the selected bubble to the top while collapsed.
562 if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) {
563 // The selected bubble cannot be raised to the first position because
564 // there is an ongoing bubble there. Instead, force the top ongoing bubble
565 // to become selected.
566 setSelectedBubbleInternal(mBubbles.get(0));
567 } else {
568 // Raise the selected bubble (and it's group) up to the front so the selected
569 // bubble remains on top.
570 mBubbles.remove(mSelectedBubble);
571 mBubbles.add(0, mSelectedBubble);
Mark Renouf6e4ddd72019-07-16 13:28:43 -0400572 mStateChange.orderChanged |= packGroup(0);
Mark Renoufba5ab512019-05-02 15:21:01 -0400573 }
574 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400575 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400576 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400577 mStateChange.expanded = shouldExpand;
578 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400579 }
580
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400581 private static long sortKey(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400582 long key = bubble.getLastUpdateTime();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400583 if (bubble.isOngoing()) {
584 // Set 2nd highest bit (signed long int), to partition between ongoing and regular
585 key |= 0x4000000000000000L;
586 }
587 return key;
588 }
589
590 /**
591 * Locates and inserts the bubble into a sorted position. The is inserted
592 * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
593 * required to keep grouping intact.
594 *
595 * @param minPosition the first insert point to consider
Lyn Han72f50902019-10-25 15:55:49 -0700596 * @param newBubble the bubble to insert
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400597 * @return the position where the bubble was inserted
598 */
599 private int insertBubble(int minPosition, Bubble newBubble) {
600 long newBubbleSortKey = sortKey(newBubble);
601 String previousGroupId = null;
602
603 for (int pos = minPosition; pos < mBubbles.size(); pos++) {
604 Bubble bubbleAtPos = mBubbles.get(pos);
605 String groupIdAtPos = bubbleAtPos.getGroupId();
606 boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
607
608 if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
609 // Insert before the start of first group which has older bubbles.
610 mBubbles.add(pos, newBubble);
611 return pos;
612 }
613 previousGroupId = groupIdAtPos;
614 }
615 mBubbles.add(newBubble);
616 return mBubbles.size() - 1;
617 }
618
619 private boolean hasBubbleWithGroupId(String groupId) {
620 return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
621 }
622
623 private int findFirstIndexForGroup(String appId) {
624 for (int i = 0; i < mBubbles.size(); i++) {
625 Bubble bubbleAtPos = mBubbles.get(i);
626 if (bubbleAtPos.getGroupId().equals(appId)) {
627 return i;
628 }
629 }
630 return 0;
631 }
632
633 /**
634 * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
635 * at positions lower than {@code position} are unchanged. Relative order within the group
636 * unchanged. Relative order of any other bubbles are also unchanged.
637 *
638 * @param position the position of the first bubble for the group
Mark Renoufba5ab512019-05-02 15:21:01 -0400639 * @return true if the position of any bubbles has changed as a result
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400640 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400641 private boolean packGroup(int position) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200642 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400643 Log.d(TAG, "packGroup: position=" + position);
644 }
645 Bubble groupStart = mBubbles.get(position);
646 final String groupAppId = groupStart.getGroupId();
647 List<Bubble> moving = new ArrayList<>();
648
649 // Walk backward, collect bubbles within the group
650 for (int i = mBubbles.size() - 1; i > position; i--) {
651 if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
652 moving.add(0, mBubbles.get(i));
653 }
654 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400655 if (moving.isEmpty()) {
656 return false;
657 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400658 mBubbles.removeAll(moving);
659 mBubbles.addAll(position + 1, moving);
Mark Renoufba5ab512019-05-02 15:21:01 -0400660 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400661 }
662
Mark Renoufba5ab512019-05-02 15:21:01 -0400663 /**
664 * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped
665 * by groupId. Each group is then sorted by the max(lastUpdated) time of it's bubbles. Bubbles
666 * within each group are then sorted by lastUpdated descending.
667 *
668 * @return true if the position of any bubbles changed as a result
669 */
670 private boolean repackAll() {
Issei Suzukia8d07312019-06-07 12:56:19 +0200671 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400672 Log.d(TAG, "repackAll()");
673 }
674 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400675 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400676 }
677 Map<String, Long> groupLastActivity = new HashMap<>();
678 for (Bubble bubble : mBubbles) {
679 long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
680 long sortKeyForBubble = sortKey(bubble);
681 if (sortKeyForBubble > maxSortKeyForGroup) {
682 groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
683 }
684 }
685
686 // Sort groups by their most recently active bubble
687 List<String> groupsByMostRecentActivity =
688 groupLastActivity.entrySet().stream()
Mark Renoufba5ab512019-05-02 15:21:01 -0400689 .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400690 .map(Map.Entry::getKey)
691 .collect(toList());
692
693 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
694
695 // For each group, add bubbles, freshest to oldest
696 for (String appId : groupsByMostRecentActivity) {
697 mBubbles.stream()
698 .filter((b) -> b.getGroupId().equals(appId))
Mark Renoufba5ab512019-05-02 15:21:01 -0400699 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400700 .forEachOrdered(repacked::add);
701 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400702 if (repacked.equals(mBubbles)) {
703 return false;
704 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400705 mBubbles.clear();
706 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400707 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400708 }
709
Mark Renouf71a3af62019-04-08 15:02:54 -0400710 private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
711 if (reason == BubbleController.DISMISS_USER_GESTURE) {
712 Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
713 PendingIntent deleteIntent = bubbleMetadata != null
714 ? bubbleMetadata.getDeleteIntent()
715 : null;
716 if (deleteIntent != null) {
717 try {
718 deleteIntent.send();
719 } catch (PendingIntent.CanceledException e) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400720 Log.w(TAG, "Failed to send delete intent for bubble with key: "
721 + entry.getKey());
Mark Renouf71a3af62019-04-08 15:02:54 -0400722 }
723 }
724 }
725 }
726
Mark Renouf71a3af62019-04-08 15:02:54 -0400727 private int indexForKey(String key) {
728 for (int i = 0; i < mBubbles.size(); i++) {
729 Bubble bubble = mBubbles.get(i);
730 if (bubble.getKey().equals(key)) {
731 return i;
732 }
733 }
734 return -1;
735 }
736
Mark Renouf71a3af62019-04-08 15:02:54 -0400737 /**
Lyn Hanb58c7562020-01-07 14:29:20 -0800738 * The set of bubbles in row.
Mark Renouf71a3af62019-04-08 15:02:54 -0400739 */
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400740 @VisibleForTesting(visibility = PRIVATE)
741 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400742 return Collections.unmodifiableList(mBubbles);
743 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800744 /**
745 * The set of bubbles in overflow.
746 */
747 @VisibleForTesting(visibility = PRIVATE)
748 public List<Bubble> getOverflowBubbles() {
749 return Collections.unmodifiableList(mOverflowBubbles);
750 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400751
752 @VisibleForTesting(visibility = PRIVATE)
753 Bubble getBubbleWithKey(String key) {
754 for (int i = 0; i < mBubbles.size(); i++) {
755 Bubble bubble = mBubbles.get(i);
756 if (bubble.getKey().equals(key)) {
757 return bubble;
758 }
759 }
760 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800761 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400762
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400763 @VisibleForTesting(visibility = PRIVATE)
Lyn Han89274b42020-03-25 00:56:26 -0700764 Bubble getOverflowBubbleWithKey(String key) {
765 for (int i = 0; i < mOverflowBubbles.size(); i++) {
766 Bubble bubble = mOverflowBubbles.get(i);
767 if (bubble.getKey().equals(key)) {
768 return bubble;
769 }
770 }
771 return null;
772 }
773
774 @VisibleForTesting(visibility = PRIVATE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400775 void setTimeSource(TimeSource timeSource) {
776 mTimeSource = timeSource;
777 }
778
Mark Renouf3bc5b362019-04-05 14:37:59 -0400779 public void setListener(Listener listener) {
780 mListener = listener;
781 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400782
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700783 /**
784 * Description of current bubble data state.
785 */
786 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
Lyn Han72f50902019-10-25 15:55:49 -0700787 pw.print("selected: ");
788 pw.println(mSelectedBubble != null
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700789 ? mSelectedBubble.getKey()
790 : "null");
Lyn Han72f50902019-10-25 15:55:49 -0700791 pw.print("expanded: ");
792 pw.println(mExpanded);
793 pw.print("count: ");
794 pw.println(mBubbles.size());
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700795 for (Bubble bubble : mBubbles) {
796 bubble.dump(fd, pw, args);
797 }
Lyn Han72f50902019-10-25 15:55:49 -0700798 pw.print("summaryKeys: ");
799 pw.println(mSuppressedGroupKeys.size());
Mady Mellore28fe102019-07-09 15:33:32 -0700800 for (String key : mSuppressedGroupKeys.keySet()) {
801 pw.println(" suppressing: " + key);
802 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400803 }
Mark Renoufc19b4732019-06-26 12:08:33 -0400804}