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