blob: 857f1dd3533cdfd8614b3974826c9278ebab3d51 [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.Notification;
26import android.app.PendingIntent;
27import android.content.Context;
28import 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;
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 Renouf71a3af62019-04-08 15:02:54 -040046import java.util.Objects;
Mady Mellor3dff9e62019-02-05 18:12:53 -080047
Mady Mellorcfd06c12019-02-13 14:32:12 -080048import javax.inject.Inject;
49import javax.inject.Singleton;
50
Mady Mellor3dff9e62019-02-05 18:12:53 -080051/**
52 * Keeps track of active bubbles.
53 */
Mady Mellorcfd06c12019-02-13 14:32:12 -080054@Singleton
Selim Cinekfdf80332019-03-07 17:29:55 -080055public class BubbleData {
Mady Mellor3dff9e62019-02-05 18:12:53 -080056
Issei Suzukia8d07312019-06-07 12:56:19 +020057 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040058
Mark Renoufba5ab512019-05-02 15:21:01 -040059 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
60 Comparator.comparing(BubbleData::sortKey).reversed();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040061
Mark Renouf82a40e62019-05-23 16:16:24 -040062 /** Contains information about changes that have been made to the state of bubbles. */
63 static final class Update {
64 boolean expandedChanged;
65 boolean selectionChanged;
66 boolean orderChanged;
67 boolean expanded;
68 @Nullable Bubble selectedBubble;
69 @Nullable Bubble addedBubble;
70 @Nullable Bubble updatedBubble;
71 // Pair with Bubble and @DismissReason Integer
72 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
73
74 // A read-only view of the bubbles list, changes there will be reflected here.
75 final List<Bubble> bubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -080076 final List<Bubble> overflowBubbles;
Mark Renouf82a40e62019-05-23 16:16:24 -040077
Lyn Hanb58c7562020-01-07 14:29:20 -080078 private Update(List<Bubble> row, List<Bubble> overflow) {
79 bubbles = Collections.unmodifiableList(row);
80 overflowBubbles = Collections.unmodifiableList(overflow);
Mark Renouf82a40e62019-05-23 16:16:24 -040081 }
82
83 boolean anythingChanged() {
84 return expandedChanged
85 || selectionChanged
86 || addedBubble != null
87 || updatedBubble != null
88 || !removedBubbles.isEmpty()
89 || orderChanged;
90 }
91
Mady Mellor1d082022020-05-12 16:35:39 +000092 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
Mark Renouf82a40e62019-05-23 16:16:24 -040093 removedBubbles.add(new Pair<>(bubbleToRemove, reason));
94 }
95 }
96
Mark Renouf3bc5b362019-04-05 14:37:59 -040097 /**
98 * This interface reports changes to the state and appearance of bubbles which should be applied
99 * as necessary to the UI.
Mark Renouf3bc5b362019-04-05 14:37:59 -0400100 */
101 interface Listener {
Mark Renouf82a40e62019-05-23 16:16:24 -0400102 /** Reports changes have have occurred as a result of the most recent operation. */
103 void applyUpdate(Update update);
Mark Renouf3bc5b362019-04-05 14:37:59 -0400104 }
105
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400106 interface TimeSource {
107 long currentTimeMillis();
108 }
109
Mark Renouf71a3af62019-04-08 15:02:54 -0400110 private final Context mContext;
Mady Mellor9ec37962020-01-10 10:21:03 -0800111 /** Bubbles that are actively in the stack. */
Mark Renouf82a40e62019-05-23 16:16:24 -0400112 private final List<Bubble> mBubbles;
Lyn Hanb58c7562020-01-07 14:29:20 -0800113 /** Bubbles that aged out to overflow. */
114 private final List<Bubble> mOverflowBubbles;
Mady Mellor9ec37962020-01-10 10:21:03 -0800115 /** Bubbles that are being loaded but haven't been added to the stack just yet. */
116 private final List<Bubble> mPendingBubbles;
Mark Renouf71a3af62019-04-08 15:02:54 -0400117 private Bubble mSelectedBubble;
Lyn Han89fb39d2020-04-07 11:51:07 -0700118 private boolean mShowingOverflow;
Mark Renouf71a3af62019-04-08 15:02:54 -0400119 private boolean mExpanded;
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800120 private final int mMaxBubbles;
Lyn Han2f6e89d2020-04-15 10:01:01 -0700121 private int mMaxOverflowBubbles;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400122
Mark Renoufba5ab512019-05-02 15:21:01 -0400123 // State tracked during an operation -- keeps track of what listener events to dispatch.
Mark Renouf82a40e62019-05-23 16:16:24 -0400124 private Update mStateChange;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400125
126 private TimeSource mTimeSource = System::currentTimeMillis;
127
128 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400129 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800130
Mady Mellorf44b6832020-01-14 13:26:14 -0800131 @Nullable
132 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
133
Mady Mellore28fe102019-07-09 15:33:32 -0700134 /**
135 * We track groups with summaries that aren't visibly displayed but still kept around because
136 * the bubble(s) associated with the summary still exist.
137 *
138 * The summary must be kept around so that developers can cancel it (and hence the bubbles
139 * associated with it). This list is used to check if the summary should be hidden from the
140 * shade.
141 *
142 * Key: group key of the NotificationEntry
143 * Value: key of the NotificationEntry
144 */
145 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
146
Mady Mellorcfd06c12019-02-13 14:32:12 -0800147 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400148 public BubbleData(Context context) {
149 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400150 mBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800151 mOverflowBubbles = new ArrayList<>();
Mady Mellor9ec37962020-01-10 10:21:03 -0800152 mPendingBubbles = new ArrayList<>();
Lyn Hanb58c7562020-01-07 14:29:20 -0800153 mStateChange = new Update(mBubbles, mOverflowBubbles);
Lyn Han3a7ce7a2019-12-10 18:16:35 -0800154 mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
Lyn Hanb58c7562020-01-07 14:29:20 -0800155 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800156 }
157
Mady Mellorf44b6832020-01-14 13:26:14 -0800158 public void setSuppressionChangedListener(
159 BubbleController.NotificationSuppressionChangedListener listener) {
160 mSuppressionListener = listener;
161 }
162
Mark Renouf71a3af62019-04-08 15:02:54 -0400163 public boolean hasBubbles() {
164 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800165 }
166
Mark Renouf71a3af62019-04-08 15:02:54 -0400167 public boolean isExpanded() {
168 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800169 }
170
Lyn Han2f6e89d2020-04-15 10:01:01 -0700171 public boolean hasAnyBubbleWithKey(String key) {
172 return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key);
173 }
174
175 public boolean hasBubbleInStackWithKey(String key) {
176 return getBubbleInStackWithKey(key) != null;
177 }
178
179 public boolean hasOverflowBubbleWithKey(String key) {
180 return getOverflowBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800181 }
182
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400183 @Nullable
184 public Bubble getSelectedBubble() {
185 return mSelectedBubble;
186 }
187
Mark Renouf71a3af62019-04-08 15:02:54 -0400188 public void setExpanded(boolean expanded) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200189 if (DEBUG_BUBBLE_DATA) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400190 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800191 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400192 setExpandedInternal(expanded);
193 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800194 }
195
Mark Renouf71a3af62019-04-08 15:02:54 -0400196 public void setSelectedBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200197 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400198 Log.d(TAG, "setSelectedBubble: " + bubble);
199 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400200 setSelectedBubbleInternal(bubble);
201 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400202 }
203
Lyn Han1e19d7f2020-02-05 19:10:58 -0800204 public void promoteBubbleFromOverflow(Bubble bubble, BubbleStackView stack,
205 BubbleIconFactory factory) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800206 if (DEBUG_BUBBLE_DATA) {
207 Log.d(TAG, "promoteBubbleFromOverflow: " + bubble);
208 }
Lyn Han18cdc1c2020-03-18 19:12:42 -0700209 moveOverflowBubbleToPending(bubble);
Lyn Han2f6e89d2020-04-15 10:01:01 -0700210 // Preserve new order for next repack, which sorts by last updated time.
Lyn Han1e19d7f2020-02-05 19:10:58 -0800211 bubble.inflate(
Lyn Han4438d9b2020-03-11 18:04:35 -0700212 b -> {
Mady Mellor76343012020-05-13 11:02:50 -0700213 b.setShouldAutoExpand(true);
214 b.markUpdatedAt(mTimeSource.currentTimeMillis());
215 notificationEntryUpdated(bubble, false /* suppressFlyout */,
216 true /* showInShade */);
Lyn Han4438d9b2020-03-11 18:04:35 -0700217 },
Pinyao Ting3c930612020-05-19 00:26:03 +0000218 mContext, stack, factory, false /* skipInflation */);
Lyn Hanb58c7562020-01-07 14:29:20 -0800219 }
220
Lyn Han89fb39d2020-04-07 11:51:07 -0700221 void setShowingOverflow(boolean showingOverflow) {
222 mShowingOverflow = showingOverflow;
223 }
224
Lyn Han18cdc1c2020-03-18 19:12:42 -0700225 private void moveOverflowBubbleToPending(Bubble b) {
Lyn Han18cdc1c2020-03-18 19:12:42 -0700226 mOverflowBubbles.remove(b);
227 mPendingBubbles.add(b);
228 }
229
Mady Mellor3df7ab02019-12-09 15:07:10 -0800230 /**
231 * Constructs a new bubble or returns an existing one. Does not add new bubbles to
232 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
233 * for that.
234 */
235 Bubble getOrCreateBubble(NotificationEntry entry) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700236 String key = entry.getKey();
237 Bubble bubble = getBubbleInStackWithKey(entry.getKey());
238 if (bubble != null) {
239 bubble.setEntry(entry);
240 } else {
241 bubble = getOverflowBubbleWithKey(key);
242 if (bubble != null) {
243 moveOverflowBubbleToPending(bubble);
244 bubble.setEntry(entry);
245 return bubble;
Lyn Hanbf1b3d62020-03-12 10:31:19 -0700246 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800247 // Check for it in pending
248 for (int i = 0; i < mPendingBubbles.size(); i++) {
249 Bubble b = mPendingBubbles.get(i);
250 if (b.getKey().equals(entry.getKey())) {
Mady Mellore45ff862020-03-24 15:54:50 -0700251 b.setEntry(entry);
Mady Mellor9ec37962020-01-10 10:21:03 -0800252 return b;
253 }
254 }
Mady Mellorf44b6832020-01-14 13:26:14 -0800255 bubble = new Bubble(entry, mSuppressionListener);
Mady Mellor9ec37962020-01-10 10:21:03 -0800256 mPendingBubbles.add(bubble);
Mady Mellor3df7ab02019-12-09 15:07:10 -0800257 }
258 return bubble;
259 }
260
261 /**
262 * When this method is called it is expected that all info in the bubble has completed loading.
263 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
264 * BubbleStackView, BubbleIconFactory).
265 */
266 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200267 if (DEBUG_BUBBLE_DATA) {
Mady Mellor3df7ab02019-12-09 15:07:10 -0800268 Log.d(TAG, "notificationEntryUpdated: " + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400269 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800270 mPendingBubbles.remove(bubble); // No longer pending once we're here
Lyn Han2f6e89d2020-04-15 10:01:01 -0700271 Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
Pinyao Ting3c930612020-05-19 00:26:03 +0000272 suppressFlyout |= bubble.getEntry() == null
273 || !bubble.getEntry().getRanking().visuallyInterruptive();
Lyn Han405e0b72019-08-13 16:07:55 -0700274
Mady Mellor3df7ab02019-12-09 15:07:10 -0800275 if (prevBubble == null) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400276 // Create a new bubble
Mark Renoufc19b4732019-06-26 12:08:33 -0400277 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400278 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400279 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400280 } else {
281 // Updates an existing bubble
Lyn Han405e0b72019-08-13 16:07:55 -0700282 bubble.setSuppressFlyout(suppressFlyout);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400283 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400284 }
Mady Mellor76343012020-05-13 11:02:50 -0700285
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700286 if (bubble.shouldAutoExpand()) {
Mady Mellor76343012020-05-13 11:02:50 -0700287 bubble.setShouldAutoExpand(false);
Mark Renouf71a3af62019-04-08 15:02:54 -0400288 setSelectedBubbleInternal(bubble);
289 if (!mExpanded) {
290 setExpandedInternal(true);
291 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400292 }
Mady Mellor3df7ab02019-12-09 15:07:10 -0800293
Mady Mellorf44b6832020-01-14 13:26:14 -0800294 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
295 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
296 bubble.setSuppressNotification(suppress);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400297 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
Mady Mellorf44b6832020-01-14 13:26:14 -0800298
299 dispatchPendingChanges();
Mark Renoufba5ab512019-05-02 15:21:01 -0400300 }
301
Pinyao Ting3c930612020-05-19 00:26:03 +0000302 /**
303 * Called when a notification associated with a bubble is removed.
304 */
305 public void notificationEntryRemoved(String key, @DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200306 if (DEBUG_BUBBLE_DATA) {
Pinyao Ting3c930612020-05-19 00:26:03 +0000307 Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400308 }
Pinyao Ting3c930612020-05-19 00:26:03 +0000309 doRemove(key, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400310 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400311 }
312
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400313 /**
Mady Mellore28fe102019-07-09 15:33:32 -0700314 * Adds a group key indicating that the summary for this group should be suppressed.
315 *
316 * @param groupKey the group key of the group whose summary should be suppressed.
317 * @param notifKey the notification entry key of that summary.
318 */
319 void addSummaryToSuppress(String groupKey, String notifKey) {
320 mSuppressedGroupKeys.put(groupKey, notifKey);
321 }
322
323 /**
324 * Retrieves the notif entry key of the summary associated with the provided group key.
325 *
326 * @param groupKey the group to look up
327 * @return the key for the {@link NotificationEntry} that is the summary of this group.
328 */
329 String getSummaryKey(String groupKey) {
330 return mSuppressedGroupKeys.get(groupKey);
331 }
332
333 /**
334 * Removes a group key indicating that summary for this group should no longer be suppressed.
335 */
336 void removeSuppressedSummary(String groupKey) {
337 mSuppressedGroupKeys.remove(groupKey);
338 }
339
340 /**
341 * Whether the summary for the provided group key is suppressed.
342 */
343 boolean isSummarySuppressed(String groupKey) {
344 return mSuppressedGroupKeys.containsKey(groupKey);
345 }
346
347 /**
Mady Mellor22f2f072019-04-18 13:26:18 -0700348 * Retrieves any bubbles that are part of the notification group represented by the provided
349 * group key.
350 */
351 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
352 ArrayList<Bubble> bubbleChildren = new ArrayList<>();
353 if (groupKey == null) {
354 return bubbleChildren;
355 }
356 for (Bubble b : mBubbles) {
Pinyao Ting3c930612020-05-19 00:26:03 +0000357 if (b.getEntry() != null && groupKey.equals(b.getEntry().getSbn().getGroupKey())) {
Mady Mellor22f2f072019-04-18 13:26:18 -0700358 bubbleChildren.add(b);
359 }
360 }
361 return bubbleChildren;
362 }
363
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400364 private void doAdd(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200365 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400366 Log.d(TAG, "doAdd: " + bubble);
367 }
Mady Mellora55133d2020-05-01 15:11:10 -0700368 mBubbles.add(0, bubble);
Mark Renouf82a40e62019-05-23 16:16:24 -0400369 mStateChange.addedBubble = bubble;
Mady Mellora55133d2020-05-01 15:11:10 -0700370 // Adding the first bubble doesn't change the order
371 mStateChange.orderChanged = mBubbles.size() > 1;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400372 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400373 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 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400396 mBubbles.remove(bubble);
Mady Mellora55133d2020-05-01 15:11:10 -0700397 mBubbles.add(0, bubble);
398 mStateChange.orderChanged = prevPos != 0;
Mark Renoufba5ab512019-05-02 15:21:01 -0400399 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400400 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400401 }
402
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400403 private void doRemove(String key, @DismissReason int reason) {
Lyn Hane1395572020-03-23 15:48:54 -0700404 if (DEBUG_BUBBLE_DATA) {
405 Log.d(TAG, "doRemove: " + key);
406 }
Mady Mellor9ec37962020-01-10 10:21:03 -0800407 // If it was pending remove it
408 for (int i = 0; i < mPendingBubbles.size(); i++) {
409 if (mPendingBubbles.get(i).getKey().equals(key)) {
410 mPendingBubbles.remove(mPendingBubbles.get(i));
411 }
412 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400413 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400414 if (indexToRemove == -1) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700415 if (hasOverflowBubbleWithKey(key)
416 && (reason == BubbleController.DISMISS_NOTIF_CANCEL
417 || reason == BubbleController.DISMISS_GROUP_CANCELLED
418 || reason == BubbleController.DISMISS_NO_LONGER_BUBBLE
419 || reason == BubbleController.DISMISS_BLOCKED)) {
420
421 Bubble b = getOverflowBubbleWithKey(key);
422 if (DEBUG_BUBBLE_DATA) {
423 Log.d(TAG, "Cancel overflow bubble: " + b);
424 }
Lyn Hane4274be2020-04-24 17:55:36 -0700425 mStateChange.bubbleRemoved(b, reason);
Mady Mellor1d082022020-05-12 16:35:39 +0000426 mOverflowBubbles.remove(b);
Lyn Han2f6e89d2020-04-15 10:01:01 -0700427 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400428 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400429 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400430 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
431 if (mBubbles.size() == 1) {
432 // Going to become empty, handle specially.
433 setExpandedInternal(false);
434 setSelectedBubbleInternal(null);
435 }
436 if (indexToRemove < mBubbles.size() - 1) {
437 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400438 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400439 }
440 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400441 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400442 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400443 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400444 }
445
Lyn Hand6981862020-02-18 18:01:43 -0800446 overflowBubble(reason, bubbleToRemove);
Lyn Hanb58c7562020-01-07 14:29:20 -0800447
Mark Renoufba5ab512019-05-02 15:21:01 -0400448 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
449 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
450 // Move selection to the new bubble at the same position.
451 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
452 Bubble newSelected = mBubbles.get(newIndex);
453 setSelectedBubbleInternal(newSelected);
454 }
Pinyao Ting3c930612020-05-19 00:26:03 +0000455 if (bubbleToRemove.getEntry() != null) {
456 maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
457 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400458 }
459
Lyn Hand6981862020-02-18 18:01:43 -0800460 void overflowBubble(@DismissReason int reason, Bubble bubble) {
Lyn Han6cb4e5f2020-04-27 15:11:18 -0700461 if (bubble.getPendingIntentCanceled()
462 || !(reason == BubbleController.DISMISS_AGED
Lyn Han2f6e89d2020-04-15 10:01:01 -0700463 || reason == BubbleController.DISMISS_USER_GESTURE)) {
464 return;
465 }
466 if (DEBUG_BUBBLE_DATA) {
467 Log.d(TAG, "Overflowing: " + bubble);
468 }
469 mOverflowBubbles.add(0, bubble);
470 bubble.stopInflation();
471 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
472 // Remove oldest bubble.
473 Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1);
Lyn Hand6981862020-02-18 18:01:43 -0800474 if (DEBUG_BUBBLE_DATA) {
Lyn Han2f6e89d2020-04-15 10:01:01 -0700475 Log.d(TAG, "Overflow full. Remove: " + oldest);
Lyn Hand6981862020-02-18 18:01:43 -0800476 }
Lyn Hane4274be2020-04-24 17:55:36 -0700477 mStateChange.bubbleRemoved(oldest, BubbleController.DISMISS_OVERFLOW_MAX_REACHED);
Mady Mellor1d082022020-05-12 16:35:39 +0000478 mOverflowBubbles.remove(oldest);
Lyn Hand6981862020-02-18 18:01:43 -0800479 }
480 }
481
Mark Renouf71a3af62019-04-08 15:02:54 -0400482 public void dismissAll(@DismissReason int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200483 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400484 Log.d(TAG, "dismissAll: reason=" + reason);
485 }
486 if (mBubbles.isEmpty()) {
487 return;
488 }
489 setExpandedInternal(false);
490 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400491 while (!mBubbles.isEmpty()) {
Lyn Hand6981862020-02-18 18:01:43 -0800492 doRemove(mBubbles.get(0).getKey(), reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400493 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400494 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400495 }
496
Mady Mellorca184aae2019-09-17 16:07:12 -0700497 /**
498 * Indicates that the provided display is no longer in use and should be cleaned up.
499 *
500 * @param displayId the id of the display to clean up.
501 */
502 void notifyDisplayEmpty(int displayId) {
503 for (Bubble b : mBubbles) {
504 if (b.getDisplayId() == displayId) {
505 if (b.getExpandedView() != null) {
506 b.getExpandedView().notifyDisplayEmpty();
507 }
508 return;
509 }
510 }
511 }
512
Mark Renoufba5ab512019-05-02 15:21:01 -0400513 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400514 if (mListener != null && mStateChange.anythingChanged()) {
515 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400516 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800517 mStateChange = new Update(mBubbles, mOverflowBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400518 }
519
520 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400521 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400522 *
523 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400524 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400525 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200526 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400527 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
528 }
Lyn Han89fb39d2020-04-07 11:51:07 -0700529 if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400530 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400531 }
Lyn Han89fb39d2020-04-07 11:51:07 -0700532 // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.
533
Lyn Han1e19d7f2020-02-05 19:10:58 -0800534 if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400535 Log.e(TAG, "Cannot select bubble which doesn't exist!"
536 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400537 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400538 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400539 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700540 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400541 }
542 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400543 mStateChange.selectedBubble = bubble;
544 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400545 }
546
Mark Renouf71a3af62019-04-08 15:02:54 -0400547 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400548 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400549 *
550 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400551 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400552 private void setExpandedInternal(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200553 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400554 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
555 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400556 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400557 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400558 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400559 if (shouldExpand) {
560 if (mBubbles.isEmpty()) {
561 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400562 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400563 }
564 if (mSelectedBubble == null) {
565 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400566 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400567 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400568 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400569 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400570 } else if (!mBubbles.isEmpty()) {
571 // Apply ordering and grouping rules from expanded -> collapsed, then save
572 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400573 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400574 // Save the state which should be returned to when expanded (with no other changes)
575
Lyn Han89fb39d2020-04-07 11:51:07 -0700576 if (mShowingOverflow) {
577 // Show previously selected bubble instead of overflow menu on next expansion.
578 setSelectedBubbleInternal(mSelectedBubble);
579 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400580 if (mBubbles.indexOf(mSelectedBubble) > 0) {
581 // Move the selected bubble to the top while collapsed.
Mady Mellora55133d2020-05-01 15:11:10 -0700582 int index = mBubbles.indexOf(mSelectedBubble);
583 if (index != 0) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400584 mBubbles.remove(mSelectedBubble);
585 mBubbles.add(0, mSelectedBubble);
Mady Mellora55133d2020-05-01 15:11:10 -0700586 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400587 }
588 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400589 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400590 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400591 mStateChange.expanded = shouldExpand;
592 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400593 }
594
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400595 private static long sortKey(Bubble bubble) {
Mady Mellora55133d2020-05-01 15:11:10 -0700596 return bubble.getLastActivity();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400597 }
598
599 /**
Mady Mellora55133d2020-05-01 15:11:10 -0700600 * This applies a full sort and group pass to all existing bubbles.
601 * Bubbles are sorted by lastUpdated descending.
Mark Renoufba5ab512019-05-02 15:21:01 -0400602 *
603 * @return true if the position of any bubbles changed as a result
604 */
605 private boolean repackAll() {
Issei Suzukia8d07312019-06-07 12:56:19 +0200606 if (DEBUG_BUBBLE_DATA) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400607 Log.d(TAG, "repackAll()");
608 }
609 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400610 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400611 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400612 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
Mady Mellora55133d2020-05-01 15:11:10 -0700613 // Add bubbles, freshest to oldest
614 mBubbles.stream()
615 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
616 .forEachOrdered(repacked::add);
Mark Renoufba5ab512019-05-02 15:21:01 -0400617 if (repacked.equals(mBubbles)) {
618 return false;
619 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400620 mBubbles.clear();
621 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400622 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400623 }
624
Pinyao Ting3c930612020-05-19 00:26:03 +0000625 private void maybeSendDeleteIntent(@DismissReason int reason,
626 @NonNull final NotificationEntry entry) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400627 if (reason == BubbleController.DISMISS_USER_GESTURE) {
628 Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
629 PendingIntent deleteIntent = bubbleMetadata != null
630 ? bubbleMetadata.getDeleteIntent()
631 : null;
632 if (deleteIntent != null) {
633 try {
634 deleteIntent.send();
635 } catch (PendingIntent.CanceledException e) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400636 Log.w(TAG, "Failed to send delete intent for bubble with key: "
637 + entry.getKey());
Mark Renouf71a3af62019-04-08 15:02:54 -0400638 }
639 }
640 }
641 }
642
Mark Renouf71a3af62019-04-08 15:02:54 -0400643 private int indexForKey(String key) {
644 for (int i = 0; i < mBubbles.size(); i++) {
645 Bubble bubble = mBubbles.get(i);
646 if (bubble.getKey().equals(key)) {
647 return i;
648 }
649 }
650 return -1;
651 }
652
Mark Renouf71a3af62019-04-08 15:02:54 -0400653 /**
Lyn Hanb58c7562020-01-07 14:29:20 -0800654 * The set of bubbles in row.
Mark Renouf71a3af62019-04-08 15:02:54 -0400655 */
Joshua Tsuji0f390fb2020-04-28 15:20:10 -0400656 @VisibleForTesting(visibility = PACKAGE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400657 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400658 return Collections.unmodifiableList(mBubbles);
659 }
Mady Mellora55133d2020-05-01 15:11:10 -0700660
Lyn Hanb58c7562020-01-07 14:29:20 -0800661 /**
662 * The set of bubbles in overflow.
663 */
664 @VisibleForTesting(visibility = PRIVATE)
Mady Mellora55133d2020-05-01 15:11:10 -0700665 List<Bubble> getOverflowBubbles() {
Lyn Hanb58c7562020-01-07 14:29:20 -0800666 return Collections.unmodifiableList(mOverflowBubbles);
667 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400668
669 @VisibleForTesting(visibility = PRIVATE)
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400670 @Nullable
Lyn Han2f6e89d2020-04-15 10:01:01 -0700671 Bubble getAnyBubbleWithkey(String key) {
672 Bubble b = getBubbleInStackWithKey(key);
673 if (b == null) {
674 b = getOverflowBubbleWithKey(key);
675 }
676 return b;
677 }
678
679 @VisibleForTesting(visibility = PRIVATE)
680 @Nullable
681 Bubble getBubbleInStackWithKey(String key) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400682 for (int i = 0; i < mBubbles.size(); i++) {
683 Bubble bubble = mBubbles.get(i);
684 if (bubble.getKey().equals(key)) {
685 return bubble;
686 }
687 }
688 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800689 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400690
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400691 @Nullable
692 Bubble getBubbleWithView(View view) {
693 for (int i = 0; i < mBubbles.size(); i++) {
694 Bubble bubble = mBubbles.get(i);
695 if (bubble.getIconView() != null && bubble.getIconView().equals(view)) {
696 return bubble;
697 }
698 }
699 return null;
700 }
701
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400702 @VisibleForTesting(visibility = PRIVATE)
Lyn Han89274b42020-03-25 00:56:26 -0700703 Bubble getOverflowBubbleWithKey(String key) {
704 for (int i = 0; i < mOverflowBubbles.size(); i++) {
705 Bubble bubble = mOverflowBubbles.get(i);
706 if (bubble.getKey().equals(key)) {
707 return bubble;
708 }
709 }
710 return null;
711 }
712
713 @VisibleForTesting(visibility = PRIVATE)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400714 void setTimeSource(TimeSource timeSource) {
715 mTimeSource = timeSource;
716 }
717
Mark Renouf3bc5b362019-04-05 14:37:59 -0400718 public void setListener(Listener listener) {
719 mListener = listener;
720 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400721
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700722 /**
Lyn Han2f6e89d2020-04-15 10:01:01 -0700723 * Set maximum number of bubbles allowed in overflow.
724 * This method should only be used in tests, not in production.
725 */
726 @VisibleForTesting
727 void setMaxOverflowBubbles(int maxOverflowBubbles) {
728 mMaxOverflowBubbles = maxOverflowBubbles;
729 }
730
731 /**
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700732 * Description of current bubble data state.
733 */
734 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
Lyn Han72f50902019-10-25 15:55:49 -0700735 pw.print("selected: ");
736 pw.println(mSelectedBubble != null
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700737 ? mSelectedBubble.getKey()
738 : "null");
Lyn Han72f50902019-10-25 15:55:49 -0700739 pw.print("expanded: ");
740 pw.println(mExpanded);
741 pw.print("count: ");
742 pw.println(mBubbles.size());
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700743 for (Bubble bubble : mBubbles) {
744 bubble.dump(fd, pw, args);
745 }
Lyn Han72f50902019-10-25 15:55:49 -0700746 pw.print("summaryKeys: ");
747 pw.println(mSuppressedGroupKeys.size());
Mady Mellore28fe102019-07-09 15:33:32 -0700748 for (String key : mSuppressedGroupKeys.keySet()) {
749 pw.println(" suppressing: " + key);
750 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400751 }
Mark Renoufc19b4732019-06-26 12:08:33 -0400752}