• 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_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