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 android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDatabase;
23import android.database.sqlite.SQLiteOpenHelper;
24import android.os.Handler;
25import android.os.HandlerThread;
26import android.os.Message;
27import android.os.SystemClock;
28import android.service.notification.StatusBarNotification;
29import android.util.Log;
30
31import com.android.server.notification.NotificationManagerService.DumpFilter;
32
33import java.io.PrintWriter;
34import java.util.HashMap;
35import java.util.Map;
36
37/**
38 * Keeps track of notification activity, display, and user interaction.
39 *
40 * <p>This class receives signals from NoMan and keeps running stats of
41 * notification usage. Some metrics are updated as events occur. Others, namely
42 * those involving durations, are updated as the notification is canceled.</p>
43 *
44 * <p>This class is thread-safe.</p>
45 *
46 * {@hide}
47 */
48public class NotificationUsageStats {
49    // WARNING: Aggregated stats can grow unboundedly with pkg+id+tag.
50    // Don't enable on production builds.
51    private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = false;
52    private static final boolean ENABLE_SQLITE_LOG = false;
53
54    private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
55
56    // Guarded by synchronized(this).
57    private final Map<String, AggregatedStats> mStats = new HashMap<String, AggregatedStats>();
58    private final SQLiteLog mSQLiteLog;
59
60    public NotificationUsageStats(Context context) {
61        mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
62    }
63
64    /**
65     * Called when a notification has been posted.
66     */
67    public synchronized void registerPostedByApp(NotificationRecord notification) {
68        notification.stats = new SingleNotificationStats();
69        notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
70        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
71            stats.numPostedByApp++;
72        }
73        if (ENABLE_SQLITE_LOG) {
74            mSQLiteLog.logPosted(notification);
75        }
76    }
77
78    /**
79     * Called when a notification has been updated.
80     */
81    public void registerUpdatedByApp(NotificationRecord notification, NotificationRecord old) {
82        notification.stats = old.stats;
83        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
84            stats.numUpdatedByApp++;
85        }
86    }
87
88    /**
89     * Called when the originating app removed the notification programmatically.
90     */
91    public synchronized void registerRemovedByApp(NotificationRecord notification) {
92        notification.stats.onRemoved();
93        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
94            stats.numRemovedByApp++;
95            stats.collect(notification.stats);
96        }
97        if (ENABLE_SQLITE_LOG) {
98            mSQLiteLog.logRemoved(notification);
99        }
100    }
101
102    /**
103     * Called when the user dismissed the notification via the UI.
104     */
105    public synchronized void registerDismissedByUser(NotificationRecord notification) {
106        notification.stats.onDismiss();
107        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
108            stats.numDismissedByUser++;
109            stats.collect(notification.stats);
110        }
111        if (ENABLE_SQLITE_LOG) {
112            mSQLiteLog.logDismissed(notification);
113        }
114    }
115
116    /**
117     * Called when the user clicked the notification in the UI.
118     */
119    public synchronized void registerClickedByUser(NotificationRecord notification) {
120        notification.stats.onClick();
121        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
122            stats.numClickedByUser++;
123        }
124        if (ENABLE_SQLITE_LOG) {
125            mSQLiteLog.logClicked(notification);
126        }
127    }
128
129    /**
130     * Called when the notification is canceled because the user clicked it.
131     *
132     * <p>Called after {@link #registerClickedByUser(NotificationRecord)}.</p>
133     */
134    public synchronized void registerCancelDueToClick(NotificationRecord notification) {
135        notification.stats.onCancel();
136        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
137            stats.collect(notification.stats);
138        }
139    }
140
141    /**
142     * Called when the notification is canceled due to unknown reasons.
143     *
144     * <p>Called for notifications of apps being uninstalled, for example.</p>
145     */
146    public synchronized void registerCancelUnknown(NotificationRecord notification) {
147        notification.stats.onCancel();
148        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
149            stats.collect(notification.stats);
150        }
151    }
152
153    // Locked by this.
154    private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
155        if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
156            return EMPTY_AGGREGATED_STATS;
157        }
158
159        StatusBarNotification n = record.sbn;
160
161        String user = String.valueOf(n.getUserId());
162        String userPackage = user + ":" + n.getPackageName();
163
164        // TODO: Use pool of arrays.
165        return new AggregatedStats[] {
166                getOrCreateAggregatedStatsLocked(user),
167                getOrCreateAggregatedStatsLocked(userPackage),
168                getOrCreateAggregatedStatsLocked(n.getKey()),
169        };
170    }
171
172    // Locked by this.
173    private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
174        AggregatedStats result = mStats.get(key);
175        if (result == null) {
176            result = new AggregatedStats(key);
177            mStats.put(key, result);
178        }
179        return result;
180    }
181
182    public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
183        if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
184            for (AggregatedStats as : mStats.values()) {
185                if (filter != null && !filter.matches(as.key))
186                    continue;
187                as.dump(pw, indent);
188            }
189        }
190        if (ENABLE_SQLITE_LOG) {
191            mSQLiteLog.dump(pw, indent, filter);
192        }
193    }
194
195    /**
196     * Aggregated notification stats.
197     */
198    private static class AggregatedStats {
199        public final String key;
200
201        // ---- Updated as the respective events occur.
202        public int numPostedByApp;
203        public int numUpdatedByApp;
204        public int numRemovedByApp;
205        public int numClickedByUser;
206        public int numDismissedByUser;
207
208        // ----  Updated when a notification is canceled.
209        public final Aggregate posttimeMs = new Aggregate();
210        public final Aggregate posttimeToDismissMs = new Aggregate();
211        public final Aggregate posttimeToFirstClickMs = new Aggregate();
212        public final Aggregate airtimeCount = new Aggregate();
213        public final Aggregate airtimeMs = new Aggregate();
214        public final Aggregate posttimeToFirstAirtimeMs = new Aggregate();
215        public final Aggregate userExpansionCount = new Aggregate();
216        public final Aggregate airtimeExpandedMs = new Aggregate();
217        public final Aggregate posttimeToFirstVisibleExpansionMs = new Aggregate();
218
219        public AggregatedStats(String key) {
220            this.key = key;
221        }
222
223        public void collect(SingleNotificationStats singleNotificationStats) {
224            posttimeMs.addSample(
225	            SystemClock.elapsedRealtime() - singleNotificationStats.posttimeElapsedMs);
226            if (singleNotificationStats.posttimeToDismissMs >= 0) {
227                posttimeToDismissMs.addSample(singleNotificationStats.posttimeToDismissMs);
228            }
229            if (singleNotificationStats.posttimeToFirstClickMs >= 0) {
230                posttimeToFirstClickMs.addSample(singleNotificationStats.posttimeToFirstClickMs);
231            }
232            airtimeCount.addSample(singleNotificationStats.airtimeCount);
233            if (singleNotificationStats.airtimeMs >= 0) {
234                airtimeMs.addSample(singleNotificationStats.airtimeMs);
235            }
236            if (singleNotificationStats.posttimeToFirstAirtimeMs >= 0) {
237                posttimeToFirstAirtimeMs.addSample(
238                        singleNotificationStats.posttimeToFirstAirtimeMs);
239            }
240            if (singleNotificationStats.posttimeToFirstVisibleExpansionMs >= 0) {
241                posttimeToFirstVisibleExpansionMs.addSample(
242                        singleNotificationStats.posttimeToFirstVisibleExpansionMs);
243            }
244            userExpansionCount.addSample(singleNotificationStats.userExpansionCount);
245            if (singleNotificationStats.airtimeExpandedMs >= 0) {
246                airtimeExpandedMs.addSample(singleNotificationStats.airtimeExpandedMs);
247            }
248        }
249
250        public void dump(PrintWriter pw, String indent) {
251            pw.println(toStringWithIndent(indent));
252        }
253
254        @Override
255        public String toString() {
256            return toStringWithIndent("");
257        }
258
259        private String toStringWithIndent(String indent) {
260            return indent + "AggregatedStats{\n" +
261                    indent + "  key='" + key + "',\n" +
262                    indent + "  numPostedByApp=" + numPostedByApp + ",\n" +
263                    indent + "  numUpdatedByApp=" + numUpdatedByApp + ",\n" +
264                    indent + "  numRemovedByApp=" + numRemovedByApp + ",\n" +
265                    indent + "  numClickedByUser=" + numClickedByUser + ",\n" +
266                    indent + "  numDismissedByUser=" + numDismissedByUser + ",\n" +
267                    indent + "  posttimeMs=" + posttimeMs + ",\n" +
268                    indent + "  posttimeToDismissMs=" + posttimeToDismissMs + ",\n" +
269                    indent + "  posttimeToFirstClickMs=" + posttimeToFirstClickMs + ",\n" +
270                    indent + "  airtimeCount=" + airtimeCount + ",\n" +
271                    indent + "  airtimeMs=" + airtimeMs + ",\n" +
272                    indent + "  posttimeToFirstAirtimeMs=" + posttimeToFirstAirtimeMs + ",\n" +
273                    indent + "  userExpansionCount=" + userExpansionCount + ",\n" +
274                    indent + "  airtimeExpandedMs=" + airtimeExpandedMs + ",\n" +
275                    indent + "  posttimeToFVEMs=" + posttimeToFirstVisibleExpansionMs + ",\n" +
276                    indent + "}";
277        }
278    }
279
280    /**
281     * Tracks usage of an individual notification that is currently active.
282     */
283    public static class SingleNotificationStats {
284        private boolean isVisible = false;
285        private boolean isExpanded = false;
286        /** SystemClock.elapsedRealtime() when the notification was posted. */
287        public long posttimeElapsedMs = -1;
288        /** Elapsed time since the notification was posted until it was first clicked, or -1. */
289        public long posttimeToFirstClickMs = -1;
290        /** Elpased time since the notification was posted until it was dismissed by the user. */
291        public long posttimeToDismissMs = -1;
292        /** Number of times the notification has been made visible. */
293        public long airtimeCount = 0;
294        /** Time in ms between the notification was posted and first shown; -1 if never shown. */
295        public long posttimeToFirstAirtimeMs = -1;
296        /**
297         * If currently visible, SystemClock.elapsedRealtime() when the notification was made
298         * visible; -1 otherwise.
299         */
300        public long currentAirtimeStartElapsedMs = -1;
301        /** Accumulated visible time. */
302        public long airtimeMs = 0;
303        /**
304         * Time in ms between the notification being posted and when it first
305         * became visible and expanded; -1 if it was never visibly expanded.
306         */
307        public long posttimeToFirstVisibleExpansionMs = -1;
308        /**
309         * If currently visible, SystemClock.elapsedRealtime() when the notification was made
310         * visible; -1 otherwise.
311         */
312        public long currentAirtimeExpandedStartElapsedMs = -1;
313        /** Accumulated visible expanded time. */
314        public long airtimeExpandedMs = 0;
315        /** Number of times the notification has been expanded by the user. */
316        public long userExpansionCount = 0;
317
318        public long getCurrentPosttimeMs() {
319            if (posttimeElapsedMs < 0) {
320                return 0;
321            }
322            return SystemClock.elapsedRealtime() - posttimeElapsedMs;
323        }
324
325        public long getCurrentAirtimeMs() {
326            long result = airtimeMs;
327            // Add incomplete airtime if currently shown.
328            if (currentAirtimeStartElapsedMs >= 0) {
329                result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
330            }
331            return result;
332        }
333
334        public long getCurrentAirtimeExpandedMs() {
335            long result = airtimeExpandedMs;
336            // Add incomplete expanded airtime if currently shown.
337            if (currentAirtimeExpandedStartElapsedMs >= 0) {
338                result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
339            }
340            return result;
341        }
342
343        /**
344         * Called when the user clicked the notification.
345         */
346        public void onClick() {
347            if (posttimeToFirstClickMs < 0) {
348                posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
349            }
350        }
351
352        /**
353         * Called when the user removed the notification.
354         */
355        public void onDismiss() {
356            if (posttimeToDismissMs < 0) {
357                posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
358            }
359            finish();
360        }
361
362        public void onCancel() {
363            finish();
364        }
365
366        public void onRemoved() {
367            finish();
368        }
369
370        public void onVisibilityChanged(boolean visible) {
371            long elapsedNowMs = SystemClock.elapsedRealtime();
372            final boolean wasVisible = isVisible;
373            isVisible = visible;
374            if (visible) {
375                if (currentAirtimeStartElapsedMs < 0) {
376                    airtimeCount++;
377                    currentAirtimeStartElapsedMs = elapsedNowMs;
378                }
379                if (posttimeToFirstAirtimeMs < 0) {
380                    posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
381                }
382            } else {
383                if (currentAirtimeStartElapsedMs >= 0) {
384                    airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
385                    currentAirtimeStartElapsedMs = -1;
386                }
387            }
388
389            if (wasVisible != isVisible) {
390                updateVisiblyExpandedStats();
391            }
392        }
393
394        public void onExpansionChanged(boolean userAction, boolean expanded) {
395            isExpanded = expanded;
396            if (isExpanded && userAction) {
397                userExpansionCount++;
398            }
399            updateVisiblyExpandedStats();
400        }
401
402        private void updateVisiblyExpandedStats() {
403            long elapsedNowMs = SystemClock.elapsedRealtime();
404            if (isExpanded && isVisible) {
405                // expanded and visible
406                if (currentAirtimeExpandedStartElapsedMs < 0) {
407                    currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
408                }
409                if (posttimeToFirstVisibleExpansionMs < 0) {
410                    posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
411                }
412            } else {
413                // not-expanded or not-visible
414                if (currentAirtimeExpandedStartElapsedMs >= 0) {
415                    airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
416                    currentAirtimeExpandedStartElapsedMs = -1;
417                }
418            }
419        }
420
421        /** The notification is leaving the system. Finalize. */
422        public void finish() {
423            onVisibilityChanged(false);
424        }
425
426        @Override
427        public String toString() {
428            return "SingleNotificationStats{" +
429                    "posttimeElapsedMs=" + posttimeElapsedMs +
430                    ", posttimeToFirstClickMs=" + posttimeToFirstClickMs +
431                    ", posttimeToDismissMs=" + posttimeToDismissMs +
432                    ", airtimeCount=" + airtimeCount +
433                    ", airtimeMs=" + airtimeMs +
434                    ", currentAirtimeStartElapsedMs=" + currentAirtimeStartElapsedMs +
435                    ", airtimeExpandedMs=" + airtimeExpandedMs +
436                    ", posttimeToFirstVisibleExpansionMs=" + posttimeToFirstVisibleExpansionMs +
437                    ", currentAirtimeExpandedSEMs=" + currentAirtimeExpandedStartElapsedMs +
438                    '}';
439        }
440    }
441
442    /**
443     * Aggregates long samples to sum and averages.
444     */
445    public static class Aggregate {
446        long numSamples;
447        double avg;
448        double sum2;
449        double var;
450
451        public void addSample(long sample) {
452            // Welford's "Method for Calculating Corrected Sums of Squares"
453            // http://www.jstor.org/stable/1266577?seq=2
454            numSamples++;
455            final double n = numSamples;
456            final double delta = sample - avg;
457            avg += (1.0 / n) * delta;
458            sum2 += ((n - 1) / n) * delta * delta;
459            final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
460            var = sum2 / divisor;
461        }
462
463        @Override
464        public String toString() {
465            return "Aggregate{" +
466                    "numSamples=" + numSamples +
467                    ", avg=" + avg +
468                    ", var=" + var +
469                    '}';
470        }
471    }
472
473    private static class SQLiteLog {
474        private static final String TAG = "NotificationSQLiteLog";
475
476        // Message types passed to the background handler.
477        private static final int MSG_POST = 1;
478        private static final int MSG_CLICK = 2;
479        private static final int MSG_REMOVE = 3;
480        private static final int MSG_DISMISS = 4;
481
482        private static final String DB_NAME = "notification_log.db";
483        private static final int DB_VERSION = 4;
484
485        /** Age in ms after which events are pruned from the DB. */
486        private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L;  // 1 week
487        /** Delay between pruning the DB. Used to throttle pruning. */
488        private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L;  // 6 hours
489        /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
490        private static final long PRUNE_MIN_WRITES = 1024;
491
492        // Table 'log'
493        private static final String TAB_LOG = "log";
494        private static final String COL_EVENT_USER_ID = "event_user_id";
495        private static final String COL_EVENT_TYPE = "event_type";
496        private static final String COL_EVENT_TIME = "event_time_ms";
497        private static final String COL_KEY = "key";
498        private static final String COL_PKG = "pkg";
499        private static final String COL_NOTIFICATION_ID = "nid";
500        private static final String COL_TAG = "tag";
501        private static final String COL_WHEN_MS = "when_ms";
502        private static final String COL_DEFAULTS = "defaults";
503        private static final String COL_FLAGS = "flags";
504        private static final String COL_PRIORITY = "priority";
505        private static final String COL_CATEGORY = "category";
506        private static final String COL_ACTION_COUNT = "action_count";
507        private static final String COL_POSTTIME_MS = "posttime_ms";
508        private static final String COL_AIRTIME_MS = "airtime_ms";
509        private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
510        private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
511        private static final String COL_EXPAND_COUNT = "expansion_count";
512
513
514        private static final int EVENT_TYPE_POST = 1;
515        private static final int EVENT_TYPE_CLICK = 2;
516        private static final int EVENT_TYPE_REMOVE = 3;
517        private static final int EVENT_TYPE_DISMISS = 4;
518
519        private static long sLastPruneMs;
520        private static long sNumWrites;
521
522        private final SQLiteOpenHelper mHelper;
523        private final Handler mWriteHandler;
524
525        private static final long DAY_MS = 24 * 60 * 60 * 1000;
526
527        public SQLiteLog(Context context) {
528            HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
529                    android.os.Process.THREAD_PRIORITY_BACKGROUND);
530            backgroundThread.start();
531            mWriteHandler = new Handler(backgroundThread.getLooper()) {
532                @Override
533                public void handleMessage(Message msg) {
534                    NotificationRecord r = (NotificationRecord) msg.obj;
535                    long nowMs = System.currentTimeMillis();
536                    switch (msg.what) {
537                        case MSG_POST:
538                            writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
539                            break;
540                        case MSG_CLICK:
541                            writeEvent(nowMs, EVENT_TYPE_CLICK, r);
542                            break;
543                        case MSG_REMOVE:
544                            writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
545                            break;
546                        case MSG_DISMISS:
547                            writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
548                            break;
549                        default:
550                            Log.wtf(TAG, "Unknown message type: " + msg.what);
551                            break;
552                    }
553                }
554            };
555            mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
556                @Override
557                public void onCreate(SQLiteDatabase db) {
558                    db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
559                            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
560                            COL_EVENT_USER_ID + " INT," +
561                            COL_EVENT_TYPE + " INT," +
562                            COL_EVENT_TIME + " INT," +
563                            COL_KEY + " TEXT," +
564                            COL_PKG + " TEXT," +
565                            COL_NOTIFICATION_ID + " INT," +
566                            COL_TAG + " TEXT," +
567                            COL_WHEN_MS + " INT," +
568                            COL_DEFAULTS + " INT," +
569                            COL_FLAGS + " INT," +
570                            COL_PRIORITY + " INT," +
571                            COL_CATEGORY + " TEXT," +
572                            COL_ACTION_COUNT + " INT," +
573                            COL_POSTTIME_MS + " INT," +
574                            COL_AIRTIME_MS + " INT," +
575                            COL_FIRST_EXPANSIONTIME_MS + " INT," +
576                            COL_AIRTIME_EXPANDED_MS + " INT," +
577                            COL_EXPAND_COUNT + " INT" +
578                            ")");
579                }
580
581                @Override
582                public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
583                    if (oldVersion <= 3) {
584                        // Version 3 creation left 'log' in a weird state. Just reset for now.
585                        db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
586                        onCreate(db);
587                    }
588                }
589            };
590        }
591
592        public void logPosted(NotificationRecord notification) {
593            mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
594        }
595
596        public void logClicked(NotificationRecord notification) {
597            mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
598        }
599
600        public void logRemoved(NotificationRecord notification) {
601            mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
602        }
603
604        public void logDismissed(NotificationRecord notification) {
605            mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
606        }
607
608        public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
609            SQLiteDatabase db = mHelper.getReadableDatabase();
610            long nowMs = System.currentTimeMillis();
611            String q = "SELECT " +
612                    COL_EVENT_USER_ID + ", " +
613                    COL_PKG + ", " +
614                    // Bucket by day by looking at 'floor((nowMs - eventTimeMs) / dayMs)'
615                    "CAST(((" + nowMs + " - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
616                        "AS day, " +
617                    "COUNT(*) AS cnt " +
618                    "FROM " + TAB_LOG + " " +
619                    "WHERE " +
620                    COL_EVENT_TYPE + "=" + EVENT_TYPE_POST + " " +
621                    "GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
622            Cursor cursor = db.rawQuery(q, null);
623            try {
624                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
625                    int userId = cursor.getInt(0);
626                    String pkg = cursor.getString(1);
627                    if (filter != null && !filter.matches(pkg)) continue;
628                    int day = cursor.getInt(2);
629                    int count = cursor.getInt(3);
630                    pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
631                            ",day=" + day + ",count=" + count + "}");
632                }
633            } finally {
634                cursor.close();
635            }
636        }
637
638        private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
639            ContentValues cv = new ContentValues();
640            cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
641            cv.put(COL_EVENT_TIME, eventTimeMs);
642            cv.put(COL_EVENT_TYPE, eventType);
643            putNotificationIdentifiers(r, cv);
644            if (eventType == EVENT_TYPE_POST) {
645                putNotificationDetails(r, cv);
646            } else {
647                putPosttimeVisibility(r, cv);
648            }
649            SQLiteDatabase db = mHelper.getWritableDatabase();
650            if (db.insert(TAB_LOG, null, cv) < 0) {
651                Log.wtf(TAG, "Error while trying to insert values: " + cv);
652            }
653            sNumWrites++;
654            pruneIfNecessary(db);
655        }
656
657        private void pruneIfNecessary(SQLiteDatabase db) {
658            // Prune if we haven't in a while.
659            long nowMs = System.currentTimeMillis();
660            if (sNumWrites > PRUNE_MIN_WRITES ||
661                    nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
662                sNumWrites = 0;
663                sLastPruneMs = nowMs;
664                long horizonStartMs = nowMs - HORIZON_MS;
665                int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
666                        new String[] { String.valueOf(horizonStartMs) });
667                Log.d(TAG, "Pruned event entries: " + deletedRows);
668            }
669        }
670
671        private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
672            outCv.put(COL_KEY, r.sbn.getKey());
673            outCv.put(COL_PKG, r.sbn.getPackageName());
674        }
675
676        private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
677            outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
678            if (r.sbn.getTag() != null) {
679                outCv.put(COL_TAG, r.sbn.getTag());
680            }
681            outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
682            outCv.put(COL_FLAGS, r.getNotification().flags);
683            outCv.put(COL_PRIORITY, r.getNotification().priority);
684            if (r.getNotification().category != null) {
685                outCv.put(COL_CATEGORY, r.getNotification().category);
686            }
687            outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
688                    r.getNotification().actions.length : 0);
689        }
690
691        private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
692            outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
693            outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
694            outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
695            outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
696            outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
697        }
698
699        public void dump(PrintWriter pw, String indent, DumpFilter filter) {
700            printPostFrequencies(pw, indent, filter);
701        }
702    }
703}
704