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 
     93         public Entry(StatusBarNotification n) {
     94             this.key = n.getKey();
     95             this.notification = n;
     96         }
     97 
     98         public void setInterruption() {
     99             interruption = true;
    100         }
    101 
    102         public boolean hasInterrupted() {
    103             return interruption;
    104         }
    105 
    106         /**
    107          * Resets the notification entry to be re-used.
    108          */
    109         public void reset() {
    110             lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
    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             lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
    126         }
    127 
    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          */
    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 
    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          */
    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 
    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          */
    227         public void abortTask() {
    228             if (mRunningTask != null) {
    229                 mRunningTask.abort();
    230                 mRunningTask = null;
    231             }
    232         }
    233 
    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 
    244         public void onInflationTaskFinished() {
    245            mRunningTask = null;
    246         }
    247 
    248         @VisibleForTesting
    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 
    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 
    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      */
    336     public ArrayList<Entry> getActiveNotifications() {
    337         return mSortedAndFiltered;
    338     }
    339 
    340     public Entry get(String key) {
    341         return mEntries.get(key);
    342     }
    343 
    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 
    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 
    364     public void updateRanking(RankingMap ranking) {
    365         updateRankingAndSort(ranking);
    366     }
    367 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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.
    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      */
    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).
    516     public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
    517         return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn);
    518     }
    519 
    520     @VisibleForTesting
    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 
    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 
    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 
    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 
    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 {
    582         public boolean isSecurelyLocked(int userId);
    583         public boolean shouldHideNotifications(int userid);
    584         public boolean shouldHideNotifications(String key);
    585         public boolean isDeviceProvisioned();
    586         public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
    587         public String getCurrentMediaNotificationKey();
    588         public NotificationGroupManager getGroupManager();
    589     }
    590 }
    591