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 
260     @GuardedBy("this")
getAggregatedStatsLocked(NotificationRecord record)261     private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
262         return getAggregatedStatsLocked(record.getSbn().getPackageName());
263     }
264 
265     @GuardedBy("this")
getAggregatedStatsLocked(String packageName)266     private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
267         if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
268             return EMPTY_AGGREGATED_STATS;
269         }
270 
271         AggregatedStats[] array = mStatsArrays.poll();
272         if (array == null) {
273             array = new AggregatedStats[2];
274         }
275         array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
276         array[1] = getOrCreateAggregatedStatsLocked(packageName);
277         return array;
278     }
279 
280     @GuardedBy("this")
releaseAggregatedStatsLocked(AggregatedStats[] array)281     private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
282         for(int i = 0; i < array.length; i++) {
283             array[i] = null;
284         }
285         mStatsArrays.offer(array);
286     }
287 
288     @GuardedBy("this")
getOrCreateAggregatedStatsLocked(String key)289     private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
290         AggregatedStats result = mStats.get(key);
291         if (result == null) {
292             result = new AggregatedStats(mContext, key);
293             mStats.put(key, result);
294         }
295         result.mLastAccessTime = SystemClock.elapsedRealtime();
296         return result;
297     }
298 
dumpJson(DumpFilter filter)299     public synchronized JSONObject dumpJson(DumpFilter filter) {
300         JSONObject dump = new JSONObject();
301         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
302             try {
303                 JSONArray aggregatedStats = new JSONArray();
304                 for (AggregatedStats as : mStats.values()) {
305                     if (filter != null && !filter.matches(as.key))
306                         continue;
307                     aggregatedStats.put(as.dumpJson());
308                 }
309                 dump.put("current", aggregatedStats);
310             } catch (JSONException e) {
311                 // pass
312             }
313         }
314         return dump;
315     }
316 
remoteViewStats(long startMs, boolean aggregate)317     public PulledStats remoteViewStats(long startMs, boolean aggregate) {
318         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
319             PulledStats stats = new PulledStats(startMs);
320             for (AggregatedStats as : mStats.values()) {
321                 if (as.numUndecoratedRemoteViews > 0) {
322                     stats.addUndecoratedPackage(as.key, as.mCreated);
323                 }
324             }
325             return stats;
326         }
327         return null;
328     }
329 
dump(PrintWriter pw, String indent, DumpFilter filter)330     public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
331         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
332             for (AggregatedStats as : mStats.values()) {
333                 if (filter != null && !filter.matches(as.key))
334                     continue;
335                 as.dump(pw, indent);
336             }
337             pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
338             pw.println(indent + "mStats.size(): " + mStats.size());
339         }
340     }
341 
emit()342     public synchronized void emit() {
343         AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
344         stats.emit();
345         mHandler.removeMessages(MSG_EMIT);
346         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
347         for(String key: mStats.keySet()) {
348             if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
349                 mStatExpiredkeys.add(key);
350             }
351         }
352         for(String key: mStatExpiredkeys) {
353             mStats.remove(key);
354         }
355         mStatExpiredkeys.clear();
356         mLastEmitTime = SystemClock.elapsedRealtime();
357     }
358 
359     /**
360      * Aggregated notification stats.
361      */
362     private static class AggregatedStats {
363 
364         private final Context mContext;
365         public final String key;
366         private final long mCreated;
367         private AggregatedStats mPrevious;
368 
369         // ---- Updated as the respective events occur.
370         public int numEnqueuedByApp;
371         public int numPostedByApp;
372         public int numUpdatedByApp;
373         public int numRemovedByApp;
374         public int numPeopleCacheHit;
375         public int numPeopleCacheMiss;;
376         public int numWithStaredPeople;
377         public int numWithValidPeople;
378         public int numBlocked;
379         public int numSuspendedByAdmin;
380         public int numWithActions;
381         public int numPrivate;
382         public int numSecret;
383         public int numWithBigText;
384         public int numWithBigPicture;
385         public int numForegroundService;
386         public int numUserInitiatedJob;
387         public int numOngoing;
388         public int numAutoCancel;
389         public int numWithLargeIcon;
390         public int numWithInbox;
391         public int numWithMediaSession;
392         public int numWithTitle;
393         public int numWithText;
394         public int numWithSubText;
395         public int numWithInfoText;
396         public int numInterrupt;
397         public ImportanceHistogram noisyImportance;
398         public ImportanceHistogram quietImportance;
399         public ImportanceHistogram finalImportance;
400         public RateEstimator enqueueRate;
401         public AlertRateLimiter alertRate;
402         public int numRateViolations;
403         public int numAlertViolations;
404         public int numQuotaViolations;
405         public int numUndecoratedRemoteViews;
406         public long mLastAccessTime;
407         public int numImagesRemoved;
408 
AggregatedStats(Context context, String key)409         public AggregatedStats(Context context, String key) {
410             this.key = key;
411             mContext = context;
412             mCreated = SystemClock.elapsedRealtime();
413             noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
414             quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
415             finalImportance = new ImportanceHistogram(context, "note_importance_");
416             enqueueRate = new RateEstimator();
417             alertRate = new AlertRateLimiter();
418         }
419 
getPrevious()420         public AggregatedStats getPrevious() {
421             if (mPrevious == null) {
422                 mPrevious = new AggregatedStats(mContext, key);
423             }
424             return mPrevious;
425         }
426 
countApiUse(NotificationRecord record)427         public void countApiUse(NotificationRecord record) {
428             final Notification n = record.getNotification();
429             if (n.actions != null) {
430                 numWithActions++;
431             }
432 
433             if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
434                 numForegroundService++;
435             }
436 
437             if ((n.flags & Notification.FLAG_USER_INITIATED_JOB) != 0) {
438                 numUserInitiatedJob++;
439             }
440 
441             if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
442                 numOngoing++;
443             }
444 
445             if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
446                 numAutoCancel++;
447             }
448 
449             if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
450                     (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
451                     n.sound != null || n.vibrate != null) {
452                 numInterrupt++;
453             }
454 
455             switch (n.visibility) {
456                 case Notification.VISIBILITY_PRIVATE:
457                     numPrivate++;
458                     break;
459                 case Notification.VISIBILITY_SECRET:
460                     numSecret++;
461                     break;
462             }
463 
464             if (record.stats.isNoisy) {
465                 noisyImportance.increment(record.stats.requestedImportance);
466             } else {
467                 quietImportance.increment(record.stats.requestedImportance);
468             }
469             finalImportance.increment(record.getImportance());
470 
471             final Set<String> names = n.extras.keySet();
472             if (names.contains(Notification.EXTRA_BIG_TEXT)) {
473                 numWithBigText++;
474             }
475             if (names.contains(Notification.EXTRA_PICTURE)) {
476                 numWithBigPicture++;
477             }
478             if (names.contains(Notification.EXTRA_LARGE_ICON)) {
479                 numWithLargeIcon++;
480             }
481             if (names.contains(Notification.EXTRA_TEXT_LINES)) {
482                 numWithInbox++;
483             }
484             if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
485                 numWithMediaSession++;
486             }
487             if (names.contains(Notification.EXTRA_TITLE) &&
488                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
489                 numWithTitle++;
490             }
491             if (names.contains(Notification.EXTRA_TEXT) &&
492                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
493                 numWithText++;
494             }
495             if (names.contains(Notification.EXTRA_SUB_TEXT) &&
496                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
497                 numWithSubText++;
498             }
499             if (names.contains(Notification.EXTRA_INFO_TEXT) &&
500                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
501                 numWithInfoText++;
502             }
503         }
504 
emit()505         public void emit() {
506             AggregatedStats previous = getPrevious();
507             maybeCount("note_enqueued", (numEnqueuedByApp - previous.numEnqueuedByApp));
508             maybeCount("note_post", (numPostedByApp - previous.numPostedByApp));
509             maybeCount("note_update", (numUpdatedByApp - previous.numUpdatedByApp));
510             maybeCount("note_remove", (numRemovedByApp - previous.numRemovedByApp));
511             maybeCount("note_with_people", (numWithValidPeople - previous.numWithValidPeople));
512             maybeCount("note_with_stars", (numWithStaredPeople - previous.numWithStaredPeople));
513             maybeCount("people_cache_hit", (numPeopleCacheHit - previous.numPeopleCacheHit));
514             maybeCount("people_cache_miss", (numPeopleCacheMiss - previous.numPeopleCacheMiss));
515             maybeCount("note_blocked", (numBlocked - previous.numBlocked));
516             maybeCount("note_suspended", (numSuspendedByAdmin - previous.numSuspendedByAdmin));
517             maybeCount("note_with_actions", (numWithActions - previous.numWithActions));
518             maybeCount("note_private", (numPrivate - previous.numPrivate));
519             maybeCount("note_secret", (numSecret - previous.numSecret));
520             maybeCount("note_interupt", (numInterrupt - previous.numInterrupt));
521             maybeCount("note_big_text", (numWithBigText - previous.numWithBigText));
522             maybeCount("note_big_pic", (numWithBigPicture - previous.numWithBigPicture));
523             maybeCount("note_fg", (numForegroundService - previous.numForegroundService));
524             maybeCount("note_uij", (numUserInitiatedJob - previous.numUserInitiatedJob));
525             maybeCount("note_ongoing", (numOngoing - previous.numOngoing));
526             maybeCount("note_auto", (numAutoCancel - previous.numAutoCancel));
527             maybeCount("note_large_icon", (numWithLargeIcon - previous.numWithLargeIcon));
528             maybeCount("note_inbox", (numWithInbox - previous.numWithInbox));
529             maybeCount("note_media", (numWithMediaSession - previous.numWithMediaSession));
530             maybeCount("note_title", (numWithTitle - previous.numWithTitle));
531             maybeCount("note_text", (numWithText - previous.numWithText));
532             maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
533             maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
534             maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
535             maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations));
536             maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
537             maybeCount("note_images_removed", (numImagesRemoved - previous.numImagesRemoved));
538             noisyImportance.maybeCount(previous.noisyImportance);
539             quietImportance.maybeCount(previous.quietImportance);
540             finalImportance.maybeCount(previous.finalImportance);
541 
542             previous.numEnqueuedByApp = numEnqueuedByApp;
543             previous.numPostedByApp = numPostedByApp;
544             previous.numUpdatedByApp = numUpdatedByApp;
545             previous.numRemovedByApp = numRemovedByApp;
546             previous.numPeopleCacheHit = numPeopleCacheHit;
547             previous.numPeopleCacheMiss = numPeopleCacheMiss;
548             previous.numWithStaredPeople = numWithStaredPeople;
549             previous.numWithValidPeople = numWithValidPeople;
550             previous.numBlocked = numBlocked;
551             previous.numSuspendedByAdmin = numSuspendedByAdmin;
552             previous.numWithActions = numWithActions;
553             previous.numPrivate = numPrivate;
554             previous.numSecret = numSecret;
555             previous.numInterrupt = numInterrupt;
556             previous.numWithBigText = numWithBigText;
557             previous.numWithBigPicture = numWithBigPicture;
558             previous.numForegroundService = numForegroundService;
559             previous.numUserInitiatedJob = numUserInitiatedJob;
560             previous.numOngoing = numOngoing;
561             previous.numAutoCancel = numAutoCancel;
562             previous.numWithLargeIcon = numWithLargeIcon;
563             previous.numWithInbox = numWithInbox;
564             previous.numWithMediaSession = numWithMediaSession;
565             previous.numWithTitle = numWithTitle;
566             previous.numWithText = numWithText;
567             previous.numWithSubText = numWithSubText;
568             previous.numWithInfoText = numWithInfoText;
569             previous.numRateViolations = numRateViolations;
570             previous.numAlertViolations = numAlertViolations;
571             previous.numQuotaViolations = numQuotaViolations;
572             previous.numImagesRemoved = numImagesRemoved;
573             noisyImportance.update(previous.noisyImportance);
574             quietImportance.update(previous.quietImportance);
575             finalImportance.update(previous.finalImportance);
576         }
577 
maybeCount(String name, int value)578         void maybeCount(String name, int value) {
579             if (value > 0) {
580                 MetricsLogger.count(mContext, name, value);
581             }
582         }
583 
dump(PrintWriter pw, String indent)584         public void dump(PrintWriter pw, String indent) {
585             pw.println(toStringWithIndent(indent));
586         }
587 
588         @Override
toString()589         public String toString() {
590             return toStringWithIndent("");
591         }
592 
593         /** @return the enqueue rate if there were a new enqueue event right now. */
getEnqueueRate()594         public float getEnqueueRate() {
595             return getEnqueueRate(SystemClock.elapsedRealtime());
596         }
597 
getEnqueueRate(long now)598         public float getEnqueueRate(long now) {
599             return enqueueRate.getRate(now);
600         }
601 
updateInterarrivalEstimate(long now)602         public void updateInterarrivalEstimate(long now) {
603             enqueueRate.update(now);
604         }
605 
isAlertRateLimited()606         public boolean isAlertRateLimited() {
607             boolean limited = alertRate.shouldRateLimitAlert(SystemClock.elapsedRealtime());
608             if (limited) {
609                 numAlertViolations++;
610             }
611             return limited;
612         }
613 
toStringWithIndent(String indent)614         private String toStringWithIndent(String indent) {
615             StringBuilder output = new StringBuilder();
616             output.append(indent).append("AggregatedStats{\n");
617             String indentPlusTwo = indent + "  ";
618             output.append(indentPlusTwo);
619             output.append("key='").append(key).append("',\n");
620             output.append(indentPlusTwo);
621             output.append("numEnqueuedByApp=").append(numEnqueuedByApp).append(",\n");
622             output.append(indentPlusTwo);
623             output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
624             output.append(indentPlusTwo);
625             output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
626             output.append(indentPlusTwo);
627             output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
628             output.append(indentPlusTwo);
629             output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
630             output.append(indentPlusTwo);
631             output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
632             output.append(indentPlusTwo);
633             output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
634             output.append(indentPlusTwo);
635             output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
636             output.append(indentPlusTwo);
637             output.append("numBlocked=").append(numBlocked).append(",\n");
638             output.append(indentPlusTwo);
639             output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
640             output.append(indentPlusTwo);
641             output.append("numWithActions=").append(numWithActions).append(",\n");
642             output.append(indentPlusTwo);
643             output.append("numPrivate=").append(numPrivate).append(",\n");
644             output.append(indentPlusTwo);
645             output.append("numSecret=").append(numSecret).append(",\n");
646             output.append(indentPlusTwo);
647             output.append("numInterrupt=").append(numInterrupt).append(",\n");
648             output.append(indentPlusTwo);
649             output.append("numWithBigText=").append(numWithBigText).append(",\n");
650             output.append(indentPlusTwo);
651             output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
652             output.append(indentPlusTwo);
653             output.append("numForegroundService=").append(numForegroundService).append("\n");
654             output.append(indentPlusTwo);
655             output.append("numUserInitiatedJob=").append(numUserInitiatedJob).append("\n");
656             output.append(indentPlusTwo);
657             output.append("numOngoing=").append(numOngoing).append("\n");
658             output.append(indentPlusTwo);
659             output.append("numAutoCancel=").append(numAutoCancel).append("\n");
660             output.append(indentPlusTwo);
661             output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
662             output.append(indentPlusTwo);
663             output.append("numWithInbox=").append(numWithInbox).append("\n");
664             output.append(indentPlusTwo);
665             output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
666             output.append(indentPlusTwo);
667             output.append("numWithTitle=").append(numWithTitle).append("\n");
668             output.append(indentPlusTwo);
669             output.append("numWithText=").append(numWithText).append("\n");
670             output.append(indentPlusTwo);
671             output.append("numWithSubText=").append(numWithSubText).append("\n");
672             output.append(indentPlusTwo);
673             output.append("numWithInfoText=").append(numWithInfoText).append("\n");
674             output.append(indentPlusTwo);
675             output.append("numRateViolations=").append(numRateViolations).append("\n");
676             output.append(indentPlusTwo);
677             output.append("numAlertViolations=").append(numAlertViolations).append("\n");
678             output.append(indentPlusTwo);
679             output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
680             output.append(indentPlusTwo);
681             output.append("numImagesRemoved=").append(numImagesRemoved).append("\n");
682             output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
683             output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
684             output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
685             output.append(indentPlusTwo);
686             output.append("numUndecorateRVs=").append(numUndecoratedRemoteViews).append("\n");
687             output.append(indent).append("}");
688             return output.toString();
689         }
690 
dumpJson()691         public JSONObject dumpJson() throws JSONException {
692             AggregatedStats previous = getPrevious();
693             JSONObject dump = new JSONObject();
694             dump.put("key", key);
695             dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
696             maybePut(dump, "numEnqueuedByApp", numEnqueuedByApp);
697             maybePut(dump, "numPostedByApp", numPostedByApp);
698             maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
699             maybePut(dump, "numRemovedByApp", numRemovedByApp);
700             maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
701             maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
702             maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
703             maybePut(dump, "numWithValidPeople", numWithValidPeople);
704             maybePut(dump, "numBlocked", numBlocked);
705             maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
706             maybePut(dump, "numWithActions", numWithActions);
707             maybePut(dump, "numPrivate", numPrivate);
708             maybePut(dump, "numSecret", numSecret);
709             maybePut(dump, "numInterrupt", numInterrupt);
710             maybePut(dump, "numWithBigText", numWithBigText);
711             maybePut(dump, "numWithBigPicture", numWithBigPicture);
712             maybePut(dump, "numForegroundService", numForegroundService);
713             maybePut(dump, "numUserInitiatedJob", numUserInitiatedJob);
714             maybePut(dump, "numOngoing", numOngoing);
715             maybePut(dump, "numAutoCancel", numAutoCancel);
716             maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
717             maybePut(dump, "numWithInbox", numWithInbox);
718             maybePut(dump, "numWithMediaSession", numWithMediaSession);
719             maybePut(dump, "numWithTitle", numWithTitle);
720             maybePut(dump, "numWithText", numWithText);
721             maybePut(dump, "numWithSubText", numWithSubText);
722             maybePut(dump, "numWithInfoText", numWithInfoText);
723             maybePut(dump, "numRateViolations", numRateViolations);
724             maybePut(dump, "numQuotaLViolations", numQuotaViolations);
725             maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
726             maybePut(dump, "numAlertViolations", numAlertViolations);
727             maybePut(dump, "numImagesRemoved", numImagesRemoved);
728             noisyImportance.maybePut(dump, previous.noisyImportance);
729             quietImportance.maybePut(dump, previous.quietImportance);
730             finalImportance.maybePut(dump, previous.finalImportance);
731 
732             return dump;
733         }
734 
maybePut(JSONObject dump, String name, int value)735         private void maybePut(JSONObject dump, String name, int value) throws JSONException {
736             if (value > 0) {
737                 dump.put(name, value);
738             }
739         }
740 
maybePut(JSONObject dump, String name, float value)741         private void maybePut(JSONObject dump, String name, float value) throws JSONException {
742             if (value > 0.0) {
743                 dump.put(name, value);
744             }
745         }
746     }
747 
748     private static class ImportanceHistogram {
749         // TODO define these somewhere else
750         private static final int NUM_IMPORTANCES = 6;
751         private static final String[] IMPORTANCE_NAMES =
752                 {"none", "min", "low", "default", "high", "max"};
753         private final Context mContext;
754         private final String[] mCounterNames;
755         private final String mPrefix;
756         private int[] mCount;
757 
ImportanceHistogram(Context context, String prefix)758         ImportanceHistogram(Context context, String prefix) {
759             mContext = context;
760             mCount = new int[NUM_IMPORTANCES];
761             mCounterNames = new String[NUM_IMPORTANCES];
762             mPrefix = prefix;
763             for (int i = 0; i < NUM_IMPORTANCES; i++) {
764                 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
765             }
766         }
767 
increment(int imp)768         void increment(int imp) {
769             imp = Math.max(0, Math.min(imp, mCount.length - 1));
770             mCount[imp]++;
771         }
772 
maybeCount(ImportanceHistogram prev)773         void maybeCount(ImportanceHistogram prev) {
774             for (int i = 0; i < NUM_IMPORTANCES; i++) {
775                 final int value = mCount[i] - prev.mCount[i];
776                 if (value > 0) {
777                     MetricsLogger.count(mContext, mCounterNames[i], value);
778                 }
779             }
780         }
781 
update(ImportanceHistogram that)782         void update(ImportanceHistogram that) {
783             for (int i = 0; i < NUM_IMPORTANCES; i++) {
784                 mCount[i] = that.mCount[i];
785             }
786         }
787 
maybePut(JSONObject dump, ImportanceHistogram prev)788         public void maybePut(JSONObject dump, ImportanceHistogram prev)
789                 throws JSONException {
790             dump.put(mPrefix, new JSONArray(mCount));
791         }
792 
793         @Override
toString()794         public String toString() {
795             StringBuilder output = new StringBuilder();
796             output.append(mPrefix).append(": [");
797             for (int i = 0; i < NUM_IMPORTANCES; i++) {
798                 output.append(mCount[i]);
799                 if (i < (NUM_IMPORTANCES-1)) {
800                     output.append(", ");
801                 }
802             }
803             output.append("]");
804             return output.toString();
805         }
806     }
807 
808     /**
809      * Tracks usage of an individual notification that is currently active.
810      */
811     public static class SingleNotificationStats {
812         private boolean isVisible = false;
813         private boolean isExpanded = false;
814         /** SystemClock.elapsedRealtime() when the notification was posted. */
815         public long posttimeElapsedMs = -1;
816         /** Elapsed time since the notification was posted until it was first clicked, or -1. */
817         public long posttimeToFirstClickMs = -1;
818         /** Elpased time since the notification was posted until it was dismissed by the user. */
819         public long posttimeToDismissMs = -1;
820         /** Number of times the notification has been made visible. */
821         public long airtimeCount = 0;
822         /** Time in ms between the notification was posted and first shown; -1 if never shown. */
823         public long posttimeToFirstAirtimeMs = -1;
824         /**
825          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
826          * visible; -1 otherwise.
827          */
828         public long currentAirtimeStartElapsedMs = -1;
829         /** Accumulated visible time. */
830         public long airtimeMs = 0;
831         /**
832          * Time in ms between the notification being posted and when it first
833          * became visible and expanded; -1 if it was never visibly expanded.
834          */
835         public long posttimeToFirstVisibleExpansionMs = -1;
836         /**
837          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
838          * visible; -1 otherwise.
839          */
840         public long currentAirtimeExpandedStartElapsedMs = -1;
841         /** Accumulated visible expanded time. */
842         public long airtimeExpandedMs = 0;
843         /** Number of times the notification has been expanded by the user. */
844         public long userExpansionCount = 0;
845         /** Importance directly requested by the app. */
846         public int requestedImportance;
847         /** Did the app include sound or vibration on the notificaiton. */
848         public boolean isNoisy;
849         /** Importance after initial filtering for noise and other features */
850         public int naturalImportance;
851 
getCurrentPosttimeMs()852         public long getCurrentPosttimeMs() {
853             if (posttimeElapsedMs < 0) {
854                 return 0;
855             }
856             return SystemClock.elapsedRealtime() - posttimeElapsedMs;
857         }
858 
getCurrentAirtimeMs()859         public long getCurrentAirtimeMs() {
860             long result = airtimeMs;
861             // Add incomplete airtime if currently shown.
862             if (currentAirtimeStartElapsedMs >= 0) {
863                 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
864             }
865             return result;
866         }
867 
getCurrentAirtimeExpandedMs()868         public long getCurrentAirtimeExpandedMs() {
869             long result = airtimeExpandedMs;
870             // Add incomplete expanded airtime if currently shown.
871             if (currentAirtimeExpandedStartElapsedMs >= 0) {
872                 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
873             }
874             return result;
875         }
876 
877         /**
878          * Called when the user clicked the notification.
879          */
onClick()880         public void onClick() {
881             if (posttimeToFirstClickMs < 0) {
882                 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
883             }
884         }
885 
886         /**
887          * Called when the user removed the notification.
888          */
onDismiss()889         public void onDismiss() {
890             if (posttimeToDismissMs < 0) {
891                 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
892             }
893             finish();
894         }
895 
onCancel()896         public void onCancel() {
897             finish();
898         }
899 
onRemoved()900         public void onRemoved() {
901             finish();
902         }
903 
onVisibilityChanged(boolean visible)904         public void onVisibilityChanged(boolean visible) {
905             long elapsedNowMs = SystemClock.elapsedRealtime();
906             final boolean wasVisible = isVisible;
907             isVisible = visible;
908             if (visible) {
909                 if (currentAirtimeStartElapsedMs < 0) {
910                     airtimeCount++;
911                     currentAirtimeStartElapsedMs = elapsedNowMs;
912                 }
913                 if (posttimeToFirstAirtimeMs < 0) {
914                     posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
915                 }
916             } else {
917                 if (currentAirtimeStartElapsedMs >= 0) {
918                     airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
919                     currentAirtimeStartElapsedMs = -1;
920                 }
921             }
922 
923             if (wasVisible != isVisible) {
924                 updateVisiblyExpandedStats();
925             }
926         }
927 
onExpansionChanged(boolean userAction, boolean expanded)928         public void onExpansionChanged(boolean userAction, boolean expanded) {
929             isExpanded = expanded;
930             if (isExpanded && userAction) {
931                 userExpansionCount++;
932             }
933             updateVisiblyExpandedStats();
934         }
935 
936         /**
937          * Returns whether this notification has been visible and expanded at the same.
938          */
hasBeenVisiblyExpanded()939         public boolean hasBeenVisiblyExpanded() {
940             return posttimeToFirstVisibleExpansionMs >= 0;
941         }
942 
updateVisiblyExpandedStats()943         private void updateVisiblyExpandedStats() {
944             long elapsedNowMs = SystemClock.elapsedRealtime();
945             if (isExpanded && isVisible) {
946                 // expanded and visible
947                 if (currentAirtimeExpandedStartElapsedMs < 0) {
948                     currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
949                 }
950                 if (posttimeToFirstVisibleExpansionMs < 0) {
951                     posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
952                 }
953             } else {
954                 // not-expanded or not-visible
955                 if (currentAirtimeExpandedStartElapsedMs >= 0) {
956                     airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
957                     currentAirtimeExpandedStartElapsedMs = -1;
958                 }
959             }
960         }
961 
962         /** The notification is leaving the system. Finalize. */
finish()963         public void finish() {
964             onVisibilityChanged(false);
965         }
966 
967         @Override
toString()968         public String toString() {
969             StringBuilder output = new StringBuilder();
970             output.append("SingleNotificationStats{");
971 
972             output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
973             output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
974             output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
975             output.append("airtimeCount=").append(airtimeCount).append(", ");
976             output.append("airtimeMs=").append(airtimeMs).append(", ");
977             output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
978                     .append(", ");
979             output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
980             output.append("posttimeToFirstVisibleExpansionMs=")
981                     .append(posttimeToFirstVisibleExpansionMs).append(", ");
982             output.append("currentAirtimeExpandedStartElapsedMs=")
983                     .append(currentAirtimeExpandedStartElapsedMs).append(", ");
984             output.append("requestedImportance=").append(requestedImportance).append(", ");
985             output.append("naturalImportance=").append(naturalImportance).append(", ");
986             output.append("isNoisy=").append(isNoisy);
987             output.append('}');
988             return output.toString();
989         }
990 
991         /** Copy useful information out of the stats from the pre-update notifications. */
updateFrom(SingleNotificationStats old)992         public void updateFrom(SingleNotificationStats old) {
993             posttimeElapsedMs = old.posttimeElapsedMs;
994             posttimeToFirstClickMs = old.posttimeToFirstClickMs;
995             airtimeCount = old.airtimeCount;
996             posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
997             currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
998             airtimeMs = old.airtimeMs;
999             posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
1000             currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
1001             airtimeExpandedMs = old.airtimeExpandedMs;
1002             userExpansionCount = old.userExpansionCount;
1003         }
1004     }
1005 
1006     /**
1007      * Aggregates long samples to sum and averages.
1008      */
1009     public static class Aggregate {
1010         long numSamples;
1011         double avg;
1012         double sum2;
1013         double var;
1014 
addSample(long sample)1015         public void addSample(long sample) {
1016             // Welford's "Method for Calculating Corrected Sums of Squares"
1017             // http://www.jstor.org/stable/1266577?seq=2
1018             numSamples++;
1019             final double n = numSamples;
1020             final double delta = sample - avg;
1021             avg += (1.0 / n) * delta;
1022             sum2 += ((n - 1) / n) * delta * delta;
1023             final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
1024             var = sum2 / divisor;
1025         }
1026 
1027         @Override
toString()1028         public String toString() {
1029             return "Aggregate{" +
1030                     "numSamples=" + numSamples +
1031                     ", avg=" + avg +
1032                     ", var=" + var +
1033                     '}';
1034         }
1035     }
1036 }
1037