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.AppGlobals;
20 import android.app.Notification;
21 import android.app.NotificationChannel;
22 import android.app.NotificationManager;
23 import android.content.pm.IPackageManager;
24 import android.content.pm.PackageManager;
25 import android.content.Context;
26 import android.graphics.drawable.Icon;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.RemoteException;
30 import android.os.SystemClock;
31 import android.service.notification.NotificationListenerService;
32 import android.service.notification.NotificationListenerService.Ranking;
33 import android.service.notification.NotificationListenerService.RankingMap;
34 import android.service.notification.SnoozeCriterion;
35 import android.service.notification.StatusBarNotification;
36 import android.util.ArrayMap;
37 import android.view.View;
38 import android.widget.ImageView;
39 import android.widget.RemoteViews;
40 import android.Manifest;
41 
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.internal.messages.nano.SystemMessageProto;
44 import com.android.internal.statusbar.StatusBarIcon;
45 import com.android.internal.util.NotificationColorUtil;
46 import com.android.systemui.Dependency;
47 import com.android.systemui.ForegroundServiceController;
48 import com.android.systemui.statusbar.notification.InflationException;
49 import com.android.systemui.statusbar.phone.NotificationGroupManager;
50 import com.android.systemui.statusbar.phone.StatusBar;
51 import com.android.systemui.statusbar.policy.HeadsUpManager;
52 
53 import java.io.PrintWriter;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.Comparator;
57 import java.util.List;
58 import java.util.Objects;
59 
60 /**
61  * The list of currently displaying notifications.
62  */
63 public class NotificationData {
64 
65     private final Environment mEnvironment;
66     private HeadsUpManager mHeadsUpManager;
67 
68     public static final class Entry {
69         private static final long LAUNCH_COOLDOWN = 2000;
70         private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
71         private static final int COLOR_INVALID = 1;
72         public String key;
73         public StatusBarNotification notification;
74         public NotificationChannel channel;
75         public StatusBarIconView icon;
76         public StatusBarIconView expandedIcon;
77         public ExpandableNotificationRow row; // the outer expanded view
78         private boolean interruption;
79         public boolean autoRedacted; // whether the redacted notification was generated by us
80         public int targetSdk;
81         private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
82         public RemoteViews cachedContentView;
83         public RemoteViews cachedBigContentView;
84         public RemoteViews cachedHeadsUpContentView;
85         public RemoteViews cachedPublicContentView;
86         public RemoteViews cachedAmbientContentView;
87         public CharSequence remoteInputText;
88         public List<SnoozeCriterion> snoozeCriteria;
89         private int mCachedContrastColor = COLOR_INVALID;
90         private int mCachedContrastColorIsFor = COLOR_INVALID;
91         private InflationTask mRunningTask = null;
92 
Entry(StatusBarNotification n)93         public Entry(StatusBarNotification n) {
94             this.key = n.getKey();
95             this.notification = n;
96         }
97 
setInterruption()98         public void setInterruption() {
99             interruption = true;
100         }
101 
hasInterrupted()102         public boolean hasInterrupted() {
103             return interruption;
104         }
105 
106         /**
107          * Resets the notification entry to be re-used.
108          */
reset()109         public void reset() {
110             lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
111             if (row != null) {
112                 row.reset();
113             }
114         }
115 
getExpandedContentView()116         public View getExpandedContentView() {
117             return row.getPrivateLayout().getExpandedChild();
118         }
119 
getPublicContentView()120         public View getPublicContentView() {
121             return row.getPublicLayout().getContractedChild();
122         }
123 
notifyFullScreenIntentLaunched()124         public void notifyFullScreenIntentLaunched() {
125             lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
126         }
127 
hasJustLaunchedFullScreenIntent()128         public boolean hasJustLaunchedFullScreenIntent() {
129             return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
130         }
131 
132         /**
133          * Create the icons for a notification
134          * @param context the context to create the icons with
135          * @param sbn the notification
136          * @throws InflationException
137          */
createIcons(Context context, StatusBarNotification sbn)138         public void createIcons(Context context, StatusBarNotification sbn)
139                 throws InflationException {
140             Notification n = sbn.getNotification();
141             final Icon smallIcon = n.getSmallIcon();
142             if (smallIcon == null) {
143                 throw new InflationException("No small icon in notification from "
144                         + sbn.getPackageName());
145             }
146 
147             // Construct the icon.
148             icon = new StatusBarIconView(context,
149                     sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
150             icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
151 
152             // Construct the expanded icon.
153             expandedIcon = new StatusBarIconView(context,
154                     sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
155             expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
156             final StatusBarIcon ic = new StatusBarIcon(
157                     sbn.getUser(),
158                     sbn.getPackageName(),
159                     smallIcon,
160                     n.iconLevel,
161                     n.number,
162                     StatusBarIconView.contentDescForNotification(context, n));
163             if (!icon.set(ic) || !expandedIcon.set(ic)) {
164                 icon = null;
165                 expandedIcon = null;
166                 throw new InflationException("Couldn't create icon: " + ic);
167             }
168             expandedIcon.setVisibility(View.INVISIBLE);
169             expandedIcon.setOnVisibilityChangedListener(
170                     newVisibility -> {
171                         if (row != null) {
172                             row.setIconsVisible(newVisibility != View.VISIBLE);
173                         }
174                     });
175         }
176 
setIconTag(int key, Object tag)177         public void setIconTag(int key, Object tag) {
178             if (icon != null) {
179                 icon.setTag(key, tag);
180                 expandedIcon.setTag(key, tag);
181             }
182         }
183 
184         /**
185          * Update the notification icons.
186          * @param context the context to create the icons with.
187          * @param n the notification to read the icon from.
188          * @throws InflationException
189          */
updateIcons(Context context, StatusBarNotification sbn)190         public void updateIcons(Context context, StatusBarNotification sbn)
191                 throws InflationException {
192             if (icon != null) {
193                 // Update the icon
194                 Notification n = sbn.getNotification();
195                 final StatusBarIcon ic = new StatusBarIcon(
196                         notification.getUser(),
197                         notification.getPackageName(),
198                         n.getSmallIcon(),
199                         n.iconLevel,
200                         n.number,
201                         StatusBarIconView.contentDescForNotification(context, n));
202                 icon.setNotification(sbn);
203                 expandedIcon.setNotification(sbn);
204                 if (!icon.set(ic) || !expandedIcon.set(ic)) {
205                     throw new InflationException("Couldn't update icon: " + ic);
206                 }
207             }
208         }
209 
getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)210         public int getContrastedColor(Context context, boolean isLowPriority,
211                 int backgroundColor) {
212             int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
213                     notification.getNotification().color;
214             if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
215                 return mCachedContrastColor;
216             }
217             final int contrasted = NotificationColorUtil.resolveContrastColor(context, rawColor,
218                     backgroundColor);
219             mCachedContrastColorIsFor = rawColor;
220             mCachedContrastColor = contrasted;
221             return mCachedContrastColor;
222         }
223 
224         /**
225          * Abort all existing inflation tasks
226          */
abortTask()227         public void abortTask() {
228             if (mRunningTask != null) {
229                 mRunningTask.abort();
230                 mRunningTask = null;
231             }
232         }
233 
setInflationTask(InflationTask abortableTask)234         public void setInflationTask(InflationTask abortableTask) {
235             // abort any existing inflation
236             InflationTask existing = mRunningTask;
237             abortTask();
238             mRunningTask = abortableTask;
239             if (existing != null && mRunningTask != null) {
240                 mRunningTask.supersedeTask(existing);
241             }
242         }
243 
onInflationTaskFinished()244         public void onInflationTaskFinished() {
245            mRunningTask = null;
246         }
247 
248         @VisibleForTesting
getRunningTask()249         public InflationTask getRunningTask() {
250             return mRunningTask;
251         }
252     }
253 
254     private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
255     private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>();
256 
257     private NotificationGroupManager mGroupManager;
258 
259     private RankingMap mRankingMap;
260     private final Ranking mTmpRanking = new Ranking();
261 
setHeadsUpManager(HeadsUpManager headsUpManager)262     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
263         mHeadsUpManager = headsUpManager;
264     }
265 
266     private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
267         private final Ranking mRankingA = new Ranking();
268         private final Ranking mRankingB = new Ranking();
269 
270         @Override
271         public int compare(Entry a, Entry b) {
272             final StatusBarNotification na = a.notification;
273             final StatusBarNotification nb = b.notification;
274             int aImportance = NotificationManager.IMPORTANCE_DEFAULT;
275             int bImportance = NotificationManager.IMPORTANCE_DEFAULT;
276             int aRank = 0;
277             int bRank = 0;
278 
279             if (mRankingMap != null) {
280                 // RankingMap as received from NoMan
281                 mRankingMap.getRanking(a.key, mRankingA);
282                 mRankingMap.getRanking(b.key, mRankingB);
283                 aImportance = mRankingA.getImportance();
284                 bImportance = mRankingB.getImportance();
285                 aRank = mRankingA.getRank();
286                 bRank = mRankingB.getRank();
287             }
288 
289             String mediaNotification = mEnvironment.getCurrentMediaNotificationKey();
290 
291             // IMPORTANCE_MIN media streams are allowed to drift to the bottom
292             final boolean aMedia = a.key.equals(mediaNotification)
293                     && aImportance > NotificationManager.IMPORTANCE_MIN;
294             final boolean bMedia = b.key.equals(mediaNotification)
295                     && bImportance > NotificationManager.IMPORTANCE_MIN;
296 
297             boolean aSystemMax = aImportance >= NotificationManager.IMPORTANCE_HIGH &&
298                     isSystemNotification(na);
299             boolean bSystemMax = bImportance >= NotificationManager.IMPORTANCE_HIGH &&
300                     isSystemNotification(nb);
301 
302             boolean isHeadsUp = a.row.isHeadsUp();
303             if (isHeadsUp != b.row.isHeadsUp()) {
304                 return isHeadsUp ? -1 : 1;
305             } else if (isHeadsUp) {
306                 // Provide consistent ranking with headsUpManager
307                 return mHeadsUpManager.compare(a, b);
308             } else if (aMedia != bMedia) {
309                 // Upsort current media notification.
310                 return aMedia ? -1 : 1;
311             } else if (aSystemMax != bSystemMax) {
312                 // Upsort PRIORITY_MAX system notifications
313                 return aSystemMax ? -1 : 1;
314             } else if (aRank != bRank) {
315                 return aRank - bRank;
316             } else {
317                 return Long.compare(nb.getNotification().when, na.getNotification().when);
318             }
319         }
320     };
321 
NotificationData(Environment environment)322     public NotificationData(Environment environment) {
323         mEnvironment = environment;
324         mGroupManager = environment.getGroupManager();
325     }
326 
327     /**
328      * Returns the sorted list of active notifications (depending on {@link Environment}
329      *
330      * <p>
331      * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
332      * when the environment changes.
333      * <p>
334      * Don't hold on to or modify the returned list.
335      */
getActiveNotifications()336     public ArrayList<Entry> getActiveNotifications() {
337         return mSortedAndFiltered;
338     }
339 
get(String key)340     public Entry get(String key) {
341         return mEntries.get(key);
342     }
343 
add(Entry entry)344     public void add(Entry entry) {
345         synchronized (mEntries) {
346             mEntries.put(entry.notification.getKey(), entry);
347         }
348         mGroupManager.onEntryAdded(entry);
349 
350         updateRankingAndSort(mRankingMap);
351     }
352 
remove(String key, RankingMap ranking)353     public Entry remove(String key, RankingMap ranking) {
354         Entry removed = null;
355         synchronized (mEntries) {
356             removed = mEntries.remove(key);
357         }
358         if (removed == null) return null;
359         mGroupManager.onEntryRemoved(removed);
360         updateRankingAndSort(ranking);
361         return removed;
362     }
363 
updateRanking(RankingMap ranking)364     public void updateRanking(RankingMap ranking) {
365         updateRankingAndSort(ranking);
366     }
367 
isAmbient(String key)368     public boolean isAmbient(String key) {
369         if (mRankingMap != null) {
370             mRankingMap.getRanking(key, mTmpRanking);
371             return mTmpRanking.isAmbient();
372         }
373         return false;
374     }
375 
getVisibilityOverride(String key)376     public int getVisibilityOverride(String key) {
377         if (mRankingMap != null) {
378             mRankingMap.getRanking(key, mTmpRanking);
379             return mTmpRanking.getVisibilityOverride();
380         }
381         return Ranking.VISIBILITY_NO_OVERRIDE;
382     }
383 
shouldSuppressScreenOff(String key)384     public boolean shouldSuppressScreenOff(String key) {
385         if (mRankingMap != null) {
386             mRankingMap.getRanking(key, mTmpRanking);
387             return (mTmpRanking.getSuppressedVisualEffects()
388                     & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) != 0;
389         }
390         return false;
391     }
392 
shouldSuppressScreenOn(String key)393     public boolean shouldSuppressScreenOn(String key) {
394         if (mRankingMap != null) {
395             mRankingMap.getRanking(key, mTmpRanking);
396             return (mTmpRanking.getSuppressedVisualEffects()
397                     & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON) != 0;
398         }
399         return false;
400     }
401 
getImportance(String key)402     public int getImportance(String key) {
403         if (mRankingMap != null) {
404             mRankingMap.getRanking(key, mTmpRanking);
405             return mTmpRanking.getImportance();
406         }
407         return NotificationManager.IMPORTANCE_UNSPECIFIED;
408     }
409 
getOverrideGroupKey(String key)410     public String getOverrideGroupKey(String key) {
411         if (mRankingMap != null) {
412             mRankingMap.getRanking(key, mTmpRanking);
413             return mTmpRanking.getOverrideGroupKey();
414         }
415          return null;
416     }
417 
getSnoozeCriteria(String key)418     public List<SnoozeCriterion> getSnoozeCriteria(String key) {
419         if (mRankingMap != null) {
420             mRankingMap.getRanking(key, mTmpRanking);
421             return mTmpRanking.getSnoozeCriteria();
422         }
423         return null;
424     }
425 
getChannel(String key)426     public NotificationChannel getChannel(String key) {
427         if (mRankingMap != null) {
428             mRankingMap.getRanking(key, mTmpRanking);
429             return mTmpRanking.getChannel();
430         }
431         return null;
432     }
433 
updateRankingAndSort(RankingMap ranking)434     private void updateRankingAndSort(RankingMap ranking) {
435         if (ranking != null) {
436             mRankingMap = ranking;
437             synchronized (mEntries) {
438                 final int N = mEntries.size();
439                 for (int i = 0; i < N; i++) {
440                     Entry entry = mEntries.valueAt(i);
441                     final StatusBarNotification oldSbn = entry.notification.cloneLight();
442                     final String overrideGroupKey = getOverrideGroupKey(entry.key);
443                     if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
444                         entry.notification.setOverrideGroupKey(overrideGroupKey);
445                         mGroupManager.onEntryUpdated(entry, oldSbn);
446                     }
447                     entry.channel = getChannel(entry.key);
448                     entry.snoozeCriteria = getSnoozeCriteria(entry.key);
449                 }
450             }
451         }
452         filterAndSort();
453     }
454 
455     // TODO: This should not be public. Instead the Environment should notify this class when
456     // anything changed, and this class should call back the UI so it updates itself.
filterAndSort()457     public void filterAndSort() {
458         mSortedAndFiltered.clear();
459 
460         synchronized (mEntries) {
461             final int N = mEntries.size();
462             for (int i = 0; i < N; i++) {
463                 Entry entry = mEntries.valueAt(i);
464                 StatusBarNotification sbn = entry.notification;
465 
466                 if (shouldFilterOut(sbn)) {
467                     continue;
468                 }
469 
470                 mSortedAndFiltered.add(entry);
471             }
472         }
473 
474         Collections.sort(mSortedAndFiltered, mRankingComparator);
475     }
476 
477     /**
478      * @param sbn
479      * @return true if this notification should NOT be shown right now
480      */
shouldFilterOut(StatusBarNotification sbn)481     public boolean shouldFilterOut(StatusBarNotification sbn) {
482         if (!(mEnvironment.isDeviceProvisioned() ||
483                 showNotificationEvenIfUnprovisioned(sbn))) {
484             return true;
485         }
486 
487         if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) {
488             return true;
489         }
490 
491         if (mEnvironment.isSecurelyLocked(sbn.getUserId()) &&
492                 (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET
493                         || mEnvironment.shouldHideNotifications(sbn.getUserId())
494                         || mEnvironment.shouldHideNotifications(sbn.getKey()))) {
495             return true;
496         }
497 
498         if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS
499                 && mGroupManager.isChildInGroupWithSummary(sbn)) {
500             return true;
501         }
502 
503         final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class);
504         if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) {
505             // this is a foreground-service disclosure for a user that does not need to show one
506             return true;
507         }
508 
509         return false;
510     }
511 
512     // Q: What kinds of notifications should show during setup?
513     // A: Almost none! Only things coming from packages with permission
514     // android.permission.NOTIFICATION_DURING_SETUP that also have special "kind" tags marking them
515     // as relevant for setup (see below).
showNotificationEvenIfUnprovisioned(StatusBarNotification sbn)516     public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
517         return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn);
518     }
519 
520     @VisibleForTesting
showNotificationEvenIfUnprovisioned(IPackageManager packageManager, StatusBarNotification sbn)521     static boolean showNotificationEvenIfUnprovisioned(IPackageManager packageManager,
522             StatusBarNotification sbn) {
523         return checkUidPermission(packageManager, Manifest.permission.NOTIFICATION_DURING_SETUP,
524                 sbn.getUid()) == PackageManager.PERMISSION_GRANTED
525                 && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
526     }
527 
checkUidPermission(IPackageManager packageManager, String permission, int uid)528     private static int checkUidPermission(IPackageManager packageManager, String permission,
529             int uid) {
530         try {
531             return packageManager.checkUidPermission(permission, uid);
532         } catch (RemoteException e) {
533             throw e.rethrowFromSystemServer();
534         }
535     }
536 
dump(PrintWriter pw, String indent)537     public void dump(PrintWriter pw, String indent) {
538         int N = mSortedAndFiltered.size();
539         pw.print(indent);
540         pw.println("active notifications: " + N);
541         int active;
542         for (active = 0; active < N; active++) {
543             NotificationData.Entry e = mSortedAndFiltered.get(active);
544             dumpEntry(pw, indent, active, e);
545         }
546         synchronized (mEntries) {
547             int M = mEntries.size();
548             pw.print(indent);
549             pw.println("inactive notifications: " + (M - active));
550             int inactiveCount = 0;
551             for (int i = 0; i < M; i++) {
552                 Entry entry = mEntries.valueAt(i);
553                 if (!mSortedAndFiltered.contains(entry)) {
554                     dumpEntry(pw, indent, inactiveCount, entry);
555                     inactiveCount++;
556                 }
557             }
558         }
559     }
560 
dumpEntry(PrintWriter pw, String indent, int i, Entry e)561     private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) {
562         mRankingMap.getRanking(e.key, mTmpRanking);
563         pw.print(indent);
564         pw.println("  [" + i + "] key=" + e.key + " icon=" + e.icon);
565         StatusBarNotification n = e.notification;
566         pw.print(indent);
567         pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" +
568                 mTmpRanking.getImportance());
569         pw.print(indent);
570         pw.println("      notification=" + n.getNotification());
571     }
572 
isSystemNotification(StatusBarNotification sbn)573     private static boolean isSystemNotification(StatusBarNotification sbn) {
574         String sbnPackage = sbn.getPackageName();
575         return "android".equals(sbnPackage) || "com.android.systemui".equals(sbnPackage);
576     }
577 
578     /**
579      * Provides access to keyguard state and user settings dependent data.
580      */
581     public interface Environment {
isSecurelyLocked(int userId)582         public boolean isSecurelyLocked(int userId);
shouldHideNotifications(int userid)583         public boolean shouldHideNotifications(int userid);
shouldHideNotifications(String key)584         public boolean shouldHideNotifications(String key);
isDeviceProvisioned()585         public boolean isDeviceProvisioned();
isNotificationForCurrentProfiles(StatusBarNotification sbn)586         public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
getCurrentMediaNotificationKey()587         public String getCurrentMediaNotificationKey();
getGroupManager()588         public NotificationGroupManager getGroupManager();
589     }
590 }
591