1 /*
2  * Copyright (C) 2008 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;
18 
19 import android.app.Notification;
20 import android.os.SystemClock;
21 import android.service.notification.NotificationListenerService;
22 import android.service.notification.NotificationListenerService.Ranking;
23 import android.service.notification.NotificationListenerService.RankingMap;
24 import android.service.notification.StatusBarNotification;
25 import android.util.ArrayMap;
26 import android.view.View;
27 
28 import com.android.systemui.statusbar.phone.NotificationGroupManager;
29 import com.android.systemui.statusbar.policy.HeadsUpManager;
30 
31 import java.io.PrintWriter;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.Comparator;
35 
36 /**
37  * The list of currently displaying notifications.
38  */
39 public class NotificationData {
40 
41     private final Environment mEnvironment;
42     private HeadsUpManager mHeadsUpManager;
43 
44     public static final class Entry {
45         private static final long LAUNCH_COOLDOWN = 2000;
46         private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
47         public String key;
48         public StatusBarNotification notification;
49         public StatusBarIconView icon;
50         public ExpandableNotificationRow row; // the outer expanded view
51         private boolean interruption;
52         public boolean autoRedacted; // whether the redacted notification was generated by us
53         public boolean legacy; // whether the notification has a legacy, dark background
54         public int targetSdk;
55         private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
56 
Entry(StatusBarNotification n, StatusBarIconView ic)57         public Entry(StatusBarNotification n, StatusBarIconView ic) {
58             this.key = n.getKey();
59             this.notification = n;
60             this.icon = ic;
61         }
62 
setInterruption()63         public void setInterruption() {
64             interruption = true;
65         }
66 
hasInterrupted()67         public boolean hasInterrupted() {
68             return interruption;
69         }
70 
71         /**
72          * Resets the notification entry to be re-used.
73          */
reset()74         public void reset() {
75             // NOTE: Icon needs to be preserved for now.
76             // We should fix this at some point.
77             autoRedacted = false;
78             legacy = false;
79             lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
80             if (row != null) {
81                 row.reset();
82             }
83         }
84 
getContentView()85         public View getContentView() {
86             return row.getPrivateLayout().getContractedChild();
87         }
88 
getExpandedContentView()89         public View getExpandedContentView() {
90             return row.getPrivateLayout().getExpandedChild();
91         }
92 
getHeadsUpContentView()93         public View getHeadsUpContentView() {
94             return row.getPrivateLayout().getHeadsUpChild();
95         }
96 
getPublicContentView()97         public View getPublicContentView() {
98             return row.getPublicLayout().getContractedChild();
99         }
100 
notifyFullScreenIntentLaunched()101         public void notifyFullScreenIntentLaunched() {
102             lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
103         }
104 
hasJustLaunchedFullScreenIntent()105         public boolean hasJustLaunchedFullScreenIntent() {
106             return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
107         }
108     }
109 
110     private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
111     private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>();
112 
113     private NotificationGroupManager mGroupManager;
114 
115     private RankingMap mRankingMap;
116     private final Ranking mTmpRanking = new Ranking();
117 
setHeadsUpManager(HeadsUpManager headsUpManager)118     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
119         mHeadsUpManager = headsUpManager;
120     }
121 
122     private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
123         private final Ranking mRankingA = new Ranking();
124         private final Ranking mRankingB = new Ranking();
125 
126         @Override
127         public int compare(Entry a, Entry b) {
128             final StatusBarNotification na = a.notification;
129             final StatusBarNotification nb = b.notification;
130             final int aPriority = na.getNotification().priority;
131             final int bPriority = nb.getNotification().priority;
132 
133             String mediaNotification = mEnvironment.getCurrentMediaNotificationKey();
134 
135             // PRIORITY_MIN media streams are allowed to drift to the bottom
136             final boolean aMedia = a.key.equals(mediaNotification)
137                     && aPriority > Notification.PRIORITY_MIN;
138             final boolean bMedia = b.key.equals(mediaNotification)
139                     && bPriority > Notification.PRIORITY_MIN;
140 
141             boolean aSystemMax = aPriority >= Notification.PRIORITY_MAX &&
142                     isSystemNotification(na);
143             boolean bSystemMax = bPriority >= Notification.PRIORITY_MAX &&
144                     isSystemNotification(nb);
145             int d = nb.getScore() - na.getScore();
146 
147             boolean isHeadsUp = a.row.isHeadsUp();
148             if (isHeadsUp != b.row.isHeadsUp()) {
149                 return isHeadsUp ? -1 : 1;
150             } else if (isHeadsUp) {
151                 // Provide consistent ranking with headsUpManager
152                 return mHeadsUpManager.compare(a, b);
153             } else if (aMedia != bMedia) {
154                 // Upsort current media notification.
155                 return aMedia ? -1 : 1;
156             } else if (aSystemMax != bSystemMax) {
157                 // Upsort PRIORITY_MAX system notifications
158                 return aSystemMax ? -1 : 1;
159             } else if (mRankingMap != null) {
160                 // RankingMap as received from NoMan
161                 mRankingMap.getRanking(a.key, mRankingA);
162                 mRankingMap.getRanking(b.key, mRankingB);
163                 return mRankingA.getRank() - mRankingB.getRank();
164             } if (d != 0) {
165                 return d;
166             } else {
167                 return (int) (nb.getNotification().when - na.getNotification().when);
168             }
169         }
170     };
171 
NotificationData(Environment environment)172     public NotificationData(Environment environment) {
173         mEnvironment = environment;
174         mGroupManager = environment.getGroupManager();
175     }
176 
177     /**
178      * Returns the sorted list of active notifications (depending on {@link Environment}
179      *
180      * <p>
181      * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
182      * when the environment changes.
183      * <p>
184      * Don't hold on to or modify the returned list.
185      */
getActiveNotifications()186     public ArrayList<Entry> getActiveNotifications() {
187         return mSortedAndFiltered;
188     }
189 
get(String key)190     public Entry get(String key) {
191         return mEntries.get(key);
192     }
193 
add(Entry entry, RankingMap ranking)194     public void add(Entry entry, RankingMap ranking) {
195         mEntries.put(entry.notification.getKey(), entry);
196         updateRankingAndSort(ranking);
197         mGroupManager.onEntryAdded(entry);
198     }
199 
remove(String key, RankingMap ranking)200     public Entry remove(String key, RankingMap ranking) {
201         Entry removed = mEntries.remove(key);
202         if (removed == null) return null;
203         updateRankingAndSort(ranking);
204         mGroupManager.onEntryRemoved(removed);
205         return removed;
206     }
207 
updateRanking(RankingMap ranking)208     public void updateRanking(RankingMap ranking) {
209         updateRankingAndSort(ranking);
210     }
211 
isAmbient(String key)212     public boolean isAmbient(String key) {
213         if (mRankingMap != null) {
214             mRankingMap.getRanking(key, mTmpRanking);
215             return mTmpRanking.isAmbient();
216         }
217         return false;
218     }
219 
getVisibilityOverride(String key)220     public int getVisibilityOverride(String key) {
221         if (mRankingMap != null) {
222             mRankingMap.getRanking(key, mTmpRanking);
223             return mTmpRanking.getVisibilityOverride();
224         }
225         return NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
226     }
227 
updateRankingAndSort(RankingMap ranking)228     private void updateRankingAndSort(RankingMap ranking) {
229         if (ranking != null) {
230             mRankingMap = ranking;
231         }
232         filterAndSort();
233     }
234 
235     // TODO: This should not be public. Instead the Environment should notify this class when
236     // anything changed, and this class should call back the UI so it updates itself.
filterAndSort()237     public void filterAndSort() {
238         mSortedAndFiltered.clear();
239 
240         final int N = mEntries.size();
241         for (int i = 0; i < N; i++) {
242             Entry entry = mEntries.valueAt(i);
243             StatusBarNotification sbn = entry.notification;
244 
245             if (shouldFilterOut(sbn)) {
246                 continue;
247             }
248 
249             mSortedAndFiltered.add(entry);
250         }
251 
252         Collections.sort(mSortedAndFiltered, mRankingComparator);
253     }
254 
shouldFilterOut(StatusBarNotification sbn)255     boolean shouldFilterOut(StatusBarNotification sbn) {
256         if (!(mEnvironment.isDeviceProvisioned() ||
257                 showNotificationEvenIfUnprovisioned(sbn))) {
258             return true;
259         }
260 
261         if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) {
262             return true;
263         }
264 
265         if (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET &&
266                 mEnvironment.shouldHideSensitiveContents(sbn.getUserId())) {
267             return true;
268         }
269 
270         if (!BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS
271                 && mGroupManager.isChildInGroupWithSummary(sbn)) {
272             return true;
273         }
274         return false;
275     }
276 
277     /**
278      * Return whether there are any clearable notifications (that aren't errors).
279      */
hasActiveClearableNotifications()280     public boolean hasActiveClearableNotifications() {
281         for (Entry e : mSortedAndFiltered) {
282             if (e.getContentView() != null) { // the view successfully inflated
283                 if (e.notification.isClearable()) {
284                     return true;
285                 }
286             }
287         }
288         return false;
289     }
290 
291     // Q: What kinds of notifications should show during setup?
292     // A: Almost none! Only things coming from the system (package is "android") that also
293     // have special "kind" tags marking them as relevant for setup (see below).
showNotificationEvenIfUnprovisioned(StatusBarNotification sbn)294     public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
295         return "android".equals(sbn.getPackageName())
296                 && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
297     }
298 
dump(PrintWriter pw, String indent)299     public void dump(PrintWriter pw, String indent) {
300         int N = mSortedAndFiltered.size();
301         pw.print(indent);
302         pw.println("active notifications: " + N);
303         int active;
304         for (active = 0; active < N; active++) {
305             NotificationData.Entry e = mSortedAndFiltered.get(active);
306             dumpEntry(pw, indent, active, e);
307         }
308 
309         int M = mEntries.size();
310         pw.print(indent);
311         pw.println("inactive notifications: " + (M - active));
312         int inactiveCount = 0;
313         for (int i = 0; i < M; i++) {
314             Entry entry = mEntries.valueAt(i);
315             if (!mSortedAndFiltered.contains(entry)) {
316                 dumpEntry(pw, indent, inactiveCount, entry);
317                 inactiveCount++;
318             }
319         }
320     }
321 
dumpEntry(PrintWriter pw, String indent, int i, Entry e)322     private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) {
323         pw.print(indent);
324         pw.println("  [" + i + "] key=" + e.key + " icon=" + e.icon);
325         StatusBarNotification n = e.notification;
326         pw.print(indent);
327         pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " score=" +
328                 n.getScore());
329         pw.print(indent);
330         pw.println("      notification=" + n.getNotification());
331         pw.print(indent);
332         pw.println("      tickerText=\"" + n.getNotification().tickerText + "\"");
333     }
334 
isSystemNotification(StatusBarNotification sbn)335     private static boolean isSystemNotification(StatusBarNotification sbn) {
336         String sbnPackage = sbn.getPackageName();
337         return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
338     }
339 
340     /**
341      * Provides access to keyguard state and user settings dependent data.
342      */
343     public interface Environment {
shouldHideSensitiveContents(int userid)344         public boolean shouldHideSensitiveContents(int userid);
isDeviceProvisioned()345         public boolean isDeviceProvisioned();
isNotificationForCurrentProfiles(StatusBarNotification sbn)346         public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
getCurrentMediaNotificationKey()347         public String getCurrentMediaNotificationKey();
getGroupManager()348         public NotificationGroupManager getGroupManager();
349     }
350 }
351