blob: 7b323ce8f6091ff2ac8cd6a1b235f4aa67236d69 [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
Mark Renouf9ba6cea2019-04-17 11:53:50 -040024import static java.util.stream.Collectors.toList;
25
Pinyao Ting293b83d2020-05-06 17:10:56 -070026import android.annotation.NonNull;
Mark Renouf71a3af62019-04-08 15:02:54 -040027import android.app.Notification;
28import android.app.PendingIntent;
29import android.content.Context;
Mark Renoufbbcf07f2019-05-09 10:42:43 -040030import android.service.notification.NotificationListenerService;
Mark Renouf71a3af62019-04-08 15:02:54 -040031import android.util.Log;
Mark Renoufba5ab512019-05-02 15:21:01 -040032import android.util.Pair;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040033import android.view.View;
Mady Mellor3dff9e62019-02-05 18:12:53 -080034
Mark Renouf9ba6cea2019-04-17 11:53:50 -040035import androidx.annotation.Nullable;
36
Selim Cinekfdf80332019-03-07 17:29:55 -080037import com.android.internal.annotations.VisibleForTesting;
Mady Mellor3df7ab02019-12-09 15:07:10 -080038import com.android.systemui.R;
Mark Renouf71a3af62019-04-08 15:02:54 -040039import com.android.systemui.bubbles.BubbleController.DismissReason;
Mady Mellor3dff9e62019-02-05 18:12:53 -080040import com.android.systemui.statusbar.notification.collection.NotificationEntry;
41
Mady Mellor70cba7bb2019-07-02 15:06:07 -070042import java.io.FileDescriptor;
43import java.io.PrintWriter;
Mark Renouf71a3af62019-04-08 15:02:54 -040044import java.util.ArrayList;
Mark Renouf71a3af62019-04-08 15:02:54 -040045import java.util.Collections;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040046import java.util.Comparator;
47import java.util.HashMap;
Mark Renouf3bc5b362019-04-05 14:37:59 -040048import java.util.List;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040049import java.util.Map;
Mark Renouf71a3af62019-04-08 15:02:54 -040050import java.util.Objects;
Mady Mellor3dff9e62019-02-05 18:12:53 -080051
Mady Mellorcfd06c12019-02-13 14:32:12 -080052import javax.inject.Inject;
53import javax.inject.Singleton;
54
Mady Mellor3dff9e62019-02-05 18:12:53 -080055/**
56 * Keeps track of active bubbles.
57 */
Mady Mellorcfd06c12019-02-13 14:32:12 -080058@Singleton
Selim Cinekfdf80332019-03-07 17:29:55 -080059public class BubbleData {
Mady Mellor3dff9e62019-02-05 18:12:53 -080060
Issei Suzukia8d07312019-06-07 12:56:19 +020061 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040062
Mark Renoufba5ab512019-05-02 15:21:01 -040063 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
64 Comparator.comparing(BubbleData::sortKey).reversed();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040065
Mark Renoufba5ab512019-05-02 15:21:01 -040066 private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING =
Mark Renouf9ba6cea2019-04-17 11:53:50 -040067 Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
Mark Renouf71a3af62019-04-08 15:02:54 -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. */
123 private final List<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
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400133 private NotificationListenerService.Ranking mTmpRanking;
134
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400135 private TimeSource mTimeSource = System::currentTimeMillis;
136
137 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400138 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800139
Mady Mellorf44b6832020-01-14 13:26:14 -0800140 @Nullable
141 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
142
Mady Mellore28fe102019-07-09 15:33:32 -0700143 /**
144 * We track groups with summaries that aren't visibly displayed but still kept around because
145 * the bubble(s) associated with the summary still exist.
146 *
147 * The summary must be kept around so that developers can cancel it (and hence the bubbles
148 * associated with it). This list is used to check if the summary should be hidden from the
149 * shade.
150 *
151 * Key: group key of the NotificationEntry
152 * Value: key of the NotificationEntry
153 */
154 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
155
Mady Mellorcfd06c12019-02-13 14:32:12 -0800156 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400157 public BubbleData(Context context) {
158 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400159 mBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800160 mOverflowBubbles = new ArrayList<>();
Mady Mellor9ec37962020-01-10 10:21:03 -0800161 mPendingBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800162 mStateChange = new Update(mBubbles, mOverflowBubbles);
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800163 mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
Lyn Hanb58c7562020-01-07 14:29:20 -0800164 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800165 }
166
Mady Mellorf44b6832020-01-14 13:26:14 -0800167 public void setSuppressionChangedListener(
168 BubbleController.NotificationSuppressionChangedListener listener) {
169 mSuppressionListener = listener;
170 }
171
Mark Renouf71a3af62019-04-08 15:02:54 -0400172 public boolean hasBubbles() {
173 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800174 }
175
Mark Renouf71a3af62019-04-08 15:02:54 -0400176 public boolean isExpanded() {
177 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800178 }
179
Lyn Han2f6e89d2020-04-15 10:01:01 -0700180 public boolean hasAnyBubbleWithKey(String key) {
181 return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key);
182 }
183
184 public boolean hasBubbleInStackWithKey(String key) {
185 return getBubbleInStackWithKey(key) != null;
186 }
187
188 public boolean hasOverflowBubbleWithKey(String key) {
189 return getOverflowBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800190 }
191
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400192 @Nullable
193 public Bubble getSelectedBubble() {
194 return mSelectedBubble;
195 }
196
Mark Renouf71a3af62019-04-08 15:02:54 -0400197 public void setExpanded(boolean expanded) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200198 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400199 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800200 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400201 setExpandedInternal(expanded);
202 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800203 }
204
Mark Renouf71a3af62019-04-08 15:02:54 -0400205 public void setSelectedBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200206 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400207 Log.d(TAG, "setSelectedBubble: " + bubble);
208 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400209 setSelectedBubbleInternal(bubble);
210 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400211 }
212
Lyn Han1e19d7f2020-02-05 19:10:58 -0800213 public void promoteBubbleFromOverflow(Bubble bubble, BubbleStackView stack,
214 BubbleIconFactory factory) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800215 if (DEBUG_BUBBLE_DATA) {
216 Log.d(TAG, "promoteBubbleFromOverflow: " + bubble);
217 }
Lyn Han18cdc1c2020-03-18 19:12:42 -0700218 moveOverflowBubbleToPending(bubble);
Lyn Han2f6e89d2020-04-15 10:01:01 -0700219 // Preserve new order for next repack, which sorts by last updated time.
220 bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
Lyn Han1e19d7f2020-02-05 19:10:58 -0800221 bubble.inflate(
Lyn Han4438d9b2020-03-11 18:04:35 -0700222 b -> {
223 notificationEntryUpdated(bubble, /* suppressFlyout */
224 false, /* showInShade */ true);
Lyn Han89274b42020-03-25 00:56:26 -0700225 setSelectedBubble(bubble);
Lyn Han4438d9b2020-03-11 18:04:35 -0700226 },
Pinyao Ting293b83d2020-05-06 17:10:56 -0700227 mContext, stack, factory, false /* skipInflation */);
Lyn Hanb58c7562020-01-07 14:29:20 -0800228 dispatchPendingChanges();
229 }
230
Lyn Han89fb39d2020-04-07 11:51:07 -0700231 void setShowingOverflow(boolean showingOverflow) {
232 mShowingOverflow = showingOverflow;
233 }
234
Lyn Han18cdc1c2020-03-18 19:12:42 -0700235 private void moveOverflowBubbleToPending(Bubble b) {
Lyn Han18cdc1c2020-03-18 19:12:42 -0700236 mOverflowBubbles.remove(b);
237 mPendingBubbles.add(b);
238 }
239
Mady Mellor3df7ab02019-12-09 15:07:10 -0800240 /**
241 * Constructs a new bubble or returns an existing one. Does not add new bubbles to
242 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
243 * for that.
244 */
245 Bubble getOrCreateBubble(NotificationEntry entry) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700246 String key = entry.getKey();
247 Bubble bubble = getBubbleInStackWithKey(entry.getKey());
248 if (bubble != null) {
249 bubble.setEntry(entry);
250 } else {
251 bubble = getOverflowBubbleWithKey(key);
252 if (bubble != null) {
253 moveOverflowBubbleToPending(bubble);
254 bubble.setEntry(entry);
255 return bubble;
Lyn Hanbf1b3d62020-03-12 10:31:19 -0700256 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800257 // Check for it in pending
258 for (int i = 0; i < mPendingBubbles.size(); i++) {
259 Bubble b = mPendingBubbles.get(i);
260 if (b.getKey().equals(entry.getKey())) {
Mady Mellore45ff862020-03-24 15:54:50 -0700261 b.setEntry(entry);
Mady Mellor9ec37962020-01-10 10:21:03 -0800262 return b;
263 }
264 }
Mady Mellorf44b6832020-01-14 13:26:14 -0800265 bubble = new Bubble(entry, mSuppressionListener);
Mady Mellor9ec37962020-01-10 10:21:03 -0800266 mPendingBubbles.add(bubble);
Mady Mellor3df7ab02019-12-09 15:07:10 -0800267 }
268 return bubble;
269 }
270
271 /**
272 * When this method is called it is expected that all info in the bubble has completed loading.
273 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
274 * BubbleStackView, BubbleIconFactory).
275 */
276 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200277 if (DEBUG_BUBBLE_DATA) {
Mady Mellor3df7ab02019-12-09 15:07:10 -0800278 Log.d(TAG, "notificationEntryUpdated: " + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400279 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800280 mPendingBubbles.remove(bubble); // No longer pending once we're here
Lyn Han2f6e89d2020-04-15 10:01:01 -0700281 Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
Pinyao Ting293b83d2020-05-06 17:10:56 -0700282 suppressFlyout |= bubble.getEntry() == null
283 || !bubble.getEntry().getRanking().visuallyInterruptive();
Lyn Han405e0b72019-08-13 16:07:55 -0700284
Mady Mellor3df7ab02019-12-09 15:07:10 -0800285 if (prevBubble == null) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400286 // Create a new bubble
Mark Renoufc19b4732019-06-26 12:08:33 -0400287 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400288 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400289 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400290 } else {
291 // Updates an existing bubble
Lyn Han405e0b72019-08-13 16:07:55 -0700292 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400293 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400294 }
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700295 if (bubble.shouldAutoExpand()) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400296 setSelectedBubbleInternal(bubble);
297 if (!mExpanded) {
298 setExpandedInternal(true);
299 }
300 } else if (mSelectedBubble == null) {
301 setSelectedBubbleInternal(bubble);
302 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800303
Mady Mellorf44b6832020-01-14 13:26:14 -0800304 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
305 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
306 bubble.setSuppressNotification(suppress);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400307 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
Mady Mellorf44b6832020-01-14 13:26:14 -0800308
309 dispatchPendingChanges();
Mark Renoufba5ab512019-05-02 15:21:01 -0400310 }
311
Pinyao Ting293b83d2020-05-06 17:10:56 -0700312 /**
313 * Called when a notification associated with a bubble is removed.
314 */
315 public void notificationEntryRemoved(String key, @DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200316 if (DEBUG_BUBBLE_DATA) {
Pinyao Ting293b83d2020-05-06 17:10:56 -0700317 Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400318 }
Pinyao Ting293b83d2020-05-06 17:10:56 -0700319 doRemove(key, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400320 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400321 }
322
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400323 /**
Mady Mellore28fe102019-07-09 15:33:32 -0700324 * Adds a group key indicating that the summary for this group should be suppressed.
325 *
326 * @param groupKey the group key of the group whose summary should be suppressed.
327 * @param notifKey the notification entry key of that summary.
328 */
329 void addSummaryToSuppress(String groupKey, String notifKey) {
330 mSuppressedGroupKeys.put(groupKey, notifKey);
331 }
332
333 /**
334 * Retrieves the notif entry key of the summary associated with the provided group key.
335 *
336 * @param groupKey the group to look up
337 * @return the key for the {@link NotificationEntry} that is the summary of this group.
338 */
339 String getSummaryKey(String groupKey) {
340 return mSuppressedGroupKeys.get(groupKey);
341 }
342
343 /**
344 * Removes a group key indicating that summary for this group should no longer be suppressed.
345 */
346 void removeSuppressedSummary(String groupKey) {
347 mSuppressedGroupKeys.remove(groupKey);
348 }
349
350 /**
351 * Whether the summary for the provided group key is suppressed.
352 */
353 boolean isSummarySuppressed(String groupKey) {
354 return mSuppressedGroupKeys.containsKey(groupKey);
355 }
356
357 /**
Mady Mellor22f2f072019-04-18 13:26:18 -0700358 * Retrieves any bubbles that are part of the notification group represented by the provided
359 * group key.
360 */
361 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
362 ArrayList<Bubble> bubbleChildren = new ArrayList<>();
363 if (groupKey == null) {
364 return bubbleChildren;
365 }
366 for (Bubble b : mBubbles) {
Pinyao Ting293b83d2020-05-06 17:10:56 -0700367 if (b.getEntry() != null && groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
Mady Mellor22f2f072019-04-18 13:26:18 -0700368 bubbleChildren.add(b);
369 }
370 }
371 return bubbleChildren;
372 }
373
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400374 private void doAdd(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200375 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400376 Log.d(TAG, "doAdd: " + bubble);
377 }
378 int minInsertPoint = 0;
379 boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId());
380 if (isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400381 // first bubble of a group goes to the beginning, otherwise within the existing group
382 minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400383 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400384 if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400385 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400386 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400387 mStateChange.addedBubble = bubble;
Lyn Hanb58c7562020-01-07 14:29:20 -0800388
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400389 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400390 mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
Mark Renoufba5ab512019-05-02 15:21:01 -0400391 // Top bubble becomes selected.
392 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400393 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400394 }
395
396 private void trim() {
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800397 if (mBubbles.size() > mMaxBubbles) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400398 mBubbles.stream()
399 // sort oldest first (ascending lastActivity)
400 .sorted(Comparator.comparingLong(Bubble::getLastActivity))
401 // skip the selected bubble
402 .filter((b) -> !b.equals(mSelectedBubble))
403 .findFirst()
Mark Renoufba5ab512019-05-02 15:21:01 -0400404 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400405 }
406 }
407
408 private void doUpdate(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200409 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400410 Log.d(TAG, "doUpdate: " + bubble);
411 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400412 mStateChange.updatedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400413 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400414 // while collapsed, update causes re-pack
415 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400416 mBubbles.remove(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400417 int newPos = insertBubble(0, bubble);
418 if (prevPos != newPos) {
419 packGroup(newPos);
Mark Renouf82a40e62019-05-23 16:16:24 -0400420 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400421 }
422 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400423 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400424 }
425
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400426 private void doRemove(String key, @DismissReason int reason) {
Lyn Hane1395572020-03-23 15:48:54 -0700427 if (DEBUG_BUBBLE_DATA) {
428 Log.d(TAG, "doRemove: " + key);
429 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800430 // If it was pending remove it
431 for (int i = 0; i < mPendingBubbles.size(); i++) {
432 if (mPendingBubbles.get(i).getKey().equals(key)) {
433 mPendingBubbles.remove(mPendingBubbles.get(i));
434 }
435 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400436 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400437 if (indexToRemove == -1) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700438 if (hasOverflowBubbleWithKey(key)
439 && (reason == BubbleController.DISMISS_NOTIF_CANCEL
440 || reason == BubbleController.DISMISS_GROUP_CANCELLED
441 || reason == BubbleController.DISMISS_NO_LONGER_BUBBLE
442 || reason == BubbleController.DISMISS_BLOCKED)) {
443
444 Bubble b = getOverflowBubbleWithKey(key);
445 if (DEBUG_BUBBLE_DATA) {
446 Log.d(TAG, "Cancel overflow bubble: " + b);
447 }
Lyn Hane4274be2020-04-24 17:55:36 -0700448 mStateChange.bubbleRemoved(b, reason);
Mady Mellor1d082022020-05-12 16:35:39 +0000449 mOverflowBubbles.remove(b);
Lyn Han2f6e89d2020-04-15 10:01:01 -0700450 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400451 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400452 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400453 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
454 if (mBubbles.size() == 1) {
455 // Going to become empty, handle specially.
456 setExpandedInternal(false);
457 setSelectedBubbleInternal(null);
458 }
459 if (indexToRemove < mBubbles.size() - 1) {
460 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400461 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400462 }
463 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400464 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400465 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400466 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400467 }
468
Lyn Hand6981862020-02-18 18:01:43 -0800469 overflowBubble(reason, bubbleToRemove);
Lyn Hanb58c7562020-01-07 14:29:20 -0800470
Mark Renoufba5ab512019-05-02 15:21:01 -0400471 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
472 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
473 // Move selection to the new bubble at the same position.
474 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
475 Bubble newSelected = mBubbles.get(newIndex);
476 setSelectedBubbleInternal(newSelected);
477 }
Pinyao Ting293b83d2020-05-06 17:10:56 -0700478 if (bubbleToRemove.getEntry() != null) {
479 maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
480 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400481 }
482
Lyn Hand6981862020-02-18 18:01:43 -0800483 void overflowBubble(@DismissReason int reason, Bubble bubble) {
Lyn Han6cb4e5f2020-04-27 15:11:18 -0700484 if (bubble.getPendingIntentCanceled()
485 || !(reason == BubbleController.DISMISS_AGED
Lyn Han2f6e89d2020-04-15 10:01:01 -0700486 || reason == BubbleController.DISMISS_USER_GESTURE)) {
487 return;
488 }
489 if (DEBUG_BUBBLE_DATA) {
490 Log.d(TAG, "Overflowing: " + bubble);
491 }
492 mOverflowBubbles.add(0, bubble);
493 bubble.stopInflation();
494 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
495 // Remove oldest bubble.
496 Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1);
Lyn Hand6981862020-02-18 18:01:43 -0800497 if (DEBUG_BUBBLE_DATA) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700498 Log.d(TAG, "Overflow full. Remove: " + oldest);
Lyn Hand6981862020-02-18 18:01:43 -0800499 }
Lyn Hane4274be2020-04-24 17:55:36 -0700500 mStateChange.bubbleRemoved(oldest, BubbleController.DISMISS_OVERFLOW_MAX_REACHED);
Mady Mellor1d082022020-05-12 16:35:39 +0000501 mOverflowBubbles.remove(oldest);
Lyn Hand6981862020-02-18 18:01:43 -0800502 }
503 }
504
Mark Renouf71a3af62019-04-08 15:02:54 -0400505 public void dismissAll(@DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200506 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400507 Log.d(TAG, "dismissAll: reason=" + reason);
508 }
509 if (mBubbles.isEmpty()) {
510 return;
511 }
512 setExpandedInternal(false);
513 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400514 while (!mBubbles.isEmpty()) {
Lyn Hand6981862020-02-18 18:01:43 -0800515 doRemove(mBubbles.get(0).getKey(), reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400516 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400517 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400518 }
519
Mady Mellorca184aae2019-09-17 16:07:12 -0700520 /**
521 * Indicates that the provided display is no longer in use and should be cleaned up.
522 *
523 * @param displayId the id of the display to clean up.
524 */
525 void notifyDisplayEmpty(int displayId) {
526 for (Bubble b : mBubbles) {
527 if (b.getDisplayId() == displayId) {
528 if (b.getExpandedView() != null) {
529 b.getExpandedView().notifyDisplayEmpty();
530 }
531 return;
532 }
533 }
534 }
535
Mark Renoufba5ab512019-05-02 15:21:01 -0400536 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400537 if (mListener != null && mStateChange.anythingChanged()) {
538 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400539 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800540 mStateChange = new Update(mBubbles, mOverflowBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400541 }
542
543 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400544 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400545 *
546 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400547 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400548 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200549 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400550 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
551 }
Lyn Han89fb39d2020-04-07 11:51:07 -0700552 if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400553 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400554 }
Lyn Han89fb39d2020-04-07 11:51:07 -0700555 // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.
556
Lyn Han1e19d7f2020-02-05 19:10:58 -0800557 if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400558 Log.e(TAG, "Cannot select bubble which doesn't exist!"
559 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400560 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400561 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400562 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700563 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400564 }
565 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400566 mStateChange.selectedBubble = bubble;
567 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400568 }
569
Mark Renouf71a3af62019-04-08 15:02:54 -0400570 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400571 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400572 *
573 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400574 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400575 private void setExpandedInternal(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200576 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400577 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
578 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400579 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400580 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400581 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400582 if (shouldExpand) {
583 if (mBubbles.isEmpty()) {
584 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400585 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400586 }
587 if (mSelectedBubble == null) {
588 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400589 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400590 }
Lyn Hanb9be52d2020-03-31 22:13:34 -0700591 mSelectedBubble.markUpdatedAt(mTimeSource.currentTimeMillis());
Mark Renoufba5ab512019-05-02 15:21:01 -0400592 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400593 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400594 } else if (!mBubbles.isEmpty()) {
595 // Apply ordering and grouping rules from expanded -> collapsed, then save
596 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400597 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400598 // Save the state which should be returned to when expanded (with no other changes)
599
Lyn Han89fb39d2020-04-07 11:51:07 -0700600 if (mShowingOverflow) {
601 // Show previously selected bubble instead of overflow menu on next expansion.
602 setSelectedBubbleInternal(mSelectedBubble);
603 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400604 if (mBubbles.indexOf(mSelectedBubble) > 0) {
605 // Move the selected bubble to the top while collapsed.
606 if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) {
607 // The selected bubble cannot be raised to the first position because
608 // there is an ongoing bubble there. Instead, force the top ongoing bubble
609 // to become selected.
610 setSelectedBubbleInternal(mBubbles.get(0));
611 } else {
612 // Raise the selected bubble (and it's group) up to the front so the selected
613 // bubble remains on top.
614 mBubbles.remove(mSelectedBubble);
615 mBubbles.add(0, mSelectedBubble);
Mark Renouf6e4ddd72019-07-16 13:28:43 -0400616 mStateChange.orderChanged |= packGroup(0);
Mark Renoufba5ab512019-05-02 15:21:01 -0400617 }
618 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400619 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400620 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400621 mStateChange.expanded = shouldExpand;
622 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400623 }
624
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400625 private static long sortKey(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400626 long key = bubble.getLastUpdateTime();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400627 if (bubble.isOngoing()) {
628 // Set 2nd highest bit (signed long int), to partition between ongoing and regular
629 key |= 0x4000000000000000L;
630 }
631 return key;
632 }
633
634 /**
635 * Locates and inserts the bubble into a sorted position. The is inserted
636 * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
637 * required to keep grouping intact.
638 *
639 * @param minPosition the first insert point to consider
Lyn Han72f50902019-10-25 15:55:49 -0700640 * @param newBubble the bubble to insert
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400641 * @return the position where the bubble was inserted
642 */
643 private int insertBubble(int minPosition, Bubble newBubble) {
644 long newBubbleSortKey = sortKey(newBubble);
645 String previousGroupId = null;
646
647 for (int pos = minPosition; pos < mBubbles.size(); pos++) {
648 Bubble bubbleAtPos = mBubbles.get(pos);
649 String groupIdAtPos = bubbleAtPos.getGroupId();
650 boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
651
652 if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
653 // Insert before the start of first group which has older bubbles.
654 mBubbles.add(pos, newBubble);
655 return pos;
656 }
657 previousGroupId = groupIdAtPos;
658 }
659 mBubbles.add(newBubble);
660 return mBubbles.size() - 1;
661 }
662
663 private boolean hasBubbleWithGroupId(String groupId) {
664 return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
665 }
666
667 private int findFirstIndexForGroup(String appId) {
668 for (int i = 0; i < mBubbles.size(); i++) {
669 Bubble bubbleAtPos = mBubbles.get(i);
670 if (bubbleAtPos.getGroupId().equals(appId)) {
671 return i;
672 }
673 }
674 return 0;
675 }
676
677 /**
678 * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
679 * at positions lower than {@code position} are unchanged. Relative order within the group
680 * unchanged. Relative order of any other bubbles are also unchanged.
681 *
682 * @param position the position of the first bubble for the group
Mark Renoufba5ab512019-05-02 15:21:01 -0400683 * @return true if the position of any bubbles has changed as a result
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400684 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400685 private boolean packGroup(int position) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200686 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400687 Log.d(TAG, "packGroup: position=" + position);
688 }
689 Bubble groupStart = mBubbles.get(position);
690 final String groupAppId = groupStart.getGroupId();
691 List<Bubble> moving = new ArrayList<>();
692
693 // Walk backward, collect bubbles within the group
694 for (int i = mBubbles.size() - 1; i > position; i--) {
695 if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
696 moving.add(0, mBubbles.get(i));
697 }
698 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400699 if (moving.isEmpty()) {
700 return false;
701 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400702 mBubbles.removeAll(moving);
703 mBubbles.addAll(position + 1, moving);
Mark Renoufba5ab512019-05-02 15:21:01 -0400704 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400705 }
706
Mark Renoufba5ab512019-05-02 15:21:01 -0400707 /**
708 * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped
Lyn Hanb9be52d2020-03-31 22:13:34 -0700709 * by groupId. Each group is then sorted by the max(lastUpdated) time of its bubbles. Bubbles
Mark Renoufba5ab512019-05-02 15:21:01 -0400710 * within each group are then sorted by lastUpdated descending.
711 *
712 * @return true if the position of any bubbles changed as a result
713 */
714 private boolean repackAll() {
Issei Suzukia8d07312019-06-07 12:56:19 +0200715 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400716 Log.d(TAG, "repackAll()");
717 }
718 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400719 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400720 }
721 Map<String, Long> groupLastActivity = new HashMap<>();
722 for (Bubble bubble : mBubbles) {
723 long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
724 long sortKeyForBubble = sortKey(bubble);
725 if (sortKeyForBubble > maxSortKeyForGroup) {
726 groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
727 }
728 }
729
730 // Sort groups by their most recently active bubble
731 List<String> groupsByMostRecentActivity =
732 groupLastActivity.entrySet().stream()
Mark Renoufba5ab512019-05-02 15:21:01 -0400733 .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400734 .map(Map.Entry::getKey)
735 .collect(toList());
736
737 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
738
739 // For each group, add bubbles, freshest to oldest
740 for (String appId : groupsByMostRecentActivity) {
741 mBubbles.stream()
742 .filter((b) -> b.getGroupId().equals(appId))
Mark Renoufba5ab512019-05-02 15:21:01 -0400743 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400744 .forEachOrdered(repacked::add);
745 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400746 if (repacked.equals(mBubbles)) {
747 return false;
748 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400749 mBubbles.clear();
750 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400751 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400752 }
753
Pinyao Ting293b83d2020-05-06 17:10:56 -0700754 private void maybeSendDeleteIntent(@DismissReason int reason,
755 @NonNull final NotificationEntry entry) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400756 if (reason == BubbleController.DISMISS_USER_GESTURE) {
757 Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
758 PendingIntent deleteIntent = bubbleMetadata != null
759 ? bubbleMetadata.getDeleteIntent()
760 : null;
761 if (deleteIntent != null) {
762 try {
763 deleteIntent.send();
764 } catch (PendingIntent.CanceledException e) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400765 Log.w(TAG, "Failed to send delete intent for bubble with key: "
766 + entry.getKey());
Mark Renouf71a3af62019-04-08 15:02:54 -0400767 }
768 }
769 }
770 }
771
Mark Renouf71a3af62019-04-08 15:02:54 -0400772 private int indexForKey(String key) {
773 for (int i = 0; i < mBubbles.size(); i++) {
774 Bubble bubble = mBubbles.get(i);
775 if (bubble.getKey().equals(key)) {
776 return i;
777 }
778 }
779 return -1;
780 }
781
Mark Renouf71a3af62019-04-08 15:02:54 -0400782 /**
Lyn Hanb58c7562020-01-07 14:29:20 -0800783 * The set of bubbles in row.
Mark Renouf71a3af62019-04-08 15:02:54 -0400784 */
Joshua Tsuji0f390fb2020-04-28 15:20:10 -0400785 @VisibleForTesting(visibility = PACKAGE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400786 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400787 return Collections.unmodifiableList(mBubbles);
788 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800789 /**
790 * The set of bubbles in overflow.
791 */
792 @VisibleForTesting(visibility = PRIVATE)
793 public List<Bubble> getOverflowBubbles() {
794 return Collections.unmodifiableList(mOverflowBubbles);
795 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400796
797 @VisibleForTesting(visibility = PRIVATE)
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400798 @Nullable
Lyn Han2f6e89d2020-04-15 10:01:01 -0700799 Bubble getAnyBubbleWithkey(String key) {
800 Bubble b = getBubbleInStackWithKey(key);
801 if (b == null) {
802 b = getOverflowBubbleWithKey(key);
803 }
804 return b;
805 }
806
807 @VisibleForTesting(visibility = PRIVATE)
808 @Nullable
809 Bubble getBubbleInStackWithKey(String key) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400810 for (int i = 0; i < mBubbles.size(); i++) {
811 Bubble bubble = mBubbles.get(i);
812 if (bubble.getKey().equals(key)) {
813 return bubble;
814 }
815 }
816 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800817 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400818
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400819 @Nullable
820 Bubble getBubbleWithView(View view) {
821 for (int i = 0; i < mBubbles.size(); i++) {
822 Bubble bubble = mBubbles.get(i);
823 if (bubble.getIconView() != null && bubble.getIconView().equals(view)) {
824 return bubble;
825 }
826 }
827 return null;
828 }
829
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400830 @VisibleForTesting(visibility = PRIVATE)
Lyn Han89274b42020-03-25 00:56:26 -0700831 Bubble getOverflowBubbleWithKey(String key) {
832 for (int i = 0; i < mOverflowBubbles.size(); i++) {
833 Bubble bubble = mOverflowBubbles.get(i);
834 if (bubble.getKey().equals(key)) {
835 return bubble;
836 }
837 }
838 return null;
839 }
840
841 @VisibleForTesting(visibility = PRIVATE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400842 void setTimeSource(TimeSource timeSource) {
843 mTimeSource = timeSource;
844 }
845
Mark Renouf3bc5b362019-04-05 14:37:59 -0400846 public void setListener(Listener listener) {
847 mListener = listener;
848 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400849
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700850 /**
Lyn Han2f6e89d2020-04-15 10:01:01 -0700851 * Set maximum number of bubbles allowed in overflow.
852 * This method should only be used in tests, not in production.
853 */
854 @VisibleForTesting
855 void setMaxOverflowBubbles(int maxOverflowBubbles) {
856 mMaxOverflowBubbles = maxOverflowBubbles;
857 }
858
859 /**
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700860 * Description of current bubble data state.
861 */
862 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
Lyn Han72f50902019-10-25 15:55:49 -0700863 pw.print("selected: ");
864 pw.println(mSelectedBubble != null
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700865 ? mSelectedBubble.getKey()
866 : "null");
Lyn Han72f50902019-10-25 15:55:49 -0700867 pw.print("expanded: ");
868 pw.println(mExpanded);
869 pw.print("count: ");
870 pw.println(mBubbles.size());
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700871 for (Bubble bubble : mBubbles) {
872 bubble.dump(fd, pw, args);
873 }
Lyn Han72f50902019-10-25 15:55:49 -0700874 pw.print("summaryKeys: ");
875 pw.println(mSuppressedGroupKeys.size());
Mady Mellore28fe102019-07-09 15:33:32 -0700876 for (String key : mSuppressedGroupKeys.keySet()) {
877 pw.println(" suppressing: " + key);
878 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400879 }
Mark Renoufc19b4732019-06-26 12:08:33 -0400880}