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