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