• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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 android.app.Notification;
20 import android.content.Context;
21 import android.os.Handler;
22 import android.os.Message;
23 import android.os.SystemClock;
24 import android.service.notification.RateEstimator;
25 import android.text.TextUtils;
26 import android.util.ArraySet;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.logging.MetricsLogger;
31 import com.android.server.notification.NotificationManagerService.DumpFilter;
32 
33 import org.json.JSONArray;
34 import org.json.JSONException;
35 import org.json.JSONObject;
36 
37 import java.io.PrintWriter;
38 import java.util.ArrayDeque;
39 import java.util.HashMap;
40 import java.util.Map;
41 import java.util.Set;
42 
43 /**
44  * Keeps track of notification activity, display, and user interaction.
45  *
46  * <p>This class receives signals from NoMan and keeps running stats of
47  * notification usage. Some metrics are updated as events occur. Others, namely
48  * those involving durations, are updated as the notification is canceled.</p>
49  *
50  * <p>This class is thread-safe.</p>
51  *
52  * {@hide}
53  */
54 public class NotificationUsageStats {
55     private static final String TAG = "NotificationUsageStats";
56 
57     private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
58     private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
59     private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
60     private static final int MSG_EMIT = 1;
61 
62     private static final boolean DEBUG = false;
63     public static final int TEN_SECONDS = 1000 * 10;
64     public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
65     private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
66 
67     @GuardedBy("this")
68     private final Map<String, AggregatedStats> mStats = new HashMap<>();
69     @GuardedBy("this")
70     private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
71     @GuardedBy("this")
72     private ArraySet<String> mStatExpiredkeys = new ArraySet<>();
73     private final Context mContext;
74     private final Handler mHandler;
75     @GuardedBy("this")
76     private long mLastEmitTime;
77 
NotificationUsageStats(Context context)78     public NotificationUsageStats(Context context) {
79         mContext = context;
80         mLastEmitTime = SystemClock.elapsedRealtime();
81         mHandler = new Handler(mContext.getMainLooper()) {
82             @Override
83             public void handleMessage(Message msg) {
84                 switch (msg.what) {
85                     case MSG_EMIT:
86                         emit();
87                         break;
88                     default:
89                         Log.wtf(TAG, "Unknown message type: " + msg.what);
90                         break;
91                 }
92             }
93         };
94         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
95     }
96 
97     /**
98      * Called when a notification has been posted.
99      */
getAppEnqueueRate(String packageName)100     public synchronized float getAppEnqueueRate(String packageName) {
101         AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
102         return stats.getEnqueueRate(SystemClock.elapsedRealtime());
103     }
104 
105     /**
106      * Called when a notification wants to alert.
107      */
isAlertRateLimited(String packageName)108     public synchronized boolean isAlertRateLimited(String packageName) {
109         AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
110         return stats.isAlertRateLimited();
111     }
112 
113     /**
114      * Called when a notification is tentatively enqueued by an app, before rate checking.
115      */
registerEnqueuedByApp(String packageName)116     public synchronized void registerEnqueuedByApp(String packageName) {
117         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
118         for (AggregatedStats stats : aggregatedStatsArray) {
119             stats.numEnqueuedByApp++;
120         }
121         releaseAggregatedStatsLocked(aggregatedStatsArray);
122     }
123 
124     /**
125      * Called when a notification that was enqueued by an app is effectively enqueued to be
126      * posted. This is after rate checking, to update the rate.
127      *
128      * <p>Note that if we updated the arrival estimate <em>before</em> checking it, then an app
129      * enqueueing at slightly above the acceptable rate would never get their notifications
130      * accepted; updating afterwards allows the rate to dip below the threshold and thus lets
131      * through some of them.
132      */
registerEnqueuedByAppAndAccepted(String packageName)133     public synchronized void registerEnqueuedByAppAndAccepted(String packageName) {
134         final long now = SystemClock.elapsedRealtime();
135         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
136         for (AggregatedStats stats : aggregatedStatsArray) {
137             stats.updateInterarrivalEstimate(now);
138         }
139         releaseAggregatedStatsLocked(aggregatedStatsArray);
140     }
141 
142     /**
143      * Called when a notification has been posted.
144      */
registerPostedByApp(NotificationRecord notification)145     public synchronized void registerPostedByApp(NotificationRecord notification) {
146         notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
147 
148         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
149         for (AggregatedStats stats : aggregatedStatsArray) {
150             stats.numPostedByApp++;
151             stats.countApiUse(notification);
152             stats.numUndecoratedRemoteViews += (notification.hasUndecoratedRemoteView() ? 1 : 0);
153         }
154         releaseAggregatedStatsLocked(aggregatedStatsArray);
155     }
156 
157     /**
158      * Called when a notification has been updated.
159      */
registerUpdatedByApp(NotificationRecord notification, NotificationRecord old)160     public synchronized void registerUpdatedByApp(NotificationRecord notification,
161             NotificationRecord old) {
162         notification.stats.updateFrom(old.stats);
163         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
164         for (AggregatedStats stats : aggregatedStatsArray) {
165             stats.numUpdatedByApp++;
166             stats.countApiUse(notification);
167         }
168         releaseAggregatedStatsLocked(aggregatedStatsArray);
169     }
170 
171     /**
172      * Called when the originating app removed the notification programmatically.
173      */
registerRemovedByApp(NotificationRecord notification)174     public synchronized void registerRemovedByApp(NotificationRecord notification) {
175         notification.stats.onRemoved();
176         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
177         for (AggregatedStats stats : aggregatedStatsArray) {
178             stats.numRemovedByApp++;
179         }
180         releaseAggregatedStatsLocked(aggregatedStatsArray);
181     }
182 
183     /**
184      * Called when the user dismissed the notification via the UI.
185      */
registerDismissedByUser(NotificationRecord notification)186     public synchronized void registerDismissedByUser(NotificationRecord notification) {
187         MetricsLogger.histogram(mContext, "note_dismiss_longevity",
188                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
189         notification.stats.onDismiss();
190     }
191 
192     /**
193      * Called when the user clicked the notification in the UI.
194      */
registerClickedByUser(NotificationRecord notification)195     public synchronized void registerClickedByUser(NotificationRecord notification) {
196         MetricsLogger.histogram(mContext, "note_click_longevity",
197                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
198         notification.stats.onClick();
199     }
200 
registerPeopleAffinity(NotificationRecord notification, boolean valid, boolean starred, boolean cached)201     public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
202             boolean starred, boolean cached) {
203         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
204         for (AggregatedStats stats : aggregatedStatsArray) {
205             if (valid) {
206                 stats.numWithValidPeople++;
207             }
208             if (starred) {
209                 stats.numWithStaredPeople++;
210             }
211             if (cached) {
212                 stats.numPeopleCacheHit++;
213             } else {
214                 stats.numPeopleCacheMiss++;
215             }
216         }
217         releaseAggregatedStatsLocked(aggregatedStatsArray);
218     }
219 
registerBlocked(NotificationRecord notification)220     public synchronized void registerBlocked(NotificationRecord notification) {
221         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
222         for (AggregatedStats stats : aggregatedStatsArray) {
223             stats.numBlocked++;
224         }
225         releaseAggregatedStatsLocked(aggregatedStatsArray);
226     }
227 
registerSuspendedByAdmin(NotificationRecord notification)228     public synchronized void registerSuspendedByAdmin(NotificationRecord notification) {
229         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
230         for (AggregatedStats stats : aggregatedStatsArray) {
231             stats.numSuspendedByAdmin++;
232         }
233         releaseAggregatedStatsLocked(aggregatedStatsArray);
234     }
235 
registerOverRateQuota(String packageName)236     public synchronized void registerOverRateQuota(String packageName) {
237         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
238         for (AggregatedStats stats : aggregatedStatsArray) {
239             stats.numRateViolations++;
240         }
241     }
242 
registerOverCountQuota(String packageName)243     public synchronized void registerOverCountQuota(String packageName) {
244         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
245         for (AggregatedStats stats : aggregatedStatsArray) {
246             stats.numQuotaViolations++;
247         }
248     }
249 
250     /**
251      * Call this when RemoteViews object has been removed from a notification because the images
252      * it contains are too big (even after rescaling).
253      */
registerImageRemoved(String packageName)254     public synchronized void registerImageRemoved(String packageName) {
255         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
256         for (AggregatedStats stats : aggregatedStatsArray) {
257             stats.numImagesRemoved++;
258         }
259     }
260 
registerTooOldBlocked(NotificationRecord notification)261     public synchronized void registerTooOldBlocked(NotificationRecord notification) {
262         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
263         for (AggregatedStats stats : aggregatedStatsArray) {
264             stats.numTooOld++;
265         }
266         releaseAggregatedStatsLocked(aggregatedStatsArray);
267     }
268 
269     @GuardedBy("this")
getAggregatedStatsLocked(NotificationRecord record)270     private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
271         return getAggregatedStatsLocked(record.getSbn().getPackageName());
272     }
273 
274     @GuardedBy("this")
getAggregatedStatsLocked(String packageName)275     private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
276         if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
277             return EMPTY_AGGREGATED_STATS;
278         }
279 
280         AggregatedStats[] array = mStatsArrays.poll();
281         if (array == null) {
282             array = new AggregatedStats[2];
283         }
284         array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
285         array[1] = getOrCreateAggregatedStatsLocked(packageName);
286         return array;
287     }
288 
289     @GuardedBy("this")
releaseAggregatedStatsLocked(AggregatedStats[] array)290     private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
291         for(int i = 0; i < array.length; i++) {
292             array[i] = null;
293         }
294         mStatsArrays.offer(array);
295     }
296 
297     @GuardedBy("this")
getOrCreateAggregatedStatsLocked(String key)298     private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
299         AggregatedStats result = mStats.get(key);
300         if (result == null) {
301             result = new AggregatedStats(mContext, key);
302             mStats.put(key, result);
303         }
304         result.mLastAccessTime = SystemClock.elapsedRealtime();
305         return result;
306     }
307 
dumpJson(DumpFilter filter)308     public synchronized JSONObject dumpJson(DumpFilter filter) {
309         JSONObject dump = new JSONObject();
310         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
311             try {
312                 JSONArray aggregatedStats = new JSONArray();
313                 for (AggregatedStats as : mStats.values()) {
314                     if (filter != null && !filter.matches(as.key))
315                         continue;
316                     aggregatedStats.put(as.dumpJson());
317                 }
318                 dump.put("current", aggregatedStats);
319             } catch (JSONException e) {
320                 // pass
321             }
322         }
323         return dump;
324     }
325 
remoteViewStats(long startMs, boolean aggregate)326     public PulledStats remoteViewStats(long startMs, boolean aggregate) {
327         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
328             PulledStats stats = new PulledStats(startMs);
329             for (AggregatedStats as : mStats.values()) {
330                 if (as.numUndecoratedRemoteViews > 0) {
331                     stats.addUndecoratedPackage(as.key, as.mCreated);
332                 }
333             }
334             return stats;
335         }
336         return null;
337     }
338 
dump(PrintWriter pw, String indent, DumpFilter filter)339     public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
340         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
341             for (AggregatedStats as : mStats.values()) {
342                 if (filter != null && !filter.matches(as.key))
343                     continue;
344                 as.dump(pw, indent);
345             }
346             pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
347             pw.println(indent + "mStats.size(): " + mStats.size());
348         }
349     }
350 
emit()351     public synchronized void emit() {
352         AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
353         stats.emit();
354         mHandler.removeMessages(MSG_EMIT);
355         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
356         for(String key: mStats.keySet()) {
357             if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
358                 mStatExpiredkeys.add(key);
359             }
360         }
361         for(String key: mStatExpiredkeys) {
362             mStats.remove(key);
363         }
364         mStatExpiredkeys.clear();
365         mLastEmitTime = SystemClock.elapsedRealtime();
366     }
367 
368     /**
369      * Aggregated notification stats.
370      */
371     private static class AggregatedStats {
372 
373         private final Context mContext;
374         public final String key;
375         private final long mCreated;
376         private AggregatedStats mPrevious;
377 
378         // ---- Updated as the respective events occur.
379         public int numEnqueuedByApp;
380         public int numPostedByApp;
381         public int numUpdatedByApp;
382         public int numRemovedByApp;
383         public int numPeopleCacheHit;
384         public int numPeopleCacheMiss;;
385         public int numWithStaredPeople;
386         public int numWithValidPeople;
387         public int numBlocked;
388         public int numSuspendedByAdmin;
389         public int numWithActions;
390         public int numPrivate;
391         public int numSecret;
392         public int numWithBigText;
393         public int numWithBigPicture;
394         public int numForegroundService;
395         public int numUserInitiatedJob;
396         public int numOngoing;
397         public int numAutoCancel;
398         public int numWithLargeIcon;
399         public int numWithInbox;
400         public int numWithMediaSession;
401         public int numWithTitle;
402         public int numWithText;
403         public int numWithSubText;
404         public int numWithInfoText;
405         public int numInterrupt;
406         public ImportanceHistogram noisyImportance;
407         public ImportanceHistogram quietImportance;
408         public ImportanceHistogram finalImportance;
409         public RateEstimator enqueueRate;
410         public AlertRateLimiter alertRate;
411         public int numRateViolations;
412         public int numAlertViolations;
413         public int numQuotaViolations;
414         public int numUndecoratedRemoteViews;
415         public long mLastAccessTime;
416         public int numImagesRemoved;
417         public int numTooOld;
418 
AggregatedStats(Context context, String key)419         public AggregatedStats(Context context, String key) {
420             this.key = key;
421             mContext = context;
422             mCreated = SystemClock.elapsedRealtime();
423             noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
424             quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
425             finalImportance = new ImportanceHistogram(context, "note_importance_");
426             enqueueRate = new RateEstimator();
427             alertRate = new AlertRateLimiter();
428         }
429 
getPrevious()430         public AggregatedStats getPrevious() {
431             if (mPrevious == null) {
432                 mPrevious = new AggregatedStats(mContext, key);
433             }
434             return mPrevious;
435         }
436 
countApiUse(NotificationRecord record)437         public void countApiUse(NotificationRecord record) {
438             final Notification n = record.getNotification();
439             if (n.actions != null) {
440                 numWithActions++;
441             }
442 
443             if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
444                 numForegroundService++;
445             }
446 
447             if ((n.flags & Notification.FLAG_USER_INITIATED_JOB) != 0) {
448                 numUserInitiatedJob++;
449             }
450 
451             if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
452                 numOngoing++;
453             }
454 
455             if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
456                 numAutoCancel++;
457             }
458 
459             if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
460                     (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
461                     n.sound != null || n.vibrate != null) {
462                 numInterrupt++;
463             }
464 
465             switch (n.visibility) {
466                 case Notification.VISIBILITY_PRIVATE:
467                     numPrivate++;
468                     break;
469                 case Notification.VISIBILITY_SECRET:
470                     numSecret++;
471                     break;
472             }
473 
474             if (record.stats.isNoisy) {
475                 noisyImportance.increment(record.stats.requestedImportance);
476             } else {
477                 quietImportance.increment(record.stats.requestedImportance);
478             }
479             finalImportance.increment(record.getImportance());
480 
481             final Set<String> names = n.extras.keySet();
482             if (names.contains(Notification.EXTRA_BIG_TEXT)) {
483                 numWithBigText++;
484             }
485             if (names.contains(Notification.EXTRA_PICTURE)) {
486                 numWithBigPicture++;
487             }
488             if (names.contains(Notification.EXTRA_LARGE_ICON)) {
489                 numWithLargeIcon++;
490             }
491             if (names.contains(Notification.EXTRA_TEXT_LINES)) {
492                 numWithInbox++;
493             }
494             if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
495                 numWithMediaSession++;
496             }
497             if (names.contains(Notification.EXTRA_TITLE) &&
498                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
499                 numWithTitle++;
500             }
501             if (names.contains(Notification.EXTRA_TEXT) &&
502                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
503                 numWithText++;
504             }
505             if (names.contains(Notification.EXTRA_SUB_TEXT) &&
506                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
507                 numWithSubText++;
508             }
509             if (names.contains(Notification.EXTRA_INFO_TEXT) &&
510                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
511                 numWithInfoText++;
512             }
513         }
514 
emit()515         public void emit() {
516             AggregatedStats previous = getPrevious();
517             maybeCount("note_enqueued", (numEnqueuedByApp - previous.numEnqueuedByApp));
518             maybeCount("note_post", (numPostedByApp - previous.numPostedByApp));
519             maybeCount("note_update", (numUpdatedByApp - previous.numUpdatedByApp));
520             maybeCount("note_remove", (numRemovedByApp - previous.numRemovedByApp));
521             maybeCount("note_with_people", (numWithValidPeople - previous.numWithValidPeople));
522             maybeCount("note_with_stars", (numWithStaredPeople - previous.numWithStaredPeople));
523             maybeCount("people_cache_hit", (numPeopleCacheHit - previous.numPeopleCacheHit));
524             maybeCount("people_cache_miss", (numPeopleCacheMiss - previous.numPeopleCacheMiss));
525             maybeCount("note_blocked", (numBlocked - previous.numBlocked));
526             maybeCount("note_suspended", (numSuspendedByAdmin - previous.numSuspendedByAdmin));
527             maybeCount("note_with_actions", (numWithActions - previous.numWithActions));
528             maybeCount("note_private", (numPrivate - previous.numPrivate));
529             maybeCount("note_secret", (numSecret - previous.numSecret));
530             maybeCount("note_interupt", (numInterrupt - previous.numInterrupt));
531             maybeCount("note_big_text", (numWithBigText - previous.numWithBigText));
532             maybeCount("note_big_pic", (numWithBigPicture - previous.numWithBigPicture));
533             maybeCount("note_fg", (numForegroundService - previous.numForegroundService));
534             maybeCount("note_uij", (numUserInitiatedJob - previous.numUserInitiatedJob));
535             maybeCount("note_ongoing", (numOngoing - previous.numOngoing));
536             maybeCount("note_auto", (numAutoCancel - previous.numAutoCancel));
537             maybeCount("note_large_icon", (numWithLargeIcon - previous.numWithLargeIcon));
538             maybeCount("note_inbox", (numWithInbox - previous.numWithInbox));
539             maybeCount("note_media", (numWithMediaSession - previous.numWithMediaSession));
540             maybeCount("note_title", (numWithTitle - previous.numWithTitle));
541             maybeCount("note_text", (numWithText - previous.numWithText));
542             maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
543             maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
544             maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
545             maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations));
546             maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
547             maybeCount("note_images_removed", (numImagesRemoved - previous.numImagesRemoved));
548             maybeCount("not_too_old", (numTooOld - previous.numTooOld));
549             noisyImportance.maybeCount(previous.noisyImportance);
550             quietImportance.maybeCount(previous.quietImportance);
551             finalImportance.maybeCount(previous.finalImportance);
552 
553             previous.numEnqueuedByApp = numEnqueuedByApp;
554             previous.numPostedByApp = numPostedByApp;
555             previous.numUpdatedByApp = numUpdatedByApp;
556             previous.numRemovedByApp = numRemovedByApp;
557             previous.numPeopleCacheHit = numPeopleCacheHit;
558             previous.numPeopleCacheMiss = numPeopleCacheMiss;
559             previous.numWithStaredPeople = numWithStaredPeople;
560             previous.numWithValidPeople = numWithValidPeople;
561             previous.numBlocked = numBlocked;
562             previous.numSuspendedByAdmin = numSuspendedByAdmin;
563             previous.numWithActions = numWithActions;
564             previous.numPrivate = numPrivate;
565             previous.numSecret = numSecret;
566             previous.numInterrupt = numInterrupt;
567             previous.numWithBigText = numWithBigText;
568             previous.numWithBigPicture = numWithBigPicture;
569             previous.numForegroundService = numForegroundService;
570             previous.numUserInitiatedJob = numUserInitiatedJob;
571             previous.numOngoing = numOngoing;
572             previous.numAutoCancel = numAutoCancel;
573             previous.numWithLargeIcon = numWithLargeIcon;
574             previous.numWithInbox = numWithInbox;
575             previous.numWithMediaSession = numWithMediaSession;
576             previous.numWithTitle = numWithTitle;
577             previous.numWithText = numWithText;
578             previous.numWithSubText = numWithSubText;
579             previous.numWithInfoText = numWithInfoText;
580             previous.numRateViolations = numRateViolations;
581             previous.numAlertViolations = numAlertViolations;
582             previous.numQuotaViolations = numQuotaViolations;
583             previous.numImagesRemoved = numImagesRemoved;
584             previous.numTooOld = numTooOld;
585             noisyImportance.update(previous.noisyImportance);
586             quietImportance.update(previous.quietImportance);
587             finalImportance.update(previous.finalImportance);
588         }
589 
maybeCount(String name, int value)590         void maybeCount(String name, int value) {
591             if (value > 0) {
592                 MetricsLogger.count(mContext, name, value);
593             }
594         }
595 
dump(PrintWriter pw, String indent)596         public void dump(PrintWriter pw, String indent) {
597             pw.println(toStringWithIndent(indent));
598         }
599 
600         @Override
toString()601         public String toString() {
602             return toStringWithIndent("");
603         }
604 
605         /** @return the enqueue rate if there were a new enqueue event right now. */
getEnqueueRate()606         public float getEnqueueRate() {
607             return getEnqueueRate(SystemClock.elapsedRealtime());
608         }
609 
getEnqueueRate(long now)610         public float getEnqueueRate(long now) {
611             return enqueueRate.getRate(now);
612         }
613 
updateInterarrivalEstimate(long now)614         public void updateInterarrivalEstimate(long now) {
615             enqueueRate.update(now);
616         }
617 
isAlertRateLimited()618         public boolean isAlertRateLimited() {
619             boolean limited = alertRate.shouldRateLimitAlert(SystemClock.elapsedRealtime());
620             if (limited) {
621                 numAlertViolations++;
622             }
623             return limited;
624         }
625 
toStringWithIndent(String indent)626         private String toStringWithIndent(String indent) {
627             StringBuilder output = new StringBuilder();
628             output.append(indent).append("AggregatedStats{\n");
629             String indentPlusTwo = indent + "  ";
630             output.append(indentPlusTwo);
631             output.append("key='").append(key).append("',\n");
632             output.append(indentPlusTwo);
633             output.append("numEnqueuedByApp=").append(numEnqueuedByApp).append(",\n");
634             output.append(indentPlusTwo);
635             output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
636             output.append(indentPlusTwo);
637             output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
638             output.append(indentPlusTwo);
639             output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
640             output.append(indentPlusTwo);
641             output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
642             output.append(indentPlusTwo);
643             output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
644             output.append(indentPlusTwo);
645             output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
646             output.append(indentPlusTwo);
647             output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
648             output.append(indentPlusTwo);
649             output.append("numBlocked=").append(numBlocked).append(",\n");
650             output.append(indentPlusTwo);
651             output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
652             output.append(indentPlusTwo);
653             output.append("numWithActions=").append(numWithActions).append(",\n");
654             output.append(indentPlusTwo);
655             output.append("numPrivate=").append(numPrivate).append(",\n");
656             output.append(indentPlusTwo);
657             output.append("numSecret=").append(numSecret).append(",\n");
658             output.append(indentPlusTwo);
659             output.append("numInterrupt=").append(numInterrupt).append(",\n");
660             output.append(indentPlusTwo);
661             output.append("numWithBigText=").append(numWithBigText).append(",\n");
662             output.append(indentPlusTwo);
663             output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
664             output.append(indentPlusTwo);
665             output.append("numForegroundService=").append(numForegroundService).append("\n");
666             output.append(indentPlusTwo);
667             output.append("numUserInitiatedJob=").append(numUserInitiatedJob).append("\n");
668             output.append(indentPlusTwo);
669             output.append("numOngoing=").append(numOngoing).append("\n");
670             output.append(indentPlusTwo);
671             output.append("numAutoCancel=").append(numAutoCancel).append("\n");
672             output.append(indentPlusTwo);
673             output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
674             output.append(indentPlusTwo);
675             output.append("numWithInbox=").append(numWithInbox).append("\n");
676             output.append(indentPlusTwo);
677             output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
678             output.append(indentPlusTwo);
679             output.append("numWithTitle=").append(numWithTitle).append("\n");
680             output.append(indentPlusTwo);
681             output.append("numWithText=").append(numWithText).append("\n");
682             output.append(indentPlusTwo);
683             output.append("numWithSubText=").append(numWithSubText).append("\n");
684             output.append(indentPlusTwo);
685             output.append("numWithInfoText=").append(numWithInfoText).append("\n");
686             output.append(indentPlusTwo);
687             output.append("numRateViolations=").append(numRateViolations).append("\n");
688             output.append(indentPlusTwo);
689             output.append("numAlertViolations=").append(numAlertViolations).append("\n");
690             output.append(indentPlusTwo);
691             output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
692             output.append(indentPlusTwo);
693             output.append("numImagesRemoved=").append(numImagesRemoved).append("\n");
694             output.append(indentPlusTwo);
695             output.append("numTooOld=").append(numTooOld).append("\n");
696             output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
697             output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
698             output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
699             output.append(indentPlusTwo);
700             output.append("numUndecorateRVs=").append(numUndecoratedRemoteViews).append("\n");
701             output.append(indent).append("}");
702             return output.toString();
703         }
704 
dumpJson()705         public JSONObject dumpJson() throws JSONException {
706             AggregatedStats previous = getPrevious();
707             JSONObject dump = new JSONObject();
708             dump.put("key", key);
709             dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
710             maybePut(dump, "numEnqueuedByApp", numEnqueuedByApp);
711             maybePut(dump, "numPostedByApp", numPostedByApp);
712             maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
713             maybePut(dump, "numRemovedByApp", numRemovedByApp);
714             maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
715             maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
716             maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
717             maybePut(dump, "numWithValidPeople", numWithValidPeople);
718             maybePut(dump, "numBlocked", numBlocked);
719             maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
720             maybePut(dump, "numWithActions", numWithActions);
721             maybePut(dump, "numPrivate", numPrivate);
722             maybePut(dump, "numSecret", numSecret);
723             maybePut(dump, "numInterrupt", numInterrupt);
724             maybePut(dump, "numWithBigText", numWithBigText);
725             maybePut(dump, "numWithBigPicture", numWithBigPicture);
726             maybePut(dump, "numForegroundService", numForegroundService);
727             maybePut(dump, "numUserInitiatedJob", numUserInitiatedJob);
728             maybePut(dump, "numOngoing", numOngoing);
729             maybePut(dump, "numAutoCancel", numAutoCancel);
730             maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
731             maybePut(dump, "numWithInbox", numWithInbox);
732             maybePut(dump, "numWithMediaSession", numWithMediaSession);
733             maybePut(dump, "numWithTitle", numWithTitle);
734             maybePut(dump, "numWithText", numWithText);
735             maybePut(dump, "numWithSubText", numWithSubText);
736             maybePut(dump, "numWithInfoText", numWithInfoText);
737             maybePut(dump, "numRateViolations", numRateViolations);
738             maybePut(dump, "numQuotaLViolations", numQuotaViolations);
739             maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
740             maybePut(dump, "numAlertViolations", numAlertViolations);
741             maybePut(dump, "numImagesRemoved", numImagesRemoved);
742             maybePut(dump, "numTooOld", numTooOld);
743             noisyImportance.maybePut(dump, previous.noisyImportance);
744             quietImportance.maybePut(dump, previous.quietImportance);
745             finalImportance.maybePut(dump, previous.finalImportance);
746 
747             return dump;
748         }
749 
maybePut(JSONObject dump, String name, int value)750         private void maybePut(JSONObject dump, String name, int value) throws JSONException {
751             if (value > 0) {
752                 dump.put(name, value);
753             }
754         }
755 
maybePut(JSONObject dump, String name, float value)756         private void maybePut(JSONObject dump, String name, float value) throws JSONException {
757             if (value > 0.0) {
758                 dump.put(name, value);
759             }
760         }
761     }
762 
763     private static class ImportanceHistogram {
764         // TODO define these somewhere else
765         private static final int NUM_IMPORTANCES = 6;
766         private static final String[] IMPORTANCE_NAMES =
767                 {"none", "min", "low", "default", "high", "max"};
768         private final Context mContext;
769         private final String[] mCounterNames;
770         private final String mPrefix;
771         private int[] mCount;
772 
ImportanceHistogram(Context context, String prefix)773         ImportanceHistogram(Context context, String prefix) {
774             mContext = context;
775             mCount = new int[NUM_IMPORTANCES];
776             mCounterNames = new String[NUM_IMPORTANCES];
777             mPrefix = prefix;
778             for (int i = 0; i < NUM_IMPORTANCES; i++) {
779                 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
780             }
781         }
782 
increment(int imp)783         void increment(int imp) {
784             imp = Math.max(0, Math.min(imp, mCount.length - 1));
785             mCount[imp]++;
786         }
787 
maybeCount(ImportanceHistogram prev)788         void maybeCount(ImportanceHistogram prev) {
789             for (int i = 0; i < NUM_IMPORTANCES; i++) {
790                 final int value = mCount[i] - prev.mCount[i];
791                 if (value > 0) {
792                     MetricsLogger.count(mContext, mCounterNames[i], value);
793                 }
794             }
795         }
796 
update(ImportanceHistogram that)797         void update(ImportanceHistogram that) {
798             for (int i = 0; i < NUM_IMPORTANCES; i++) {
799                 mCount[i] = that.mCount[i];
800             }
801         }
802 
maybePut(JSONObject dump, ImportanceHistogram prev)803         public void maybePut(JSONObject dump, ImportanceHistogram prev)
804                 throws JSONException {
805             dump.put(mPrefix, new JSONArray(mCount));
806         }
807 
808         @Override
toString()809         public String toString() {
810             StringBuilder output = new StringBuilder();
811             output.append(mPrefix).append(": [");
812             for (int i = 0; i < NUM_IMPORTANCES; i++) {
813                 output.append(mCount[i]);
814                 if (i < (NUM_IMPORTANCES-1)) {
815                     output.append(", ");
816                 }
817             }
818             output.append("]");
819             return output.toString();
820         }
821     }
822 
823     /**
824      * Tracks usage of an individual notification that is currently active.
825      */
826     public static class SingleNotificationStats {
827         private boolean isVisible = false;
828         private boolean isExpanded = false;
829         /** SystemClock.elapsedRealtime() when the notification was posted. */
830         public long posttimeElapsedMs = -1;
831         /** Elapsed time since the notification was posted until it was first clicked, or -1. */
832         public long posttimeToFirstClickMs = -1;
833         /** Elpased time since the notification was posted until it was dismissed by the user. */
834         public long posttimeToDismissMs = -1;
835         /** Number of times the notification has been made visible. */
836         public long airtimeCount = 0;
837         /** Time in ms between the notification was posted and first shown; -1 if never shown. */
838         public long posttimeToFirstAirtimeMs = -1;
839         /**
840          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
841          * visible; -1 otherwise.
842          */
843         public long currentAirtimeStartElapsedMs = -1;
844         /** Accumulated visible time. */
845         public long airtimeMs = 0;
846         /**
847          * Time in ms between the notification being posted and when it first
848          * became visible and expanded; -1 if it was never visibly expanded.
849          */
850         public long posttimeToFirstVisibleExpansionMs = -1;
851         /**
852          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
853          * visible; -1 otherwise.
854          */
855         public long currentAirtimeExpandedStartElapsedMs = -1;
856         /** Accumulated visible expanded time. */
857         public long airtimeExpandedMs = 0;
858         /** Number of times the notification has been expanded by the user. */
859         public long userExpansionCount = 0;
860         /** Importance directly requested by the app. */
861         public int requestedImportance;
862         /** Did the app include sound or vibration on the notificaiton. */
863         public boolean isNoisy;
864         /** Importance after initial filtering for noise and other features */
865         public int naturalImportance;
866 
getCurrentPosttimeMs()867         public long getCurrentPosttimeMs() {
868             if (posttimeElapsedMs < 0) {
869                 return 0;
870             }
871             return SystemClock.elapsedRealtime() - posttimeElapsedMs;
872         }
873 
getCurrentAirtimeMs()874         public long getCurrentAirtimeMs() {
875             long result = airtimeMs;
876             // Add incomplete airtime if currently shown.
877             if (currentAirtimeStartElapsedMs >= 0) {
878                 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
879             }
880             return result;
881         }
882 
getCurrentAirtimeExpandedMs()883         public long getCurrentAirtimeExpandedMs() {
884             long result = airtimeExpandedMs;
885             // Add incomplete expanded airtime if currently shown.
886             if (currentAirtimeExpandedStartElapsedMs >= 0) {
887                 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
888             }
889             return result;
890         }
891 
892         /**
893          * Called when the user clicked the notification.
894          */
onClick()895         public void onClick() {
896             if (posttimeToFirstClickMs < 0) {
897                 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
898             }
899         }
900 
901         /**
902          * Called when the user removed the notification.
903          */
onDismiss()904         public void onDismiss() {
905             if (posttimeToDismissMs < 0) {
906                 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
907             }
908             finish();
909         }
910 
onCancel()911         public void onCancel() {
912             finish();
913         }
914 
onRemoved()915         public void onRemoved() {
916             finish();
917         }
918 
onVisibilityChanged(boolean visible)919         public void onVisibilityChanged(boolean visible) {
920             long elapsedNowMs = SystemClock.elapsedRealtime();
921             final boolean wasVisible = isVisible;
922             isVisible = visible;
923             if (visible) {
924                 if (currentAirtimeStartElapsedMs < 0) {
925                     airtimeCount++;
926                     currentAirtimeStartElapsedMs = elapsedNowMs;
927                 }
928                 if (posttimeToFirstAirtimeMs < 0) {
929                     posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
930                 }
931             } else {
932                 if (currentAirtimeStartElapsedMs >= 0) {
933                     airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
934                     currentAirtimeStartElapsedMs = -1;
935                 }
936             }
937 
938             if (wasVisible != isVisible) {
939                 updateVisiblyExpandedStats();
940             }
941         }
942 
onExpansionChanged(boolean userAction, boolean expanded)943         public void onExpansionChanged(boolean userAction, boolean expanded) {
944             isExpanded = expanded;
945             if (isExpanded && userAction) {
946                 userExpansionCount++;
947             }
948             updateVisiblyExpandedStats();
949         }
950 
951         /**
952          * Returns whether this notification has been visible and expanded at the same.
953          */
hasBeenVisiblyExpanded()954         public boolean hasBeenVisiblyExpanded() {
955             return posttimeToFirstVisibleExpansionMs >= 0;
956         }
957 
updateVisiblyExpandedStats()958         private void updateVisiblyExpandedStats() {
959             long elapsedNowMs = SystemClock.elapsedRealtime();
960             if (isExpanded && isVisible) {
961                 // expanded and visible
962                 if (currentAirtimeExpandedStartElapsedMs < 0) {
963                     currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
964                 }
965                 if (posttimeToFirstVisibleExpansionMs < 0) {
966                     posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
967                 }
968             } else {
969                 // not-expanded or not-visible
970                 if (currentAirtimeExpandedStartElapsedMs >= 0) {
971                     airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
972                     currentAirtimeExpandedStartElapsedMs = -1;
973                 }
974             }
975         }
976 
977         /** The notification is leaving the system. Finalize. */
finish()978         public void finish() {
979             onVisibilityChanged(false);
980         }
981 
982         @Override
toString()983         public String toString() {
984             StringBuilder output = new StringBuilder();
985             output.append("SingleNotificationStats{");
986 
987             output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
988             output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
989             output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
990             output.append("airtimeCount=").append(airtimeCount).append(", ");
991             output.append("airtimeMs=").append(airtimeMs).append(", ");
992             output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
993                     .append(", ");
994             output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
995             output.append("posttimeToFirstVisibleExpansionMs=")
996                     .append(posttimeToFirstVisibleExpansionMs).append(", ");
997             output.append("currentAirtimeExpandedStartElapsedMs=")
998                     .append(currentAirtimeExpandedStartElapsedMs).append(", ");
999             output.append("requestedImportance=").append(requestedImportance).append(", ");
1000             output.append("naturalImportance=").append(naturalImportance).append(", ");
1001             output.append("isNoisy=").append(isNoisy);
1002             output.append('}');
1003             return output.toString();
1004         }
1005 
1006         /** Copy useful information out of the stats from the pre-update notifications. */
updateFrom(SingleNotificationStats old)1007         public void updateFrom(SingleNotificationStats old) {
1008             posttimeElapsedMs = old.posttimeElapsedMs;
1009             posttimeToFirstClickMs = old.posttimeToFirstClickMs;
1010             airtimeCount = old.airtimeCount;
1011             posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
1012             currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
1013             airtimeMs = old.airtimeMs;
1014             posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
1015             currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
1016             airtimeExpandedMs = old.airtimeExpandedMs;
1017             userExpansionCount = old.userExpansionCount;
1018         }
1019     }
1020 
1021     /**
1022      * Aggregates long samples to sum and averages.
1023      */
1024     public static class Aggregate {
1025         long numSamples;
1026         double avg;
1027         double sum2;
1028         double var;
1029 
addSample(long sample)1030         public void addSample(long sample) {
1031             // Welford's "Method for Calculating Corrected Sums of Squares"
1032             // http://www.jstor.org/stable/1266577?seq=2
1033             numSamples++;
1034             final double n = numSamples;
1035             final double delta = sample - avg;
1036             avg += (1.0 / n) * delta;
1037             sum2 += ((n - 1) / n) * delta * delta;
1038             final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
1039             var = sum2 / divisor;
1040         }
1041 
1042         @Override
toString()1043         public String toString() {
1044             return "Aggregate{" +
1045                     "numSamples=" + numSamples +
1046                     ", avg=" + avg +
1047                     ", var=" + var +
1048                     '}';
1049         }
1050     }
1051 }
1052