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