• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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