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