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