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.NonNull; 25 import android.annotation.Nullable; 26 import android.app.Notification; 27 import android.app.NotificationChannel; 28 import android.app.Person; 29 import android.os.Bundle; 30 import android.service.notification.NotificationListenerService; 31 import android.service.notification.NotificationStats; 32 33 import com.android.internal.logging.InstanceId; 34 import com.android.internal.logging.UiEvent; 35 import com.android.internal.logging.UiEventLogger; 36 37 import java.util.ArrayList; 38 import java.util.Objects; 39 40 /** 41 * Interface for writing NotificationReported atoms to statsd log. Use NotificationRecordLoggerImpl 42 * in production. Use NotificationRecordLoggerFake for testing. 43 * @hide 44 */ 45 public interface NotificationRecordLogger { 46 47 // The high-level interface used by clients. 48 49 /** 50 * May log a NotificationReported atom reflecting the posting or update of a notification. 51 * @param r The new NotificationRecord. If null, no action is taken. 52 * @param old The previous NotificationRecord. Null if there was no previous record. 53 * @param position The position at which this notification is ranked. 54 * @param buzzBeepBlink Logging code reflecting whether this notification alerted the user. 55 * @param groupId The instance Id of the group summary notification, or null. 56 */ maybeLogNotificationPosted(@ullable NotificationRecord r, @Nullable NotificationRecord old, int position, int buzzBeepBlink, InstanceId groupId)57 void maybeLogNotificationPosted(@Nullable NotificationRecord r, 58 @Nullable NotificationRecord old, 59 int position, int buzzBeepBlink, 60 InstanceId groupId); 61 62 /** 63 * Logs a NotificationReported atom reflecting an adjustment to a notification. 64 * Unlike maybeLogNotificationPosted, this method is guaranteed to log a notification update, 65 * so the caller must take responsibility for checking that that logging update is necessary, 66 * and that the notification is meaningfully changed. 67 * @param r The NotificationRecord. If null, no action is taken. 68 * @param position The position at which this notification is ranked. 69 * @param buzzBeepBlink Logging code reflecting whether this notification alerted the user. 70 * @param groupId The instance Id of the group summary notification, or null. 71 */ logNotificationAdjusted(@ullable NotificationRecord r, int position, int buzzBeepBlink, InstanceId groupId)72 void logNotificationAdjusted(@Nullable NotificationRecord r, 73 int position, int buzzBeepBlink, 74 InstanceId groupId); 75 76 /** 77 * Logs a notification cancel / dismiss event using UiEventReported (event ids from the 78 * NotificationCancelledEvents enum). 79 * @param r The NotificationRecord. If null, no action is taken. 80 * @param reason The reason the notification was canceled. 81 * @param dismissalSurface The surface the notification was dismissed from. 82 */ logNotificationCancelled(@ullable NotificationRecord r, @NotificationListenerService.NotificationCancelReason int reason, @NotificationStats.DismissalSurface int dismissalSurface)83 default void logNotificationCancelled(@Nullable NotificationRecord r, 84 @NotificationListenerService.NotificationCancelReason int reason, 85 @NotificationStats.DismissalSurface int dismissalSurface) { 86 log(NotificationCancelledEvent.fromCancelReason(reason, dismissalSurface), r); 87 } 88 89 /** 90 * Logs a notification visibility change event using UiEventReported (event ids from the 91 * NotificationEvents enum). 92 * @param r The NotificationRecord. If null, no action is taken. 93 * @param visible True if the notification became visible. 94 */ logNotificationVisibility(@ullable NotificationRecord r, boolean visible)95 default void logNotificationVisibility(@Nullable NotificationRecord r, boolean visible) { 96 log(NotificationEvent.fromVisibility(visible), r); 97 } 98 99 // The UiEventReported logging methods are implemented in terms of this lower-level interface. 100 101 /** Logs a UiEventReported event for the given notification. */ log(UiEventLogger.UiEventEnum event, NotificationRecord r)102 void log(UiEventLogger.UiEventEnum event, NotificationRecord r); 103 104 /** Logs a UiEventReported event that is not associated with any notification. */ log(UiEventLogger.UiEventEnum event)105 void log(UiEventLogger.UiEventEnum event); 106 107 /** 108 * The UiEvent enums that this class can log. 109 */ 110 enum NotificationReportedEvent implements UiEventLogger.UiEventEnum { 111 @UiEvent(doc = "New notification enqueued to post") 112 NOTIFICATION_POSTED(162), 113 @UiEvent(doc = "Notification substantially updated, or alerted again.") 114 NOTIFICATION_UPDATED(163), 115 @UiEvent(doc = "Notification adjusted by assistant.") 116 NOTIFICATION_ADJUSTED(908); 117 118 private final int mId; NotificationReportedEvent(int id)119 NotificationReportedEvent(int id) { 120 mId = id; 121 } getId()122 @Override public int getId() { 123 return mId; 124 } 125 fromRecordPair(NotificationRecordPair p)126 public static NotificationReportedEvent fromRecordPair(NotificationRecordPair p) { 127 return (p.old != null) ? NotificationReportedEvent.NOTIFICATION_UPDATED : 128 NotificationReportedEvent.NOTIFICATION_POSTED; 129 } 130 } 131 132 enum NotificationCancelledEvent implements UiEventLogger.UiEventEnum { 133 INVALID(0), 134 @UiEvent(doc = "Notification was canceled due to a notification click.") 135 NOTIFICATION_CANCEL_CLICK(164), 136 @UiEvent(doc = "Notification was canceled due to a user dismissal, surface not specified.") 137 NOTIFICATION_CANCEL_USER_OTHER(165), 138 @UiEvent(doc = "Notification was canceled due to a user dismiss-all (from the notification" 139 + " shade).") 140 NOTIFICATION_CANCEL_USER_CANCEL_ALL(166), 141 @UiEvent(doc = "Notification was canceled due to an inflation error.") 142 NOTIFICATION_CANCEL_ERROR(167), 143 @UiEvent(doc = "Notification was canceled by the package manager modifying the package.") 144 NOTIFICATION_CANCEL_PACKAGE_CHANGED(168), 145 @UiEvent(doc = "Notification was canceled by the owning user context being stopped.") 146 NOTIFICATION_CANCEL_USER_STOPPED(169), 147 @UiEvent(doc = "Notification was canceled by the user banning the package.") 148 NOTIFICATION_CANCEL_PACKAGE_BANNED(170), 149 @UiEvent(doc = "Notification was canceled by the app canceling this specific notification.") 150 NOTIFICATION_CANCEL_APP_CANCEL(171), 151 @UiEvent(doc = "Notification was canceled by the app cancelling all its notifications.") 152 NOTIFICATION_CANCEL_APP_CANCEL_ALL(172), 153 @UiEvent(doc = "Notification was canceled by a listener reporting a user dismissal.") 154 NOTIFICATION_CANCEL_LISTENER_CANCEL(173), 155 @UiEvent(doc = "Notification was canceled by a listener reporting a user dismiss all.") 156 NOTIFICATION_CANCEL_LISTENER_CANCEL_ALL(174), 157 @UiEvent(doc = "Notification was canceled because it was a member of a canceled group.") 158 NOTIFICATION_CANCEL_GROUP_SUMMARY_CANCELED(175), 159 @UiEvent(doc = "Notification was canceled because it was an invisible member of a group.") 160 NOTIFICATION_CANCEL_GROUP_OPTIMIZATION(176), 161 @UiEvent(doc = "Notification was canceled by the device administrator suspending the " 162 + "package.") 163 NOTIFICATION_CANCEL_PACKAGE_SUSPENDED(177), 164 @UiEvent(doc = "Notification was canceled by the owning managed profile being turned off.") 165 NOTIFICATION_CANCEL_PROFILE_TURNED_OFF(178), 166 @UiEvent(doc = "Autobundled summary notification was canceled because its group was " 167 + "unbundled") 168 NOTIFICATION_CANCEL_UNAUTOBUNDLED(179), 169 @UiEvent(doc = "Notification was canceled by the user banning the channel.") 170 NOTIFICATION_CANCEL_CHANNEL_BANNED(180), 171 @UiEvent(doc = "Notification was snoozed.") 172 NOTIFICATION_CANCEL_SNOOZED(181), 173 @UiEvent(doc = "Notification was canceled due to timeout") 174 NOTIFICATION_CANCEL_TIMEOUT(182), 175 @UiEvent(doc = "Notification was canceled due to the backing channel being deleted") 176 NOTIFICATION_CANCEL_CHANNEL_REMOVED(1261), 177 @UiEvent(doc = "Notification was canceled due to the app's storage being cleared") 178 NOTIFICATION_CANCEL_CLEAR_DATA(1262), 179 // Values above this line must remain in the same order as the corresponding 180 // NotificationCancelReason enum values. 181 @UiEvent(doc = "Notification was canceled due to user dismissal of a peeking notification.") 182 NOTIFICATION_CANCEL_USER_PEEK(190), 183 @UiEvent(doc = "Notification was canceled due to user dismissal from the always-on display") 184 NOTIFICATION_CANCEL_USER_AOD(191), 185 @UiEvent(doc = "Notification was canceled due to user dismissal from a bubble") 186 NOTIFICATION_CANCEL_USER_BUBBLE(1228), 187 @UiEvent(doc = "Notification was canceled due to user dismissal from the lockscreen") 188 NOTIFICATION_CANCEL_USER_LOCKSCREEN(193), 189 @UiEvent(doc = "Notification was canceled due to user dismissal from the notification" 190 + " shade.") 191 NOTIFICATION_CANCEL_USER_SHADE(192), 192 @UiEvent(doc = "Notification was canceled due to an assistant adjustment update.") 193 NOTIFICATION_CANCEL_ASSISTANT(906); 194 195 private final int mId; NotificationCancelledEvent(int id)196 NotificationCancelledEvent(int id) { 197 mId = id; 198 } getId()199 @Override public int getId() { 200 return mId; 201 } 202 fromCancelReason( @otificationListenerService.NotificationCancelReason int reason, @NotificationStats.DismissalSurface int surface)203 public static NotificationCancelledEvent fromCancelReason( 204 @NotificationListenerService.NotificationCancelReason int reason, 205 @NotificationStats.DismissalSurface int surface) { 206 // Shouldn't be possible to get a non-dismissed notification here. 207 if (surface == NotificationStats.DISMISSAL_NOT_DISMISSED) { 208 if (NotificationManagerService.DBG) { 209 throw new IllegalArgumentException("Unexpected surface " + surface); 210 } 211 return INVALID; 212 } 213 // Most cancel reasons do not have a meaningful surface. Reason codes map directly 214 // to NotificationCancelledEvent codes. 215 if (surface == NotificationStats.DISMISSAL_OTHER) { 216 if ((REASON_CLICK <= reason) && (reason <= REASON_CLEAR_DATA)) { 217 return NotificationCancelledEvent.values()[reason]; 218 } 219 if (reason == REASON_ASSISTANT_CANCEL) { 220 return NotificationCancelledEvent.NOTIFICATION_CANCEL_ASSISTANT; 221 } 222 if (NotificationManagerService.DBG) { 223 throw new IllegalArgumentException("Unexpected cancel reason " + reason); 224 } 225 return INVALID; 226 } 227 // User cancels have a meaningful surface, which we differentiate by. See b/149038335 228 // for caveats. 229 if (reason != REASON_CANCEL) { 230 if (NotificationManagerService.DBG) { 231 throw new IllegalArgumentException("Unexpected cancel with surface " + reason); 232 } 233 return INVALID; 234 } 235 switch (surface) { 236 case NotificationStats.DISMISSAL_PEEK: 237 return NOTIFICATION_CANCEL_USER_PEEK; 238 case NotificationStats.DISMISSAL_AOD: 239 return NOTIFICATION_CANCEL_USER_AOD; 240 case NotificationStats.DISMISSAL_SHADE: 241 return NOTIFICATION_CANCEL_USER_SHADE; 242 case NotificationStats.DISMISSAL_BUBBLE: 243 return NOTIFICATION_CANCEL_USER_BUBBLE; 244 case NotificationStats.DISMISSAL_LOCKSCREEN: 245 return NOTIFICATION_CANCEL_USER_LOCKSCREEN; 246 default: 247 if (NotificationManagerService.DBG) { 248 throw new IllegalArgumentException("Unexpected surface for user-dismiss " 249 + reason); 250 } 251 return INVALID; 252 } 253 } 254 } 255 256 enum NotificationEvent implements UiEventLogger.UiEventEnum { 257 @UiEvent(doc = "Notification became visible.") 258 NOTIFICATION_OPEN(197), 259 @UiEvent(doc = "Notification stopped being visible.") 260 NOTIFICATION_CLOSE(198), 261 @UiEvent(doc = "Notification was snoozed.") 262 NOTIFICATION_SNOOZED(317), 263 @UiEvent(doc = "Notification was not posted because its app is snoozed.") 264 NOTIFICATION_NOT_POSTED_SNOOZED(319), 265 @UiEvent(doc = "Notification was clicked.") 266 NOTIFICATION_CLICKED(320), 267 @UiEvent(doc = "Notification action was clicked; unexpected position.") 268 NOTIFICATION_ACTION_CLICKED(321), 269 @UiEvent(doc = "Notification detail was expanded due to non-user action.") 270 NOTIFICATION_DETAIL_OPEN_SYSTEM(327), 271 @UiEvent(doc = "Notification detail was collapsed due to non-user action.") 272 NOTIFICATION_DETAIL_CLOSE_SYSTEM(328), 273 @UiEvent(doc = "Notification detail was expanded due to user action.") 274 NOTIFICATION_DETAIL_OPEN_USER(329), 275 @UiEvent(doc = "Notification detail was collapsed due to user action.") 276 NOTIFICATION_DETAIL_CLOSE_USER(330), 277 @UiEvent(doc = "Notification direct reply action was used.") 278 NOTIFICATION_DIRECT_REPLIED(331), 279 @UiEvent(doc = "Notification smart reply action was used.") 280 NOTIFICATION_SMART_REPLIED(332), 281 @UiEvent(doc = "Notification smart reply action was visible.") 282 NOTIFICATION_SMART_REPLY_VISIBLE(333), 283 @UiEvent(doc = "App-generated notification action at position 0 was clicked.") 284 NOTIFICATION_ACTION_CLICKED_0(450), 285 @UiEvent(doc = "App-generated notification action at position 1 was clicked.") 286 NOTIFICATION_ACTION_CLICKED_1(451), 287 @UiEvent(doc = "App-generated notification action at position 2 was clicked.") 288 NOTIFICATION_ACTION_CLICKED_2(452), 289 @UiEvent(doc = "Contextual notification action at position 0 was clicked.") 290 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_0(453), 291 @UiEvent(doc = "Contextual notification action at position 1 was clicked.") 292 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_1(454), 293 @UiEvent(doc = "Contextual notification action at position 2 was clicked.") 294 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_2(455), 295 @UiEvent(doc = "Notification assistant generated notification action at 0 was clicked.") 296 NOTIFICATION_ASSIST_ACTION_CLICKED_0(456), 297 @UiEvent(doc = "Notification assistant generated notification action at 1 was clicked.") 298 NOTIFICATION_ASSIST_ACTION_CLICKED_1(457), 299 @UiEvent(doc = "Notification assistant generated notification action at 2 was clicked.") 300 NOTIFICATION_ASSIST_ACTION_CLICKED_2(458); 301 302 private final int mId; NotificationEvent(int id)303 NotificationEvent(int id) { 304 mId = id; 305 } getId()306 @Override public int getId() { 307 return mId; 308 } 309 fromVisibility(boolean visible)310 public static NotificationEvent fromVisibility(boolean visible) { 311 return visible ? NOTIFICATION_OPEN : NOTIFICATION_CLOSE; 312 } fromExpanded(boolean expanded, boolean userAction)313 public static NotificationEvent fromExpanded(boolean expanded, boolean userAction) { 314 if (userAction) { 315 return expanded ? NOTIFICATION_DETAIL_OPEN_USER : NOTIFICATION_DETAIL_CLOSE_USER; 316 } 317 return expanded ? NOTIFICATION_DETAIL_OPEN_SYSTEM : NOTIFICATION_DETAIL_CLOSE_SYSTEM; 318 } fromAction(int index, boolean isAssistant, boolean isContextual)319 public static NotificationEvent fromAction(int index, boolean isAssistant, 320 boolean isContextual) { 321 if (index < 0 || index > 2) { 322 return NOTIFICATION_ACTION_CLICKED; 323 } 324 if (isAssistant) { // Assistant actions are contextual by definition 325 return NotificationEvent.values()[ 326 NOTIFICATION_ASSIST_ACTION_CLICKED_0.ordinal() + index]; 327 } 328 if (isContextual) { 329 return NotificationEvent.values()[ 330 NOTIFICATION_CONTEXTUAL_ACTION_CLICKED_0.ordinal() + index]; 331 } 332 return NotificationEvent.values()[NOTIFICATION_ACTION_CLICKED_0.ordinal() + index]; 333 } 334 } 335 336 enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { 337 @UiEvent(doc = "Notification panel became visible.") 338 NOTIFICATION_PANEL_OPEN(325), 339 @UiEvent(doc = "Notification panel stopped being visible.") 340 NOTIFICATION_PANEL_CLOSE(326); 341 342 private final int mId; NotificationPanelEvent(int id)343 NotificationPanelEvent(int id) { 344 mId = id; 345 } getId()346 @Override public int getId() { 347 return mId; 348 } 349 } 350 351 /** 352 * A helper for extracting logging information from one or two NotificationRecords. 353 */ 354 class NotificationRecordPair { 355 public final NotificationRecord r, old; 356 /** 357 * Construct from one or two NotificationRecords. 358 * @param r The new NotificationRecord. If null, only shouldLog() method is usable. 359 * @param old The previous NotificationRecord. Null if there was no previous record. 360 */ NotificationRecordPair(@ullable NotificationRecord r, @Nullable NotificationRecord old)361 NotificationRecordPair(@Nullable NotificationRecord r, @Nullable NotificationRecord old) { 362 this.r = r; 363 this.old = old; 364 } 365 366 /** 367 * @return True if old is null, alerted, or important logged fields have changed. 368 */ shouldLogReported(int buzzBeepBlink)369 boolean shouldLogReported(int buzzBeepBlink) { 370 if (r == null) { 371 return false; 372 } 373 if ((old == null) || (buzzBeepBlink > 0)) { 374 return true; 375 } 376 377 return !(Objects.equals(r.getSbn().getChannelIdLogTag(), 378 old.getSbn().getChannelIdLogTag()) 379 && Objects.equals(r.getSbn().getGroupLogTag(), old.getSbn().getGroupLogTag()) 380 && (r.getSbn().getNotification().isGroupSummary() 381 == old.getSbn().getNotification().isGroupSummary()) 382 && Objects.equals(r.getSbn().getNotification().category, 383 old.getSbn().getNotification().category) 384 && (r.getImportance() == old.getImportance()) 385 && (getLoggingImportance(r) == getLoggingImportance(old)) 386 && r.rankingScoreMatches(old.getRankingScore())); 387 } 388 389 /** 390 * @return hash code for the notification style class, or 0 if none exists. 391 */ getStyle()392 public int getStyle() { 393 return getStyle(r.getSbn().getNotification().extras); 394 } 395 getStyle(@ullable Bundle extras)396 private int getStyle(@Nullable Bundle extras) { 397 if (extras != null) { 398 String template = extras.getString(Notification.EXTRA_TEMPLATE); 399 if (template != null && !template.isEmpty()) { 400 return template.hashCode(); 401 } 402 } 403 return 0; 404 } 405 getNumPeople()406 int getNumPeople() { 407 return getNumPeople(r.getSbn().getNotification().extras); 408 } 409 getNumPeople(@ullable Bundle extras)410 private int getNumPeople(@Nullable Bundle extras) { 411 if (extras != null) { 412 ArrayList<Person> people = extras.getParcelableArrayList( 413 Notification.EXTRA_PEOPLE_LIST); 414 if (people != null && !people.isEmpty()) { 415 return people.size(); 416 } 417 } 418 return 0; 419 } 420 getAssistantHash()421 int getAssistantHash() { 422 String assistant = r.getAdjustmentIssuer(); 423 return (assistant == null) ? 0 : assistant.hashCode(); 424 } 425 getInstanceId()426 int getInstanceId() { 427 return (r.getSbn().getInstanceId() == null ? 0 : r.getSbn().getInstanceId().getId()); 428 } 429 430 /** 431 * @return Small hash of the notification ID, and tag (if present). 432 */ getNotificationIdHash()433 int getNotificationIdHash() { 434 return SmallHash.hash(Objects.hashCode(r.getSbn().getTag()) ^ r.getSbn().getId()); 435 } 436 437 /** 438 * @return Small hash of the channel ID, if present, or 0 otherwise. 439 */ getChannelIdHash()440 int getChannelIdHash() { 441 return SmallHash.hash(r.getSbn().getNotification().getChannelId()); 442 } 443 444 /** 445 * @return Small hash of the group ID, respecting group override if present. 0 otherwise. 446 */ getGroupIdHash()447 int getGroupIdHash() { 448 return SmallHash.hash(r.getSbn().getGroup()); 449 } 450 451 } 452 453 /** 454 * @param r NotificationRecord 455 * @return Logging importance of record, taking important conversation channels into account. 456 */ getLoggingImportance(@onNull NotificationRecord r)457 static int getLoggingImportance(@NonNull NotificationRecord r) { 458 final int importance = r.getImportance(); 459 final NotificationChannel channel = r.getChannel(); 460 if (channel == null) { 461 return importance; 462 } 463 return NotificationChannelLogger.getLoggingImportance(channel, importance); 464 } 465 466 /** 467 * @param r NotificationRecord 468 * @return Whether the notification is a foreground service notification. 469 */ isForegroundService(@onNull NotificationRecord r)470 static boolean isForegroundService(@NonNull NotificationRecord r) { 471 if (r.getSbn() == null || r.getSbn().getNotification() == null) { 472 return false; 473 } 474 return (r.getSbn().getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 475 } 476 } 477