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