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