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