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