2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.android.server.notification;
18 import static android.app.NotificationManager.IMPORTANCE_MIN;
19 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
20 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
21 import static android.app.NotificationManager.IMPORTANCE_HIGH;
22 import static android.app.NotificationManager.IMPORTANCE_LOW;
24 import android.app.Notification;
25 import android.app.NotificationChannel;
26 import android.content.Context;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.res.Resources;
31 import android.graphics.Bitmap;
32 import android.graphics.drawable.Icon;
33 import android.media.AudioAttributes;
34 import android.media.AudioSystem;
35 import android.metrics.LogMaker;
36 import android.net.Uri;
37 import android.os.Build;
38 import android.os.UserHandle;
39 import android.provider.Settings;
40 import android.service.notification.NotificationListenerService;
41 import android.service.notification.NotificationRecordProto;
42 import android.service.notification.SnoozeCriterion;
43 import android.service.notification.StatusBarNotification;
44 import android.text.TextUtils;
45 import android.util.Log;
46 import android.util.Slog;
47 import android.util.TimeUtils;
48 import android.util.proto.ProtoOutputStream;
49 import android.widget.RemoteViews;
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.internal.logging.MetricsLogger;
53 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
54 import com.android.server.EventLogTags;
56 import java.io.PrintWriter;
57 import java.lang.reflect.Array;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.Objects;
63 * Holds data about notifications that should not be shared with the
64 * {@link android.service.notification.NotificationListenerService}s.
66 * <p>These objects should not be mutated unless the code is synchronized
67 * on {@link NotificationManagerService#mNotificationLock}, and any
68 * modification should be followed by a sorting of that list.</p>
70 * <p>Is sortable by {@link NotificationComparator}.</p>
74 public final class NotificationRecord {
75 static final String TAG = "NotificationRecord";
76 static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
77 private static final int MAX_LOGTAG_LENGTH = 35;
78 final StatusBarNotification sbn;
79 final int mOriginalFlags;
80 private final Context mContext;
82 NotificationUsageStats.SingleNotificationStats stats;
84 /** Whether the notification was seen by the user via one of the notification listeners. */
87 // These members are used by NotificationSignalExtractors
88 // to communicate with the ranking module.
89 private float mContactAffinity;
90 private boolean mRecentlyIntrusive;
91 private long mLastIntrusive;
93 // is this notification currently being intercepted by Zen Mode?
94 private boolean mIntercept;
96 // The timestamp used for ranking.
97 private long mRankingTimeMs;
99 // The first post time, stable across updates.
100 private long mCreationTimeMs;
102 // The most recent visibility event.
103 private long mVisibleSinceMs;
105 // The most recent update time, or the creation time if no updates.
106 private long mUpdateTimeMs;
108 // Is this record an update of an old record?
109 public boolean isUpdate;
110 private int mPackagePriority;
112 private int mAuthoritativeRank;
113 private String mGlobalSortKey;
114 private int mPackageVisibility;
115 private int mUserImportance = IMPORTANCE_UNSPECIFIED;
116 private int mImportance = IMPORTANCE_UNSPECIFIED;
117 private CharSequence mImportanceExplanation = null;
119 private int mSuppressedVisualEffects = 0;
120 private String mUserExplanation;
121 private String mPeopleExplanation;
122 private boolean mPreChannelsNotification = true;
124 private long[] mVibration;
125 private AudioAttributes mAttributes;
126 private NotificationChannel mChannel;
127 private ArrayList<String> mPeopleOverride;
128 private ArrayList<SnoozeCriterion> mSnoozeCriteria;
129 private boolean mShowBadge;
130 private LogMaker mLogMaker;
131 private Light mLight;
132 private String mGroupLogTag;
133 private String mChannelIdLogTag;
136 public NotificationRecord(Context context, StatusBarNotification sbn,
137 NotificationChannel channel)
140 mOriginalFlags = sbn.getNotification().flags;
141 mRankingTimeMs = calculateRankingTimeMs(0L);
142 mCreationTimeMs = sbn.getPostTime();
143 mUpdateTimeMs = mCreationTimeMs;
145 stats = new NotificationUsageStats.SingleNotificationStats();
147 mPreChannelsNotification = isPreChannelsNotification();
148 mSound = calculateSound();
149 mVibration = calculateVibration();
150 mAttributes = calculateAttributes();
151 mImportance = calculateImportance();
152 mLight = calculateLights();
155 private boolean isPreChannelsNotification() {
157 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(getChannel().getId())) {
158 final ApplicationInfo applicationInfo =
159 mContext.getPackageManager().getApplicationInfoAsUser(sbn.getPackageName(),
160 0, UserHandle.getUserId(sbn.getUid()));
161 if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.O) {
165 } catch (NameNotFoundException e) {
166 Slog.e(TAG, "Can't find package", e);
171 private Uri calculateSound() {
172 final Notification n = sbn.getNotification();
174 // No notification sounds on tv
175 if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
179 Uri sound = mChannel.getSound();
180 if (mPreChannelsNotification && (getChannel().getUserLockedFields()
181 & NotificationChannel.USER_LOCKED_SOUND) == 0) {
183 final boolean useDefaultSound = (n.defaults & Notification.DEFAULT_SOUND) != 0;
184 if (useDefaultSound) {
185 sound = Settings.System.DEFAULT_NOTIFICATION_URI;
193 private Light calculateLights() {
194 int defaultLightColor = mContext.getResources().getColor(
195 com.android.internal.R.color.config_defaultNotificationColor);
196 int defaultLightOn = mContext.getResources().getInteger(
197 com.android.internal.R.integer.config_defaultNotificationLedOn);
198 int defaultLightOff = mContext.getResources().getInteger(
199 com.android.internal.R.integer.config_defaultNotificationLedOff);
201 int channelLightColor = getChannel().getLightColor() != 0 ? getChannel().getLightColor()
203 Light light = getChannel().shouldShowLights() ? new Light(channelLightColor,
204 defaultLightOn, defaultLightOff) : null;
205 if (mPreChannelsNotification
206 && (getChannel().getUserLockedFields()
207 & NotificationChannel.USER_LOCKED_LIGHTS) == 0) {
208 final Notification notification = sbn.getNotification();
209 if ((notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0) {
210 light = new Light(notification.ledARGB, notification.ledOnMS,
211 notification.ledOffMS);
212 if ((notification.defaults & Notification.DEFAULT_LIGHTS) != 0) {
213 light = new Light(defaultLightColor, defaultLightOn,
223 private long[] calculateVibration() {
225 final long[] defaultVibration = NotificationManagerService.getLongArray(
226 mContext.getResources(),
227 com.android.internal.R.array.config_defaultNotificationVibePattern,
228 NotificationManagerService.VIBRATE_PATTERN_MAXLEN,
229 NotificationManagerService.DEFAULT_VIBRATE_PATTERN);
230 if (getChannel().shouldVibrate()) {
231 vibration = getChannel().getVibrationPattern() == null
232 ? defaultVibration : getChannel().getVibrationPattern();
236 if (mPreChannelsNotification
237 && (getChannel().getUserLockedFields()
238 & NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
239 final Notification notification = sbn.getNotification();
240 final boolean useDefaultVibrate =
241 (notification.defaults & Notification.DEFAULT_VIBRATE) != 0;
242 if (useDefaultVibrate) {
243 vibration = defaultVibration;
245 vibration = notification.vibrate;
251 private AudioAttributes calculateAttributes() {
252 final Notification n = sbn.getNotification();
253 AudioAttributes attributes = getChannel().getAudioAttributes();
254 if (attributes == null) {
255 attributes = Notification.AUDIO_ATTRIBUTES_DEFAULT;
258 if (mPreChannelsNotification
259 && (getChannel().getUserLockedFields()
260 & NotificationChannel.USER_LOCKED_SOUND) == 0) {
261 if (n.audioAttributes != null) {
262 // prefer audio attributes to stream type
263 attributes = n.audioAttributes;
264 } else if (n.audioStreamType >= 0
265 && n.audioStreamType < AudioSystem.getNumStreamTypes()) {
266 // the stream type is valid, use it
267 attributes = new AudioAttributes.Builder()
268 .setInternalLegacyStreamType(n.audioStreamType)
270 } else if (n.audioStreamType != AudioSystem.STREAM_DEFAULT) {
271 Log.w(TAG, String.format("Invalid stream type: %d", n.audioStreamType));
277 private int calculateImportance() {
278 final Notification n = sbn.getNotification();
279 int importance = getChannel().getImportance();
280 int requestedImportance = IMPORTANCE_DEFAULT;
282 // Migrate notification flags to scores
283 if (0 != (n.flags & Notification.FLAG_HIGH_PRIORITY)) {
284 n.priority = Notification.PRIORITY_MAX;
287 n.priority = NotificationManagerService.clamp(n.priority, Notification.PRIORITY_MIN,
288 Notification.PRIORITY_MAX);
289 switch (n.priority) {
290 case Notification.PRIORITY_MIN:
291 requestedImportance = IMPORTANCE_MIN;
293 case Notification.PRIORITY_LOW:
294 requestedImportance = IMPORTANCE_LOW;
296 case Notification.PRIORITY_DEFAULT:
297 requestedImportance = IMPORTANCE_DEFAULT;
299 case Notification.PRIORITY_HIGH:
300 case Notification.PRIORITY_MAX:
301 requestedImportance = IMPORTANCE_HIGH;
304 stats.requestedImportance = requestedImportance;
305 stats.isNoisy = mSound != null || mVibration != null;
307 if (mPreChannelsNotification
308 && (importance == IMPORTANCE_UNSPECIFIED
309 || (getChannel().getUserLockedFields()
310 & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0)) {
311 if (!stats.isNoisy && requestedImportance > IMPORTANCE_LOW) {
312 requestedImportance = IMPORTANCE_LOW;
316 if (requestedImportance < IMPORTANCE_DEFAULT) {
317 requestedImportance = IMPORTANCE_DEFAULT;
321 if (n.fullScreenIntent != null) {
322 requestedImportance = IMPORTANCE_HIGH;
324 importance = requestedImportance;
327 stats.naturalImportance = importance;
331 // copy any notes that the ranking system may have made before the update
332 public void copyRankingInformation(NotificationRecord previous) {
333 mContactAffinity = previous.mContactAffinity;
334 mRecentlyIntrusive = previous.mRecentlyIntrusive;
335 mPackagePriority = previous.mPackagePriority;
336 mPackageVisibility = previous.mPackageVisibility;
337 mIntercept = previous.mIntercept;
338 mRankingTimeMs = calculateRankingTimeMs(previous.getRankingTimeMs());
339 mCreationTimeMs = previous.mCreationTimeMs;
340 mVisibleSinceMs = previous.mVisibleSinceMs;
341 if (previous.sbn.getOverrideGroupKey() != null && !sbn.isAppGroup()) {
342 sbn.setOverrideGroupKey(previous.sbn.getOverrideGroupKey());
344 // Don't copy importance information or mGlobalSortKey, recompute them.
347 public Notification getNotification() { return sbn.getNotification(); }
348 public int getFlags() { return sbn.getNotification().flags; }
349 public UserHandle getUser() { return sbn.getUser(); }
350 public String getKey() { return sbn.getKey(); }
351 /** @deprecated Use {@link #getUser()} instead. */
352 public int getUserId() { return sbn.getUserId(); }
354 void dump(ProtoOutputStream proto, boolean redact) {
355 proto.write(NotificationRecordProto.KEY, sbn.getKey());
356 if (getChannel() != null) {
357 proto.write(NotificationRecordProto.CHANNEL_ID, getChannel().getId());
359 proto.write(NotificationRecordProto.CAN_SHOW_LIGHT, getLight() != null);
360 proto.write(NotificationRecordProto.CAN_VIBRATE, getVibration() != null);
361 proto.write(NotificationRecordProto.FLAGS, sbn.getNotification().flags);
362 proto.write(NotificationRecordProto.GROUP_KEY, getGroupKey());
363 proto.write(NotificationRecordProto.IMPORTANCE, getImportance());
364 if (getSound() != null) {
365 proto.write(NotificationRecordProto.SOUND, getSound().toString());
367 if (getAudioAttributes() != null) {
368 proto.write(NotificationRecordProto.SOUND_USAGE, getAudioAttributes().getUsage());
372 String formatRemoteViews(RemoteViews rv) {
373 if (rv == null) return "null";
374 return String.format("%s/0x%08x (%d bytes): %s",
375 rv.getPackage(), rv.getLayoutId(), rv.estimateMemoryUsage(), rv.toString());
378 void dump(PrintWriter pw, String prefix, Context baseContext, boolean redact) {
379 final Notification notification = sbn.getNotification();
380 final Icon icon = notification.getSmallIcon();
381 String iconStr = String.valueOf(icon);
382 if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
383 iconStr += " / " + idDebugString(baseContext, icon.getResPackage(), icon.getResId());
385 pw.println(prefix + this);
386 prefix = prefix + " ";
387 pw.println(prefix + "uid=" + sbn.getUid() + " userId=" + sbn.getUserId());
388 pw.println(prefix + "icon=" + iconStr);
389 pw.println(prefix + "flags=0x" + Integer.toHexString(notification.flags));
390 pw.println(prefix + "pri=" + notification.priority);
391 pw.println(prefix + "key=" + sbn.getKey());
392 pw.println(prefix + "seen=" + mIsSeen);
393 pw.println(prefix + "groupKey=" + getGroupKey());
394 pw.println(prefix + "fullscreenIntent=" + notification.fullScreenIntent);
395 pw.println(prefix + "contentIntent=" + notification.contentIntent);
396 pw.println(prefix + "deleteIntent=" + notification.deleteIntent);
398 pw.print(prefix + "tickerText=");
399 if (!TextUtils.isEmpty(notification.tickerText)) {
400 final String ticker = notification.tickerText.toString();
402 // if the string is long enough, we allow ourselves a few bytes for debugging
403 pw.print(ticker.length() > 16 ? ticker.substring(0,8) : "");
411 pw.println(prefix + "contentView=" + formatRemoteViews(notification.contentView));
412 pw.println(prefix + "bigContentView=" + formatRemoteViews(notification.bigContentView));
413 pw.println(prefix + "headsUpContentView="
414 + formatRemoteViews(notification.headsUpContentView));
415 pw.print(prefix + String.format("color=0x%08x", notification.color));
416 pw.println(prefix + "timeout="
417 + TimeUtils.formatForLogging(notification.getTimeoutAfter()));
418 if (notification.actions != null && notification.actions.length > 0) {
419 pw.println(prefix + "actions={");
420 final int N = notification.actions.length;
421 for (int i = 0; i < N; i++) {
422 final Notification.Action action = notification.actions[i];
423 if (action != null) {
424 pw.println(String.format("%s [%d] \"%s\" -> %s",
428 action.actionIntent == null ? "null" : action.actionIntent.toString()
432 pw.println(prefix + " }");
434 if (notification.extras != null && notification.extras.size() > 0) {
435 pw.println(prefix + "extras={");
436 for (String key : notification.extras.keySet()) {
437 pw.print(prefix + " " + key + "=");
438 Object val = notification.extras.get(key);
442 pw.print(val.getClass().getSimpleName());
443 if (redact && (val instanceof CharSequence || val instanceof String)) {
444 // redact contents from bugreports
445 } else if (val instanceof Bitmap) {
446 pw.print(String.format(" (%dx%d)",
447 ((Bitmap) val).getWidth(),
448 ((Bitmap) val).getHeight()));
449 } else if (val.getClass().isArray()) {
450 final int N = Array.getLength(val);
451 pw.print(" (" + N + ")");
453 for (int j = 0; j < N; j++) {
455 pw.print(String.format("%s [%d] %s",
456 prefix, j, String.valueOf(Array.get(val, j))));
460 pw.print(" (" + String.valueOf(val) + ")");
465 pw.println(prefix + "}");
467 pw.println(prefix + "stats=" + stats.toString());
468 pw.println(prefix + "mContactAffinity=" + mContactAffinity);
469 pw.println(prefix + "mRecentlyIntrusive=" + mRecentlyIntrusive);
470 pw.println(prefix + "mPackagePriority=" + mPackagePriority);
471 pw.println(prefix + "mPackageVisibility=" + mPackageVisibility);
472 pw.println(prefix + "mUserImportance="
473 + NotificationListenerService.Ranking.importanceToString(mUserImportance));
474 pw.println(prefix + "mImportance="
475 + NotificationListenerService.Ranking.importanceToString(mImportance));
476 pw.println(prefix + "mImportanceExplanation=" + mImportanceExplanation);
477 pw.println(prefix + "mIntercept=" + mIntercept);
478 pw.println(prefix + "mGlobalSortKey=" + mGlobalSortKey);
479 pw.println(prefix + "mRankingTimeMs=" + mRankingTimeMs);
480 pw.println(prefix + "mCreationTimeMs=" + mCreationTimeMs);
481 pw.println(prefix + "mVisibleSinceMs=" + mVisibleSinceMs);
482 pw.println(prefix + "mUpdateTimeMs=" + mUpdateTimeMs);
483 pw.println(prefix + "mSuppressedVisualEffects= " + mSuppressedVisualEffects);
484 if (mPreChannelsNotification) {
485 pw.println(prefix + String.format("defaults=0x%08x flags=0x%08x",
486 notification.defaults, notification.flags));
487 pw.println(prefix + "n.sound=" + notification.sound);
488 pw.println(prefix + "n.audioStreamType=" + notification.audioStreamType);
489 pw.println(prefix + "n.audioAttributes=" + notification.audioAttributes);
490 pw.println(prefix + String.format(" led=0x%08x onMs=%d offMs=%d",
491 notification.ledARGB, notification.ledOnMS, notification.ledOffMS));
492 pw.println(prefix + "vibrate=" + Arrays.toString(notification.vibrate));
494 pw.println(prefix + "mSound= " + mSound);
495 pw.println(prefix + "mVibration= " + mVibration);
496 pw.println(prefix + "mAttributes= " + mAttributes);
497 pw.println(prefix + "mLight= " + mLight);
498 pw.println(prefix + "mShowBadge=" + mShowBadge);
499 pw.println(prefix + "mColorized=" + notification.isColorized());
500 pw.println(prefix + "effectiveNotificationChannel=" + getChannel());
501 if (getPeopleOverride() != null) {
502 pw.println(prefix + "overridePeople= " + TextUtils.join(",", getPeopleOverride()));
504 if (getSnoozeCriteria() != null) {
505 pw.println(prefix + "snoozeCriteria=" + TextUtils.join(",", getSnoozeCriteria()));
510 static String idDebugString(Context baseContext, String packageName, int id) {
513 if (packageName != null) {
515 c = baseContext.createPackageContext(packageName, 0);
516 } catch (NameNotFoundException e) {
523 Resources r = c.getResources();
525 return r.getResourceName(id);
526 } catch (Resources.NotFoundException e) {
527 return "<name unknown>";
532 public final String toString() {
533 return String.format(
534 "NotificationRecord(0x%08x: pkg=%s user=%s id=%d tag=%s importance=%d key=%s" +
536 System.identityHashCode(this),
537 this.sbn.getPackageName(), this.sbn.getUser(), this.sbn.getId(),
538 this.sbn.getTag(), this.mImportance, this.sbn.getKey(),
539 this.sbn.getNotification());
542 public void setContactAffinity(float contactAffinity) {
543 mContactAffinity = contactAffinity;
544 if (mImportance < IMPORTANCE_DEFAULT &&
545 mContactAffinity > ValidateNotificationPeople.VALID_CONTACT) {
546 setImportance(IMPORTANCE_DEFAULT, getPeopleExplanation());
550 public float getContactAffinity() {
551 return mContactAffinity;
554 public void setRecentlyIntrusive(boolean recentlyIntrusive) {
555 mRecentlyIntrusive = recentlyIntrusive;
556 if (recentlyIntrusive) {
557 mLastIntrusive = System.currentTimeMillis();
561 public boolean isRecentlyIntrusive() {
562 return mRecentlyIntrusive;
565 public long getLastIntrusive() {
566 return mLastIntrusive;
569 public void setPackagePriority(int packagePriority) {
570 mPackagePriority = packagePriority;
573 public int getPackagePriority() {
574 return mPackagePriority;
577 public void setPackageVisibilityOverride(int packageVisibility) {
578 mPackageVisibility = packageVisibility;
581 public int getPackageVisibilityOverride() {
582 return mPackageVisibility;
585 public void setUserImportance(int importance) {
586 mUserImportance = importance;
587 applyUserImportance();
590 private String getUserExplanation() {
591 if (mUserExplanation == null) {
592 mUserExplanation = mContext.getResources().getString(
593 com.android.internal.R.string.importance_from_user);
595 return mUserExplanation;
598 private String getPeopleExplanation() {
599 if (mPeopleExplanation == null) {
600 mPeopleExplanation = mContext.getResources().getString(
601 com.android.internal.R.string.importance_from_person);
603 return mPeopleExplanation;
606 private void applyUserImportance() {
607 if (mUserImportance != IMPORTANCE_UNSPECIFIED) {
608 mImportance = mUserImportance;
609 mImportanceExplanation = getUserExplanation();
613 public int getUserImportance() {
614 return mUserImportance;
617 public void setImportance(int importance, CharSequence explanation) {
618 if (importance != IMPORTANCE_UNSPECIFIED) {
619 mImportance = importance;
620 mImportanceExplanation = explanation;
622 applyUserImportance();
625 public int getImportance() {
629 public CharSequence getImportanceExplanation() {
630 return mImportanceExplanation;
633 public boolean setIntercepted(boolean intercept) {
634 mIntercept = intercept;
638 public boolean isIntercepted() {
642 public void setSuppressedVisualEffects(int effects) {
643 mSuppressedVisualEffects = effects;
646 public int getSuppressedVisualEffects() {
647 return mSuppressedVisualEffects;
650 public boolean isCategory(String category) {
651 return Objects.equals(getNotification().category, category);
654 public boolean isAudioStream(int stream) {
655 return getNotification().audioStreamType == stream;
658 public boolean isAudioAttributesUsage(int usage) {
659 final AudioAttributes attributes = getNotification().audioAttributes;
660 return attributes != null && attributes.getUsage() == usage;
664 * Returns the timestamp to use for time-based sorting in the ranker.
666 public long getRankingTimeMs() {
667 return mRankingTimeMs;
671 * @param now this current time in milliseconds.
672 * @returns the number of milliseconds since the most recent update, or the post time if none.
674 public int getFreshnessMs(long now) {
675 return (int) (now - mUpdateTimeMs);
679 * @param now this current time in milliseconds.
680 * @returns the number of milliseconds since the the first post, ignoring updates.
682 public int getLifespanMs(long now) {
683 return (int) (now - mCreationTimeMs);
687 * @param now this current time in milliseconds.
688 * @returns the number of milliseconds since the most recent visibility event, or 0 if never.
690 public int getExposureMs(long now) {
691 return mVisibleSinceMs == 0 ? 0 : (int) (now - mVisibleSinceMs);
695 * Set the visibility of the notification.
697 public void setVisibility(boolean visible, int rank) {
698 final long now = System.currentTimeMillis();
699 mVisibleSinceMs = visible ? now : mVisibleSinceMs;
700 stats.onVisibilityChanged(visible);
701 MetricsLogger.action(getLogMaker(now)
702 .setCategory(MetricsEvent.NOTIFICATION_ITEM)
703 .setType(visible ? MetricsEvent.TYPE_OPEN : MetricsEvent.TYPE_CLOSE)
704 .addTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX, rank));
706 MetricsLogger.histogram(mContext, "note_freshness", getFreshnessMs(now));
708 EventLogTags.writeNotificationVisibility(getKey(), visible ? 1 : 0,
716 * @param previousRankingTimeMs for updated notifications, {@link #getRankingTimeMs()}
717 * of the previous notification record, 0 otherwise
719 private long calculateRankingTimeMs(long previousRankingTimeMs) {
720 Notification n = getNotification();
721 // Take developer provided 'when', unless it's in the future.
722 if (n.when != 0 && n.when <= sbn.getPostTime()) {
725 // If we've ranked a previous instance with a timestamp, inherit it. This case is
726 // important in order to have ranking stability for updating notifications.
727 if (previousRankingTimeMs > 0) {
728 return previousRankingTimeMs;
730 return sbn.getPostTime();
733 public void setGlobalSortKey(String globalSortKey) {
734 mGlobalSortKey = globalSortKey;
737 public String getGlobalSortKey() {
738 return mGlobalSortKey;
741 /** Check if any of the listeners have marked this notification as seen by the user. */
742 public boolean isSeen() {
746 /** Mark the notification as seen by the user. */
747 public void setSeen() {
751 public void setAuthoritativeRank(int authoritativeRank) {
752 mAuthoritativeRank = authoritativeRank;
755 public int getAuthoritativeRank() {
756 return mAuthoritativeRank;
759 public String getGroupKey() {
760 return sbn.getGroupKey();
763 public void setOverrideGroupKey(String overrideGroupKey) {
764 sbn.setOverrideGroupKey(overrideGroupKey);
768 private String getGroupLogTag() {
769 if (mGroupLogTag == null) {
770 mGroupLogTag = shortenTag(sbn.getGroup());
775 private String getChannelIdLogTag() {
776 if (mChannelIdLogTag == null) {
777 mChannelIdLogTag = shortenTag(mChannel.getId());
779 return mChannelIdLogTag;
782 private String shortenTag(String longTag) {
783 if (longTag == null) {
786 if (longTag.length() < MAX_LOGTAG_LENGTH) {
789 return longTag.substring(0, MAX_LOGTAG_LENGTH - 8) + "-" +
790 Integer.toHexString(longTag.hashCode());
794 public boolean isImportanceFromUser() {
795 return mImportance == mUserImportance;
798 public NotificationChannel getChannel() {
802 protected void updateNotificationChannel(NotificationChannel channel) {
803 if (channel != null) {
805 calculateImportance();
809 public void setShowBadge(boolean showBadge) {
810 mShowBadge = showBadge;
813 public boolean canShowBadge() {
817 public Light getLight() {
821 public Uri getSound() {
825 public long[] getVibration() {
829 public AudioAttributes getAudioAttributes() {
833 public ArrayList<String> getPeopleOverride() {
834 return mPeopleOverride;
837 protected void setPeopleOverride(ArrayList<String> people) {
838 mPeopleOverride = people;
841 public ArrayList<SnoozeCriterion> getSnoozeCriteria() {
842 return mSnoozeCriteria;
845 protected void setSnoozeCriteria(ArrayList<SnoozeCriterion> snoozeCriteria) {
846 mSnoozeCriteria = snoozeCriteria;
849 public LogMaker getLogMaker(long now) {
850 if (mLogMaker == null) {
851 // initialize fields that only change on update (so a new record)
852 mLogMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN)
853 .setPackageName(sbn.getPackageName())
854 .addTaggedData(MetricsEvent.NOTIFICATION_ID, sbn.getId())
855 .addTaggedData(MetricsEvent.NOTIFICATION_TAG, sbn.getTag())
856 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, getChannelIdLogTag());
858 // reset fields that can change between updates, or are used by multiple logs
863 .clearTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX)
864 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, mImportance)
865 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_ID, getGroupLogTag())
866 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_SUMMARY,
867 sbn.getNotification().isGroupSummary() ? 1 : 0)
868 .addTaggedData(MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS, getLifespanMs(now))
869 .addTaggedData(MetricsEvent.NOTIFICATION_SINCE_UPDATE_MILLIS, getFreshnessMs(now))
870 .addTaggedData(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS, getExposureMs(now));
873 public LogMaker getLogMaker() {
874 return getLogMaker(System.currentTimeMillis());
878 static final class Light {
879 public final int color;
880 public final int onMs;
881 public final int offMs;
883 public Light(int color, int onMs, int offMs) {
890 public boolean equals(Object o) {
891 if (this == o) return true;
892 if (o == null || getClass() != o.getClass()) return false;
894 Light light = (Light) o;
896 if (color != light.color) return false;
897 if (onMs != light.onMs) return false;
898 return offMs == light.offMs;
903 public int hashCode() {
905 result = 31 * result + onMs;
906 result = 31 * result + offMs;
911 public String toString() {