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