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