1 /* 2 * Copyright (C) 2020 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.server.notification; 18 19 import static android.service.notification.NotificationListenerService.REASON_ASSISTANT_CANCEL; 20 import static android.service.notification.NotificationListenerService.REASON_CANCEL; 21 import static android.service.notification.NotificationListenerService.REASON_CLEAR_DATA; 22 import static android.service.notification.NotificationListenerService.REASON_CLICK; 23 24 import android.annotation.DurationMillisLong; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.app.Notification; 28 import android.app.NotificationChannel; 29 import android.app.Person; 30 import android.os.Bundle; 31 import android.service.notification.NotificationListenerService; 32 import android.service.notification.NotificationStats; 33 import android.util.Log; 34 35 import com.android.internal.logging.InstanceId; 36 import com.android.internal.logging.UiEvent; 37 import com.android.internal.logging.UiEventLogger; 38 import com.android.internal.util.FrameworkStatsLog; 39 40 import java.time.Duration; 41 import java.util.ArrayList; 42 import java.util.Objects; 43 44 /** 45 * Interface for writing NotificationReported atoms to statsd log. Use NotificationRecordLoggerImpl 46 * in production. Use NotificationRecordLoggerFake for testing. 47 * @hide 48 */ 49 interface NotificationRecordLogger { 50 51 static final String TAG = "NotificationRecordLogger"; 52 53 // The high-level interface used by clients. 54 55 /** 56 * Prepare to log an atom reflecting the posting or update of a notification. 57 * 58 * The returned {@link NotificationReported} object, if any, should be supplied to 59 * {@link #logNotificationPosted}. Because only some updates are considered "interesting 60 * enough" to log, this method may return {@code null}. In that case, the follow-up call 61 * should not be performed. 62 * 63 * @param r The new {@link NotificationRecord}. 64 * @param old The previous {@link NotificationRecord}. Null if there was no previous record. 65 * @param position The position at which this notification is ranked. 66 * @param buzzBeepBlink Logging code reflecting whether this notification alerted the user. 67 * @param groupId The {@link InstanceId} of the group summary notification, or null. 68 */ 69 @Nullable prepareToLogNotificationPosted(@ullable NotificationRecord r, @Nullable NotificationRecord old, int position, int buzzBeepBlink, InstanceId groupId)70 default NotificationReported prepareToLogNotificationPosted(@Nullable NotificationRecord r, 71 @Nullable NotificationRecord old, 72 int position, int buzzBeepBlink, 73 InstanceId groupId) { 74 NotificationRecordPair p = new NotificationRecordPair(r, old); 75 if (!p.shouldLogReported(buzzBeepBlink)) { 76 return null; 77 } 78 return new NotificationReported(p, NotificationReportedEvent.fromRecordPair(p), position, 79 buzzBeepBlink, groupId); 80 } 81 82 /** 83 * Log a NotificationReported atom reflecting the posting or update of a notification. 84 */ logNotificationPosted(NotificationReported nr)85 void logNotificationPosted(NotificationReported nr); 86 87 /** 88 * Logs a NotificationReported atom reflecting an adjustment to a notification. 89 * Unlike for posted notifications, this method is guaranteed to log a notification update, 90 * so the caller must take responsibility for checking that that logging update is necessary, 91 * and that the notification is meaningfully changed. 92 * @param r The NotificationRecord. If null, no action is taken. 93 * @param position The position at which this notification is ranked. 94 * @param buzzBeepBlink Logging code reflecting whether this notification alerted the user. 95 * @param groupId The instance Id of the group summary notification, or null. 96 */ logNotificationAdjusted(@ullable NotificationRecord r, int position, int buzzBeepBlink, InstanceId groupId)97 void logNotificationAdjusted(@Nullable NotificationRecord r, 98 int position, int buzzBeepBlink, 99 InstanceId groupId); 100 101 /** 102 * Logs a notification cancel / dismiss event using UiEventReported (event ids from the 103 * NotificationCancelledEvents enum). 104 * @param r The NotificationRecord. If null, no action is taken. 105 * @param reason The reason the notification was canceled. 106 * @param dismissalSurface The surface the notification was dismissed from. 107 */ logNotificationCancelled(@ullable NotificationRecord r, @NotificationListenerService.NotificationCancelReason int reason, @NotificationStats.DismissalSurface int dismissalSurface)108 default void logNotificationCancelled(@Nullable NotificationRecord r, 109 @NotificationListenerService.NotificationCancelReason int reason, 110 @NotificationStats.DismissalSurface int dismissalSurface) { 111 log(NotificationCancelledEvent.fromCancelReason(reason, dismissalSurface), r); 112 } 113 114 /** 115 * Logs a notification visibility change event using UiEventReported (event ids from the 116 * NotificationEvents enum). 117 * @param r The NotificationRecord. If null, no action is taken. 118 * @param visible True if the notification became visible. 119 */ logNotificationVisibility(@ullable NotificationRecord r, boolean visible)120 default void logNotificationVisibility(@Nullable NotificationRecord r, boolean visible) { 121 log(NotificationEvent.fromVisibility(visible), r); 122 } 123 124 // The UiEventReported logging methods are implemented in terms of this lower-level interface. 125 126 /** Logs a UiEventReported event for the given notification. */ log(UiEventLogger.UiEventEnum event, NotificationRecord r)127 void log(UiEventLogger.UiEventEnum event, NotificationRecord r); 128 129 /** Logs a UiEventReported event that is not associated with any notification. */ log(UiEventLogger.UiEventEnum event)130 void log(UiEventLogger.UiEventEnum event); 131 132 /** 133 * The UiEvent enums that this class can log. 134 */ 135 enum NotificationReportedEvent implements UiEventLogger.UiEventEnum { 136 @UiEvent(doc = "New notification enqueued to post") 137 NOTIFICATION_POSTED(162), 138 @UiEvent(doc = "Notification substantially updated, or alerted again.") 139 NOTIFICATION_UPDATED(163), 140 @UiEvent(doc = "Notification adjusted by assistant.") 141 NOTIFICATION_ADJUSTED(908); 142 143 private final int mId; NotificationReportedEvent(int id)144 NotificationReportedEvent(int id) { 145 mId = id; 146 } getId()147 @Override public int getId() { 148 return mId; 149 } 150 fromRecordPair(NotificationRecordPair p)151 public static NotificationReportedEvent fromRecordPair(NotificationRecordPair p) { 152 return (p.old != null) ? NotificationReportedEvent.NOTIFICATION_UPDATED : 153 NotificationReportedEvent.NOTIFICATION_POSTED; 154 } 155 } 156 157 enum NotificationCancelledEvent implements UiEventLogger.UiEventEnum { 158 INVALID(0), 159 @UiEvent(doc = "Notification was canceled due to a notification click.") 160 NOTIFICATION_CANCEL_CLICK(164), 161 @UiEvent(doc = "Notification was canceled due to a user dismissal, surface not specified.") 162 NOTIFICATION_CANCEL_USER_OTHER(165), 163 @UiEvent(doc = "Notification was canceled due to a user dismiss-all (from the notification" 164 + " shade).") 165 NOTIFICATION_CANCEL_USER_CANCEL_ALL(166), 166 @UiEvent(doc = "Notification was canceled due to an inflation error.") 167 NOTIFICATION_CANCEL_ERROR(167), 168 @UiEvent(doc = "Notification was canceled by the package manager modifying the package.") 169 NOTIFICATION_CANCEL_PACKAGE_CHANGED(168), 170 @UiEvent(doc = "Notification was canceled by the owning user context being stopped.") 171 NOTIFICATION_CANCEL_USER_STOPPED(169), 172 @UiEvent(doc = "Notification was canceled by the user banning the package.") 173 NOTIFICATION_CANCEL_PACKAGE_BANNED(170), 174 @UiEvent(doc = "Notification was canceled by the app canceling this specific notification.") 175 NOTIFICATION_CANCEL_APP_CANCEL(171), 176 @UiEvent(doc = "Notification was canceled by the app cancelling all its notifications.") 177 NOTIFICATION_CANCEL_APP_CANCEL_ALL(172), 178 @UiEvent(doc = "Notification was canceled by a listener reporting a user dismissal.") 179 NOTIFICATION_CANCEL_LISTENER_CANCEL(173), 180 @UiEvent(doc = "Notification was canceled by a listener reporting a user dismiss all.") 181 NOTIFICATION_CANCEL_LISTENER_CANCEL_ALL(174), 182 @UiEvent(doc = "Notification was canceled because it was a member of a canceled group.") 183 NOTIFICATION_CANCEL_GROUP_SUMMARY_CANCELED(175), 184 @UiEvent(doc = "Notification was canceled because it was an invisible member of a group.") 185 NOTIFICATION_CANCEL_GROUP_OPTIMIZATION(176), 186 @UiEvent(doc = "Notification was canceled by the device administrator suspending the " 187 + "package.") 188 NOTIFICATION_CANCEL_PACKAGE_SUSPENDED(177), 189 @UiEvent(doc = "Notification was canceled by the owning managed profile being turned off.") 190 NOTIFICATION_CANCEL_PROFILE_TURNED_OFF(178), 191 @UiEvent(doc = "Autobundled summary notification was canceled because its group was " 192 + "unbundled") 193 NOTIFICATION_CANCEL_UNAUTOBUNDLED(179), 194 @UiEvent(doc = "Notification was canceled by the user banning the channel.") 195 NOTIFICATION_CANCEL_CHANNEL_BANNED(180), 196 @UiEvent(doc = "Notification was snoozed.") 197 NOTIFICATION_CANCEL_SNOOZED(181), 198 @UiEvent(doc = "Notification was canceled due to timeout") 199 NOTIFICATION_CANCEL_TIMEOUT(182), 200 @UiEvent(doc = "Notification was canceled due to the backing channel being deleted") 201 NOTIFICATION_CANCEL_CHANNEL_REMOVED(1261), 202 @UiEvent(doc = "Notification was canceled due to the app's storage being cleared") 203 NOTIFICATION_CANCEL_CLEAR_DATA(1262), 204 // Values above this line must remain in the same order as the corresponding 205 // NotificationCancelReason enum values. 206 @UiEvent(doc = "Notification was canceled due to user dismissal of a peeking notification.") 207 NOTIFICATION_CANCEL_USER_PEEK(190), 208 @UiEvent(doc = "Notification was canceled due to user dismissal from the always-on display") 209 NOTIFICATION_CANCEL_USER_AOD(191), 210 @UiEvent(doc = "Notification was canceled due to user dismissal from a bubble") 211 NOTIFICATION_CANCEL_USER_BUBBLE(1228), 212 @UiEvent(doc = "Notification was canceled due to user dismissal from the lockscreen") 213 NOTIFICATION_CANCEL_USER_LOCKSCREEN(193), 214 @UiEvent(doc = "Notification was canceled due to user dismissal from the notification" 215 + " shade.") 216 NOTIFICATION_CANCEL_USER_SHADE(192), 217 @UiEvent(doc = "Notification was canceled due to an assistant adjustment update.") 218 NOTIFICATION_CANCEL_ASSISTANT(906); 219 220 private final int mId; NotificationCancelledEvent(int id)221 NotificationCancelledEvent(int id) { 222 mId = id; 223 } getId()224 @Override public int getId() { 225 return mId; 226 } 227 fromCancelReason( @otificationListenerService.NotificationCancelReason int reason, @NotificationStats.DismissalSurface int surface)228 public static NotificationCancelledEvent fromCancelReason( 229 @NotificationListenerService.NotificationCancelReason int reason, 230 @NotificationStats.DismissalSurface int surface) { 231 // Shouldn't be possible to get a non-dismissed notification here. 232 if (surface == NotificationStats.DISMISSAL_NOT_DISMISSED) { 233 Log.wtf(TAG, "Unexpected surface: " + surface + " with reason " + reason); 234 return INVALID; 235 } 236 237 // User cancels have a meaningful surface, which we differentiate by. See b/149038335 238 // for caveats. 239 if (reason == REASON_CANCEL) { 240 switch (surface) { 241 case NotificationStats.DISMISSAL_PEEK: 242 return NOTIFICATION_CANCEL_USER_PEEK; 243 case NotificationStats.DISMISSAL_AOD: 244 return NOTIFICATION_CANCEL_USER_AOD; 245 case NotificationStats.DISMISSAL_SHADE: 246 return NOTIFICATION_CANCEL_USER_SHADE; 247 case NotificationStats.DISMISSAL_BUBBLE: 248 return NOTIFICATION_CANCEL_USER_BUBBLE; 249 case NotificationStats.DISMISSAL_LOCKSCREEN: 250 return NOTIFICATION_CANCEL_USER_LOCKSCREEN; 251 case NotificationStats.DISMISSAL_OTHER: 252 return NOTIFICATION_CANCEL_USER_OTHER; 253 default: 254 Log.wtf(TAG, "Unexpected surface: " + surface + " with reason " + reason); 255 return INVALID; 256 } 257 } else { 258 if ((REASON_CLICK <= reason) && (reason <= REASON_CLEAR_DATA)) { 259 return NotificationCancelledEvent.values()[reason]; 260 } 261 if (reason == REASON_ASSISTANT_CANCEL) { 262 return NotificationCancelledEvent.NOTIFICATION_CANCEL_ASSISTANT; 263 } 264 Log.wtf(TAG, "Unexpected reason: " + reason + " with surface " + surface); 265 return INVALID; 266 } 267 } 268 } 269 270 enum NotificationEvent implements UiEventLogger.UiEventEnum { 271 @UiEvent(doc = "Notification became visible.") 272 NOTIFICATION_OPEN(197), 273 @UiEvent(doc = "Notification stopped being visible.") 274 NOTIFICATION_CLOSE(198), 275 @UiEvent(doc = "Notification was snoozed.") 276 NOTIFICATION_SNOOZED(317), 277 @UiEvent(doc = "Notification was not posted because its app is snoozed.") 278 NOTIFICATION_NOT_POSTED_SNOOZED(319), 279 @UiEvent(doc = "Notification was clicked.") 280 NOTIFICATION_CLICKED(320), 281 @UiEvent(doc = "Notification action was clicked; unexpected position.") 282 NOTIFICATION_ACTION_CLICKED(321), 283 @UiEvent(doc = "Notification detail was expanded due to non-user action.") 284 NOTIFICATION_DETAIL_OPEN_SYSTEM(327), 285 @UiEvent(doc = "Notification detail was collapsed due to non-user action.") 286 NOTIFICATION_DETAIL_CLOSE_SYSTEM(328), 287 @UiEvent(doc = "Notification detail was expanded due to user action.") 288 NOTIFICATION_DETAIL_OPEN_USER(329), 289 @UiEvent(doc = "Notification detail was collapsed due to user action.") 290 NOTIFICATION_DETAIL_CLOSE_USER(330), 291 @UiEvent(doc = "Notification direct reply action was used.") 292 NOTIFICATION_DIRECT_REPLIED(331), 293 @UiEvent(doc = "Notification smart reply action was used.") 294 NOTIFICATION_SMART_REPLIED(332), 295 @UiEvent(doc = "Notification smart reply action was visible.") 296 NOTIFICATION_SMART_REPLY_VISIBLE(333), 297 @UiEvent(doc = "App-generated notification action at position 0 was clicked.") 298 NOTIFICATION_ACTION_CLICKED_0(450), 299 @UiEvent(doc = "App-generated notification action at position 1 was clicked.") 300 NOTIFICATION_ACTION_CLICKED_1(451), 301 @UiEvent(doc = "App-generated notification action at position 2 was clicked.") 302 NOTIFICATION_ACTION_CLICKED_2(452), 303 @UiEvent(doc = "Contextual notification action at position 0 was clicked.") 304 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_0(453), 305 @UiEvent(doc = "Contextual notification action at position 1 was clicked.") 306 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_1(454), 307 @UiEvent(doc = "Contextual notification action at position 2 was clicked.") 308 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_2(455), 309 @UiEvent(doc = "Notification assistant generated notification action at 0 was clicked.") 310 NOTIFICATION_ASSIST_ACTION_CLICKED_0(456), 311 @UiEvent(doc = "Notification assistant generated notification action at 1 was clicked.") 312 NOTIFICATION_ASSIST_ACTION_CLICKED_1(457), 313 @UiEvent(doc = "Notification assistant generated notification action at 2 was clicked.") 314 NOTIFICATION_ASSIST_ACTION_CLICKED_2(458), 315 @UiEvent(doc = "Notification was force autogrouped.") 316 NOTIFICATION_FORCE_GROUP(1843), 317 @UiEvent(doc = "Notification summary was force autogrouped.") 318 NOTIFICATION_FORCE_GROUP_SUMMARY(1844); 319 320 private final int mId; NotificationEvent(int id)321 NotificationEvent(int id) { 322 mId = id; 323 } getId()324 @Override public int getId() { 325 return mId; 326 } 327 fromVisibility(boolean visible)328 public static NotificationEvent fromVisibility(boolean visible) { 329 return visible ? NOTIFICATION_OPEN : NOTIFICATION_CLOSE; 330 } fromExpanded(boolean expanded, boolean userAction)331 public static NotificationEvent fromExpanded(boolean expanded, boolean userAction) { 332 if (userAction) { 333 return expanded ? NOTIFICATION_DETAIL_OPEN_USER : NOTIFICATION_DETAIL_CLOSE_USER; 334 } 335 return expanded ? NOTIFICATION_DETAIL_OPEN_SYSTEM : NOTIFICATION_DETAIL_CLOSE_SYSTEM; 336 } fromAction(int index, boolean isAssistant, boolean isContextual)337 public static NotificationEvent fromAction(int index, boolean isAssistant, 338 boolean isContextual) { 339 if (index < 0 || index > 2) { 340 return NOTIFICATION_ACTION_CLICKED; 341 } 342 if (isAssistant) { // Assistant actions are contextual by definition 343 return NotificationEvent.values()[ 344 NOTIFICATION_ASSIST_ACTION_CLICKED_0.ordinal() + index]; 345 } 346 if (isContextual) { 347 return NotificationEvent.values()[ 348 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_0.ordinal() + index]; 349 } 350 return NotificationEvent.values()[NOTIFICATION_ACTION_CLICKED_0.ordinal() + index]; 351 } 352 } 353 354 enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { 355 @UiEvent(doc = "Notification panel became visible.") 356 NOTIFICATION_PANEL_OPEN(325), 357 @UiEvent(doc = "Notification panel stopped being visible.") 358 NOTIFICATION_PANEL_CLOSE(326); 359 360 private final int mId; NotificationPanelEvent(int id)361 NotificationPanelEvent(int id) { 362 mId = id; 363 } getId()364 @Override public int getId() { 365 return mId; 366 } 367 } 368 369 enum NotificationPullStatsEvent implements UiEventLogger.UiEventEnum { 370 @UiEvent(doc = "Notification Bundle Preferences pulled.") 371 NOTIFICATION_BUNDLE_PREFERENCES_PULLED(2072); 372 373 private final int mId; NotificationPullStatsEvent(int id)374 NotificationPullStatsEvent(int id) { 375 mId = id; 376 } getId()377 @Override public int getId() { 378 return mId; 379 } 380 } 381 382 /** 383 * A helper for extracting logging information from one or two NotificationRecords. 384 */ 385 class NotificationRecordPair { 386 public final NotificationRecord r, old; 387 /** 388 * Construct from one or two NotificationRecords. 389 * @param r The new NotificationRecord. If null, only shouldLog() method is usable. 390 * @param old The previous NotificationRecord. Null if there was no previous record. 391 */ NotificationRecordPair(@ullable NotificationRecord r, @Nullable NotificationRecord old)392 NotificationRecordPair(@Nullable NotificationRecord r, @Nullable NotificationRecord old) { 393 this.r = r; 394 this.old = old; 395 } 396 397 /** 398 * @return True if old is null, alerted, or important logged fields have changed. 399 */ shouldLogReported(int buzzBeepBlink)400 boolean shouldLogReported(int buzzBeepBlink) { 401 if (r == null) { 402 return false; 403 } 404 if ((old == null) || (buzzBeepBlink > 0)) { 405 return true; 406 } 407 408 return !(Objects.equals(r.getSbn().getChannelIdLogTag(), 409 old.getSbn().getChannelIdLogTag()) 410 && Objects.equals(r.getSbn().getGroupLogTag(), old.getSbn().getGroupLogTag()) 411 && (r.getSbn().getNotification().isGroupSummary() 412 == old.getSbn().getNotification().isGroupSummary()) 413 && Objects.equals(r.getSbn().getNotification().category, 414 old.getSbn().getNotification().category) 415 && (r.getImportance() == old.getImportance()) 416 && (getLoggingImportance(r) == getLoggingImportance(old)) 417 && r.rankingScoreMatches(old.getRankingScore())); 418 } 419 420 /** 421 * @return hash code for the notification style class, or 0 if none exists. 422 */ getStyle()423 public int getStyle() { 424 return getStyle(r.getSbn().getNotification().extras); 425 } 426 getStyle(@ullable Bundle extras)427 private int getStyle(@Nullable Bundle extras) { 428 if (extras != null) { 429 String template = extras.getString(Notification.EXTRA_TEMPLATE); 430 if (template != null && !template.isEmpty()) { 431 return template.hashCode(); 432 } 433 } 434 return 0; 435 } 436 getNumPeople()437 int getNumPeople() { 438 return getNumPeople(r.getSbn().getNotification().extras); 439 } 440 getNumPeople(@ullable Bundle extras)441 private int getNumPeople(@Nullable Bundle extras) { 442 if (extras != null) { 443 ArrayList<Person> people = extras.getParcelableArrayList( 444 Notification.EXTRA_PEOPLE_LIST, android.app.Person.class); 445 if (people != null && !people.isEmpty()) { 446 return people.size(); 447 } 448 } 449 return 0; 450 } 451 getAssistantHash()452 int getAssistantHash() { 453 String assistant = r.getAdjustmentIssuer(); 454 return (assistant == null) ? 0 : assistant.hashCode(); 455 } 456 getInstanceId()457 int getInstanceId() { 458 return (r.getSbn().getInstanceId() == null ? 0 : r.getSbn().getInstanceId().getId()); 459 } 460 461 /** 462 * @return Small hash of the notification ID, and tag (if present). 463 */ getNotificationIdHash()464 int getNotificationIdHash() { 465 return SmallHash.hash(Objects.hashCode(r.getSbn().getTag()) ^ r.getSbn().getId()); 466 } 467 468 /** 469 * @return Small hash of the channel ID, if present, or 0 otherwise. 470 */ getChannelIdHash()471 int getChannelIdHash() { 472 return SmallHash.hash(r.getSbn().getNotification().getChannelId()); 473 } 474 475 /** 476 * @return Small hash of the group ID, respecting group override if present. 0 otherwise. 477 */ getGroupIdHash()478 int getGroupIdHash() { 479 return SmallHash.hash(r.getSbn().getGroup()); 480 } 481 482 } 483 484 /** Data object corresponding to a NotificationReported atom. 485 * 486 * Fields must be kept in sync with frameworks/proto_logging/stats/atoms.proto. 487 */ 488 class NotificationReported { 489 final int event_id; 490 final int uid; 491 final String package_name; 492 final int instance_id; 493 final int notification_id_hash; 494 final int channel_id_hash; 495 final int group_id_hash; 496 final int group_instance_id; 497 final boolean is_group_summary; 498 final String category; 499 final int style; 500 final int num_people; 501 final int position; 502 final int importance; 503 final int alerting; 504 final int importance_source; 505 final int importance_initial; 506 final int importance_initial_source; 507 final int importance_asst; 508 final int assistant_hash; 509 final float assistant_ranking_score; 510 final boolean is_ongoing; 511 final boolean is_foreground_service; 512 final long timeout_millis; 513 final boolean is_non_dismissible; 514 final int fsi_state; 515 final boolean is_locked; 516 final int age_in_minutes; 517 final boolean is_promoted_ongoing; 518 final boolean has_promotable_characteristics; 519 @DurationMillisLong long post_duration_millis; // Not final; calculated at the end. 520 NotificationReported(NotificationRecordPair p, NotificationReportedEvent eventType, int position, int buzzBeepBlink, InstanceId groupId)521 NotificationReported(NotificationRecordPair p, 522 NotificationReportedEvent eventType, int position, int buzzBeepBlink, 523 InstanceId groupId) { 524 final Notification notification = p.r.getSbn().getNotification(); 525 this.event_id = eventType.getId(); 526 this.uid = p.r.getUid(); 527 this.package_name = p.r.getSbn().getPackageName(); 528 this.instance_id = p.getInstanceId(); 529 this.notification_id_hash = p.getNotificationIdHash(); 530 this.channel_id_hash = p.getChannelIdHash(); 531 this.group_id_hash = p.getGroupIdHash(); 532 this.group_instance_id = (groupId == null) ? 0 : groupId.getId(); 533 this.is_group_summary = notification.isGroupSummary(); 534 this.category = notification.category; 535 this.style = p.getStyle(); 536 this.num_people = p.getNumPeople(); 537 this.position = position; 538 this.importance = NotificationRecordLogger.getLoggingImportance(p.r); 539 this.alerting = buzzBeepBlink; 540 this.importance_source = p.r.getImportanceExplanationCode(); 541 this.importance_initial = p.r.getInitialImportance(); 542 this.importance_initial_source = p.r.getInitialImportanceExplanationCode(); 543 this.importance_asst = p.r.getAssistantImportance(); 544 this.assistant_hash = p.getAssistantHash(); 545 this.assistant_ranking_score = p.r.getRankingScore(); 546 this.is_ongoing = p.r.getSbn().isOngoing(); 547 this.is_foreground_service = NotificationRecordLogger.isForegroundService(p.r); 548 this.timeout_millis = notification.getTimeoutAfter(); 549 this.is_non_dismissible = NotificationRecordLogger.isNonDismissible(p.r); 550 final boolean hasFullScreenIntent = notification.fullScreenIntent != null; 551 final boolean hasFsiRequestedButDeniedFlag = 552 (notification.flags & Notification.FLAG_FSI_REQUESTED_BUT_DENIED) != 0; 553 this.fsi_state = NotificationRecordLogger.getFsiState( 554 hasFullScreenIntent, hasFsiRequestedButDeniedFlag, eventType); 555 this.is_locked = p.r.isLocked(); 556 this.age_in_minutes = NotificationRecordLogger.getAgeInMinutes( 557 p.r.getSbn().getPostTime(), notification.getWhen()); 558 this.is_promoted_ongoing = notification.isPromotedOngoing(); 559 this.has_promotable_characteristics = notification.hasPromotableCharacteristics(); 560 } 561 } 562 563 /** 564 * @param r NotificationRecord 565 * @return Logging importance of record, taking important conversation channels into account. 566 */ getLoggingImportance(@onNull NotificationRecord r)567 static int getLoggingImportance(@NonNull NotificationRecord r) { 568 final int importance = r.getImportance(); 569 final NotificationChannel channel = r.getChannel(); 570 if (channel == null) { 571 return importance; 572 } 573 return NotificationChannelLogger.getLoggingImportance(channel, importance); 574 } 575 576 /** 577 * @param r NotificationRecord 578 * @return Whether the notification is a foreground service notification. 579 */ isForegroundService(@onNull NotificationRecord r)580 static boolean isForegroundService(@NonNull NotificationRecord r) { 581 if (r.getSbn() == null || r.getSbn().getNotification() == null) { 582 return false; 583 } 584 return (r.getSbn().getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 585 } 586 587 /** 588 * @return Whether the notification is a non-dismissible notification. 589 */ isNonDismissible(@onNull NotificationRecord r)590 static boolean isNonDismissible(@NonNull NotificationRecord r) { 591 if (r.getSbn() == null || r.getSbn().getNotification() == null) { 592 return false; 593 } 594 return (r.getNotification().flags & Notification.FLAG_NO_DISMISS) != 0; 595 } 596 597 /** 598 * @return FrameworkStatsLog enum of the state of the full screen intent posted with this 599 * notification. 600 */ getFsiState(boolean hasFullScreenIntent, boolean hasFsiRequestedButDeniedFlag, NotificationReportedEvent eventType)601 static int getFsiState(boolean hasFullScreenIntent, 602 boolean hasFsiRequestedButDeniedFlag, 603 NotificationReportedEvent eventType) { 604 if (eventType == NotificationReportedEvent.NOTIFICATION_UPDATED) { 605 // Zeroes in protos take zero bandwidth, but non-zero numbers take bandwidth, 606 // so we should log 0 when possible. 607 return 0; 608 } 609 if (hasFullScreenIntent) { 610 return FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__FSI_ALLOWED; 611 } 612 if (hasFsiRequestedButDeniedFlag) { 613 return FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__FSI_DENIED; 614 } 615 return FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__NO_FSI; 616 } 617 618 /** 619 * @param postTimeMs time (in {@link System#currentTimeMillis} time) the notification was posted 620 * @param whenMs A timestamp related to this notification, in milliseconds since the epoch. 621 * @return difference in duration as an integer in minutes 622 */ getAgeInMinutes(long postTimeMs, long whenMs)623 static int getAgeInMinutes(long postTimeMs, long whenMs) { 624 return (int) Duration.ofMillis(postTimeMs - whenMs).toMinutes(); 625 } 626 } 627