OSDN Git Service

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