• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.server.notification;
18 
19 import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_HIGH;
20 
21 import android.app.Notification;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteOpenHelper;
27 import android.os.Handler;
28 import android.os.HandlerThread;
29 import android.os.Message;
30 import android.os.SystemClock;
31 import android.text.TextUtils;
32 import android.util.ArraySet;
33 import android.util.Log;
34 
35 import com.android.internal.logging.MetricsLogger;
36 import com.android.server.notification.NotificationManagerService.DumpFilter;
37 
38 import org.json.JSONArray;
39 import org.json.JSONException;
40 import org.json.JSONObject;
41 
42 import java.io.PrintWriter;
43 import java.util.ArrayDeque;
44 import java.util.Calendar;
45 import java.util.GregorianCalendar;
46 import java.util.HashMap;
47 import java.util.Map;
48 import java.util.Set;
49 
50 /**
51  * Keeps track of notification activity, display, and user interaction.
52  *
53  * <p>This class receives signals from NoMan and keeps running stats of
54  * notification usage. Some metrics are updated as events occur. Others, namely
55  * those involving durations, are updated as the notification is canceled.</p>
56  *
57  * <p>This class is thread-safe.</p>
58  *
59  * {@hide}
60  */
61 public class NotificationUsageStats {
62     private static final String TAG = "NotificationUsageStats";
63 
64     private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
65     private static final boolean ENABLE_SQLITE_LOG = true;
66     private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
67     private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
68     private static final int MSG_EMIT = 1;
69 
70     private static final boolean DEBUG = false;
71     public static final int TEN_SECONDS = 1000 * 10;
72     public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
73     private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
74 
75     // Guarded by synchronized(this).
76     private final Map<String, AggregatedStats> mStats = new HashMap<>();
77     private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
78     private ArraySet<String> mStatExpiredkeys = new ArraySet<>();
79     private final SQLiteLog mSQLiteLog;
80     private final Context mContext;
81     private final Handler mHandler;
82     private long mLastEmitTime;
83 
NotificationUsageStats(Context context)84     public NotificationUsageStats(Context context) {
85         mContext = context;
86         mLastEmitTime = SystemClock.elapsedRealtime();
87         mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
88         mHandler = new Handler(mContext.getMainLooper()) {
89             @Override
90             public void handleMessage(Message msg) {
91                 switch (msg.what) {
92                     case MSG_EMIT:
93                         emit();
94                         break;
95                     default:
96                         Log.wtf(TAG, "Unknown message type: " + msg.what);
97                         break;
98                 }
99             }
100         };
101         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
102     }
103 
104     /**
105      * Called when a notification has been posted.
106      */
getAppEnqueueRate(String packageName)107     public synchronized float getAppEnqueueRate(String packageName) {
108         AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
109         if (stats != null) {
110             return stats.getEnqueueRate(SystemClock.elapsedRealtime());
111         } else {
112             return 0f;
113         }
114     }
115 
116     /**
117      * Called when a notification is tentatively enqueued by an app, before rate checking.
118      */
registerEnqueuedByApp(String packageName)119     public synchronized void registerEnqueuedByApp(String packageName) {
120         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
121         for (AggregatedStats stats : aggregatedStatsArray) {
122             stats.numEnqueuedByApp++;
123         }
124         releaseAggregatedStatsLocked(aggregatedStatsArray);
125     }
126 
127     /**
128      * Called when a notification has been posted.
129      */
registerPostedByApp(NotificationRecord notification)130     public synchronized void registerPostedByApp(NotificationRecord notification) {
131         final long now = SystemClock.elapsedRealtime();
132         notification.stats.posttimeElapsedMs = now;
133 
134         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
135         for (AggregatedStats stats : aggregatedStatsArray) {
136             stats.numPostedByApp++;
137             stats.updateInterarrivalEstimate(now);
138             stats.countApiUse(notification);
139         }
140         releaseAggregatedStatsLocked(aggregatedStatsArray);
141         if (ENABLE_SQLITE_LOG) {
142             mSQLiteLog.logPosted(notification);
143         }
144     }
145 
146     /**
147      * Called when a notification has been updated.
148      */
registerUpdatedByApp(NotificationRecord notification, NotificationRecord old)149     public synchronized void registerUpdatedByApp(NotificationRecord notification,
150             NotificationRecord old) {
151         notification.stats.updateFrom(old.stats);
152         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
153         for (AggregatedStats stats : aggregatedStatsArray) {
154             stats.numUpdatedByApp++;
155             stats.updateInterarrivalEstimate(SystemClock.elapsedRealtime());
156             stats.countApiUse(notification);
157         }
158         releaseAggregatedStatsLocked(aggregatedStatsArray);
159         if (ENABLE_SQLITE_LOG) {
160             mSQLiteLog.logPosted(notification);
161         }
162     }
163 
164     /**
165      * Called when the originating app removed the notification programmatically.
166      */
registerRemovedByApp(NotificationRecord notification)167     public synchronized void registerRemovedByApp(NotificationRecord notification) {
168         notification.stats.onRemoved();
169         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
170         for (AggregatedStats stats : aggregatedStatsArray) {
171             stats.numRemovedByApp++;
172         }
173         releaseAggregatedStatsLocked(aggregatedStatsArray);
174         if (ENABLE_SQLITE_LOG) {
175             mSQLiteLog.logRemoved(notification);
176         }
177     }
178 
179     /**
180      * Called when the user dismissed the notification via the UI.
181      */
registerDismissedByUser(NotificationRecord notification)182     public synchronized void registerDismissedByUser(NotificationRecord notification) {
183         MetricsLogger.histogram(mContext, "note_dismiss_longevity",
184                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
185         notification.stats.onDismiss();
186         if (ENABLE_SQLITE_LOG) {
187             mSQLiteLog.logDismissed(notification);
188         }
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         if (ENABLE_SQLITE_LOG) {
199             mSQLiteLog.logClicked(notification);
200         }
201     }
202 
registerPeopleAffinity(NotificationRecord notification, boolean valid, boolean starred, boolean cached)203     public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
204             boolean starred, boolean cached) {
205         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
206         for (AggregatedStats stats : aggregatedStatsArray) {
207             if (valid) {
208                 stats.numWithValidPeople++;
209             }
210             if (starred) {
211                 stats.numWithStaredPeople++;
212             }
213             if (cached) {
214                 stats.numPeopleCacheHit++;
215             } else {
216                 stats.numPeopleCacheMiss++;
217             }
218         }
219         releaseAggregatedStatsLocked(aggregatedStatsArray);
220     }
221 
registerBlocked(NotificationRecord notification)222     public synchronized void registerBlocked(NotificationRecord notification) {
223         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
224         for (AggregatedStats stats : aggregatedStatsArray) {
225             stats.numBlocked++;
226         }
227         releaseAggregatedStatsLocked(aggregatedStatsArray);
228     }
229 
registerSuspendedByAdmin(NotificationRecord notification)230     public synchronized void registerSuspendedByAdmin(NotificationRecord notification) {
231         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
232         for (AggregatedStats stats : aggregatedStatsArray) {
233             stats.numSuspendedByAdmin++;
234         }
235         releaseAggregatedStatsLocked(aggregatedStatsArray);
236     }
237 
registerOverRateQuota(String packageName)238     public synchronized void registerOverRateQuota(String packageName) {
239         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
240         for (AggregatedStats stats : aggregatedStatsArray) {
241             stats.numRateViolations++;
242         }
243     }
244 
registerOverCountQuota(String packageName)245     public synchronized void registerOverCountQuota(String packageName) {
246         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
247         for (AggregatedStats stats : aggregatedStatsArray) {
248             stats.numQuotaViolations++;
249         }
250     }
251 
252     // Locked by this.
getAggregatedStatsLocked(NotificationRecord record)253     private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
254         return getAggregatedStatsLocked(record.sbn.getPackageName());
255     }
256 
257     // Locked by this.
getAggregatedStatsLocked(String packageName)258     private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
259         if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
260             return EMPTY_AGGREGATED_STATS;
261         }
262 
263         AggregatedStats[] array = mStatsArrays.poll();
264         if (array == null) {
265             array = new AggregatedStats[2];
266         }
267         array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
268         array[1] = getOrCreateAggregatedStatsLocked(packageName);
269         return array;
270     }
271 
272     // Locked by this.
releaseAggregatedStatsLocked(AggregatedStats[] array)273     private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
274         for(int i = 0; i < array.length; i++) {
275             array[i] = null;
276         }
277         mStatsArrays.offer(array);
278     }
279 
280     // Locked by this.
getOrCreateAggregatedStatsLocked(String key)281     private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
282         AggregatedStats result = mStats.get(key);
283         if (result == null) {
284             result = new AggregatedStats(mContext, key);
285             mStats.put(key, result);
286         }
287         result.mLastAccessTime = SystemClock.elapsedRealtime();
288         return result;
289     }
290 
dumpJson(DumpFilter filter)291     public synchronized JSONObject dumpJson(DumpFilter filter) {
292         JSONObject dump = new JSONObject();
293         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
294             try {
295                 JSONArray aggregatedStats = new JSONArray();
296                 for (AggregatedStats as : mStats.values()) {
297                     if (filter != null && !filter.matches(as.key))
298                         continue;
299                     aggregatedStats.put(as.dumpJson());
300                 }
301                 dump.put("current", aggregatedStats);
302             } catch (JSONException e) {
303                 // pass
304             }
305         }
306         if (ENABLE_SQLITE_LOG) {
307             try {
308                 dump.put("historical", mSQLiteLog.dumpJson(filter));
309             } catch (JSONException e) {
310                 // pass
311             }
312         }
313         return dump;
314     }
315 
dump(PrintWriter pw, String indent, DumpFilter filter)316     public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
317         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
318             for (AggregatedStats as : mStats.values()) {
319                 if (filter != null && !filter.matches(as.key))
320                     continue;
321                 as.dump(pw, indent);
322             }
323             pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
324             pw.println(indent + "mStats.size(): " + mStats.size());
325         }
326         if (ENABLE_SQLITE_LOG) {
327             mSQLiteLog.dump(pw, indent, filter);
328         }
329     }
330 
emit()331     public synchronized void emit() {
332         AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
333         stats.emit();
334         mHandler.removeMessages(MSG_EMIT);
335         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
336         for(String key: mStats.keySet()) {
337             if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
338                 mStatExpiredkeys.add(key);
339             }
340         }
341         for(String key: mStatExpiredkeys) {
342             mStats.remove(key);
343         }
344         mStatExpiredkeys.clear();
345         mLastEmitTime = SystemClock.elapsedRealtime();
346     }
347 
348     /**
349      * Aggregated notification stats.
350      */
351     private static class AggregatedStats {
352 
353         private final Context mContext;
354         public final String key;
355         private final long mCreated;
356         private AggregatedStats mPrevious;
357 
358         // ---- Updated as the respective events occur.
359         public int numEnqueuedByApp;
360         public int numPostedByApp;
361         public int numUpdatedByApp;
362         public int numRemovedByApp;
363         public int numPeopleCacheHit;
364         public int numPeopleCacheMiss;;
365         public int numWithStaredPeople;
366         public int numWithValidPeople;
367         public int numBlocked;
368         public int numSuspendedByAdmin;
369         public int numWithActions;
370         public int numPrivate;
371         public int numSecret;
372         public int numWithBigText;
373         public int numWithBigPicture;
374         public int numForegroundService;
375         public int numOngoing;
376         public int numAutoCancel;
377         public int numWithLargeIcon;
378         public int numWithInbox;
379         public int numWithMediaSession;
380         public int numWithTitle;
381         public int numWithText;
382         public int numWithSubText;
383         public int numWithInfoText;
384         public int numInterrupt;
385         public ImportanceHistogram noisyImportance;
386         public ImportanceHistogram quietImportance;
387         public ImportanceHistogram finalImportance;
388         public RateEstimator enqueueRate;
389         public int numRateViolations;
390         public int numQuotaViolations;
391         public long mLastAccessTime;
392 
AggregatedStats(Context context, String key)393         public AggregatedStats(Context context, String key) {
394             this.key = key;
395             mContext = context;
396             mCreated = SystemClock.elapsedRealtime();
397             noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
398             quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
399             finalImportance = new ImportanceHistogram(context, "note_importance_");
400             enqueueRate = new RateEstimator();
401         }
402 
getPrevious()403         public AggregatedStats getPrevious() {
404             if (mPrevious == null) {
405                 mPrevious = new AggregatedStats(mContext, key);
406             }
407             return mPrevious;
408         }
409 
countApiUse(NotificationRecord record)410         public void countApiUse(NotificationRecord record) {
411             final Notification n = record.getNotification();
412             if (n.actions != null) {
413                 numWithActions++;
414             }
415 
416             if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
417                 numForegroundService++;
418             }
419 
420             if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
421                 numOngoing++;
422             }
423 
424             if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
425                 numAutoCancel++;
426             }
427 
428             if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
429                     (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
430                     n.sound != null || n.vibrate != null) {
431                 numInterrupt++;
432             }
433 
434             switch (n.visibility) {
435                 case Notification.VISIBILITY_PRIVATE:
436                     numPrivate++;
437                     break;
438                 case Notification.VISIBILITY_SECRET:
439                     numSecret++;
440                     break;
441             }
442 
443             if (record.stats.isNoisy) {
444                 noisyImportance.increment(record.stats.requestedImportance);
445             } else {
446                 quietImportance.increment(record.stats.requestedImportance);
447             }
448             finalImportance.increment(record.getImportance());
449 
450             final Set<String> names = n.extras.keySet();
451             if (names.contains(Notification.EXTRA_BIG_TEXT)) {
452                 numWithBigText++;
453             }
454             if (names.contains(Notification.EXTRA_PICTURE)) {
455                 numWithBigPicture++;
456             }
457             if (names.contains(Notification.EXTRA_LARGE_ICON)) {
458                 numWithLargeIcon++;
459             }
460             if (names.contains(Notification.EXTRA_TEXT_LINES)) {
461                 numWithInbox++;
462             }
463             if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
464                 numWithMediaSession++;
465             }
466             if (names.contains(Notification.EXTRA_TITLE) &&
467                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
468                 numWithTitle++;
469             }
470             if (names.contains(Notification.EXTRA_TEXT) &&
471                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
472                 numWithText++;
473             }
474             if (names.contains(Notification.EXTRA_SUB_TEXT) &&
475                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
476                 numWithSubText++;
477             }
478             if (names.contains(Notification.EXTRA_INFO_TEXT) &&
479                     !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
480                 numWithInfoText++;
481             }
482         }
483 
emit()484         public void emit() {
485             AggregatedStats previous = getPrevious();
486             maybeCount("note_enqueued", (numEnqueuedByApp - previous.numEnqueuedByApp));
487             maybeCount("note_post", (numPostedByApp - previous.numPostedByApp));
488             maybeCount("note_update", (numUpdatedByApp - previous.numUpdatedByApp));
489             maybeCount("note_remove", (numRemovedByApp - previous.numRemovedByApp));
490             maybeCount("note_with_people", (numWithValidPeople - previous.numWithValidPeople));
491             maybeCount("note_with_stars", (numWithStaredPeople - previous.numWithStaredPeople));
492             maybeCount("people_cache_hit", (numPeopleCacheHit - previous.numPeopleCacheHit));
493             maybeCount("people_cache_miss", (numPeopleCacheMiss - previous.numPeopleCacheMiss));
494             maybeCount("note_blocked", (numBlocked - previous.numBlocked));
495             maybeCount("note_suspended", (numSuspendedByAdmin - previous.numSuspendedByAdmin));
496             maybeCount("note_with_actions", (numWithActions - previous.numWithActions));
497             maybeCount("note_private", (numPrivate - previous.numPrivate));
498             maybeCount("note_secret", (numSecret - previous.numSecret));
499             maybeCount("note_interupt", (numInterrupt - previous.numInterrupt));
500             maybeCount("note_big_text", (numWithBigText - previous.numWithBigText));
501             maybeCount("note_big_pic", (numWithBigPicture - previous.numWithBigPicture));
502             maybeCount("note_fg", (numForegroundService - previous.numForegroundService));
503             maybeCount("note_ongoing", (numOngoing - previous.numOngoing));
504             maybeCount("note_auto", (numAutoCancel - previous.numAutoCancel));
505             maybeCount("note_large_icon", (numWithLargeIcon - previous.numWithLargeIcon));
506             maybeCount("note_inbox", (numWithInbox - previous.numWithInbox));
507             maybeCount("note_media", (numWithMediaSession - previous.numWithMediaSession));
508             maybeCount("note_title", (numWithTitle - previous.numWithTitle));
509             maybeCount("note_text", (numWithText - previous.numWithText));
510             maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
511             maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
512             maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
513             maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
514             noisyImportance.maybeCount(previous.noisyImportance);
515             quietImportance.maybeCount(previous.quietImportance);
516             finalImportance.maybeCount(previous.finalImportance);
517 
518             previous.numEnqueuedByApp = numEnqueuedByApp;
519             previous.numPostedByApp = numPostedByApp;
520             previous.numUpdatedByApp = numUpdatedByApp;
521             previous.numRemovedByApp = numRemovedByApp;
522             previous.numPeopleCacheHit = numPeopleCacheHit;
523             previous.numPeopleCacheMiss = numPeopleCacheMiss;
524             previous.numWithStaredPeople = numWithStaredPeople;
525             previous.numWithValidPeople = numWithValidPeople;
526             previous.numBlocked = numBlocked;
527             previous.numSuspendedByAdmin = numSuspendedByAdmin;
528             previous.numWithActions = numWithActions;
529             previous.numPrivate = numPrivate;
530             previous.numSecret = numSecret;
531             previous.numInterrupt = numInterrupt;
532             previous.numWithBigText = numWithBigText;
533             previous.numWithBigPicture = numWithBigPicture;
534             previous.numForegroundService = numForegroundService;
535             previous.numOngoing = numOngoing;
536             previous.numAutoCancel = numAutoCancel;
537             previous.numWithLargeIcon = numWithLargeIcon;
538             previous.numWithInbox = numWithInbox;
539             previous.numWithMediaSession = numWithMediaSession;
540             previous.numWithTitle = numWithTitle;
541             previous.numWithText = numWithText;
542             previous.numWithSubText = numWithSubText;
543             previous.numWithInfoText = numWithInfoText;
544             previous.numRateViolations = numRateViolations;
545             previous.numQuotaViolations = numQuotaViolations;
546             noisyImportance.update(previous.noisyImportance);
547             quietImportance.update(previous.quietImportance);
548             finalImportance.update(previous.finalImportance);
549         }
550 
maybeCount(String name, int value)551         void maybeCount(String name, int value) {
552             if (value > 0) {
553                 MetricsLogger.count(mContext, name, value);
554             }
555         }
556 
dump(PrintWriter pw, String indent)557         public void dump(PrintWriter pw, String indent) {
558             pw.println(toStringWithIndent(indent));
559         }
560 
561         @Override
toString()562         public String toString() {
563             return toStringWithIndent("");
564         }
565 
566         /** @return the enqueue rate if there were a new enqueue event right now. */
getEnqueueRate()567         public float getEnqueueRate() {
568             return getEnqueueRate(SystemClock.elapsedRealtime());
569         }
570 
getEnqueueRate(long now)571         public float getEnqueueRate(long now) {
572             return enqueueRate.getRate(now);
573         }
574 
updateInterarrivalEstimate(long now)575         public void updateInterarrivalEstimate(long now) {
576             enqueueRate.update(now);
577         }
578 
toStringWithIndent(String indent)579         private String toStringWithIndent(String indent) {
580             StringBuilder output = new StringBuilder();
581             output.append(indent).append("AggregatedStats{\n");
582             String indentPlusTwo = indent + "  ";
583             output.append(indentPlusTwo);
584             output.append("key='").append(key).append("',\n");
585             output.append(indentPlusTwo);
586             output.append("numEnqueuedByApp=").append(numEnqueuedByApp).append(",\n");
587             output.append(indentPlusTwo);
588             output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
589             output.append(indentPlusTwo);
590             output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
591             output.append(indentPlusTwo);
592             output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
593             output.append(indentPlusTwo);
594             output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
595             output.append(indentPlusTwo);
596             output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
597             output.append(indentPlusTwo);
598             output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
599             output.append(indentPlusTwo);
600             output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
601             output.append(indentPlusTwo);
602             output.append("numBlocked=").append(numBlocked).append(",\n");
603             output.append(indentPlusTwo);
604             output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
605             output.append(indentPlusTwo);
606             output.append("numWithActions=").append(numWithActions).append(",\n");
607             output.append(indentPlusTwo);
608             output.append("numPrivate=").append(numPrivate).append(",\n");
609             output.append(indentPlusTwo);
610             output.append("numSecret=").append(numSecret).append(",\n");
611             output.append(indentPlusTwo);
612             output.append("numInterrupt=").append(numInterrupt).append(",\n");
613             output.append(indentPlusTwo);
614             output.append("numWithBigText=").append(numWithBigText).append(",\n");
615             output.append(indentPlusTwo);
616             output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
617             output.append(indentPlusTwo);
618             output.append("numForegroundService=").append(numForegroundService).append("\n");
619             output.append(indentPlusTwo);
620             output.append("numOngoing=").append(numOngoing).append("\n");
621             output.append(indentPlusTwo);
622             output.append("numAutoCancel=").append(numAutoCancel).append("\n");
623             output.append(indentPlusTwo);
624             output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
625             output.append(indentPlusTwo);
626             output.append("numWithInbox=").append(numWithInbox).append("\n");
627             output.append(indentPlusTwo);
628             output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
629             output.append(indentPlusTwo);
630             output.append("numWithTitle=").append(numWithTitle).append("\n");
631             output.append(indentPlusTwo);
632             output.append("numWithText=").append(numWithText).append("\n");
633             output.append(indentPlusTwo);
634             output.append("numWithSubText=").append(numWithSubText).append("\n");
635             output.append(indentPlusTwo);
636             output.append("numWithInfoText=").append(numWithInfoText).append("\n");
637             output.append("numRateViolations=").append(numRateViolations).append("\n");
638             output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
639             output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
640             output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
641             output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
642             output.append(indent).append("}");
643             return output.toString();
644         }
645 
dumpJson()646         public JSONObject dumpJson() throws JSONException {
647             AggregatedStats previous = getPrevious();
648             JSONObject dump = new JSONObject();
649             dump.put("key", key);
650             dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
651             maybePut(dump, "numEnqueuedByApp", numEnqueuedByApp);
652             maybePut(dump, "numPostedByApp", numPostedByApp);
653             maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
654             maybePut(dump, "numRemovedByApp", numRemovedByApp);
655             maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
656             maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
657             maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
658             maybePut(dump, "numWithValidPeople", numWithValidPeople);
659             maybePut(dump, "numBlocked", numBlocked);
660             maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
661             maybePut(dump, "numWithActions", numWithActions);
662             maybePut(dump, "numPrivate", numPrivate);
663             maybePut(dump, "numSecret", numSecret);
664             maybePut(dump, "numInterrupt", numInterrupt);
665             maybePut(dump, "numWithBigText", numWithBigText);
666             maybePut(dump, "numWithBigPicture", numWithBigPicture);
667             maybePut(dump, "numForegroundService", numForegroundService);
668             maybePut(dump, "numOngoing", numOngoing);
669             maybePut(dump, "numAutoCancel", numAutoCancel);
670             maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
671             maybePut(dump, "numWithInbox", numWithInbox);
672             maybePut(dump, "numWithMediaSession", numWithMediaSession);
673             maybePut(dump, "numWithTitle", numWithTitle);
674             maybePut(dump, "numWithText", numWithText);
675             maybePut(dump, "numWithSubText", numWithSubText);
676             maybePut(dump, "numWithInfoText", numWithInfoText);
677             maybePut(dump, "numRateViolations", numRateViolations);
678             maybePut(dump, "numQuotaLViolations", numQuotaViolations);
679             maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
680             noisyImportance.maybePut(dump, previous.noisyImportance);
681             quietImportance.maybePut(dump, previous.quietImportance);
682             finalImportance.maybePut(dump, previous.finalImportance);
683 
684             return dump;
685         }
686 
maybePut(JSONObject dump, String name, int value)687         private void maybePut(JSONObject dump, String name, int value) throws JSONException {
688             if (value > 0) {
689                 dump.put(name, value);
690             }
691         }
692 
maybePut(JSONObject dump, String name, float value)693         private void maybePut(JSONObject dump, String name, float value) throws JSONException {
694             if (value > 0.0) {
695                 dump.put(name, value);
696             }
697         }
698     }
699 
700     private static class ImportanceHistogram {
701         // TODO define these somewhere else
702         private static final int NUM_IMPORTANCES = 6;
703         private static final String[] IMPORTANCE_NAMES =
704                 {"none", "min", "low", "default", "high", "max"};
705         private final Context mContext;
706         private final String[] mCounterNames;
707         private final String mPrefix;
708         private int[] mCount;
709 
ImportanceHistogram(Context context, String prefix)710         ImportanceHistogram(Context context, String prefix) {
711             mContext = context;
712             mCount = new int[NUM_IMPORTANCES];
713             mCounterNames = new String[NUM_IMPORTANCES];
714             mPrefix = prefix;
715             for (int i = 0; i < NUM_IMPORTANCES; i++) {
716                 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
717             }
718         }
719 
increment(int imp)720         void increment(int imp) {
721             imp = imp < 0 ? 0 : imp > NUM_IMPORTANCES ? NUM_IMPORTANCES : imp;
722             mCount[imp] ++;
723         }
724 
maybeCount(ImportanceHistogram prev)725         void maybeCount(ImportanceHistogram prev) {
726             for (int i = 0; i < NUM_IMPORTANCES; i++) {
727                 final int value = mCount[i] - prev.mCount[i];
728                 if (value > 0) {
729                     MetricsLogger.count(mContext, mCounterNames[i], value);
730                 }
731             }
732         }
733 
update(ImportanceHistogram that)734         void update(ImportanceHistogram that) {
735             for (int i = 0; i < NUM_IMPORTANCES; i++) {
736                 mCount[i] = that.mCount[i];
737             }
738         }
739 
maybePut(JSONObject dump, ImportanceHistogram prev)740         public void maybePut(JSONObject dump, ImportanceHistogram prev)
741                 throws JSONException {
742             dump.put(mPrefix, new JSONArray(mCount));
743         }
744 
745         @Override
toString()746         public String toString() {
747             StringBuilder output = new StringBuilder();
748             output.append(mPrefix).append(": [");
749             for (int i = 0; i < NUM_IMPORTANCES; i++) {
750                 output.append(mCount[i]);
751                 if (i < (NUM_IMPORTANCES-1)) {
752                     output.append(", ");
753                 }
754             }
755             output.append("]");
756             return output.toString();
757         }
758     }
759 
760     /**
761      * Tracks usage of an individual notification that is currently active.
762      */
763     public static class SingleNotificationStats {
764         private boolean isVisible = false;
765         private boolean isExpanded = false;
766         /** SystemClock.elapsedRealtime() when the notification was posted. */
767         public long posttimeElapsedMs = -1;
768         /** Elapsed time since the notification was posted until it was first clicked, or -1. */
769         public long posttimeToFirstClickMs = -1;
770         /** Elpased time since the notification was posted until it was dismissed by the user. */
771         public long posttimeToDismissMs = -1;
772         /** Number of times the notification has been made visible. */
773         public long airtimeCount = 0;
774         /** Time in ms between the notification was posted and first shown; -1 if never shown. */
775         public long posttimeToFirstAirtimeMs = -1;
776         /**
777          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
778          * visible; -1 otherwise.
779          */
780         public long currentAirtimeStartElapsedMs = -1;
781         /** Accumulated visible time. */
782         public long airtimeMs = 0;
783         /**
784          * Time in ms between the notification being posted and when it first
785          * became visible and expanded; -1 if it was never visibly expanded.
786          */
787         public long posttimeToFirstVisibleExpansionMs = -1;
788         /**
789          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
790          * visible; -1 otherwise.
791          */
792         public long currentAirtimeExpandedStartElapsedMs = -1;
793         /** Accumulated visible expanded time. */
794         public long airtimeExpandedMs = 0;
795         /** Number of times the notification has been expanded by the user. */
796         public long userExpansionCount = 0;
797         /** Importance directly requested by the app. */
798         public int requestedImportance;
799         /** Did the app include sound or vibration on the notificaiton. */
800         public boolean isNoisy;
801         /** Importance after initial filtering for noise and other features */
802         public int naturalImportance;
803 
getCurrentPosttimeMs()804         public long getCurrentPosttimeMs() {
805             if (posttimeElapsedMs < 0) {
806                 return 0;
807             }
808             return SystemClock.elapsedRealtime() - posttimeElapsedMs;
809         }
810 
getCurrentAirtimeMs()811         public long getCurrentAirtimeMs() {
812             long result = airtimeMs;
813             // Add incomplete airtime if currently shown.
814             if (currentAirtimeStartElapsedMs >= 0) {
815                 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
816             }
817             return result;
818         }
819 
getCurrentAirtimeExpandedMs()820         public long getCurrentAirtimeExpandedMs() {
821             long result = airtimeExpandedMs;
822             // Add incomplete expanded airtime if currently shown.
823             if (currentAirtimeExpandedStartElapsedMs >= 0) {
824                 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
825             }
826             return result;
827         }
828 
829         /**
830          * Called when the user clicked the notification.
831          */
onClick()832         public void onClick() {
833             if (posttimeToFirstClickMs < 0) {
834                 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
835             }
836         }
837 
838         /**
839          * Called when the user removed the notification.
840          */
onDismiss()841         public void onDismiss() {
842             if (posttimeToDismissMs < 0) {
843                 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
844             }
845             finish();
846         }
847 
onCancel()848         public void onCancel() {
849             finish();
850         }
851 
onRemoved()852         public void onRemoved() {
853             finish();
854         }
855 
onVisibilityChanged(boolean visible)856         public void onVisibilityChanged(boolean visible) {
857             long elapsedNowMs = SystemClock.elapsedRealtime();
858             final boolean wasVisible = isVisible;
859             isVisible = visible;
860             if (visible) {
861                 if (currentAirtimeStartElapsedMs < 0) {
862                     airtimeCount++;
863                     currentAirtimeStartElapsedMs = elapsedNowMs;
864                 }
865                 if (posttimeToFirstAirtimeMs < 0) {
866                     posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
867                 }
868             } else {
869                 if (currentAirtimeStartElapsedMs >= 0) {
870                     airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
871                     currentAirtimeStartElapsedMs = -1;
872                 }
873             }
874 
875             if (wasVisible != isVisible) {
876                 updateVisiblyExpandedStats();
877             }
878         }
879 
onExpansionChanged(boolean userAction, boolean expanded)880         public void onExpansionChanged(boolean userAction, boolean expanded) {
881             isExpanded = expanded;
882             if (isExpanded && userAction) {
883                 userExpansionCount++;
884             }
885             updateVisiblyExpandedStats();
886         }
887 
updateVisiblyExpandedStats()888         private void updateVisiblyExpandedStats() {
889             long elapsedNowMs = SystemClock.elapsedRealtime();
890             if (isExpanded && isVisible) {
891                 // expanded and visible
892                 if (currentAirtimeExpandedStartElapsedMs < 0) {
893                     currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
894                 }
895                 if (posttimeToFirstVisibleExpansionMs < 0) {
896                     posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
897                 }
898             } else {
899                 // not-expanded or not-visible
900                 if (currentAirtimeExpandedStartElapsedMs >= 0) {
901                     airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
902                     currentAirtimeExpandedStartElapsedMs = -1;
903                 }
904             }
905         }
906 
907         /** The notification is leaving the system. Finalize. */
finish()908         public void finish() {
909             onVisibilityChanged(false);
910         }
911 
912         @Override
toString()913         public String toString() {
914             StringBuilder output = new StringBuilder();
915             output.append("SingleNotificationStats{");
916 
917             output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
918             output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
919             output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
920             output.append("airtimeCount=").append(airtimeCount).append(", ");
921             output.append("airtimeMs=").append(airtimeMs).append(", ");
922             output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
923                     .append(", ");
924             output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
925             output.append("posttimeToFirstVisibleExpansionMs=")
926                     .append(posttimeToFirstVisibleExpansionMs).append(", ");
927             output.append("currentAirtimeExpandedStartElapsedMs=")
928                     .append(currentAirtimeExpandedStartElapsedMs).append(", ");
929             output.append("requestedImportance=").append(requestedImportance).append(", ");
930             output.append("naturalImportance=").append(naturalImportance).append(", ");
931             output.append("isNoisy=").append(isNoisy);
932             output.append('}');
933             return output.toString();
934         }
935 
936         /** Copy useful information out of the stats from the pre-update notifications. */
updateFrom(SingleNotificationStats old)937         public void updateFrom(SingleNotificationStats old) {
938             posttimeElapsedMs = old.posttimeElapsedMs;
939             posttimeToFirstClickMs = old.posttimeToFirstClickMs;
940             airtimeCount = old.airtimeCount;
941             posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
942             currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
943             airtimeMs = old.airtimeMs;
944             posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
945             currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
946             airtimeExpandedMs = old.airtimeExpandedMs;
947             userExpansionCount = old.userExpansionCount;
948         }
949     }
950 
951     /**
952      * Aggregates long samples to sum and averages.
953      */
954     public static class Aggregate {
955         long numSamples;
956         double avg;
957         double sum2;
958         double var;
959 
addSample(long sample)960         public void addSample(long sample) {
961             // Welford's "Method for Calculating Corrected Sums of Squares"
962             // http://www.jstor.org/stable/1266577?seq=2
963             numSamples++;
964             final double n = numSamples;
965             final double delta = sample - avg;
966             avg += (1.0 / n) * delta;
967             sum2 += ((n - 1) / n) * delta * delta;
968             final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
969             var = sum2 / divisor;
970         }
971 
972         @Override
toString()973         public String toString() {
974             return "Aggregate{" +
975                     "numSamples=" + numSamples +
976                     ", avg=" + avg +
977                     ", var=" + var +
978                     '}';
979         }
980     }
981 
982     private static class SQLiteLog {
983         private static final String TAG = "NotificationSQLiteLog";
984 
985         // Message types passed to the background handler.
986         private static final int MSG_POST = 1;
987         private static final int MSG_CLICK = 2;
988         private static final int MSG_REMOVE = 3;
989         private static final int MSG_DISMISS = 4;
990 
991         private static final String DB_NAME = "notification_log.db";
992         private static final int DB_VERSION = 5;
993 
994         /** Age in ms after which events are pruned from the DB. */
995         private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L;  // 1 week
996         /** Delay between pruning the DB. Used to throttle pruning. */
997         private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L;  // 6 hours
998         /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
999         private static final long PRUNE_MIN_WRITES = 1024;
1000 
1001         // Table 'log'
1002         private static final String TAB_LOG = "log";
1003         private static final String COL_EVENT_USER_ID = "event_user_id";
1004         private static final String COL_EVENT_TYPE = "event_type";
1005         private static final String COL_EVENT_TIME = "event_time_ms";
1006         private static final String COL_KEY = "key";
1007         private static final String COL_PKG = "pkg";
1008         private static final String COL_NOTIFICATION_ID = "nid";
1009         private static final String COL_TAG = "tag";
1010         private static final String COL_WHEN_MS = "when_ms";
1011         private static final String COL_DEFAULTS = "defaults";
1012         private static final String COL_FLAGS = "flags";
1013         private static final String COL_IMPORTANCE_REQ = "importance_request";
1014         private static final String COL_IMPORTANCE_FINAL = "importance_final";
1015         private static final String COL_NOISY = "noisy";
1016         private static final String COL_MUTED = "muted";
1017         private static final String COL_DEMOTED = "demoted";
1018         private static final String COL_CATEGORY = "category";
1019         private static final String COL_ACTION_COUNT = "action_count";
1020         private static final String COL_POSTTIME_MS = "posttime_ms";
1021         private static final String COL_AIRTIME_MS = "airtime_ms";
1022         private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
1023         private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
1024         private static final String COL_EXPAND_COUNT = "expansion_count";
1025 
1026 
1027         private static final int EVENT_TYPE_POST = 1;
1028         private static final int EVENT_TYPE_CLICK = 2;
1029         private static final int EVENT_TYPE_REMOVE = 3;
1030         private static final int EVENT_TYPE_DISMISS = 4;
1031         private static long sLastPruneMs;
1032 
1033         private static long sNumWrites;
1034         private final SQLiteOpenHelper mHelper;
1035 
1036         private final Handler mWriteHandler;
1037         private static final long DAY_MS = 24 * 60 * 60 * 1000;
1038         private static final String STATS_QUERY = "SELECT " +
1039                 COL_EVENT_USER_ID + ", " +
1040                 COL_PKG + ", " +
1041                 // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
1042                 "CAST(((%d - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
1043                 "AS day, " +
1044                 "COUNT(*) AS cnt, " +
1045                 "SUM(" + COL_MUTED + ") as muted, " +
1046                 "SUM(" + COL_NOISY + ") as noisy, " +
1047                 "SUM(" + COL_DEMOTED + ") as demoted " +
1048                 "FROM " + TAB_LOG + " " +
1049                 "WHERE " +
1050                 COL_EVENT_TYPE + "=" + EVENT_TYPE_POST +
1051                 " AND " + COL_EVENT_TIME + " > %d " +
1052                 " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
1053 
SQLiteLog(Context context)1054         public SQLiteLog(Context context) {
1055             HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
1056                     android.os.Process.THREAD_PRIORITY_BACKGROUND);
1057             backgroundThread.start();
1058             mWriteHandler = new Handler(backgroundThread.getLooper()) {
1059                 @Override
1060                 public void handleMessage(Message msg) {
1061                     NotificationRecord r = (NotificationRecord) msg.obj;
1062                     long nowMs = System.currentTimeMillis();
1063                     switch (msg.what) {
1064                         case MSG_POST:
1065                             writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
1066                             break;
1067                         case MSG_CLICK:
1068                             writeEvent(nowMs, EVENT_TYPE_CLICK, r);
1069                             break;
1070                         case MSG_REMOVE:
1071                             writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
1072                             break;
1073                         case MSG_DISMISS:
1074                             writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
1075                             break;
1076                         default:
1077                             Log.wtf(TAG, "Unknown message type: " + msg.what);
1078                             break;
1079                     }
1080                 }
1081             };
1082             mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
1083                 @Override
1084                 public void onCreate(SQLiteDatabase db) {
1085                     db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
1086                             "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1087                             COL_EVENT_USER_ID + " INT," +
1088                             COL_EVENT_TYPE + " INT," +
1089                             COL_EVENT_TIME + " INT," +
1090                             COL_KEY + " TEXT," +
1091                             COL_PKG + " TEXT," +
1092                             COL_NOTIFICATION_ID + " INT," +
1093                             COL_TAG + " TEXT," +
1094                             COL_WHEN_MS + " INT," +
1095                             COL_DEFAULTS + " INT," +
1096                             COL_FLAGS + " INT," +
1097                             COL_IMPORTANCE_REQ + " INT," +
1098                             COL_IMPORTANCE_FINAL + " INT," +
1099                             COL_NOISY + " INT," +
1100                             COL_MUTED + " INT," +
1101                             COL_DEMOTED + " INT," +
1102                             COL_CATEGORY + " TEXT," +
1103                             COL_ACTION_COUNT + " INT," +
1104                             COL_POSTTIME_MS + " INT," +
1105                             COL_AIRTIME_MS + " INT," +
1106                             COL_FIRST_EXPANSIONTIME_MS + " INT," +
1107                             COL_AIRTIME_EXPANDED_MS + " INT," +
1108                             COL_EXPAND_COUNT + " INT" +
1109                             ")");
1110                 }
1111 
1112                 @Override
1113                 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
1114                     if (oldVersion != newVersion) {
1115                         db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
1116                         onCreate(db);
1117                     }
1118                 }
1119             };
1120         }
1121 
logPosted(NotificationRecord notification)1122         public void logPosted(NotificationRecord notification) {
1123             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
1124         }
1125 
logClicked(NotificationRecord notification)1126         public void logClicked(NotificationRecord notification) {
1127             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
1128         }
1129 
logRemoved(NotificationRecord notification)1130         public void logRemoved(NotificationRecord notification) {
1131             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
1132         }
1133 
logDismissed(NotificationRecord notification)1134         public void logDismissed(NotificationRecord notification) {
1135             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
1136         }
1137 
jsonPostFrequencies(DumpFilter filter)1138         private JSONArray jsonPostFrequencies(DumpFilter filter) throws JSONException {
1139             JSONArray frequencies = new JSONArray();
1140             SQLiteDatabase db = mHelper.getReadableDatabase();
1141             long midnight = getMidnightMs();
1142             String q = String.format(STATS_QUERY, midnight, filter.since);
1143             Cursor cursor = db.rawQuery(q, null);
1144             try {
1145                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1146                     int userId = cursor.getInt(0);
1147                     String pkg = cursor.getString(1);
1148                     if (filter != null && !filter.matches(pkg)) continue;
1149                     int day = cursor.getInt(2);
1150                     int count = cursor.getInt(3);
1151                     int muted = cursor.getInt(4);
1152                     int noisy = cursor.getInt(5);
1153                     int demoted = cursor.getInt(6);
1154                     JSONObject row = new JSONObject();
1155                     row.put("user_id", userId);
1156                     row.put("package", pkg);
1157                     row.put("day", day);
1158                     row.put("count", count);
1159                     row.put("noisy", noisy);
1160                     row.put("muted", muted);
1161                     row.put("demoted", demoted);
1162                     frequencies.put(row);
1163                 }
1164             } finally {
1165                 cursor.close();
1166             }
1167             return frequencies;
1168         }
1169 
printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter)1170         public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
1171             SQLiteDatabase db = mHelper.getReadableDatabase();
1172             long midnight = getMidnightMs();
1173             String q = String.format(STATS_QUERY, midnight, filter.since);
1174             Cursor cursor = db.rawQuery(q, null);
1175             try {
1176                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1177                     int userId = cursor.getInt(0);
1178                     String pkg = cursor.getString(1);
1179                     if (filter != null && !filter.matches(pkg)) continue;
1180                     int day = cursor.getInt(2);
1181                     int count = cursor.getInt(3);
1182                     int muted = cursor.getInt(4);
1183                     int noisy = cursor.getInt(5);
1184                     int demoted = cursor.getInt(6);
1185                     pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
1186                             ",day=" + day + ",count=" + count + ",muted=" + muted + "/" + noisy +
1187                             ",demoted=" + demoted + "}");
1188                 }
1189             } finally {
1190                 cursor.close();
1191             }
1192         }
1193 
getMidnightMs()1194         private long getMidnightMs() {
1195             GregorianCalendar midnight = new GregorianCalendar();
1196             midnight.set(midnight.get(Calendar.YEAR), midnight.get(Calendar.MONTH),
1197                     midnight.get(Calendar.DATE), 23, 59, 59);
1198             return midnight.getTimeInMillis();
1199         }
1200 
writeEvent(long eventTimeMs, int eventType, NotificationRecord r)1201         private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
1202             ContentValues cv = new ContentValues();
1203             cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
1204             cv.put(COL_EVENT_TIME, eventTimeMs);
1205             cv.put(COL_EVENT_TYPE, eventType);
1206             putNotificationIdentifiers(r, cv);
1207             if (eventType == EVENT_TYPE_POST) {
1208                 putNotificationDetails(r, cv);
1209             } else {
1210                 putPosttimeVisibility(r, cv);
1211             }
1212             SQLiteDatabase db = mHelper.getWritableDatabase();
1213             if (db.insert(TAB_LOG, null, cv) < 0) {
1214                 Log.wtf(TAG, "Error while trying to insert values: " + cv);
1215             }
1216             sNumWrites++;
1217             pruneIfNecessary(db);
1218         }
1219 
pruneIfNecessary(SQLiteDatabase db)1220         private void pruneIfNecessary(SQLiteDatabase db) {
1221             // Prune if we haven't in a while.
1222             long nowMs = System.currentTimeMillis();
1223             if (sNumWrites > PRUNE_MIN_WRITES ||
1224                     nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
1225                 sNumWrites = 0;
1226                 sLastPruneMs = nowMs;
1227                 long horizonStartMs = nowMs - HORIZON_MS;
1228                 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
1229                         new String[] { String.valueOf(horizonStartMs) });
1230                 Log.d(TAG, "Pruned event entries: " + deletedRows);
1231             }
1232         }
1233 
putNotificationIdentifiers(NotificationRecord r, ContentValues outCv)1234         private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
1235             outCv.put(COL_KEY, r.sbn.getKey());
1236             outCv.put(COL_PKG, r.sbn.getPackageName());
1237         }
1238 
putNotificationDetails(NotificationRecord r, ContentValues outCv)1239         private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
1240             outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
1241             if (r.sbn.getTag() != null) {
1242                 outCv.put(COL_TAG, r.sbn.getTag());
1243             }
1244             outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
1245             outCv.put(COL_FLAGS, r.getNotification().flags);
1246             final int before = r.stats.requestedImportance;
1247             final int after = r.getImportance();
1248             final boolean noisy = r.stats.isNoisy;
1249             outCv.put(COL_IMPORTANCE_REQ, before);
1250             outCv.put(COL_IMPORTANCE_FINAL, after);
1251             outCv.put(COL_DEMOTED, after < before ? 1 : 0);
1252             outCv.put(COL_NOISY, noisy);
1253             if (noisy && after < IMPORTANCE_HIGH) {
1254                 outCv.put(COL_MUTED, 1);
1255             } else {
1256                 outCv.put(COL_MUTED, 0);
1257             }
1258             if (r.getNotification().category != null) {
1259                 outCv.put(COL_CATEGORY, r.getNotification().category);
1260             }
1261             outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
1262                     r.getNotification().actions.length : 0);
1263         }
1264 
putPosttimeVisibility(NotificationRecord r, ContentValues outCv)1265         private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
1266             outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
1267             outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
1268             outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
1269             outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
1270             outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
1271         }
1272 
dump(PrintWriter pw, String indent, DumpFilter filter)1273         public void dump(PrintWriter pw, String indent, DumpFilter filter) {
1274             printPostFrequencies(pw, indent, filter);
1275         }
1276 
dumpJson(DumpFilter filter)1277         public JSONObject dumpJson(DumpFilter filter) {
1278             JSONObject dump = new JSONObject();
1279             try {
1280                 dump.put("post_frequency", jsonPostFrequencies(filter));
1281                 dump.put("since", filter.since);
1282                 dump.put("now", System.currentTimeMillis());
1283             } catch (JSONException e) {
1284                 // pass
1285             }
1286             return dump;
1287         }
1288     }
1289 }
1290