blob: 5575b35a12ae34b5f40ba184e8c7e2906295ac25 [file] [log] [blame]
Mady Mellor3dff9e62019-02-05 18:12:53 -08001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.systemui.bubbles;
17
Mark Renouf71a3af62019-04-08 15:02:54 -040018import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
19
Mark Renouf9ba6cea2019-04-17 11:53:50 -040020import static java.util.stream.Collectors.toList;
21
Mark Renouf71a3af62019-04-08 15:02:54 -040022import android.app.Notification;
23import android.app.PendingIntent;
24import android.content.Context;
Mark Renoufbbcf07f2019-05-09 10:42:43 -040025import android.service.notification.NotificationListenerService;
26import android.service.notification.NotificationListenerService.RankingMap;
Mark Renouf71a3af62019-04-08 15:02:54 -040027import android.util.Log;
Mark Renoufba5ab512019-05-02 15:21:01 -040028import android.util.Pair;
Mady Mellor3dff9e62019-02-05 18:12:53 -080029
Mark Renouf9ba6cea2019-04-17 11:53:50 -040030import androidx.annotation.Nullable;
31
Selim Cinekfdf80332019-03-07 17:29:55 -080032import com.android.internal.annotations.VisibleForTesting;
Mark Renouf71a3af62019-04-08 15:02:54 -040033import com.android.systemui.bubbles.BubbleController.DismissReason;
Mady Mellor3dff9e62019-02-05 18:12:53 -080034import com.android.systemui.statusbar.notification.collection.NotificationEntry;
35
Mark Renouf71a3af62019-04-08 15:02:54 -040036import java.util.ArrayList;
Mark Renouf71a3af62019-04-08 15:02:54 -040037import java.util.Collections;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040038import java.util.Comparator;
39import java.util.HashMap;
Mark Renouf71a3af62019-04-08 15:02:54 -040040import java.util.Iterator;
Mark Renouf3bc5b362019-04-05 14:37:59 -040041import java.util.List;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040042import java.util.Map;
Mark Renouf71a3af62019-04-08 15:02:54 -040043import java.util.Objects;
Mady Mellor3dff9e62019-02-05 18:12:53 -080044
Mady Mellorcfd06c12019-02-13 14:32:12 -080045import javax.inject.Inject;
46import javax.inject.Singleton;
47
Mady Mellor3dff9e62019-02-05 18:12:53 -080048/**
49 * Keeps track of active bubbles.
50 */
Mady Mellorcfd06c12019-02-13 14:32:12 -080051@Singleton
Selim Cinekfdf80332019-03-07 17:29:55 -080052public class BubbleData {
Mady Mellor3dff9e62019-02-05 18:12:53 -080053
Mark Renouf71a3af62019-04-08 15:02:54 -040054 private static final String TAG = "BubbleData";
Mark Renouf9ba6cea2019-04-17 11:53:50 -040055 private static final boolean DEBUG = false;
56
57 private static final int MAX_BUBBLES = 5;
58
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 Renoufba5ab512019-05-02 15:21:01 -040062 private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING =
Mark Renouf9ba6cea2019-04-17 11:53:50 -040063 Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
Mark Renouf71a3af62019-04-08 15:02:54 -040064
Mark Renouf82a40e62019-05-23 16:16:24 -040065 /** Contains information about changes that have been made to the state of bubbles. */
66 static final class Update {
67 boolean expandedChanged;
68 boolean selectionChanged;
69 boolean orderChanged;
70 boolean expanded;
71 @Nullable Bubble selectedBubble;
72 @Nullable Bubble addedBubble;
73 @Nullable Bubble updatedBubble;
74 // Pair with Bubble and @DismissReason Integer
75 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
76
77 // A read-only view of the bubbles list, changes there will be reflected here.
78 final List<Bubble> bubbles;
79
80 private Update(List<Bubble> bubbleOrder) {
81 bubbles = Collections.unmodifiableList(bubbleOrder);
82 }
83
84 boolean anythingChanged() {
85 return expandedChanged
86 || selectionChanged
87 || addedBubble != null
88 || updatedBubble != null
89 || !removedBubbles.isEmpty()
90 || orderChanged;
91 }
92
93 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
94 removedBubbles.add(new Pair<>(bubbleToRemove, reason));
95 }
96 }
97
Mark Renouf3bc5b362019-04-05 14:37:59 -040098 /**
99 * This interface reports changes to the state and appearance of bubbles which should be applied
100 * as necessary to the UI.
Mark Renouf3bc5b362019-04-05 14:37:59 -0400101 */
102 interface Listener {
Mark Renouf82a40e62019-05-23 16:16:24 -0400103 /** Reports changes have have occurred as a result of the most recent operation. */
104 void applyUpdate(Update update);
Mark Renouf3bc5b362019-04-05 14:37:59 -0400105 }
106
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400107 interface TimeSource {
108 long currentTimeMillis();
109 }
110
Mark Renouf71a3af62019-04-08 15:02:54 -0400111 private final Context mContext;
Mark Renouf82a40e62019-05-23 16:16:24 -0400112 private final List<Bubble> mBubbles;
Mark Renouf71a3af62019-04-08 15:02:54 -0400113 private Bubble mSelectedBubble;
114 private boolean mExpanded;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400115
Mark Renoufba5ab512019-05-02 15:21:01 -0400116 // State tracked during an operation -- keeps track of what listener events to dispatch.
Mark Renouf82a40e62019-05-23 16:16:24 -0400117 private Update mStateChange;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400118
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400119 private NotificationListenerService.Ranking mTmpRanking;
120
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400121 private TimeSource mTimeSource = System::currentTimeMillis;
122
123 @Nullable
Mark Renouf3bc5b362019-04-05 14:37:59 -0400124 private Listener mListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800125
Mady Mellorcfd06c12019-02-13 14:32:12 -0800126 @Inject
Mark Renouf71a3af62019-04-08 15:02:54 -0400127 public BubbleData(Context context) {
128 mContext = context;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400129 mBubbles = new ArrayList<>();
Mark Renouf82a40e62019-05-23 16:16:24 -0400130 mStateChange = new Update(mBubbles);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800131 }
132
Mark Renouf71a3af62019-04-08 15:02:54 -0400133 public boolean hasBubbles() {
134 return !mBubbles.isEmpty();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800135 }
136
Mark Renouf71a3af62019-04-08 15:02:54 -0400137 public boolean isExpanded() {
138 return mExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800139 }
140
Mark Renouf71a3af62019-04-08 15:02:54 -0400141 public boolean hasBubbleWithKey(String key) {
142 return getBubbleWithKey(key) != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800143 }
144
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400145 @Nullable
146 public Bubble getSelectedBubble() {
147 return mSelectedBubble;
148 }
149
Mark Renouf71a3af62019-04-08 15:02:54 -0400150 public void setExpanded(boolean expanded) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400151 if (DEBUG) {
152 Log.d(TAG, "setExpanded: " + expanded);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800153 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400154 setExpandedInternal(expanded);
155 dispatchPendingChanges();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800156 }
157
Mark Renouf71a3af62019-04-08 15:02:54 -0400158 public void setSelectedBubble(Bubble bubble) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400159 if (DEBUG) {
160 Log.d(TAG, "setSelectedBubble: " + bubble);
161 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400162 setSelectedBubbleInternal(bubble);
163 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400164 }
165
166 public void notificationEntryUpdated(NotificationEntry entry) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400167 if (DEBUG) {
168 Log.d(TAG, "notificationEntryUpdated: " + entry);
169 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400170 Bubble bubble = getBubbleWithKey(entry.key);
171 if (bubble == null) {
172 // Create a new bubble
Lyn Han6c40fe72019-05-08 14:06:33 -0700173 bubble = new Bubble(mContext, entry, this::onBubbleBlocked);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400174 doAdd(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400175 trim();
Mark Renouf71a3af62019-04-08 15:02:54 -0400176 } else {
177 // Updates an existing bubble
178 bubble.setEntry(entry);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400179 doUpdate(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400180 }
181 if (shouldAutoExpand(entry)) {
182 setSelectedBubbleInternal(bubble);
183 if (!mExpanded) {
184 setExpandedInternal(true);
185 }
186 } else if (mSelectedBubble == null) {
187 setSelectedBubbleInternal(bubble);
188 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400189 dispatchPendingChanges();
190 }
191
192 public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
193 if (DEBUG) {
194 Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
195 }
196 doRemove(entry.key, reason);
197 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400198 }
199
Mark Renoufbbcf07f2019-05-09 10:42:43 -0400200 /**
201 * Called when NotificationListener has received adjusted notification rank and reapplied
202 * filtering and sorting. This is used to dismiss any bubbles which should no longer be shown
203 * due to changes in permissions on the notification channel or the global setting.
204 *
205 * @param rankingMap the updated ranking map from NotificationListenerService
206 */
207 public void notificationRankingUpdated(RankingMap rankingMap) {
208 if (mTmpRanking == null) {
209 mTmpRanking = new NotificationListenerService.Ranking();
210 }
211
212 String[] orderedKeys = rankingMap.getOrderedKeys();
213 for (int i = 0; i < orderedKeys.length; i++) {
214 String key = orderedKeys[i];
215 if (hasBubbleWithKey(key)) {
216 rankingMap.getRanking(key, mTmpRanking);
217 if (!mTmpRanking.canBubble()) {
218 doRemove(key, BubbleController.DISMISS_BLOCKED);
219 }
220 }
221 }
222 dispatchPendingChanges();
223 }
224
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400225 private void doAdd(Bubble bubble) {
226 if (DEBUG) {
227 Log.d(TAG, "doAdd: " + bubble);
228 }
229 int minInsertPoint = 0;
230 boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId());
231 if (isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400232 // first bubble of a group goes to the beginning, otherwise within the existing group
233 minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400234 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400235 if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400236 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400237 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400238 mStateChange.addedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400239 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400240 mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
Mark Renoufba5ab512019-05-02 15:21:01 -0400241 // Top bubble becomes selected.
242 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400243 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400244 }
245
246 private void trim() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400247 if (mBubbles.size() > MAX_BUBBLES) {
248 mBubbles.stream()
249 // sort oldest first (ascending lastActivity)
250 .sorted(Comparator.comparingLong(Bubble::getLastActivity))
251 // skip the selected bubble
252 .filter((b) -> !b.equals(mSelectedBubble))
253 .findFirst()
Mark Renoufba5ab512019-05-02 15:21:01 -0400254 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400255 }
256 }
257
258 private void doUpdate(Bubble bubble) {
259 if (DEBUG) {
260 Log.d(TAG, "doUpdate: " + bubble);
261 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400262 mStateChange.updatedBubble = bubble;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400263 if (!isExpanded()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400264 // while collapsed, update causes re-pack
265 int prevPos = mBubbles.indexOf(bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400266 mBubbles.remove(bubble);
Mark Renoufba5ab512019-05-02 15:21:01 -0400267 int newPos = insertBubble(0, bubble);
268 if (prevPos != newPos) {
269 packGroup(newPos);
Mark Renouf82a40e62019-05-23 16:16:24 -0400270 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400271 }
272 setSelectedBubbleInternal(mBubbles.get(0));
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400273 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400274 }
275
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400276 private void doRemove(String key, @DismissReason int reason) {
277 int indexToRemove = indexForKey(key);
Mark Renoufba5ab512019-05-02 15:21:01 -0400278 if (indexToRemove == -1) {
279 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400280 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400281 Bubble bubbleToRemove = mBubbles.get(indexToRemove);
282 if (mBubbles.size() == 1) {
283 // Going to become empty, handle specially.
284 setExpandedInternal(false);
285 setSelectedBubbleInternal(null);
286 }
287 if (indexToRemove < mBubbles.size() - 1) {
288 // Removing anything but the last bubble means positions will change.
Mark Renouf82a40e62019-05-23 16:16:24 -0400289 mStateChange.orderChanged = true;
Mark Renoufba5ab512019-05-02 15:21:01 -0400290 }
291 mBubbles.remove(indexToRemove);
Mark Renouf82a40e62019-05-23 16:16:24 -0400292 mStateChange.bubbleRemoved(bubbleToRemove, reason);
Mark Renoufba5ab512019-05-02 15:21:01 -0400293 if (!isExpanded()) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400294 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400295 }
296
297 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
298 if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
299 // Move selection to the new bubble at the same position.
300 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
301 Bubble newSelected = mBubbles.get(newIndex);
302 setSelectedBubbleInternal(newSelected);
303 }
304 bubbleToRemove.setDismissed();
305 maybeSendDeleteIntent(reason, bubbleToRemove.entry);
Mark Renouf71a3af62019-04-08 15:02:54 -0400306 }
307
308 public void dismissAll(@DismissReason int reason) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400309 if (DEBUG) {
310 Log.d(TAG, "dismissAll: reason=" + reason);
311 }
312 if (mBubbles.isEmpty()) {
313 return;
314 }
315 setExpandedInternal(false);
316 setSelectedBubbleInternal(null);
Mark Renouf71a3af62019-04-08 15:02:54 -0400317 while (!mBubbles.isEmpty()) {
318 Bubble bubble = mBubbles.remove(0);
319 bubble.setDismissed();
320 maybeSendDeleteIntent(reason, bubble.entry);
Mark Renouf82a40e62019-05-23 16:16:24 -0400321 mStateChange.bubbleRemoved(bubble, reason);
Mark Renouf71a3af62019-04-08 15:02:54 -0400322 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400323 dispatchPendingChanges();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400324 }
325
Mark Renoufba5ab512019-05-02 15:21:01 -0400326 private void dispatchPendingChanges() {
Mark Renouf82a40e62019-05-23 16:16:24 -0400327 if (mListener != null && mStateChange.anythingChanged()) {
328 mListener.applyUpdate(mStateChange);
Mark Renoufba5ab512019-05-02 15:21:01 -0400329 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400330 mStateChange = new Update(mBubbles);
Mark Renouf71a3af62019-04-08 15:02:54 -0400331 }
332
333 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400334 * Requests a change to the selected bubble.
Mark Renouf71a3af62019-04-08 15:02:54 -0400335 *
336 * @param bubble the new selected bubble
Mark Renouf71a3af62019-04-08 15:02:54 -0400337 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400338 private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400339 if (DEBUG) {
340 Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
341 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400342 if (Objects.equals(bubble, mSelectedBubble)) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400343 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400344 }
345 if (bubble != null && !mBubbles.contains(bubble)) {
346 Log.e(TAG, "Cannot select bubble which doesn't exist!"
347 + " (" + bubble + ") bubbles=" + mBubbles);
Mark Renoufba5ab512019-05-02 15:21:01 -0400348 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400349 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400350 if (mExpanded && bubble != null) {
Mady Mellor73310bc12019-05-01 20:47:49 -0700351 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf71a3af62019-04-08 15:02:54 -0400352 }
353 mSelectedBubble = bubble;
Mark Renouf82a40e62019-05-23 16:16:24 -0400354 mStateChange.selectedBubble = bubble;
355 mStateChange.selectionChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400356 }
357
Mark Renouf71a3af62019-04-08 15:02:54 -0400358 /**
Mark Renouf82a40e62019-05-23 16:16:24 -0400359 * Requests a change to the expanded state.
Mark Renouf71a3af62019-04-08 15:02:54 -0400360 *
361 * @param shouldExpand the new requested state
Mark Renouf71a3af62019-04-08 15:02:54 -0400362 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400363 private void setExpandedInternal(boolean shouldExpand) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400364 if (DEBUG) {
365 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
366 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400367 if (mExpanded == shouldExpand) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400368 return;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400369 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400370 if (shouldExpand) {
371 if (mBubbles.isEmpty()) {
372 Log.e(TAG, "Attempt to expand stack when empty!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400373 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400374 }
375 if (mSelectedBubble == null) {
376 Log.e(TAG, "Attempt to expand stack without selected bubble!");
Mark Renoufba5ab512019-05-02 15:21:01 -0400377 return;
Mark Renouf71a3af62019-04-08 15:02:54 -0400378 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400379 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
Mark Renouf82a40e62019-05-23 16:16:24 -0400380 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400381 } else if (!mBubbles.isEmpty()) {
382 // Apply ordering and grouping rules from expanded -> collapsed, then save
383 // the result.
Mark Renouf82a40e62019-05-23 16:16:24 -0400384 mStateChange.orderChanged |= repackAll();
Mark Renoufba5ab512019-05-02 15:21:01 -0400385 // Save the state which should be returned to when expanded (with no other changes)
386
387 if (mBubbles.indexOf(mSelectedBubble) > 0) {
388 // Move the selected bubble to the top while collapsed.
389 if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) {
390 // The selected bubble cannot be raised to the first position because
391 // there is an ongoing bubble there. Instead, force the top ongoing bubble
392 // to become selected.
393 setSelectedBubbleInternal(mBubbles.get(0));
394 } else {
395 // Raise the selected bubble (and it's group) up to the front so the selected
396 // bubble remains on top.
397 mBubbles.remove(mSelectedBubble);
398 mBubbles.add(0, mSelectedBubble);
399 packGroup(0);
400 }
401 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400402 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400403 mExpanded = shouldExpand;
Mark Renouf82a40e62019-05-23 16:16:24 -0400404 mStateChange.expanded = shouldExpand;
405 mStateChange.expandedChanged = true;
Mark Renouf71a3af62019-04-08 15:02:54 -0400406 }
407
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400408 private static long sortKey(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400409 long key = bubble.getLastUpdateTime();
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400410 if (bubble.isOngoing()) {
411 // Set 2nd highest bit (signed long int), to partition between ongoing and regular
412 key |= 0x4000000000000000L;
413 }
414 return key;
415 }
416
417 /**
418 * Locates and inserts the bubble into a sorted position. The is inserted
419 * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
420 * required to keep grouping intact.
421 *
422 * @param minPosition the first insert point to consider
423 * @param newBubble the bubble to insert
424 * @return the position where the bubble was inserted
425 */
426 private int insertBubble(int minPosition, Bubble newBubble) {
427 long newBubbleSortKey = sortKey(newBubble);
428 String previousGroupId = null;
429
430 for (int pos = minPosition; pos < mBubbles.size(); pos++) {
431 Bubble bubbleAtPos = mBubbles.get(pos);
432 String groupIdAtPos = bubbleAtPos.getGroupId();
433 boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
434
435 if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
436 // Insert before the start of first group which has older bubbles.
437 mBubbles.add(pos, newBubble);
438 return pos;
439 }
440 previousGroupId = groupIdAtPos;
441 }
442 mBubbles.add(newBubble);
443 return mBubbles.size() - 1;
444 }
445
446 private boolean hasBubbleWithGroupId(String groupId) {
447 return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
448 }
449
450 private int findFirstIndexForGroup(String appId) {
451 for (int i = 0; i < mBubbles.size(); i++) {
452 Bubble bubbleAtPos = mBubbles.get(i);
453 if (bubbleAtPos.getGroupId().equals(appId)) {
454 return i;
455 }
456 }
457 return 0;
458 }
459
460 /**
461 * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
462 * at positions lower than {@code position} are unchanged. Relative order within the group
463 * unchanged. Relative order of any other bubbles are also unchanged.
464 *
465 * @param position the position of the first bubble for the group
Mark Renoufba5ab512019-05-02 15:21:01 -0400466 * @return true if the position of any bubbles has changed as a result
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400467 */
Mark Renoufba5ab512019-05-02 15:21:01 -0400468 private boolean packGroup(int position) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400469 if (DEBUG) {
470 Log.d(TAG, "packGroup: position=" + position);
471 }
472 Bubble groupStart = mBubbles.get(position);
473 final String groupAppId = groupStart.getGroupId();
474 List<Bubble> moving = new ArrayList<>();
475
476 // Walk backward, collect bubbles within the group
477 for (int i = mBubbles.size() - 1; i > position; i--) {
478 if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
479 moving.add(0, mBubbles.get(i));
480 }
481 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400482 if (moving.isEmpty()) {
483 return false;
484 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400485 mBubbles.removeAll(moving);
486 mBubbles.addAll(position + 1, moving);
Mark Renoufba5ab512019-05-02 15:21:01 -0400487 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400488 }
489
Mark Renoufba5ab512019-05-02 15:21:01 -0400490 /**
491 * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped
492 * by groupId. Each group is then sorted by the max(lastUpdated) time of it's bubbles. Bubbles
493 * within each group are then sorted by lastUpdated descending.
494 *
495 * @return true if the position of any bubbles changed as a result
496 */
497 private boolean repackAll() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400498 if (DEBUG) {
499 Log.d(TAG, "repackAll()");
500 }
501 if (mBubbles.isEmpty()) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400502 return false;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400503 }
504 Map<String, Long> groupLastActivity = new HashMap<>();
505 for (Bubble bubble : mBubbles) {
506 long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
507 long sortKeyForBubble = sortKey(bubble);
508 if (sortKeyForBubble > maxSortKeyForGroup) {
509 groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
510 }
511 }
512
513 // Sort groups by their most recently active bubble
514 List<String> groupsByMostRecentActivity =
515 groupLastActivity.entrySet().stream()
Mark Renoufba5ab512019-05-02 15:21:01 -0400516 .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400517 .map(Map.Entry::getKey)
518 .collect(toList());
519
520 List<Bubble> repacked = new ArrayList<>(mBubbles.size());
521
522 // For each group, add bubbles, freshest to oldest
523 for (String appId : groupsByMostRecentActivity) {
524 mBubbles.stream()
525 .filter((b) -> b.getGroupId().equals(appId))
Mark Renoufba5ab512019-05-02 15:21:01 -0400526 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400527 .forEachOrdered(repacked::add);
528 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400529 if (repacked.equals(mBubbles)) {
530 return false;
531 }
Mark Renouf82a40e62019-05-23 16:16:24 -0400532 mBubbles.clear();
533 mBubbles.addAll(repacked);
Mark Renoufba5ab512019-05-02 15:21:01 -0400534 return true;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400535 }
536
Mark Renouf71a3af62019-04-08 15:02:54 -0400537 private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
538 if (reason == BubbleController.DISMISS_USER_GESTURE) {
539 Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
540 PendingIntent deleteIntent = bubbleMetadata != null
541 ? bubbleMetadata.getDeleteIntent()
542 : null;
543 if (deleteIntent != null) {
544 try {
545 deleteIntent.send();
546 } catch (PendingIntent.CanceledException e) {
547 Log.w(TAG, "Failed to send delete intent for bubble with key: " + entry.key);
548 }
549 }
550 }
551 }
552
553 private void onBubbleBlocked(NotificationEntry entry) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400554 final String blockedGroupId = Bubble.groupId(entry);
555 int selectedIndex = mBubbles.indexOf(mSelectedBubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400556 for (Iterator<Bubble> i = mBubbles.iterator(); i.hasNext(); ) {
557 Bubble bubble = i.next();
Mark Renoufba5ab512019-05-02 15:21:01 -0400558 if (bubble.getGroupId().equals(blockedGroupId)) {
Mark Renouf82a40e62019-05-23 16:16:24 -0400559 mStateChange.bubbleRemoved(bubble, BubbleController.DISMISS_BLOCKED);
Mark Renouf71a3af62019-04-08 15:02:54 -0400560 i.remove();
Mark Renouf71a3af62019-04-08 15:02:54 -0400561 }
562 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400563 if (mBubbles.isEmpty()) {
564 setExpandedInternal(false);
565 setSelectedBubbleInternal(null);
566 } else if (!mBubbles.contains(mSelectedBubble)) {
567 // choose a new one
568 int newIndex = Math.min(selectedIndex, mBubbles.size() - 1);
569 Bubble newSelected = mBubbles.get(newIndex);
570 setSelectedBubbleInternal(newSelected);
Mark Renouf71a3af62019-04-08 15:02:54 -0400571 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400572 dispatchPendingChanges();
Mark Renouf71a3af62019-04-08 15:02:54 -0400573 }
574
575 private int indexForKey(String key) {
576 for (int i = 0; i < mBubbles.size(); i++) {
577 Bubble bubble = mBubbles.get(i);
578 if (bubble.getKey().equals(key)) {
579 return i;
580 }
581 }
582 return -1;
583 }
584
Mark Renouf71a3af62019-04-08 15:02:54 -0400585 /**
586 * The set of bubbles.
Mark Renouf71a3af62019-04-08 15:02:54 -0400587 */
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400588 @VisibleForTesting(visibility = PRIVATE)
589 public List<Bubble> getBubbles() {
Mark Renouf71a3af62019-04-08 15:02:54 -0400590 return Collections.unmodifiableList(mBubbles);
591 }
592
593 @VisibleForTesting(visibility = PRIVATE)
594 Bubble getBubbleWithKey(String key) {
595 for (int i = 0; i < mBubbles.size(); i++) {
596 Bubble bubble = mBubbles.get(i);
597 if (bubble.getKey().equals(key)) {
598 return bubble;
599 }
600 }
601 return null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800602 }
Mark Renouf3bc5b362019-04-05 14:37:59 -0400603
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400604 @VisibleForTesting(visibility = PRIVATE)
605 void setTimeSource(TimeSource timeSource) {
606 mTimeSource = timeSource;
607 }
608
Mark Renouf3bc5b362019-04-05 14:37:59 -0400609 public void setListener(Listener listener) {
610 mListener = listener;
611 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400612
613 boolean shouldAutoExpand(NotificationEntry entry) {
614 Notification.BubbleMetadata metadata = entry.getBubbleMetadata();
615 return metadata != null && metadata.getAutoExpandBubble()
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400616 && BubbleController.isForegroundApp(mContext, entry.notification.getPackageName());
Mark Renouf71a3af62019-04-08 15:02:54 -0400617 }
618}