blob: 04ff58b36c944b31c5628d2e2bab7e92af9e807e [file] [log] [blame]
Ned Burnsf81c4c42019-01-07 14:10:43 -05001/*
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 */
16
17package com.android.systemui.statusbar.notification.collection;
18
Julia Reynoldsefcd8bd2019-02-06 08:56:15 -050019import android.app.Notification;
Ned Burnsf81c4c42019-01-07 14:10:43 -050020import android.app.NotificationChannel;
21import android.app.NotificationManager;
Julia Reynoldsefcd8bd2019-02-06 08:56:15 -050022import android.app.Person;
Ned Burnsf81c4c42019-01-07 14:10:43 -050023import android.service.notification.NotificationListenerService.Ranking;
24import android.service.notification.NotificationListenerService.RankingMap;
25import android.service.notification.SnoozeCriterion;
26import android.service.notification.StatusBarNotification;
27import android.util.ArrayMap;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.systemui.Dependency;
31import com.android.systemui.statusbar.NotificationMediaManager;
32import com.android.systemui.statusbar.notification.NotificationFilter;
33import com.android.systemui.statusbar.phone.NotificationGroupManager;
34import com.android.systemui.statusbar.policy.HeadsUpManager;
35
36import java.io.PrintWriter;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.Comparator;
40import java.util.List;
41import java.util.Objects;
42
43/**
44 * The list of currently displaying notifications.
45 */
46public class NotificationData {
47
48 private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class);
49
50 /**
51 * These dependencies are late init-ed
52 */
53 private KeyguardEnvironment mEnvironment;
54 private NotificationMediaManager mMediaManager;
55
56 private HeadsUpManager mHeadsUpManager;
57
58 private final ArrayMap<String, NotificationEntry> mEntries = new ArrayMap<>();
59 private final ArrayList<NotificationEntry> mSortedAndFiltered = new ArrayList<>();
60 private final ArrayList<NotificationEntry> mFilteredForUser = new ArrayList<>();
61
62 private final NotificationGroupManager mGroupManager =
63 Dependency.get(NotificationGroupManager.class);
64
65 private RankingMap mRankingMap;
66 private final Ranking mTmpRanking = new Ranking();
67
68 public void setHeadsUpManager(HeadsUpManager headsUpManager) {
69 mHeadsUpManager = headsUpManager;
70 }
71
72 private final Comparator<NotificationEntry> mRankingComparator =
73 new Comparator<NotificationEntry>() {
74 private final Ranking mRankingA = new Ranking();
75 private final Ranking mRankingB = new Ranking();
76
77 @Override
78 public int compare(NotificationEntry a, NotificationEntry b) {
79 final StatusBarNotification na = a.notification;
80 final StatusBarNotification nb = b.notification;
81 int aImportance = NotificationManager.IMPORTANCE_DEFAULT;
82 int bImportance = NotificationManager.IMPORTANCE_DEFAULT;
83 int aRank = 0;
84 int bRank = 0;
85
86 if (mRankingMap != null) {
87 // RankingMap as received from NoMan
88 getRanking(a.key, mRankingA);
89 getRanking(b.key, mRankingB);
90 aImportance = mRankingA.getImportance();
91 bImportance = mRankingB.getImportance();
92 aRank = mRankingA.getRank();
93 bRank = mRankingB.getRank();
94 }
95
96 String mediaNotification = getMediaManager().getMediaNotificationKey();
97
98 // IMPORTANCE_MIN media streams are allowed to drift to the bottom
99 final boolean aMedia = a.key.equals(mediaNotification)
100 && aImportance > NotificationManager.IMPORTANCE_MIN;
101 final boolean bMedia = b.key.equals(mediaNotification)
102 && bImportance > NotificationManager.IMPORTANCE_MIN;
103
104 boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH
105 && isSystemNotification(na);
106 boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH
107 && isSystemNotification(nb);
108
109 boolean isHeadsUp = a.getRow().isHeadsUp();
110 if (isHeadsUp != b.getRow().isHeadsUp()) {
111 return isHeadsUp ? -1 : 1;
112 } else if (isHeadsUp) {
113 // Provide consistent ranking with headsUpManager
114 return mHeadsUpManager.compare(a, b);
Selim Cinek459aee32019-02-20 11:18:56 -0800115 } else if (a.getRow().showingAmbientPulsing() != b.getRow().showingAmbientPulsing()) {
116 return a.getRow().showingAmbientPulsing() ? -1 : 1;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500117 } else if (aMedia != bMedia) {
118 // Upsort current media notification.
119 return aMedia ? -1 : 1;
120 } else if (aSystemMax != bSystemMax) {
121 // Upsort PRIORITY_MAX system notifications
122 return aSystemMax ? -1 : 1;
123 } else if (aRank != bRank) {
124 return aRank - bRank;
125 } else {
126 return Long.compare(nb.getNotification().when, na.getNotification().when);
127 }
128 }
129 };
130
131 private KeyguardEnvironment getEnvironment() {
132 if (mEnvironment == null) {
133 mEnvironment = Dependency.get(KeyguardEnvironment.class);
134 }
135 return mEnvironment;
136 }
137
138 private NotificationMediaManager getMediaManager() {
139 if (mMediaManager == null) {
140 mMediaManager = Dependency.get(NotificationMediaManager.class);
141 }
142 return mMediaManager;
143 }
144
145 /**
146 * Returns the sorted list of active notifications (depending on {@link KeyguardEnvironment}
147 *
148 * <p>
149 * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
150 * when the environment changes.
151 * <p>
152 * Don't hold on to or modify the returned list.
153 */
154 public ArrayList<NotificationEntry> getActiveNotifications() {
155 return mSortedAndFiltered;
156 }
157
158 public ArrayList<NotificationEntry> getNotificationsForCurrentUser() {
159 mFilteredForUser.clear();
160
161 synchronized (mEntries) {
162 final int len = mEntries.size();
163 for (int i = 0; i < len; i++) {
164 NotificationEntry entry = mEntries.valueAt(i);
165 final StatusBarNotification sbn = entry.notification;
166 if (!getEnvironment().isNotificationForCurrentProfiles(sbn)) {
167 continue;
168 }
169 mFilteredForUser.add(entry);
170 }
171 }
172 return mFilteredForUser;
173 }
174
175 public NotificationEntry get(String key) {
176 return mEntries.get(key);
177 }
178
179 public void add(NotificationEntry entry) {
180 synchronized (mEntries) {
181 mEntries.put(entry.notification.getKey(), entry);
182 }
183 mGroupManager.onEntryAdded(entry);
184
185 updateRankingAndSort(mRankingMap);
186 }
187
188 public NotificationEntry remove(String key, RankingMap ranking) {
189 NotificationEntry removed;
190 synchronized (mEntries) {
191 removed = mEntries.remove(key);
192 }
193 if (removed == null) return null;
194 mGroupManager.onEntryRemoved(removed);
195 updateRankingAndSort(ranking);
196 return removed;
197 }
198
199 /** Updates the given notification entry with the provided ranking. */
200 public void update(
201 NotificationEntry entry,
202 RankingMap ranking,
203 StatusBarNotification notification) {
204 updateRanking(ranking);
205 final StatusBarNotification oldNotification = entry.notification;
206 entry.notification = notification;
207 mGroupManager.onEntryUpdated(entry, oldNotification);
208 }
209
210 public void updateRanking(RankingMap ranking) {
211 updateRankingAndSort(ranking);
212 }
213
214 public void updateAppOp(int appOp, int uid, String pkg, String key, boolean showIcon) {
215 synchronized (mEntries) {
216 final int len = mEntries.size();
217 for (int i = 0; i < len; i++) {
218 NotificationEntry entry = mEntries.valueAt(i);
219 if (uid == entry.notification.getUid()
220 && pkg.equals(entry.notification.getPackageName())
221 && key.equals(entry.key)) {
222 if (showIcon) {
223 entry.mActiveAppOps.add(appOp);
224 } else {
225 entry.mActiveAppOps.remove(appOp);
226 }
227 }
228 }
229 }
230 }
231
232 /**
233 * Returns true if this notification should be displayed in the high-priority notifications
234 * section (and on the lockscreen and status bar).
235 */
236 public boolean isHighPriority(StatusBarNotification statusBarNotification) {
237 if (mRankingMap != null) {
238 getRanking(statusBarNotification.getKey(), mTmpRanking);
239 if (mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT
Julia Reynoldsefcd8bd2019-02-06 08:56:15 -0500240 || isImportantOngoing(statusBarNotification.getNotification())
241 || statusBarNotification.getNotification().hasMediaSession()
242 || hasPerson(statusBarNotification.getNotification())
243 || hasStyle(statusBarNotification.getNotification(),
244 Notification.MessagingStyle.class)) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500245 return true;
246 }
247 if (mGroupManager.isSummaryOfGroup(statusBarNotification)) {
248 final ArrayList<NotificationEntry> logicalChildren =
249 mGroupManager.getLogicalChildren(statusBarNotification);
250 for (NotificationEntry child : logicalChildren) {
251 if (isHighPriority(child.notification)) {
252 return true;
253 }
254 }
255 }
256 }
257 return false;
258 }
259
Julia Reynoldsefcd8bd2019-02-06 08:56:15 -0500260 private boolean isImportantOngoing(Notification notification) {
261 return notification.isForegroundService()
262 && mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_LOW;
263 }
264
265 private boolean hasStyle(Notification notification, Class targetStyle) {
266 Class<? extends Notification.Style> style = notification.getNotificationStyle();
267 return targetStyle.equals(style);
268 }
269
270 private boolean hasPerson(Notification notification) {
271 // TODO: cache favorite and recent contacts to check contact affinity
272 ArrayList<Person> people = notification.extras != null
273 ? notification.extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST)
274 : new ArrayList<>();
275 return people != null && !people.isEmpty();
276 }
277
Ned Burnsf81c4c42019-01-07 14:10:43 -0500278 public boolean isAmbient(String key) {
279 if (mRankingMap != null) {
280 getRanking(key, mTmpRanking);
281 return mTmpRanking.isAmbient();
282 }
283 return false;
284 }
285
286 public int getVisibilityOverride(String key) {
287 if (mRankingMap != null) {
288 getRanking(key, mTmpRanking);
289 return mTmpRanking.getVisibilityOverride();
290 }
291 return Ranking.VISIBILITY_NO_OVERRIDE;
292 }
293
294 public int getImportance(String key) {
295 if (mRankingMap != null) {
296 getRanking(key, mTmpRanking);
297 return mTmpRanking.getImportance();
298 }
299 return NotificationManager.IMPORTANCE_UNSPECIFIED;
300 }
301
302 public String getOverrideGroupKey(String key) {
303 if (mRankingMap != null) {
304 getRanking(key, mTmpRanking);
305 return mTmpRanking.getOverrideGroupKey();
306 }
307 return null;
308 }
309
310 public List<SnoozeCriterion> getSnoozeCriteria(String key) {
311 if (mRankingMap != null) {
312 getRanking(key, mTmpRanking);
313 return mTmpRanking.getSnoozeCriteria();
314 }
315 return null;
316 }
317
318 public NotificationChannel getChannel(String key) {
319 if (mRankingMap != null) {
320 getRanking(key, mTmpRanking);
321 return mTmpRanking.getChannel();
322 }
323 return null;
324 }
325
326 public int getRank(String key) {
327 if (mRankingMap != null) {
328 getRanking(key, mTmpRanking);
329 return mTmpRanking.getRank();
330 }
331 return 0;
332 }
333
334 public boolean shouldHide(String key) {
335 if (mRankingMap != null) {
336 getRanking(key, mTmpRanking);
337 return mTmpRanking.isSuspended();
338 }
339 return false;
340 }
341
342 private void updateRankingAndSort(RankingMap ranking) {
343 if (ranking != null) {
344 mRankingMap = ranking;
345 synchronized (mEntries) {
346 final int len = mEntries.size();
347 for (int i = 0; i < len; i++) {
348 NotificationEntry entry = mEntries.valueAt(i);
349 if (!getRanking(entry.key, mTmpRanking)) {
350 continue;
351 }
352 final StatusBarNotification oldSbn = entry.notification.cloneLight();
353 final String overrideGroupKey = getOverrideGroupKey(entry.key);
354 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
355 entry.notification.setOverrideGroupKey(overrideGroupKey);
356 mGroupManager.onEntryUpdated(entry, oldSbn);
357 }
358 entry.populateFromRanking(mTmpRanking);
Gus Prevascaed15c2019-01-18 14:19:51 -0500359 entry.setIsHighPriority(isHighPriority(entry.notification));
Ned Burnsf81c4c42019-01-07 14:10:43 -0500360 }
361 }
362 }
363 filterAndSort();
364 }
365
366 /**
367 * Get the ranking from the current ranking map.
368 *
369 * @param key the key to look up
370 * @param outRanking the ranking to populate
371 *
372 * @return {@code true} if the ranking was properly obtained.
373 */
374 @VisibleForTesting
375 protected boolean getRanking(String key, Ranking outRanking) {
376 return mRankingMap.getRanking(key, outRanking);
377 }
378
379 // TODO: This should not be public. Instead the Environment should notify this class when
380 // anything changed, and this class should call back the UI so it updates itself.
381 public void filterAndSort() {
382 mSortedAndFiltered.clear();
383
384 synchronized (mEntries) {
385 final int len = mEntries.size();
386 for (int i = 0; i < len; i++) {
387 NotificationEntry entry = mEntries.valueAt(i);
388
389 if (mNotificationFilter.shouldFilterOut(entry)) {
390 continue;
391 }
392
393 mSortedAndFiltered.add(entry);
394 }
395 }
396
397 Collections.sort(mSortedAndFiltered, mRankingComparator);
398 }
399
400 public void dump(PrintWriter pw, String indent) {
401 int filteredLen = mSortedAndFiltered.size();
402 pw.print(indent);
403 pw.println("active notifications: " + filteredLen);
404 int active;
405 for (active = 0; active < filteredLen; active++) {
406 NotificationEntry e = mSortedAndFiltered.get(active);
407 dumpEntry(pw, indent, active, e);
408 }
409 synchronized (mEntries) {
410 int totalLen = mEntries.size();
411 pw.print(indent);
412 pw.println("inactive notifications: " + (totalLen - active));
413 int inactiveCount = 0;
414 for (int i = 0; i < totalLen; i++) {
415 NotificationEntry entry = mEntries.valueAt(i);
416 if (!mSortedAndFiltered.contains(entry)) {
417 dumpEntry(pw, indent, inactiveCount, entry);
418 inactiveCount++;
419 }
420 }
421 }
422 }
423
424 private void dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e) {
425 getRanking(e.key, mTmpRanking);
426 pw.print(indent);
427 pw.println(" [" + i + "] key=" + e.key + " icon=" + e.icon);
428 StatusBarNotification n = e.notification;
429 pw.print(indent);
430 pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance="
431 + mTmpRanking.getImportance());
432 pw.print(indent);
433 pw.println(" notification=" + n.getNotification());
434 }
435
436 private static boolean isSystemNotification(StatusBarNotification sbn) {
437 String sbnPackage = sbn.getPackageName();
438 return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
439 }
440
441 /**
442 * Provides access to keyguard state and user settings dependent data.
443 */
444 public interface KeyguardEnvironment {
445 boolean isDeviceProvisioned();
446 boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
447 }
448}