NotificationRecord.java revision 5b03ce95c77e16b46a177af32e640d71b7ff4e12
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 */
16package com.android.server.notification;
17
18import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MIN;
19import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
20import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_DEFAULT;
21import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_HIGH;
22import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_LOW;
23import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MAX;
24
25import android.app.Notification;
26import android.content.Context;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.drawable.Icon;
31import android.media.AudioAttributes;
32import android.os.UserHandle;
33import android.service.notification.NotificationListenerService;
34import android.service.notification.StatusBarNotification;
35import android.util.Log;
36
37import com.android.internal.annotations.VisibleForTesting;
38import com.android.server.EventLogTags;
39
40import java.io.PrintWriter;
41import java.lang.reflect.Array;
42import java.util.Arrays;
43import 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 */
57public 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