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.content.Context;
21 import android.os.SystemClock;
22 import android.service.notification.NotificationListenerService;
23 import android.service.notification.NotificationListenerService.Ranking;
24 import android.service.notification.NotificationListenerService.RankingMap;
25 import android.service.notification.StatusBarNotification;
26 import android.util.ArrayMap;
27 import android.view.View;
28 import android.widget.RemoteViews;
29 
30 import com.android.systemui.statusbar.phone.NotificationGroupManager;
31 import com.android.systemui.statusbar.policy.HeadsUpManager;
32 
33 import java.io.PrintWriter;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.Comparator;
37 import java.util.Map;
38 import java.util.Objects;
39 
40 /**
41  * The list of currently displaying notifications.
42  */
43 public class NotificationData {
44 
45     private final Environment mEnvironment;
46     private HeadsUpManager mHeadsUpManager;
47 
48     public static final class Entry {
49         private static final long LAUNCH_COOLDOWN = 2000;
50         private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
51         public String key;
52         public StatusBarNotification notification;
53         public StatusBarIconView icon;
54         public ExpandableNotificationRow row; // the outer expanded view
55         private boolean interruption;
56         public boolean autoRedacted; // whether the redacted notification was generated by us
57         public boolean legacy; // whether the notification has a legacy, dark background
58         public int targetSdk;
59         private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
60         public RemoteViews cachedContentView;
61         public RemoteViews cachedBigContentView;
62         public RemoteViews cachedHeadsUpContentView;
63         public RemoteViews cachedPublicContentView;
64         public CharSequence remoteInputText;
65 
Entry(StatusBarNotification n, StatusBarIconView ic)66         public Entry(StatusBarNotification n, StatusBarIconView ic) {
67             this.key = n.getKey();
68             this.notification = n;
69             this.icon = ic;
70         }
71 
setInterruption()72         public void setInterruption() {
73             interruption = true;
74         }
75 
hasInterrupted()76         public boolean hasInterrupted() {
77             return interruption;
78         }
79 
80         /**
81          * Resets the notification entry to be re-used.
82          */
reset()83         public void reset() {
84             // NOTE: Icon needs to be preserved for now.
85             // We should fix this at some point.
86             autoRedacted = false;
87             legacy = false;
88             lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
89             if (row != null) {
90                 row.reset();
91             }
92         }
93 
getContentView()94         public View getContentView() {
95             return row.getPrivateLayout().getContractedChild();
96         }
97 
getExpandedContentView()98         public View getExpandedContentView() {
99             return row.getPrivateLayout().getExpandedChild();
100         }
101 
getHeadsUpContentView()102         public View getHeadsUpContentView() {
103             return row.getPrivateLayout().getHeadsUpChild();
104         }
105 
getPublicContentView()106         public View getPublicContentView() {
107             return row.getPublicLayout().getContractedChild();
108         }
109 
cacheContentViews(Context ctx, Notification updatedNotification)110         public boolean cacheContentViews(Context ctx, Notification updatedNotification) {
111             boolean applyInPlace = false;
112             if (updatedNotification != null) {
113                 final Notification.Builder updatedNotificationBuilder
114                         = Notification.Builder.recoverBuilder(ctx, updatedNotification);
115                 final RemoteViews newContentView = updatedNotificationBuilder.createContentView();
116                 final RemoteViews newBigContentView =
117                         updatedNotificationBuilder.createBigContentView();
118                 final RemoteViews newHeadsUpContentView =
119                         updatedNotificationBuilder.createHeadsUpContentView();
120                 final RemoteViews newPublicNotification
121                         = updatedNotificationBuilder.makePublicContentView();
122 
123                 boolean sameCustomView = Objects.equals(
124                         notification.getNotification().extras.getBoolean(
125                                 Notification.EXTRA_CONTAINS_CUSTOM_VIEW),
126                         updatedNotification.extras.getBoolean(
127                                 Notification.EXTRA_CONTAINS_CUSTOM_VIEW));
128                 applyInPlace = compareRemoteViews(cachedContentView, newContentView)
129                         && compareRemoteViews(cachedBigContentView, newBigContentView)
130                         && compareRemoteViews(cachedHeadsUpContentView, newHeadsUpContentView)
131                         && compareRemoteViews(cachedPublicContentView, newPublicNotification)
132                         && sameCustomView;
133                 cachedPublicContentView = newPublicNotification;
134                 cachedHeadsUpContentView = newHeadsUpContentView;
135                 cachedBigContentView = newBigContentView;
136                 cachedContentView = newContentView;
137             } else {
138                 final Notification.Builder builder
139                         = Notification.Builder.recoverBuilder(ctx, notification.getNotification());
140 
141                 cachedContentView = builder.createContentView();
142                 cachedBigContentView = builder.createBigContentView();
143                 cachedHeadsUpContentView = builder.createHeadsUpContentView();
144                 cachedPublicContentView = builder.makePublicContentView();
145 
146                 applyInPlace = false;
147             }
148             return applyInPlace;
149         }
150 
151         // Returns true if the RemoteViews are the same.
compareRemoteViews(final RemoteViews a, final RemoteViews b)152         private boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) {
153             return (a == null && b == null) ||
154                     (a != null && b != null
155                     && b.getPackage() != null
156                     && a.getPackage() != null
157                     && a.getPackage().equals(b.getPackage())
158                     && a.getLayoutId() == b.getLayoutId());
159         }
160 
notifyFullScreenIntentLaunched()161         public void notifyFullScreenIntentLaunched() {
162             lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
163         }
164 
hasJustLaunchedFullScreenIntent()165         public boolean hasJustLaunchedFullScreenIntent() {
166             return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
167         }
168     }
169 
170     private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
171     private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>();
172 
173     private NotificationGroupManager mGroupManager;
174 
175     private RankingMap mRankingMap;
176     private final Ranking mTmpRanking = new Ranking();
177 
setHeadsUpManager(HeadsUpManager headsUpManager)178     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
179         mHeadsUpManager = headsUpManager;
180     }
181 
182     private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
183         private final Ranking mRankingA = new Ranking();
184         private final Ranking mRankingB = new Ranking();
185 
186         @Override
187         public int compare(Entry a, Entry b) {
188             final StatusBarNotification na = a.notification;
189             final StatusBarNotification nb = b.notification;
190             int aImportance = Ranking.IMPORTANCE_DEFAULT;
191             int bImportance = Ranking.IMPORTANCE_DEFAULT;
192             int aRank = 0;
193             int bRank = 0;
194 
195             if (mRankingMap != null) {
196                 // RankingMap as received from NoMan
197                 mRankingMap.getRanking(a.key, mRankingA);
198                 mRankingMap.getRanking(b.key, mRankingB);
199                 aImportance = mRankingA.getImportance();
200                 bImportance = mRankingB.getImportance();
201                 aRank = mRankingA.getRank();
202                 bRank = mRankingB.getRank();
203             }
204 
205             String mediaNotification = mEnvironment.getCurrentMediaNotificationKey();
206 
207             // IMPORTANCE_MIN media streams are allowed to drift to the bottom
208             final boolean aMedia = a.key.equals(mediaNotification)
209                     && aImportance > Ranking.IMPORTANCE_MIN;
210             final boolean bMedia = b.key.equals(mediaNotification)
211                     && bImportance > Ranking.IMPORTANCE_MIN;
212 
213             boolean aSystemMax = aImportance >= Ranking.IMPORTANCE_MAX &&
214                     isSystemNotification(na);
215             boolean bSystemMax = bImportance >= Ranking.IMPORTANCE_MAX &&
216                     isSystemNotification(nb);
217 
218             boolean isHeadsUp = a.row.isHeadsUp();
219             if (isHeadsUp != b.row.isHeadsUp()) {
220                 return isHeadsUp ? -1 : 1;
221             } else if (isHeadsUp) {
222                 // Provide consistent ranking with headsUpManager
223                 return mHeadsUpManager.compare(a, b);
224             } else if (aMedia != bMedia) {
225                 // Upsort current media notification.
226                 return aMedia ? -1 : 1;
227             } else if (aSystemMax != bSystemMax) {
228                 // Upsort PRIORITY_MAX system notifications
229                 return aSystemMax ? -1 : 1;
230             } else if (aRank != bRank) {
231                 return aRank - bRank;
232             } else {
233                 return (int) (nb.getNotification().when - na.getNotification().when);
234             }
235         }
236     };
237 
NotificationData(Environment environment)238     public NotificationData(Environment environment) {
239         mEnvironment = environment;
240         mGroupManager = environment.getGroupManager();
241     }
242 
243     /**
244      * Returns the sorted list of active notifications (depending on {@link Environment}
245      *
246      * <p>
247      * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
248      * when the environment changes.
249      * <p>
250      * Don't hold on to or modify the returned list.
251      */
getActiveNotifications()252     public ArrayList<Entry> getActiveNotifications() {
253         return mSortedAndFiltered;
254     }
255 
get(String key)256     public Entry get(String key) {
257         return mEntries.get(key);
258     }
259 
add(Entry entry, RankingMap ranking)260     public void add(Entry entry, RankingMap ranking) {
261         synchronized (mEntries) {
262             mEntries.put(entry.notification.getKey(), entry);
263         }
264         mGroupManager.onEntryAdded(entry);
265         updateRankingAndSort(ranking);
266     }
267 
remove(String key, RankingMap ranking)268     public Entry remove(String key, RankingMap ranking) {
269         Entry removed = null;
270         synchronized (mEntries) {
271             removed = mEntries.remove(key);
272         }
273         if (removed == null) return null;
274         mGroupManager.onEntryRemoved(removed);
275         updateRankingAndSort(ranking);
276         return removed;
277     }
278 
updateRanking(RankingMap ranking)279     public void updateRanking(RankingMap ranking) {
280         updateRankingAndSort(ranking);
281     }
282 
isAmbient(String key)283     public boolean isAmbient(String key) {
284         if (mRankingMap != null) {
285             mRankingMap.getRanking(key, mTmpRanking);
286             return mTmpRanking.isAmbient();
287         }
288         return false;
289     }
290 
getVisibilityOverride(String key)291     public int getVisibilityOverride(String key) {
292         if (mRankingMap != null) {
293             mRankingMap.getRanking(key, mTmpRanking);
294             return mTmpRanking.getVisibilityOverride();
295         }
296         return Ranking.VISIBILITY_NO_OVERRIDE;
297     }
298 
shouldSuppressScreenOff(String key)299     public boolean shouldSuppressScreenOff(String key) {
300         if (mRankingMap != null) {
301             mRankingMap.getRanking(key, mTmpRanking);
302             return (mTmpRanking.getSuppressedVisualEffects()
303                     & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) != 0;
304         }
305         return false;
306     }
307 
shouldSuppressScreenOn(String key)308     public boolean shouldSuppressScreenOn(String key) {
309         if (mRankingMap != null) {
310             mRankingMap.getRanking(key, mTmpRanking);
311             return (mTmpRanking.getSuppressedVisualEffects()
312                     & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON) != 0;
313         }
314         return false;
315     }
316 
getImportance(String key)317     public int getImportance(String key) {
318         if (mRankingMap != null) {
319             mRankingMap.getRanking(key, mTmpRanking);
320             return mTmpRanking.getImportance();
321         }
322         return Ranking.IMPORTANCE_UNSPECIFIED;
323     }
324 
getOverrideGroupKey(String key)325     public String getOverrideGroupKey(String key) {
326         if (mRankingMap != null) {
327             mRankingMap.getRanking(key, mTmpRanking);
328             return mTmpRanking.getOverrideGroupKey();
329         }
330          return null;
331     }
332 
updateRankingAndSort(RankingMap ranking)333     private void updateRankingAndSort(RankingMap ranking) {
334         if (ranking != null) {
335             mRankingMap = ranking;
336             synchronized (mEntries) {
337                 final int N = mEntries.size();
338                 for (int i = 0; i < N; i++) {
339                     Entry entry = mEntries.valueAt(i);
340                     final StatusBarNotification oldSbn = entry.notification.clone();
341                     final String overrideGroupKey = getOverrideGroupKey(entry.key);
342                     if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
343                         entry.notification.setOverrideGroupKey(overrideGroupKey);
344                         mGroupManager.onEntryUpdated(entry, oldSbn);
345                     }
346                     //mGroupManager.onEntryBundlingUpdated(entry, getOverrideGroupKey(entry.key));
347                 }
348             }
349         }
350         filterAndSort();
351     }
352 
353     // TODO: This should not be public. Instead the Environment should notify this class when
354     // anything changed, and this class should call back the UI so it updates itself.
filterAndSort()355     public void filterAndSort() {
356         mSortedAndFiltered.clear();
357 
358         synchronized (mEntries) {
359             final int N = mEntries.size();
360             for (int i = 0; i < N; i++) {
361                 Entry entry = mEntries.valueAt(i);
362                 StatusBarNotification sbn = entry.notification;
363 
364                 if (shouldFilterOut(sbn)) {
365                     continue;
366                 }
367 
368                 mSortedAndFiltered.add(entry);
369             }
370         }
371 
372         Collections.sort(mSortedAndFiltered, mRankingComparator);
373     }
374 
shouldFilterOut(StatusBarNotification sbn)375     boolean shouldFilterOut(StatusBarNotification sbn) {
376         if (!(mEnvironment.isDeviceProvisioned() ||
377                 showNotificationEvenIfUnprovisioned(sbn))) {
378             return true;
379         }
380 
381         if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) {
382             return true;
383         }
384 
385         if (mEnvironment.onSecureLockScreen() &&
386                 (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET
387                         || mEnvironment.shouldHideNotifications(sbn.getUserId())
388                         || mEnvironment.shouldHideNotifications(sbn.getKey()))) {
389             return true;
390         }
391 
392         if (!BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS
393                 && mGroupManager.isChildInGroupWithSummary(sbn)) {
394             return true;
395         }
396         return false;
397     }
398 
399     /**
400      * Return whether there are any clearable notifications (that aren't errors).
401      */
hasActiveClearableNotifications()402     public boolean hasActiveClearableNotifications() {
403         for (Entry e : mSortedAndFiltered) {
404             if (e.getContentView() != null) { // the view successfully inflated
405                 if (e.notification.isClearable()) {
406                     return true;
407                 }
408             }
409         }
410         return false;
411     }
412 
413     // Q: What kinds of notifications should show during setup?
414     // A: Almost none! Only things coming from the system (package is "android") that also
415     // have special "kind" tags marking them as relevant for setup (see below).
showNotificationEvenIfUnprovisioned(StatusBarNotification sbn)416     public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
417         return "android".equals(sbn.getPackageName())
418                 && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
419     }
420 
dump(PrintWriter pw, String indent)421     public void dump(PrintWriter pw, String indent) {
422         int N = mSortedAndFiltered.size();
423         pw.print(indent);
424         pw.println("active notifications: " + N);
425         int active;
426         for (active = 0; active < N; active++) {
427             NotificationData.Entry e = mSortedAndFiltered.get(active);
428             dumpEntry(pw, indent, active, e);
429         }
430         synchronized (mEntries) {
431             int M = mEntries.size();
432             pw.print(indent);
433             pw.println("inactive notifications: " + (M - active));
434             int inactiveCount = 0;
435             for (int i = 0; i < M; i++) {
436                 Entry entry = mEntries.valueAt(i);
437                 if (!mSortedAndFiltered.contains(entry)) {
438                     dumpEntry(pw, indent, inactiveCount, entry);
439                     inactiveCount++;
440                 }
441             }
442         }
443     }
444 
dumpEntry(PrintWriter pw, String indent, int i, Entry e)445     private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) {
446         mRankingMap.getRanking(e.key, mTmpRanking);
447         pw.print(indent);
448         pw.println("  [" + i + "] key=" + e.key + " icon=" + e.icon);
449         StatusBarNotification n = e.notification;
450         pw.print(indent);
451         pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" +
452                 mTmpRanking.getImportance());
453         pw.print(indent);
454         pw.println("      notification=" + n.getNotification());
455         pw.print(indent);
456         pw.println("      tickerText=\"" + n.getNotification().tickerText + "\"");
457     }
458 
isSystemNotification(StatusBarNotification sbn)459     private static boolean isSystemNotification(StatusBarNotification sbn) {
460         String sbnPackage = sbn.getPackageName();
461         return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
462     }
463 
464     /**
465      * Provides access to keyguard state and user settings dependent data.
466      */
467     public interface Environment {
onSecureLockScreen()468         public boolean onSecureLockScreen();
shouldHideNotifications(int userid)469         public boolean shouldHideNotifications(int userid);
shouldHideNotifications(String key)470         public boolean shouldHideNotifications(String key);
isDeviceProvisioned()471         public boolean isDeviceProvisioned();
isNotificationForCurrentProfiles(StatusBarNotification sbn)472         public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
getCurrentMediaNotificationKey()473         public String getCurrentMediaNotificationKey();
getGroupManager()474         public NotificationGroupManager getGroupManager();
475     }
476 }
477