Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2014 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 package com.android.server.notification;
     17 
     18 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MIN;
     19 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
     20 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_DEFAULT;
     21 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_HIGH;
     22 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_LOW;
     23 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MAX;
     24 
     25 import android.app.Notification;
     26 import android.content.Context;
     27 import android.content.pm.PackageManager.NameNotFoundException;
     28 import android.content.res.Resources;
     29 import android.graphics.Bitmap;
     30 import android.graphics.drawable.Icon;
     31 import android.media.AudioAttributes;
     32 import android.os.UserHandle;
     33 import android.service.notification.NotificationListenerService;
     34 import android.service.notification.StatusBarNotification;
     35 import android.util.Log;
     36 
     37 import com.android.internal.annotations.VisibleForTesting;
     38 import com.android.server.EventLogTags;
     39 
     40 import java.io.PrintWriter;
     41 import java.lang.reflect.Array;
     42 import java.util.Arrays;
     43 import java.util.Objects;
     44 
     45 /**
     46  * Holds data about notifications that should not be shared with the
     47  * {@link android.service.notification.NotificationListenerService}s.
     48  *
     49  * <p>These objects should not be mutated unless the code is synchronized
     50  * on {@link NotificationManagerService#mNotificationList}, and any
     51  * modification should be followed by a sorting of that list.</p>
     52  *
     53  * <p>Is sortable by {@link NotificationComparator}.</p>
     54  *
     55  * {@hide}
     56  */
     57 public final class NotificationRecord {
     58     static final String TAG = "NotificationRecord";
     59     static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     60     final StatusBarNotification sbn;
     61     final int mOriginalFlags;
     62     private final Context mContext;
     63 
     64     NotificationUsageStats.SingleNotificationStats stats;
     65     boolean isCanceled;
     66     /** Whether the notification was seen by the user via one of the notification listeners. */
     67     boolean mIsSeen;
     68 
     69     // These members are used by NotificationSignalExtractors
     70     // to communicate with the ranking module.
     71     private float mContactAffinity;
     72     private boolean mRecentlyIntrusive;
     73 
     74     // is this notification currently being intercepted by Zen Mode?
     75     private boolean mIntercept;
     76 
     77     // The timestamp used for ranking.
     78     private long mRankingTimeMs;
     79 
     80     // The first post time, stable across updates.
     81     private long mCreationTimeMs;
     82 
     83     // The most recent visibility event.
     84     private long mVisibleSinceMs;
     85 
     86     // The most recent update time, or the creation time if no updates.
     87     private long mUpdateTimeMs;
     88 
     89     // Is this record an update of an old record?
     90     public boolean isUpdate;
     91     private int mPackagePriority;
     92 
     93     private int mAuthoritativeRank;
     94     private String mGlobalSortKey;
     95     private int mPackageVisibility;
     96     private int mUserImportance = IMPORTANCE_UNSPECIFIED;
     97     private int mImportance = IMPORTANCE_UNSPECIFIED;
     98     private CharSequence mImportanceExplanation = null;
     99 
    100     private int mSuppressedVisualEffects = 0;
    101     private String mUserExplanation;
    102     private String mPeopleExplanation;
    103 
    104     @VisibleForTesting
    105     public NotificationRecord(Context context, StatusBarNotification sbn)
    106     {
    107         this.sbn = sbn;
    108         mOriginalFlags = sbn.getNotification().flags;
    109         mRankingTimeMs = calculateRankingTimeMs(0L);
    110         mCreationTimeMs = sbn.getPostTime();
    111         mUpdateTimeMs = mCreationTimeMs;
    112         mContext = context;
    113         stats = new NotificationUsageStats.SingleNotificationStats();
    114         mImportance = defaultImportance();
    115     }
    116 
    117     private int defaultImportance() {
    118         final Notification n = sbn.getNotification();
    119         int importance = IMPORTANCE_DEFAULT;
    120 
    121         // Migrate notification flags to scores
    122         if (0 != (n.flags & Notification.FLAG_HIGH_PRIORITY)) {
    123             n.priority = Notification.PRIORITY_MAX;
    124         }
    125 
    126         switch (n.priority) {
    127             case Notification.PRIORITY_MIN:
    128                 importance = IMPORTANCE_MIN;
    129                 break;
    130             case Notification.PRIORITY_LOW:
    131                 importance = IMPORTANCE_LOW;
    132                 break;
    133             case Notification.PRIORITY_DEFAULT:
    134                 importance = IMPORTANCE_DEFAULT;
    135                 break;
    136             case Notification.PRIORITY_HIGH:
    137                 importance = IMPORTANCE_HIGH;
    138                 break;
    139             case Notification.PRIORITY_MAX:
    140                 importance = IMPORTANCE_MAX;
    141                 break;
    142         }
    143         stats.requestedImportance = importance;
    144 
    145         boolean isNoisy = (n.defaults & Notification.DEFAULT_SOUND) != 0
    146                 || (n.defaults & Notification.DEFAULT_VIBRATE) != 0
    147                 || n.sound != null
    148                 || n.vibrate != null;
    149         stats.isNoisy = isNoisy;
    150 
    151         if (!isNoisy && importance > IMPORTANCE_LOW) {
    152             importance = IMPORTANCE_LOW;
    153         }
    154 
    155         if (isNoisy) {
    156             if (importance < IMPORTANCE_DEFAULT) {
    157                 importance = IMPORTANCE_DEFAULT;
    158             }
    159         }
    160 
    161         if (n.fullScreenIntent != null) {
    162             importance = IMPORTANCE_MAX;
    163         }
    164 
    165         stats.naturalImportance = importance;
    166         return importance;
    167     }
    168 
    169     // copy any notes that the ranking system may have made before the update
    170     public void copyRankingInformation(NotificationRecord previous) {
    171         mContactAffinity = previous.mContactAffinity;
    172         mRecentlyIntrusive = previous.mRecentlyIntrusive;
    173         mPackagePriority = previous.mPackagePriority;
    174         mPackageVisibility = previous.mPackageVisibility;
    175         mIntercept = previous.mIntercept;
    176         mRankingTimeMs = calculateRankingTimeMs(previous.getRankingTimeMs());
    177         mCreationTimeMs = previous.mCreationTimeMs;
    178         mVisibleSinceMs = previous.mVisibleSinceMs;
    179         if (previous.sbn.getOverrideGroupKey() != null && !sbn.isAppGroup()) {
    180             sbn.setOverrideGroupKey(previous.sbn.getOverrideGroupKey());
    181         }
    182         // Don't copy importance information or mGlobalSortKey, recompute them.
    183     }
    184 
    185     public Notification getNotification() { return sbn.getNotification(); }
    186     public int getFlags() { return sbn.getNotification().flags; }
    187     public UserHandle getUser() { return sbn.getUser(); }
    188     public String getKey() { return sbn.getKey(); }
    189     /** @deprecated Use {@link #getUser()} instead. */
    190     public int getUserId() { return sbn.getUserId(); }
    191 
    192     void dump(PrintWriter pw, String prefix, Context baseContext, boolean redact) {
    193         final Notification notification = sbn.getNotification();
    194         final Icon icon = notification.getSmallIcon();
    195         String iconStr = String.valueOf(icon);
    196         if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
    197             iconStr += " / " + idDebugString(baseContext, icon.getResPackage(), icon.getResId());
    198         }
    199         pw.println(prefix + this);
    200         pw.println(prefix + "  uid=" + sbn.getUid() + " userId=" + sbn.getUserId());
    201         pw.println(prefix + "  icon=" + iconStr);
    202         pw.println(prefix + "  pri=" + notification.priority);
    203         pw.println(prefix + "  key=" + sbn.getKey());
    204         pw.println(prefix + "  seen=" + mIsSeen);
    205         pw.println(prefix + "  groupKey=" + getGroupKey());
    206         pw.println(prefix + "  contentIntent=" + notification.contentIntent);
    207         pw.println(prefix + "  deleteIntent=" + notification.deleteIntent);
    208         pw.println(prefix + "  tickerText=" + notification.tickerText);
    209         pw.println(prefix + "  contentView=" + notification.contentView);
    210         pw.println(prefix + String.format("  defaults=0x%08x flags=0x%08x",
    211                 notification.defaults, notification.flags));
    212         pw.println(prefix + "  sound=" + notification.sound);
    213         pw.println(prefix + "  audioStreamType=" + notification.audioStreamType);
    214         pw.println(prefix + "  audioAttributes=" + notification.audioAttributes);
    215         pw.println(prefix + String.format("  color=0x%08x", notification.color));
    216         pw.println(prefix + "  vibrate=" + Arrays.toString(notification.vibrate));
    217         pw.println(prefix + String.format("  led=0x%08x onMs=%d offMs=%d",
    218                 notification.ledARGB, notification.ledOnMS, notification.ledOffMS));
    219         if (notification.actions != null && notification.actions.length > 0) {
    220             pw.println(prefix + "  actions={");
    221             final int N = notification.actions.length;
    222             for (int i=0; i<N; i++) {
    223                 final Notification.Action action = notification.actions[i];
    224                 if (action != null) {
    225                     pw.println(String.format("%s    [%d] \"%s\" -> %s",
    226                             prefix,
    227                             i,
    228                             action.title,
    229                             action.actionIntent == null ? "null" : action.actionIntent.toString()
    230                     ));
    231                 }
    232             }
    233             pw.println(prefix + "  }");
    234         }
    235         if (notification.extras != null && notification.extras.size() > 0) {
    236             pw.println(prefix + "  extras={");
    237             for (String key : notification.extras.keySet()) {
    238                 pw.print(prefix + "    " + key + "=");
    239                 Object val = notification.extras.get(key);
    240                 if (val == null) {
    241                     pw.println("null");
    242                 } else {
    243                     pw.print(val.getClass().getSimpleName());
    244                     if (redact && (val instanceof CharSequence || val instanceof String)) {
    245                         // redact contents from bugreports
    246                     } else if (val instanceof Bitmap) {
    247                         pw.print(String.format(" (%dx%d)",
    248                                 ((Bitmap) val).getWidth(),
    249                                 ((Bitmap) val).getHeight()));
    250                     } else if (val.getClass().isArray()) {
    251                         final int N = Array.getLength(val);
    252                         pw.print(" (" + N + ")");
    253                         if (!redact) {
    254                             for (int j=0; j<N; j++) {
    255                                 pw.println();
    256                                 pw.print(String.format("%s      [%d] %s",
    257                                         prefix, j, String.valueOf(Array.get(val, j))));
    258                             }
    259                         }
    260                     } else {
    261                         pw.print(" (" + String.valueOf(val) + ")");
    262                     }
    263                     pw.println();
    264                 }
    265             }
    266             pw.println(prefix + "  }");
    267         }
    268         pw.println(prefix + "  stats=" + stats.toString());
    269         pw.println(prefix + "  mContactAffinity=" + mContactAffinity);
    270         pw.println(prefix + "  mRecentlyIntrusive=" + mRecentlyIntrusive);
    271         pw.println(prefix + "  mPackagePriority=" + mPackagePriority);
    272         pw.println(prefix + "  mPackageVisibility=" + mPackageVisibility);
    273         pw.println(prefix + "  mUserImportance="
    274                 + NotificationListenerService.Ranking.importanceToString(mUserImportance));
    275         pw.println(prefix + "  mImportance="
    276                 + NotificationListenerService.Ranking.importanceToString(mImportance));
    277         pw.println(prefix + "  mImportanceExplanation=" + mImportanceExplanation);
    278         pw.println(prefix + "  mIntercept=" + mIntercept);
    279         pw.println(prefix + "  mGlobalSortKey=" + mGlobalSortKey);
    280         pw.println(prefix + "  mRankingTimeMs=" + mRankingTimeMs);
    281         pw.println(prefix + "  mCreationTimeMs=" + mCreationTimeMs);
    282         pw.println(prefix + "  mVisibleSinceMs=" + mVisibleSinceMs);
    283         pw.println(prefix + "  mUpdateTimeMs=" + mUpdateTimeMs);
    284         pw.println(prefix + "  mSuppressedVisualEffects= " + mSuppressedVisualEffects);
    285     }
    286 
    287 
    288     static String idDebugString(Context baseContext, String packageName, int id) {
    289         Context c;
    290 
    291         if (packageName != null) {
    292             try {
    293                 c = baseContext.createPackageContext(packageName, 0);
    294             } catch (NameNotFoundException e) {
    295                 c = baseContext;
    296             }
    297         } else {
    298             c = baseContext;
    299         }
    300 
    301         Resources r = c.getResources();
    302         try {
    303             return r.getResourceName(id);
    304         } catch (Resources.NotFoundException e) {
    305             return "<name unknown>";
    306         }
    307     }
    308 
    309     @Override
    310     public final String toString() {
    311         return String.format(
    312                 "NotificationRecord(0x%08x: pkg=%s user=%s id=%d tag=%s importance=%d key=%s: %s)",
    313                 System.identityHashCode(this),
    314                 this.sbn.getPackageName(), this.sbn.getUser(), this.sbn.getId(),
    315                 this.sbn.getTag(), this.mImportance, this.sbn.getKey(),
    316                 this.sbn.getNotification());
    317     }
    318 
    319     public void setContactAffinity(float contactAffinity) {
    320         mContactAffinity = contactAffinity;
    321         if (mImportance < IMPORTANCE_DEFAULT &&
    322                 mContactAffinity > ValidateNotificationPeople.VALID_CONTACT) {
    323             setImportance(IMPORTANCE_DEFAULT, getPeopleExplanation());
    324         }
    325     }
    326 
    327     public float getContactAffinity() {
    328         return mContactAffinity;
    329     }
    330 
    331     public void setRecentlyIntrusive(boolean recentlyIntrusive) {
    332         mRecentlyIntrusive = recentlyIntrusive;
    333     }
    334 
    335     public boolean isRecentlyIntrusive() {
    336         return mRecentlyIntrusive;
    337     }
    338 
    339     public void setPackagePriority(int packagePriority) {
    340         mPackagePriority = packagePriority;
    341     }
    342 
    343     public int getPackagePriority() {
    344         return mPackagePriority;
    345     }
    346 
    347     public void setPackageVisibilityOverride(int packageVisibility) {
    348         mPackageVisibility = packageVisibility;
    349     }
    350 
    351     public int getPackageVisibilityOverride() {
    352         return mPackageVisibility;
    353     }
    354 
    355     public void setUserImportance(int importance) {
    356         mUserImportance = importance;
    357         applyUserImportance();
    358     }
    359 
    360     private String getUserExplanation() {
    361         if (mUserExplanation == null) {
    362             mUserExplanation =
    363                     mContext.getString(com.android.internal.R.string.importance_from_user);
    364         }
    365         return mUserExplanation;
    366     }
    367 
    368     private String getPeopleExplanation() {
    369         if (mPeopleExplanation == null) {
    370             mPeopleExplanation =
    371                     mContext.getString(com.android.internal.R.string.importance_from_person);
    372         }
    373         return mPeopleExplanation;
    374     }
    375 
    376     private void applyUserImportance() {
    377         if (mUserImportance != NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED) {
    378             mImportance = mUserImportance;
    379             mImportanceExplanation = getUserExplanation();
    380         }
    381     }
    382 
    383     public int getUserImportance() {
    384         return mUserImportance;
    385     }
    386 
    387     public void setImportance(int importance, CharSequence explanation) {
    388         if (importance != NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED) {
    389             mImportance = importance;
    390             mImportanceExplanation = explanation;
    391         }
    392         applyUserImportance();
    393     }
    394 
    395     public int getImportance() {
    396         return mImportance;
    397     }
    398 
    399     public CharSequence getImportanceExplanation() {
    400         return mImportanceExplanation;
    401     }
    402 
    403     public boolean setIntercepted(boolean intercept) {
    404         mIntercept = intercept;
    405         return mIntercept;
    406     }
    407 
    408     public boolean isIntercepted() {
    409         return mIntercept;
    410     }
    411 
    412     public void setSuppressedVisualEffects(int effects) {
    413         mSuppressedVisualEffects = effects;
    414     }
    415 
    416     public int getSuppressedVisualEffects() {
    417         return mSuppressedVisualEffects;
    418     }
    419 
    420     public boolean isCategory(String category) {
    421         return Objects.equals(getNotification().category, category);
    422     }
    423 
    424     public boolean isAudioStream(int stream) {
    425         return getNotification().audioStreamType == stream;
    426     }
    427 
    428     public boolean isAudioAttributesUsage(int usage) {
    429         final AudioAttributes attributes = getNotification().audioAttributes;
    430         return attributes != null && attributes.getUsage() == usage;
    431     }
    432 
    433     /**
    434      * Returns the timestamp to use for time-based sorting in the ranker.
    435      */
    436     public long getRankingTimeMs() {
    437         return mRankingTimeMs;
    438     }
    439 
    440     /**
    441      * @param now this current time in milliseconds.
    442      * @returns the number of milliseconds since the most recent update, or the post time if none.
    443      */
    444     public int getFreshnessMs(long now) {
    445         return (int) (now - mUpdateTimeMs);
    446     }
    447 
    448     /**
    449      * @param now this current time in milliseconds.
    450      * @returns the number of milliseconds since the the first post, ignoring updates.
    451      */
    452     public int getLifespanMs(long now) {
    453         return (int) (now - mCreationTimeMs);
    454     }
    455 
    456     /**
    457      * @param now this current time in milliseconds.
    458      * @returns the number of milliseconds since the most recent visibility event, or 0 if never.
    459      */
    460     public int getExposureMs(long now) {
    461         return mVisibleSinceMs == 0 ? 0 : (int) (now - mVisibleSinceMs);
    462     }
    463 
    464     /**
    465      * Set the visibility of the notification.
    466      */
    467     public void setVisibility(boolean visible, int rank) {
    468         final long now = System.currentTimeMillis();
    469         mVisibleSinceMs = visible ? now : mVisibleSinceMs;
    470         stats.onVisibilityChanged(visible);
    471         EventLogTags.writeNotificationVisibility(getKey(), visible ? 1 : 0,
    472                 (int) (now - mCreationTimeMs),
    473                 (int) (now - mUpdateTimeMs),
    474                 0, // exposure time
    475                 rank);
    476     }
    477 
    478     /**
    479      * @param previousRankingTimeMs for updated notifications, {@link #getRankingTimeMs()}
    480      *     of the previous notification record, 0 otherwise
    481      */
    482     private long calculateRankingTimeMs(long previousRankingTimeMs) {
    483         Notification n = getNotification();
    484         // Take developer provided 'when', unless it's in the future.
    485         if (n.when != 0 && n.when <= sbn.getPostTime()) {
    486             return n.when;
    487         }
    488         // If we've ranked a previous instance with a timestamp, inherit it. This case is
    489         // important in order to have ranking stability for updating notifications.
    490         if (previousRankingTimeMs > 0) {
    491             return previousRankingTimeMs;
    492         }
    493         return sbn.getPostTime();
    494     }
    495 
    496     public void setGlobalSortKey(String globalSortKey) {
    497         mGlobalSortKey = globalSortKey;
    498     }
    499 
    500     public String getGlobalSortKey() {
    501         return mGlobalSortKey;
    502     }
    503 
    504     /** Check if any of the listeners have marked this notification as seen by the user. */
    505     public boolean isSeen() {
    506         return mIsSeen;
    507     }
    508 
    509     /** Mark the notification as seen by the user. */
    510     public void setSeen() {
    511         mIsSeen = true;
    512     }
    513 
    514     public void setAuthoritativeRank(int authoritativeRank) {
    515         mAuthoritativeRank = authoritativeRank;
    516     }
    517 
    518     public int getAuthoritativeRank() {
    519         return mAuthoritativeRank;
    520     }
    521 
    522     public String getGroupKey() {
    523         return sbn.getGroupKey();
    524     }
    525 
    526     public boolean isImportanceFromUser() {
    527         return mImportance == mUserImportance;
    528     }
    529 }
    530