blob: c170ee271e1dbb32cb961c6f3dbc892776dbe039 [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
Joshua Tsuji0f390fb2020-04-28 15:20:10 -040018import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
Mark Renouf71a3af62019-04-08 15:02:54 -040019import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
Issei Suzukia8d07312019-06-07 12:56:19 +020020import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
21import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
22import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
Mark Renouf71a3af62019-04-08 15:02:54 -040023
Pinyao Ting3c930612020-05-19 00:26:03 +000024import android.annotation.NonNull;
Mark Renouf71a3af62019-04-08 15:02:54 -040025import android.app.PendingIntent;
26import android.content.Context;
Joshua Tsuji33477392020-06-19 18:35:35 -040027import android.content.pm.ShortcutInfo;
Mark Renouf71a3af62019-04-08 15:02:54 -040028import android.util.Log;
Mark Renoufba5ab512019-05-02 15:21:01 -040029import android.util.Pair;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040030import android.view.View;
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;
Pinyao Ting175a5b82020-06-15 23:41:14 +000037import com.android.systemui.statusbar.notification.NotificationEntryManager;
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;
Joshua Tsuji33477392020-06-19 18:35:35 -040046import java.util.HashSet;
Mark Renouf3bc5b362019-04-05 14:37:59 -040047import java.util.List;
Mark Renouf71a3af62019-04-08 15:02:54 -040048import java.util.Objects;
Joshua Tsuji33477392020-06-19 18:35:35 -040049import java.util.Set;
50import java.util.function.Consumer;
51import java.util.function.Predicate;
Mady Mellor3dff9e62019-02-05 18:12:53 -080052
Mady Mellorcfd06c12019-02-13 14:32:12 -080053import javax.inject.Inject;
54import javax.inject.Singleton;
55
Mady Mellor3dff9e62019-02-05 18:12:53 -080056/**
57 * Keeps track of active bubbles.
58 */
Mady Mellorcfd06c12019-02-13 14:32:12 -080059@Singleton
Selim Cinekfdf80332019-03-07 17:29:55 -080060public class BubbleData {
Mady Mellor3dff9e62019-02-05 18:12:53 -080061
Lyn Han9aee7832020-05-26 12:11:06 -070062 BubbleLogger mLogger = new BubbleLoggerImpl();
63
Issei Suzukia8d07312019-06-07 12:56:19 +020064 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040065
Mark Renoufba5ab512019-05-02 15:21:01 -040066 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
67 Comparator.comparing(BubbleData::sortKey).reversed();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040068
Mark Renouf82a40e62019-05-23 16:16:24 -040069 /** Contains information about changes that have been made to the state of bubbles. */
70 static final class Update {
71 boolean expandedChanged;
72 boolean selectionChanged;
73 boolean orderChanged;
74 boolean expanded;
75 @Nullable Bubble selectedBubble;
76 @Nullable Bubble addedBubble;
77 @Nullable Bubble updatedBubble;
78 // Pair with Bubble and @DismissReason Integer
79 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
80
81 // A read-only view of the bubbles list, changes there will be reflected here.
82 final List<Bubble> bubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -080083 final List<Bubble> overflowBubbles;
Mark Renouf82a40e62019-05-23 16:16:24 -040084
Lyn Hanb58c7562020-01-07 14:29:20 -080085 private Update(List<Bubble> row, List<Bubble> overflow) {
86 bubbles = Collections.unmodifiableList(row);
87 overflowBubbles = Collections.unmodifiableList(overflow);
Mark Renouf82a40e62019-05-23 16:16:24 -040088 }
89
90 boolean anythingChanged() {
91 return expandedChanged
92 || selectionChanged
93 || addedBubble != null
94 || updatedBubble != null
95 || !removedBubbles.isEmpty()
96 || orderChanged;
97 }
98
Mady Mellor1d082022020-05-12 16:35:39 +000099 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400100 removedBubbles.add(new Pair<>(bubbleToRemove, reason));
101 }
102 }
103
Mark Renouf3bc5b362019-04-05 14:37:59 -0400104 /**
105 * This interface reports changes to the state and appearance of bubbles which should be applied
106 * as necessary to the UI.
Mark Renouf3bc5b362019-04-05 14:37:59 -0400107 */
108 interface Listener {
Mark Renouf82a40e62019-05-23 16:16:24 -0400109 /** Reports changes have have occurred as a result of the most recent operation. */
110 void applyUpdate(Update update);
Mark Renouf3bc5b362019-04-05 14:37:59 -0400111 }
112
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400113 interface TimeSource {
114 long currentTimeMillis();
115 }
116
Mark Renouf71a3af62019-04-08 15:02:54 -0400117 private final Context mContext;
Mady Mellor9ec37962020-01-10 10:21:03 -0800118 /** Bubbles that are actively in the stack. */
Mark Renouf82a40e62019-05-23 16:16:24 -0400119 private final List<Bubble> mBubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -0800120 /** Bubbles that aged out to overflow. */
121 private final List<Bubble> mOverflowBubbles;
Mady Mellor9ec37962020-01-10 10:21:03 -0800122 /** Bubbles that are being loaded but haven't been added to the stack just yet. */
Mady Mellor458a6262020-06-07 21:09:19 -0700123 private final HashMap<String, Bubble> mPendingBubbles;
Mark Renouf71a3af62019-04-08 15:02:54 -0400124 private Bubble mSelectedBubble;
Lyn Han89fb39d2020-04-07 11:51:07 -0700125 private boolean mShowingOverflow;
Mark Renouf71a3af62019-04-08 15:02:54 -0400126 private boolean mExpanded;
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800127 private final int mMaxBubbles;
Lyn Han2f6e89d2020-04-15 10:01:01 -0700128 private int mMaxOverflowBubbles;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400129
Mark Renoufba5ab512019-05-02 15:21:01 -0400130 // State tracked during an operation -- keeps track of what listener events to dispatch.
Mark Renouf82a40e62019-05-23 16:16:24 -0400131 private Update mStateChange;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400132
133 private TimeSource mTimeSource = System::currentTimeMillis;
134
135 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400136 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800137
Mady Mellorf44b6832020-01-14 13:26:14 -0800138 @Nullable
139 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
140
Mady Mellore28fe102019-07-09 15:33:32 -0700141 /**
142 * We track groups with summaries that aren't visibly displayed but still kept around because
143 * the bubble(s) associated with the summary still exist.
144 *
145 * The summary must be kept around so that developers can cancel it (and hence the bubbles
146 * associated with it). This list is used to check if the summary should be hidden from the
147 * shade.
148 *
149 * Key: group key of the NotificationEntry
150 * Value: key of the NotificationEntry
151 */
152 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
153
Mady Mellorcfd06c12019-02-13 14:32:12 -0800154 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400155 public BubbleData(Context context) {
156 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400157 mBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800158 mOverflowBubbles = new ArrayList<>();
Mady Mellor458a6262020-06-07 21:09:19 -0700159 mPendingBubbles = new HashMap<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800160 mStateChange = new Update(mBubbles, mOverflowBubbles);
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800161 mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
Lyn Hanb58c7562020-01-07 14:29:20 -0800162 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800163 }
164
Mady Mellorf44b6832020-01-14 13:26:14 -0800165 public void setSuppressionChangedListener(
166 BubbleController.NotificationSuppressionChangedListener listener) {
167 mSuppressionListener = listener;
168 }
169
Mark Renouf71a3af62019-04-08 15:02:54 -0400170 public boolean hasBubbles() {
171 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800172 }
173
Mark Renouf71a3af62019-04-08 15:02:54 -0400174 public boolean isExpanded() {
175 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800176 }
177
Lyn Han2f6e89d2020-04-15 10:01:01 -0700178 public boolean hasAnyBubbleWithKey(String key) {
179 return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key);
180 }
181
182 public boolean hasBubbleInStackWithKey(String key) {
183 return getBubbleInStackWithKey(key) != null;
184 }
185
186 public boolean hasOverflowBubbleWithKey(String key) {
187 return getOverflowBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800188 }
189
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400190 @Nullable
191 public Bubble getSelectedBubble() {
192 return mSelectedBubble;
193 }
194
Mark Renouf71a3af62019-04-08 15:02:54 -0400195 public void setExpanded(boolean expanded) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200196 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400197 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800198 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400199 setExpandedInternal(expanded);
200 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800201 }
202
Mark Renouf71a3af62019-04-08 15:02:54 -0400203 public void setSelectedBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200204 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400205 Log.d(TAG, "setSelectedBubble: " + bubble);
206 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400207 setSelectedBubbleInternal(bubble);
208 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400209 }
210
Lyn Han89fb39d2020-04-07 11:51:07 -0700211 void setShowingOverflow(boolean showingOverflow) {
212 mShowingOverflow = showingOverflow;
213 }
214
Mady Mellor3df7ab02019-12-09 15:07:10 -0800215 /**
216 * Constructs a new bubble or returns an existing one. Does not add new bubbles to
217 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
218 * for that.
Mady Mellor458a6262020-06-07 21:09:19 -0700219 *
220 * @param entry The notification entry to use, only null if it's a bubble being promoted from
221 * the overflow that was persisted over reboot.
222 * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from
223 * the overflow that was persisted over reboot.
Mady Mellor3df7ab02019-12-09 15:07:10 -0800224 */
Mady Mellor458a6262020-06-07 21:09:19 -0700225 Bubble getOrCreateBubble(NotificationEntry entry, Bubble persistedBubble) {
226 String key = entry != null ? entry.getKey() : persistedBubble.getKey();
227 Bubble bubbleToReturn = getBubbleInStackWithKey(key);
228
229 if (bubbleToReturn == null) {
230 bubbleToReturn = getOverflowBubbleWithKey(key);
231 if (bubbleToReturn != null) {
232 // Promoting from overflow
233 mOverflowBubbles.remove(bubbleToReturn);
234 } else if (mPendingBubbles.containsKey(key)) {
235 // Update while it was pending
236 bubbleToReturn = mPendingBubbles.get(key);
237 } else if (entry != null) {
238 // New bubble
239 bubbleToReturn = new Bubble(entry, mSuppressionListener);
240 } else {
241 // Persisted bubble being promoted
242 bubbleToReturn = persistedBubble;
Lyn Hanbf1b3d62020-03-12 10:31:19 -0700243 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800244 }
Mady Mellor458a6262020-06-07 21:09:19 -0700245
246 if (entry != null) {
247 bubbleToReturn.setEntry(entry);
248 }
249 mPendingBubbles.put(key, bubbleToReturn);
250 return bubbleToReturn;
Mady Mellor3df7ab02019-12-09 15:07:10 -0800251 }
252
253 /**
254 * When this method is called it is expected that all info in the bubble has completed loading.
255 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
256 * BubbleStackView, BubbleIconFactory).
257 */
258 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200259 if (DEBUG_BUBBLE_DATA) {
Mady Mellor3df7ab02019-12-09 15:07:10 -0800260 Log.d(TAG, "notificationEntryUpdated: " + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400261 }
Mady Mellor458a6262020-06-07 21:09:19 -0700262 mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here
Lyn Han2f6e89d2020-04-15 10:01:01 -0700263 Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
Pinyao Ting175a5b82020-06-15 23:41:14 +0000264 suppressFlyout |= !bubble.isVisuallyInterruptive();
Lyn Han405e0b72019-08-13 16:07:55 -0700265
Mady Mellor3df7ab02019-12-09 15:07:10 -0800266 if (prevBubble == null) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400267 // Create a new bubble
Mark Renoufc19b4732019-06-26 12:08:33 -0400268 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400269 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400270 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400271 } else {
272 // Updates an existing bubble
Lyn Han405e0b72019-08-13 16:07:55 -0700273 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400274 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400275 }
Mady Mellor76343012020-05-13 11:02:50 -0700276
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700277 if (bubble.shouldAutoExpand()) {
Mady Mellor76343012020-05-13 11:02:50 -0700278 bubble.setShouldAutoExpand(false);
Mark Renouf71a3af62019-04-08 15:02:54 -0400279 setSelectedBubbleInternal(bubble);
280 if (!mExpanded) {
281 setExpandedInternal(true);
282 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400283 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800284
Mady Mellorf44b6832020-01-14 13:26:14 -0800285 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
286 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
287 bubble.setSuppressNotification(suppress);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400288 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
Mady Mellorf44b6832020-01-14 13:26:14 -0800289
290 dispatchPendingChanges();
Mark Renoufba5ab512019-05-02 15:21:01 -0400291 }
292
Pinyao Ting3c930612020-05-19 00:26:03 +0000293 /**
Joshua Tsuji33477392020-06-19 18:35:35 -0400294 * Dismisses the bubble with the matching key, if it exists.
Pinyao Ting3c930612020-05-19 00:26:03 +0000295 */
Joshua Tsuji33477392020-06-19 18:35:35 -0400296 public void dismissBubbleWithKey(String key, @DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200297 if (DEBUG_BUBBLE_DATA) {
Pinyao Ting3c930612020-05-19 00:26:03 +0000298 Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400299 }
Pinyao Ting3c930612020-05-19 00:26:03 +0000300 doRemove(key, 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 */
Pinyao Ting175a5b82020-06-15 23:41:14 +0000342 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey, @NonNull
343 NotificationEntryManager nem) {
Mady Mellor22f2f072019-04-18 13:26:18 -0700344 ArrayList<Bubble> bubbleChildren = new ArrayList<>();
345 if (groupKey == null) {
346 return bubbleChildren;
347 }
348 for (Bubble b : mBubbles) {
Pinyao Ting175a5b82020-06-15 23:41:14 +0000349 final NotificationEntry entry = nem.getPendingOrActiveNotif(b.getKey());
350 if (entry != null && groupKey.equals(entry.getSbn().getGroupKey())) {
Mady Mellor22f2f072019-04-18 13:26:18 -0700351 bubbleChildren.add(b);
352 }
353 }
354 return bubbleChildren;
355 }
356
Joshua Tsuji33477392020-06-19 18:35:35 -0400357 /**
358 * Removes bubbles from the given package whose shortcut are not in the provided list of valid
359 * shortcuts.
360 */
361 public void removeBubblesWithInvalidShortcuts(
362 String packageName, List<ShortcutInfo> validShortcuts, int reason) {
363
364 final Set<String> validShortcutIds = new HashSet<String>();
365 for (ShortcutInfo info : validShortcuts) {
366 validShortcutIds.add(info.getId());
367 }
368
Josh Tsujia5d19f62020-06-23 18:44:14 -0400369 final Predicate<Bubble> invalidBubblesFromPackage = bubble -> {
370 final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName());
Mady Mellor340103ba2020-06-24 16:15:48 -0700371 final boolean isShortcutBubble = bubble.hasMetadataShortcutId();
372 if (!bubbleIsFromPackage || !isShortcutBubble) {
373 return false;
374 }
Josh Tsujia5d19f62020-06-23 18:44:14 -0400375 final boolean hasShortcutIdAndValidShortcut =
376 bubble.hasMetadataShortcutId()
377 && bubble.getShortcutInfo() != null
378 && bubble.getShortcutInfo().isEnabled()
379 && validShortcutIds.contains(bubble.getShortcutInfo().getId());
380 return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut;
381 };
Joshua Tsuji33477392020-06-19 18:35:35 -0400382
383 final Consumer<Bubble> removeBubble = bubble ->
384 dismissBubbleWithKey(bubble.getKey(), reason);
385
386 performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble);
387 performActionOnBubblesMatching(
388 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble);
389 }
390
391 /** Dismisses all bubbles from the given package. */
392 public void removeBubblesWithPackageName(String packageName, int reason) {
393 final Predicate<Bubble> bubbleMatchesPackage = bubble ->
394 bubble.getPackageName().equals(packageName);
395
396 final Consumer<Bubble> removeBubble = bubble ->
397 dismissBubbleWithKey(bubble.getKey(), reason);
398
399 performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble);
400 performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble);
401 }
402
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400403 private void doAdd(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200404 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400405 Log.d(TAG, "doAdd: " + bubble);
406 }
Mady Mellora55133d2020-05-01 15:11:10 -0700407 mBubbles.add(0, bubble);
Mark Renouf82a40e62019-05-23 16:16:24 -0400408 mStateChange.addedBubble = bubble;
Mady Mellora55133d2020-05-01 15:11:10 -0700409 // Adding the first bubble doesn't change the order
410 mStateChange.orderChanged = mBubbles.size() > 1;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400411 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400412 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400413 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400414 }
415
416 private void trim() {
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800417 if (mBubbles.size() > mMaxBubbles) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400418 mBubbles.stream()
419 // sort oldest first (ascending lastActivity)
420 .sorted(Comparator.comparingLong(Bubble::getLastActivity))
421 // skip the selected bubble
422 .filter((b) -> !b.equals(mSelectedBubble))
423 .findFirst()
Mark Renoufba5ab512019-05-02 15:21:01 -0400424 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400425 }
426 }
427
428 private void doUpdate(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200429 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400430 Log.d(TAG, "doUpdate: " + bubble);
431 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400432 mStateChange.updatedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400433 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400434 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400435 mBubbles.remove(bubble);
Mady Mellora55133d2020-05-01 15:11:10 -0700436 mBubbles.add(0, bubble);
437 mStateChange.orderChanged = prevPos != 0;
Mark Renoufba5ab512019-05-02 15:21:01 -0400438 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400439 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400440 }
441
Joshua Tsuji33477392020-06-19 18:35:35 -0400442 /** Runs the given action on Bubbles that match the given predicate. */
443 private void performActionOnBubblesMatching(
444 List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) {
445 final List<Bubble> matchingBubbles = new ArrayList<>();
446 for (Bubble bubble : bubbles) {
447 if (predicate.test(bubble)) {
448 matchingBubbles.add(bubble);
449 }
450 }
451
452 for (Bubble matchingBubble : matchingBubbles) {
453 action.accept(matchingBubble);
454 }
455 }
456
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400457 private void doRemove(String key, @DismissReason int reason) {
Lyn Hane1395572020-03-23 15:48:54 -0700458 if (DEBUG_BUBBLE_DATA) {
459 Log.d(TAG, "doRemove: " + key);
460 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800461 // If it was pending remove it
Mady Mellor458a6262020-06-07 21:09:19 -0700462 if (mPendingBubbles.containsKey(key)) {
463 mPendingBubbles.remove(key);
Mady Mellor9ec37962020-01-10 10:21:03 -0800464 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400465 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400466 if (indexToRemove == -1) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700467 if (hasOverflowBubbleWithKey(key)
468 && (reason == BubbleController.DISMISS_NOTIF_CANCEL
Joshua Tsuji33477392020-06-19 18:35:35 -0400469 || reason == BubbleController.DISMISS_GROUP_CANCELLED
470 || reason == BubbleController.DISMISS_NO_LONGER_BUBBLE
471 || reason == BubbleController.DISMISS_BLOCKED
472 || reason == BubbleController.DISMISS_SHORTCUT_REMOVED
473 || reason == BubbleController.DISMISS_PACKAGE_REMOVED)) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700474
475 Bubble b = getOverflowBubbleWithKey(key);
476 if (DEBUG_BUBBLE_DATA) {
477 Log.d(TAG, "Cancel overflow bubble: " + b);
478 }
Lyn Han9aee7832020-05-26 12:11:06 -0700479 mLogger.logOverflowRemove(b, reason);
Lyn Hane4274be2020-04-24 17:55:36 -0700480 mStateChange.bubbleRemoved(b, reason);
Mady Mellor1d082022020-05-12 16:35:39 +0000481 mOverflowBubbles.remove(b);
Lyn Han2f6e89d2020-04-15 10:01:01 -0700482 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400483 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400484 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400485 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
486 if (mBubbles.size() == 1) {
487 // Going to become empty, handle specially.
488 setExpandedInternal(false);
Mady Mellor55ddc122020-06-10 22:04:23 -0700489 // Don't use setSelectedBubbleInternal because we don't want to trigger an applyUpdate
490 mSelectedBubble = null;
Mark Renoufba5ab512019-05-02 15:21:01 -0400491 }
492 if (indexToRemove < mBubbles.size() - 1) {
493 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400494 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400495 }
496 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400497 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400498 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400499 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400500 }
501
Lyn Hand6981862020-02-18 18:01:43 -0800502 overflowBubble(reason, bubbleToRemove);
Lyn Hanb58c7562020-01-07 14:29:20 -0800503
Mark Renoufba5ab512019-05-02 15:21:01 -0400504 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
505 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
506 // Move selection to the new bubble at the same position.
507 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
508 Bubble newSelected = mBubbles.get(newIndex);
509 setSelectedBubbleInternal(newSelected);
510 }
Pinyao Ting175a5b82020-06-15 23:41:14 +0000511 maybeSendDeleteIntent(reason, bubbleToRemove);
Mark Renouf71a3af62019-04-08 15:02:54 -0400512 }
513
Lyn Hand6981862020-02-18 18:01:43 -0800514 void overflowBubble(@DismissReason int reason, Bubble bubble) {
Lyn Han6cb4e5f2020-04-27 15:11:18 -0700515 if (bubble.getPendingIntentCanceled()
516 || !(reason == BubbleController.DISMISS_AGED
Lyn Han2f6e89d2020-04-15 10:01:01 -0700517 || reason == BubbleController.DISMISS_USER_GESTURE)) {
518 return;
519 }
520 if (DEBUG_BUBBLE_DATA) {
521 Log.d(TAG, "Overflowing: " + bubble);
522 }
Lyn Han9aee7832020-05-26 12:11:06 -0700523 mLogger.logOverflowAdd(bubble, reason);
Lyn Han2f6e89d2020-04-15 10:01:01 -0700524 mOverflowBubbles.add(0, bubble);
525 bubble.stopInflation();
526 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
527 // Remove oldest bubble.
528 Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1);
Lyn Hand6981862020-02-18 18:01:43 -0800529 if (DEBUG_BUBBLE_DATA) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700530 Log.d(TAG, "Overflow full. Remove: " + oldest);
Lyn Hand6981862020-02-18 18:01:43 -0800531 }
Lyn Hane4274be2020-04-24 17:55:36 -0700532 mStateChange.bubbleRemoved(oldest, BubbleController.DISMISS_OVERFLOW_MAX_REACHED);
Lyn Han9aee7832020-05-26 12:11:06 -0700533 mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED);
Mady Mellor1d082022020-05-12 16:35:39 +0000534 mOverflowBubbles.remove(oldest);
Lyn Hand6981862020-02-18 18:01:43 -0800535 }
536 }
537
Mark Renouf71a3af62019-04-08 15:02:54 -0400538 public void dismissAll(@DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200539 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400540 Log.d(TAG, "dismissAll: reason=" + reason);
541 }
542 if (mBubbles.isEmpty()) {
543 return;
544 }
545 setExpandedInternal(false);
546 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400547 while (!mBubbles.isEmpty()) {
Lyn Hand6981862020-02-18 18:01:43 -0800548 doRemove(mBubbles.get(0).getKey(), reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400549 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400550 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400551 }
552
Mady Mellorca184aae2019-09-17 16:07:12 -0700553 /**
554 * Indicates that the provided display is no longer in use and should be cleaned up.
555 *
556 * @param displayId the id of the display to clean up.
557 */
558 void notifyDisplayEmpty(int displayId) {
559 for (Bubble b : mBubbles) {
560 if (b.getDisplayId() == displayId) {
561 if (b.getExpandedView() != null) {
562 b.getExpandedView().notifyDisplayEmpty();
563 }
564 return;
565 }
566 }
567 }
568
Mark Renoufba5ab512019-05-02 15:21:01 -0400569 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400570 if (mListener != null && mStateChange.anythingChanged()) {
571 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400572 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800573 mStateChange = new Update(mBubbles, mOverflowBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400574 }
575
576 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400577 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400578 *
579 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400580 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400581 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200582 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400583 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
584 }
Lyn Han89fb39d2020-04-07 11:51:07 -0700585 if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400586 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400587 }
Lyn Han89fb39d2020-04-07 11:51:07 -0700588 // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.
589
Lyn Han1e19d7f2020-02-05 19:10:58 -0800590 if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400591 Log.e(TAG, "Cannot select bubble which doesn't exist!"
592 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400593 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400594 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400595 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700596 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400597 }
598 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400599 mStateChange.selectedBubble = bubble;
600 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400601 }
602
Mark Renouf71a3af62019-04-08 15:02:54 -0400603 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400604 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400605 *
606 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400607 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400608 private void setExpandedInternal(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200609 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400610 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
611 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400612 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400613 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400614 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400615 if (shouldExpand) {
616 if (mBubbles.isEmpty()) {
617 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400618 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400619 }
620 if (mSelectedBubble == null) {
621 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400622 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400623 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400624 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400625 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400626 } else if (!mBubbles.isEmpty()) {
627 // Apply ordering and grouping rules from expanded -> collapsed, then save
628 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400629 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400630 // Save the state which should be returned to when expanded (with no other changes)
631
Lyn Han89fb39d2020-04-07 11:51:07 -0700632 if (mShowingOverflow) {
633 // Show previously selected bubble instead of overflow menu on next expansion.
634 setSelectedBubbleInternal(mSelectedBubble);
635 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400636 if (mBubbles.indexOf(mSelectedBubble) > 0) {
637 // Move the selected bubble to the top while collapsed.
Mady Mellora55133d2020-05-01 15:11:10 -0700638 int index = mBubbles.indexOf(mSelectedBubble);
639 if (index != 0) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400640 mBubbles.remove(mSelectedBubble);
641 mBubbles.add(0, mSelectedBubble);
Mady Mellora55133d2020-05-01 15:11:10 -0700642 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400643 }
644 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400645 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400646 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400647 mStateChange.expanded = shouldExpand;
648 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400649 }
650
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400651 private static long sortKey(Bubble bubble) {
Mady Mellora55133d2020-05-01 15:11:10 -0700652 return bubble.getLastActivity();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400653 }
654
655 /**
Mady Mellora55133d2020-05-01 15:11:10 -0700656 * This applies a full sort and group pass to all existing bubbles.
657 * Bubbles are sorted by lastUpdated descending.
Mark Renoufba5ab512019-05-02 15:21:01 -0400658 *
659 * @return true if the position of any bubbles changed as a result
660 */
661 private boolean repackAll() {
Issei Suzukia8d07312019-06-07 12:56:19 +0200662 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400663 Log.d(TAG, "repackAll()");
664 }
665 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400666 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400667 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400668 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
Mady Mellora55133d2020-05-01 15:11:10 -0700669 // Add bubbles, freshest to oldest
670 mBubbles.stream()
671 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
672 .forEachOrdered(repacked::add);
Mark Renoufba5ab512019-05-02 15:21:01 -0400673 if (repacked.equals(mBubbles)) {
674 return false;
675 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400676 mBubbles.clear();
677 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400678 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400679 }
680
Pinyao Ting175a5b82020-06-15 23:41:14 +0000681 private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) {
682 if (reason != BubbleController.DISMISS_USER_GESTURE) return;
683 PendingIntent deleteIntent = bubble.getDeleteIntent();
684 if (deleteIntent == null) return;
685 try {
686 deleteIntent.send();
687 } catch (PendingIntent.CanceledException e) {
688 Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey());
Mark Renouf71a3af62019-04-08 15:02:54 -0400689 }
690 }
691
Mark Renouf71a3af62019-04-08 15:02:54 -0400692 private int indexForKey(String key) {
693 for (int i = 0; i < mBubbles.size(); i++) {
694 Bubble bubble = mBubbles.get(i);
695 if (bubble.getKey().equals(key)) {
696 return i;
697 }
698 }
699 return -1;
700 }
701
Mark Renouf71a3af62019-04-08 15:02:54 -0400702 /**
Lyn Hanb58c7562020-01-07 14:29:20 -0800703 * The set of bubbles in row.
Mark Renouf71a3af62019-04-08 15:02:54 -0400704 */
Joshua Tsuji0f390fb2020-04-28 15:20:10 -0400705 @VisibleForTesting(visibility = PACKAGE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400706 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400707 return Collections.unmodifiableList(mBubbles);
708 }
Mady Mellora55133d2020-05-01 15:11:10 -0700709
Lyn Hanb58c7562020-01-07 14:29:20 -0800710 /**
711 * The set of bubbles in overflow.
712 */
713 @VisibleForTesting(visibility = PRIVATE)
Mady Mellora55133d2020-05-01 15:11:10 -0700714 List<Bubble> getOverflowBubbles() {
Lyn Hanb58c7562020-01-07 14:29:20 -0800715 return Collections.unmodifiableList(mOverflowBubbles);
716 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400717
718 @VisibleForTesting(visibility = PRIVATE)
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400719 @Nullable
Lyn Han2f6e89d2020-04-15 10:01:01 -0700720 Bubble getAnyBubbleWithkey(String key) {
721 Bubble b = getBubbleInStackWithKey(key);
722 if (b == null) {
723 b = getOverflowBubbleWithKey(key);
724 }
725 return b;
726 }
727
728 @VisibleForTesting(visibility = PRIVATE)
729 @Nullable
730 Bubble getBubbleInStackWithKey(String key) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400731 for (int i = 0; i < mBubbles.size(); i++) {
732 Bubble bubble = mBubbles.get(i);
733 if (bubble.getKey().equals(key)) {
734 return bubble;
735 }
736 }
737 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800738 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400739
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400740 @Nullable
741 Bubble getBubbleWithView(View view) {
742 for (int i = 0; i < mBubbles.size(); i++) {
743 Bubble bubble = mBubbles.get(i);
744 if (bubble.getIconView() != null && bubble.getIconView().equals(view)) {
745 return bubble;
746 }
747 }
748 return null;
749 }
750
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400751 @VisibleForTesting(visibility = PRIVATE)
Lyn Han89274b42020-03-25 00:56:26 -0700752 Bubble getOverflowBubbleWithKey(String key) {
753 for (int i = 0; i < mOverflowBubbles.size(); i++) {
754 Bubble bubble = mOverflowBubbles.get(i);
755 if (bubble.getKey().equals(key)) {
756 return bubble;
757 }
758 }
759 return null;
760 }
761
762 @VisibleForTesting(visibility = PRIVATE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400763 void setTimeSource(TimeSource timeSource) {
764 mTimeSource = timeSource;
765 }
766
Mark Renouf3bc5b362019-04-05 14:37:59 -0400767 public void setListener(Listener listener) {
768 mListener = listener;
769 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400770
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700771 /**
Lyn Han2f6e89d2020-04-15 10:01:01 -0700772 * Set maximum number of bubbles allowed in overflow.
773 * This method should only be used in tests, not in production.
774 */
775 @VisibleForTesting
776 void setMaxOverflowBubbles(int maxOverflowBubbles) {
777 mMaxOverflowBubbles = maxOverflowBubbles;
778 }
779
780 /**
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700781 * Description of current bubble data state.
782 */
783 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
Lyn Han72f50902019-10-25 15:55:49 -0700784 pw.print("selected: ");
785 pw.println(mSelectedBubble != null
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700786 ? mSelectedBubble.getKey()
787 : "null");
Lyn Han72f50902019-10-25 15:55:49 -0700788 pw.print("expanded: ");
789 pw.println(mExpanded);
790 pw.print("count: ");
791 pw.println(mBubbles.size());
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700792 for (Bubble bubble : mBubbles) {
793 bubble.dump(fd, pw, args);
794 }
Lyn Han72f50902019-10-25 15:55:49 -0700795 pw.print("summaryKeys: ");
796 pw.println(mSuppressedGroupKeys.size());
Mady Mellore28fe102019-07-09 15:33:32 -0700797 for (String key : mSuppressedGroupKeys.keySet()) {
798 pw.println(" suppressing: " + key);
799 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400800 }
Mark Renoufc19b4732019-06-26 12:08:33 -0400801}