Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.server.notification;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.database.sqlite.SQLiteDatabase;
     23 import android.database.sqlite.SQLiteOpenHelper;
     24 import android.os.Handler;
     25 import android.os.HandlerThread;
     26 import android.os.Message;
     27 import android.os.SystemClock;
     28 import android.service.notification.StatusBarNotification;
     29 import android.util.Log;
     30 
     31 import com.android.server.notification.NotificationManagerService.DumpFilter;
     32 
     33 import java.io.PrintWriter;
     34 import java.util.HashMap;
     35 import 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  */
     48 public 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