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