Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.server.notification;
     18 
     19 import android.app.Notification;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.database.sqlite.SQLiteDatabase;
     24 import android.database.sqlite.SQLiteOpenHelper;
     25 import android.os.Handler;
     26 import android.os.HandlerThread;
     27 import android.os.Message;
     28 import android.os.SystemClock;
     29 import android.util.Log;
     30 
     31 import com.android.internal.logging.MetricsLogger;
     32 import com.android.server.notification.NotificationManagerService.DumpFilter;
     33 
     34 import org.json.JSONArray;
     35 import org.json.JSONException;
     36 import org.json.JSONObject;
     37 
     38 import java.io.PrintWriter;
     39 import java.util.ArrayDeque;
     40 import java.util.Calendar;
     41 import java.util.GregorianCalendar;
     42 import java.util.HashMap;
     43 import java.util.Map;
     44 
     45 /**
     46  * Keeps track of notification activity, display, and user interaction.
     47  *
     48  * <p>This class receives signals from NoMan and keeps running stats of
     49  * notification usage. Some metrics are updated as events occur. Others, namely
     50  * those involving durations, are updated as the notification is canceled.</p>
     51  *
     52  * <p>This class is thread-safe.</p>
     53  *
     54  * {@hide}
     55  */
     56 public class NotificationUsageStats {
     57     private static final String TAG = "NotificationUsageStats";
     58 
     59     private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
     60     private static final boolean ENABLE_SQLITE_LOG = true;
     61     private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
     62     private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
     63     private static final int MSG_EMIT = 1;
     64 
     65     private static final boolean DEBUG = false;
     66     public static final int TEN_SECONDS = 1000 * 10;
     67     public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
     68     private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
     69 
     70     // Guarded by synchronized(this).
     71     private final Map<String, AggregatedStats> mStats = new HashMap<>();
     72     private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
     73     private final SQLiteLog mSQLiteLog;
     74     private final Context mContext;
     75     private final Handler mHandler;
     76     private long mLastEmitTime;
     77 
     78     public NotificationUsageStats(Context context) {
     79         mContext = context;
     80         mLastEmitTime = SystemClock.elapsedRealtime();
     81         mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
     82         mHandler = new Handler(mContext.getMainLooper()) {
     83             @Override
     84             public void handleMessage(Message msg) {
     85                 switch (msg.what) {
     86                     case MSG_EMIT:
     87                         emit();
     88                         break;
     89                     default:
     90                         Log.wtf(TAG, "Unknown message type: " + msg.what);
     91                         break;
     92                 }
     93             }
     94         };
     95         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
     96     }
     97 
     98     /**
     99      * Called when a notification has been posted.
    100      */
    101     public synchronized void registerPostedByApp(NotificationRecord notification) {
    102         notification.stats = new SingleNotificationStats();
    103         notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
    104 
    105         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
    106         for (AggregatedStats stats : aggregatedStatsArray) {
    107             stats.numPostedByApp++;
    108             stats.countApiUse(notification);
    109         }
    110         releaseAggregatedStatsLocked(aggregatedStatsArray);
    111         if (ENABLE_SQLITE_LOG) {
    112             mSQLiteLog.logPosted(notification);
    113         }
    114     }
    115 
    116     /**
    117      * Called when a notification has been updated.
    118      */
    119     public void registerUpdatedByApp(NotificationRecord notification, NotificationRecord old) {
    120         notification.stats = old.stats;
    121         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
    122         for (AggregatedStats stats : aggregatedStatsArray) {
    123             stats.numUpdatedByApp++;
    124             stats.countApiUse(notification);
    125         }
    126         releaseAggregatedStatsLocked(aggregatedStatsArray);
    127     }
    128 
    129     /**
    130      * Called when the originating app removed the notification programmatically.
    131      */
    132     public synchronized void registerRemovedByApp(NotificationRecord notification) {
    133         notification.stats.onRemoved();
    134         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
    135         for (AggregatedStats stats : aggregatedStatsArray) {
    136             stats.numRemovedByApp++;
    137         }
    138         releaseAggregatedStatsLocked(aggregatedStatsArray);
    139         if (ENABLE_SQLITE_LOG) {
    140             mSQLiteLog.logRemoved(notification);
    141         }
    142     }
    143 
    144     /**
    145      * Called when the user dismissed the notification via the UI.
    146      */
    147     public synchronized void registerDismissedByUser(NotificationRecord notification) {
    148         MetricsLogger.histogram(mContext, "note_dismiss_longevity",
    149                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
    150         notification.stats.onDismiss();
    151         if (ENABLE_SQLITE_LOG) {
    152             mSQLiteLog.logDismissed(notification);
    153         }
    154     }
    155 
    156     /**
    157      * Called when the user clicked the notification in the UI.
    158      */
    159     public synchronized void registerClickedByUser(NotificationRecord notification) {
    160         MetricsLogger.histogram(mContext, "note_click_longevity",
    161                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
    162         notification.stats.onClick();
    163         if (ENABLE_SQLITE_LOG) {
    164             mSQLiteLog.logClicked(notification);
    165         }
    166     }
    167 
    168     public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
    169             boolean starred, boolean cached) {
    170         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
    171         for (AggregatedStats stats : aggregatedStatsArray) {
    172             if (valid) {
    173                 stats.numWithValidPeople++;
    174             }
    175             if (starred) {
    176                 stats.numWithStaredPeople++;
    177             }
    178             if (cached) {
    179                 stats.numPeopleCacheHit++;
    180             } else {
    181                 stats.numPeopleCacheMiss++;
    182             }
    183         }
    184         releaseAggregatedStatsLocked(aggregatedStatsArray);
    185     }
    186 
    187     public synchronized void registerBlocked(NotificationRecord notification) {
    188         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
    189         for (AggregatedStats stats : aggregatedStatsArray) {
    190             stats.numBlocked++;
    191         }
    192         releaseAggregatedStatsLocked(aggregatedStatsArray);
    193     }
    194 
    195     // Locked by this.
    196     private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
    197         if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
    198             return EMPTY_AGGREGATED_STATS;
    199         }
    200 
    201         // TODO: expand to package-level counts in the future.
    202         AggregatedStats[] array = mStatsArrays.poll();
    203         if (array == null) {
    204             array = new AggregatedStats[1];
    205         }
    206         array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
    207         return array;
    208     }
    209 
    210     // Locked by this.
    211     private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
    212         for(int i = 0; i < array.length; i++) {
    213             array[i] = null;
    214         }
    215         mStatsArrays.offer(array);
    216     }
    217 
    218     // Locked by this.
    219     private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
    220         AggregatedStats result = mStats.get(key);
    221         if (result == null) {
    222             result = new AggregatedStats(mContext, key);
    223             mStats.put(key, result);
    224         }
    225         return result;
    226     }
    227 
    228     public synchronized JSONObject dumpJson(DumpFilter filter) {
    229         JSONObject dump = new JSONObject();
    230         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
    231             try {
    232                 JSONArray aggregatedStats = new JSONArray();
    233                 for (AggregatedStats as : mStats.values()) {
    234                     if (filter != null && !filter.matches(as.key))
    235                         continue;
    236                     aggregatedStats.put(as.dumpJson());
    237                 }
    238                 dump.put("current", aggregatedStats);
    239             } catch (JSONException e) {
    240                 // pass
    241             }
    242         }
    243         if (ENABLE_SQLITE_LOG) {
    244             try {
    245                 dump.put("historical", mSQLiteLog.dumpJson(filter));
    246             } catch (JSONException e) {
    247                 // pass
    248             }
    249         }
    250         return dump;
    251     }
    252 
    253     public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
    254         if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
    255             for (AggregatedStats as : mStats.values()) {
    256                 if (filter != null && !filter.matches(as.key))
    257                     continue;
    258                 as.dump(pw, indent);
    259             }
    260             pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
    261         }
    262         if (ENABLE_SQLITE_LOG) {
    263             mSQLiteLog.dump(pw, indent, filter);
    264         }
    265     }
    266 
    267     public synchronized void emit() {
    268         // TODO: expand to package-level counts in the future.
    269         AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
    270         stats.emit();
    271         mLastEmitTime = SystemClock.elapsedRealtime();
    272         mHandler.removeMessages(MSG_EMIT);
    273         mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
    274     }
    275 
    276     /**
    277      * Aggregated notification stats.
    278      */
    279     private static class AggregatedStats {
    280 
    281         private final Context mContext;
    282         public final String key;
    283         private final long mCreated;
    284         private AggregatedStats mPrevious;
    285 
    286         // ---- Updated as the respective events occur.
    287         public int numPostedByApp;
    288         public int numUpdatedByApp;
    289         public int numRemovedByApp;
    290         public int numPeopleCacheHit;
    291         public int numPeopleCacheMiss;;
    292         public int numWithStaredPeople;
    293         public int numWithValidPeople;
    294         public int numBlocked;
    295         public int numWithActions;
    296         public int numPrivate;
    297         public int numSecret;
    298         public int numPriorityMax;
    299         public int numPriorityHigh;
    300         public int numPriorityLow;
    301         public int numPriorityMin;
    302         public int numWithBigText;
    303         public int numWithBigPicture;
    304         public int numForegroundService;
    305         public int numOngoing;
    306         public int numAutoCancel;
    307         public int numWithLargeIcon;
    308         public int numWithInbox;
    309         public int numWithMediaSession;
    310         public int numWithTitle;
    311         public int numWithText;
    312         public int numWithSubText;
    313         public int numWithInfoText;
    314         public int numInterrupt;
    315 
    316         public AggregatedStats(Context context, String key) {
    317             this.key = key;
    318             mContext = context;
    319             mCreated = SystemClock.elapsedRealtime();
    320         }
    321 
    322         public void countApiUse(NotificationRecord record) {
    323             final Notification n = record.getNotification();
    324             if (n.actions != null) {
    325                 numWithActions++;
    326             }
    327 
    328             if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
    329                 numForegroundService++;
    330             }
    331 
    332             if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
    333                 numOngoing++;
    334             }
    335 
    336             if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
    337                 numAutoCancel++;
    338             }
    339 
    340             if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
    341                     (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
    342                     n.sound != null || n.vibrate != null) {
    343                 numInterrupt++;
    344             }
    345 
    346             switch (n.visibility) {
    347                 case Notification.VISIBILITY_PRIVATE:
    348                     numPrivate++;
    349                     break;
    350                 case Notification.VISIBILITY_SECRET:
    351                     numSecret++;
    352                     break;
    353             }
    354 
    355             switch (n.priority) {
    356                 case Notification.PRIORITY_MAX:
    357                     numPriorityMax++;
    358                     break;
    359                 case Notification.PRIORITY_HIGH:
    360                     numPriorityHigh++;
    361                     break;
    362                 case Notification.PRIORITY_LOW:
    363                     numPriorityLow++;
    364                     break;
    365                 case Notification.PRIORITY_MIN:
    366                     numPriorityMin++;
    367                     break;
    368             }
    369 
    370             for (String Key : n.extras.keySet()) {
    371                 if (Notification.EXTRA_BIG_TEXT.equals(key)) {
    372                     numWithBigText++;
    373                 } else if (Notification.EXTRA_PICTURE.equals(key)) {
    374                     numWithBigPicture++;
    375                 } else if (Notification.EXTRA_LARGE_ICON.equals(key)) {
    376                     numWithLargeIcon++;
    377                 } else if (Notification.EXTRA_TEXT_LINES.equals(key)) {
    378                     numWithInbox++;
    379                 } else if (Notification.EXTRA_MEDIA_SESSION.equals(key)) {
    380                     numWithMediaSession++;
    381                 } else if (Notification.EXTRA_TITLE.equals(key)) {
    382                     numWithTitle++;
    383                 } else if (Notification.EXTRA_TEXT.equals(key)) {
    384                     numWithText++;
    385                 } else if (Notification.EXTRA_SUB_TEXT.equals(key)) {
    386                     numWithSubText++;
    387                 } else if (Notification.EXTRA_INFO_TEXT.equals(key)) {
    388                     numWithInfoText++;
    389                 }
    390             }
    391         }
    392 
    393         public void emit() {
    394             if (mPrevious == null) {
    395                 mPrevious = new AggregatedStats(null, key);
    396             }
    397 
    398             maybeCount("note_post", (numPostedByApp - mPrevious.numPostedByApp));
    399             maybeCount("note_update", (numUpdatedByApp - mPrevious.numUpdatedByApp));
    400             maybeCount("note_remove", (numRemovedByApp - mPrevious.numRemovedByApp));
    401             maybeCount("note_with_people", (numWithValidPeople - mPrevious.numWithValidPeople));
    402             maybeCount("note_with_stars", (numWithStaredPeople - mPrevious.numWithStaredPeople));
    403             maybeCount("people_cache_hit", (numPeopleCacheHit - mPrevious.numPeopleCacheHit));
    404             maybeCount("people_cache_miss", (numPeopleCacheMiss - mPrevious.numPeopleCacheMiss));
    405             maybeCount("note_blocked", (numBlocked - mPrevious.numBlocked));
    406             maybeCount("note_with_actions", (numWithActions - mPrevious.numWithActions));
    407             maybeCount("note_private", (numPrivate - mPrevious.numPrivate));
    408             maybeCount("note_secret", (numSecret - mPrevious.numSecret));
    409             maybeCount("note_prio_max", (numPriorityMax - mPrevious.numPriorityMax));
    410             maybeCount("note_prio_high", (numPriorityHigh - mPrevious.numPriorityHigh));
    411             maybeCount("note_prio_low", (numPriorityLow - mPrevious.numPriorityLow));
    412             maybeCount("note_prio_min", (numPriorityMin - mPrevious.numPriorityMin));
    413             maybeCount("note_interupt", (numInterrupt - mPrevious.numInterrupt));
    414             maybeCount("note_big_text", (numWithBigText - mPrevious.numWithBigText));
    415             maybeCount("note_big_pic", (numWithBigPicture - mPrevious.numWithBigPicture));
    416             maybeCount("note_fg", (numForegroundService - mPrevious.numForegroundService));
    417             maybeCount("note_ongoing", (numOngoing - mPrevious.numOngoing));
    418             maybeCount("note_auto", (numAutoCancel - mPrevious.numAutoCancel));
    419             maybeCount("note_large_icon", (numWithLargeIcon - mPrevious.numWithLargeIcon));
    420             maybeCount("note_inbox", (numWithInbox - mPrevious.numWithInbox));
    421             maybeCount("note_media", (numWithMediaSession - mPrevious.numWithMediaSession));
    422             maybeCount("note_title", (numWithTitle - mPrevious.numWithTitle));
    423             maybeCount("note_text", (numWithText - mPrevious.numWithText));
    424             maybeCount("note_sub_text", (numWithSubText - mPrevious.numWithSubText));
    425             maybeCount("note_info_text", (numWithInfoText - mPrevious.numWithInfoText));
    426 
    427             mPrevious.numPostedByApp = numPostedByApp;
    428             mPrevious.numUpdatedByApp = numUpdatedByApp;
    429             mPrevious.numRemovedByApp = numRemovedByApp;
    430             mPrevious.numPeopleCacheHit = numPeopleCacheHit;
    431             mPrevious.numPeopleCacheMiss = numPeopleCacheMiss;
    432             mPrevious.numWithStaredPeople = numWithStaredPeople;
    433             mPrevious.numWithValidPeople = numWithValidPeople;
    434             mPrevious.numBlocked = numBlocked;
    435             mPrevious.numWithActions = numWithActions;
    436             mPrevious.numPrivate = numPrivate;
    437             mPrevious.numSecret = numSecret;
    438             mPrevious.numPriorityMax = numPriorityMax;
    439             mPrevious.numPriorityHigh = numPriorityHigh;
    440             mPrevious.numPriorityLow = numPriorityLow;
    441             mPrevious.numPriorityMin = numPriorityMin;
    442             mPrevious.numInterrupt = numInterrupt;
    443             mPrevious.numWithBigText = numWithBigText;
    444             mPrevious.numWithBigPicture = numWithBigPicture;
    445             mPrevious.numForegroundService = numForegroundService;
    446             mPrevious.numOngoing = numOngoing;
    447             mPrevious.numAutoCancel = numAutoCancel;
    448             mPrevious.numWithLargeIcon = numWithLargeIcon;
    449             mPrevious.numWithInbox = numWithInbox;
    450             mPrevious.numWithMediaSession = numWithMediaSession;
    451             mPrevious.numWithTitle = numWithTitle;
    452             mPrevious.numWithText = numWithText;
    453             mPrevious.numWithSubText = numWithSubText;
    454             mPrevious.numWithInfoText = numWithInfoText;
    455         }
    456 
    457         void maybeCount(String name, int value) {
    458             if (value > 0) {
    459                 MetricsLogger.count(mContext, name, value);
    460             }
    461         }
    462 
    463         public void dump(PrintWriter pw, String indent) {
    464             pw.println(toStringWithIndent(indent));
    465         }
    466 
    467         @Override
    468         public String toString() {
    469             return toStringWithIndent("");
    470         }
    471 
    472         private String toStringWithIndent(String indent) {
    473             return indent + "AggregatedStats{\n" +
    474                     indent + "  key='" + key + "',\n" +
    475                     indent + "  numPostedByApp=" + numPostedByApp + ",\n" +
    476                     indent + "  numUpdatedByApp=" + numUpdatedByApp + ",\n" +
    477                     indent + "  numRemovedByApp=" + numRemovedByApp + ",\n" +
    478                     indent + "  numPeopleCacheHit=" + numPeopleCacheHit + ",\n" +
    479                     indent + "  numWithStaredPeople=" + numWithStaredPeople + ",\n" +
    480                     indent + "  numWithValidPeople=" + numWithValidPeople + ",\n" +
    481                     indent + "  numPeopleCacheMiss=" + numPeopleCacheMiss + ",\n" +
    482                     indent + "  numBlocked=" + numBlocked + ",\n" +
    483                     indent + "}";
    484         }
    485 
    486         public JSONObject dumpJson() throws JSONException {
    487             JSONObject dump = new JSONObject();
    488             dump.put("key", key);
    489             dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
    490             maybePut(dump, "numPostedByApp", numPostedByApp);
    491             maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
    492             maybePut(dump, "numRemovedByApp", numRemovedByApp);
    493             maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
    494             maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
    495             maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
    496             maybePut(dump, "numWithValidPeople", numWithValidPeople);
    497             maybePut(dump, "numBlocked", numBlocked);
    498             maybePut(dump, "numWithActions", numWithActions);
    499             maybePut(dump, "numPrivate", numPrivate);
    500             maybePut(dump, "numSecret", numSecret);
    501             maybePut(dump, "numPriorityMax", numPriorityMax);
    502             maybePut(dump, "numPriorityHigh", numPriorityHigh);
    503             maybePut(dump, "numPriorityLow", numPriorityLow);
    504             maybePut(dump, "numPriorityMin", numPriorityMin);
    505             maybePut(dump, "numInterrupt", numInterrupt);
    506             maybePut(dump, "numWithBigText", numWithBigText);
    507             maybePut(dump, "numWithBigPicture", numWithBigPicture);
    508             maybePut(dump, "numForegroundService", numForegroundService);
    509             maybePut(dump, "numOngoing", numOngoing);
    510             maybePut(dump, "numAutoCancel", numAutoCancel);
    511             maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
    512             maybePut(dump, "numWithInbox", numWithInbox);
    513             maybePut(dump, "numWithMediaSession", numWithMediaSession);
    514             maybePut(dump, "numWithTitle", numWithTitle);
    515             maybePut(dump, "numWithText", numWithText);
    516             maybePut(dump, "numWithSubText", numWithSubText);
    517             maybePut(dump, "numWithInfoText", numWithInfoText);
    518             return dump;
    519         }
    520 
    521         private void maybePut(JSONObject dump, String name, int value) throws JSONException {
    522             if (value > 0) {
    523                 dump.put(name, value);
    524             }
    525         }
    526     }
    527 
    528     /**
    529      * Tracks usage of an individual notification that is currently active.
    530      */
    531     public static class SingleNotificationStats {
    532         private boolean isVisible = false;
    533         private boolean isExpanded = false;
    534         /** SystemClock.elapsedRealtime() when the notification was posted. */
    535         public long posttimeElapsedMs = -1;
    536         /** Elapsed time since the notification was posted until it was first clicked, or -1. */
    537         public long posttimeToFirstClickMs = -1;
    538         /** Elpased time since the notification was posted until it was dismissed by the user. */
    539         public long posttimeToDismissMs = -1;
    540         /** Number of times the notification has been made visible. */
    541         public long airtimeCount = 0;
    542         /** Time in ms between the notification was posted and first shown; -1 if never shown. */
    543         public long posttimeToFirstAirtimeMs = -1;
    544         /**
    545          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
    546          * visible; -1 otherwise.
    547          */
    548         public long currentAirtimeStartElapsedMs = -1;
    549         /** Accumulated visible time. */
    550         public long airtimeMs = 0;
    551         /**
    552          * Time in ms between the notification being posted and when it first
    553          * became visible and expanded; -1 if it was never visibly expanded.
    554          */
    555         public long posttimeToFirstVisibleExpansionMs = -1;
    556         /**
    557          * If currently visible, SystemClock.elapsedRealtime() when the notification was made
    558          * visible; -1 otherwise.
    559          */
    560         public long currentAirtimeExpandedStartElapsedMs = -1;
    561         /** Accumulated visible expanded time. */
    562         public long airtimeExpandedMs = 0;
    563         /** Number of times the notification has been expanded by the user. */
    564         public long userExpansionCount = 0;
    565 
    566         public long getCurrentPosttimeMs() {
    567             if (posttimeElapsedMs < 0) {
    568                 return 0;
    569             }
    570             return SystemClock.elapsedRealtime() - posttimeElapsedMs;
    571         }
    572 
    573         public long getCurrentAirtimeMs() {
    574             long result = airtimeMs;
    575             // Add incomplete airtime if currently shown.
    576             if (currentAirtimeStartElapsedMs >= 0) {
    577                 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
    578             }
    579             return result;
    580         }
    581 
    582         public long getCurrentAirtimeExpandedMs() {
    583             long result = airtimeExpandedMs;
    584             // Add incomplete expanded airtime if currently shown.
    585             if (currentAirtimeExpandedStartElapsedMs >= 0) {
    586                 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
    587             }
    588             return result;
    589         }
    590 
    591         /**
    592          * Called when the user clicked the notification.
    593          */
    594         public void onClick() {
    595             if (posttimeToFirstClickMs < 0) {
    596                 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
    597             }
    598         }
    599 
    600         /**
    601          * Called when the user removed the notification.
    602          */
    603         public void onDismiss() {
    604             if (posttimeToDismissMs < 0) {
    605                 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
    606             }
    607             finish();
    608         }
    609 
    610         public void onCancel() {
    611             finish();
    612         }
    613 
    614         public void onRemoved() {
    615             finish();
    616         }
    617 
    618         public void onVisibilityChanged(boolean visible) {
    619             long elapsedNowMs = SystemClock.elapsedRealtime();
    620             final boolean wasVisible = isVisible;
    621             isVisible = visible;
    622             if (visible) {
    623                 if (currentAirtimeStartElapsedMs < 0) {
    624                     airtimeCount++;
    625                     currentAirtimeStartElapsedMs = elapsedNowMs;
    626                 }
    627                 if (posttimeToFirstAirtimeMs < 0) {
    628                     posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
    629                 }
    630             } else {
    631                 if (currentAirtimeStartElapsedMs >= 0) {
    632                     airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
    633                     currentAirtimeStartElapsedMs = -1;
    634                 }
    635             }
    636 
    637             if (wasVisible != isVisible) {
    638                 updateVisiblyExpandedStats();
    639             }
    640         }
    641 
    642         public void onExpansionChanged(boolean userAction, boolean expanded) {
    643             isExpanded = expanded;
    644             if (isExpanded && userAction) {
    645                 userExpansionCount++;
    646             }
    647             updateVisiblyExpandedStats();
    648         }
    649 
    650         private void updateVisiblyExpandedStats() {
    651             long elapsedNowMs = SystemClock.elapsedRealtime();
    652             if (isExpanded && isVisible) {
    653                 // expanded and visible
    654                 if (currentAirtimeExpandedStartElapsedMs < 0) {
    655                     currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
    656                 }
    657                 if (posttimeToFirstVisibleExpansionMs < 0) {
    658                     posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
    659                 }
    660             } else {
    661                 // not-expanded or not-visible
    662                 if (currentAirtimeExpandedStartElapsedMs >= 0) {
    663                     airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
    664                     currentAirtimeExpandedStartElapsedMs = -1;
    665                 }
    666             }
    667         }
    668 
    669         /** The notification is leaving the system. Finalize. */
    670         public void finish() {
    671             onVisibilityChanged(false);
    672         }
    673 
    674         @Override
    675         public String toString() {
    676             return "SingleNotificationStats{" +
    677                     "posttimeElapsedMs=" + posttimeElapsedMs +
    678                     ", posttimeToFirstClickMs=" + posttimeToFirstClickMs +
    679                     ", posttimeToDismissMs=" + posttimeToDismissMs +
    680                     ", airtimeCount=" + airtimeCount +
    681                     ", airtimeMs=" + airtimeMs +
    682                     ", currentAirtimeStartElapsedMs=" + currentAirtimeStartElapsedMs +
    683                     ", airtimeExpandedMs=" + airtimeExpandedMs +
    684                     ", posttimeToFirstVisibleExpansionMs=" + posttimeToFirstVisibleExpansionMs +
    685                     ", currentAirtimeExpandedSEMs=" + currentAirtimeExpandedStartElapsedMs +
    686                     '}';
    687         }
    688     }
    689 
    690     /**
    691      * Aggregates long samples to sum and averages.
    692      */
    693     public static class Aggregate {
    694         long numSamples;
    695         double avg;
    696         double sum2;
    697         double var;
    698 
    699         public void addSample(long sample) {
    700             // Welford's "Method for Calculating Corrected Sums of Squares"
    701             // http://www.jstor.org/stable/1266577?seq=2
    702             numSamples++;
    703             final double n = numSamples;
    704             final double delta = sample - avg;
    705             avg += (1.0 / n) * delta;
    706             sum2 += ((n - 1) / n) * delta * delta;
    707             final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
    708             var = sum2 / divisor;
    709         }
    710 
    711         @Override
    712         public String toString() {
    713             return "Aggregate{" +
    714                     "numSamples=" + numSamples +
    715                     ", avg=" + avg +
    716                     ", var=" + var +
    717                     '}';
    718         }
    719     }
    720 
    721     private static class SQLiteLog {
    722         private static final String TAG = "NotificationSQLiteLog";
    723 
    724         // Message types passed to the background handler.
    725         private static final int MSG_POST = 1;
    726         private static final int MSG_CLICK = 2;
    727         private static final int MSG_REMOVE = 3;
    728         private static final int MSG_DISMISS = 4;
    729 
    730         private static final String DB_NAME = "notification_log.db";
    731         private static final int DB_VERSION = 4;
    732 
    733         /** Age in ms after which events are pruned from the DB. */
    734         private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L;  // 1 week
    735         /** Delay between pruning the DB. Used to throttle pruning. */
    736         private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L;  // 6 hours
    737         /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
    738         private static final long PRUNE_MIN_WRITES = 1024;
    739 
    740         // Table 'log'
    741         private static final String TAB_LOG = "log";
    742         private static final String COL_EVENT_USER_ID = "event_user_id";
    743         private static final String COL_EVENT_TYPE = "event_type";
    744         private static final String COL_EVENT_TIME = "event_time_ms";
    745         private static final String COL_KEY = "key";
    746         private static final String COL_PKG = "pkg";
    747         private static final String COL_NOTIFICATION_ID = "nid";
    748         private static final String COL_TAG = "tag";
    749         private static final String COL_WHEN_MS = "when_ms";
    750         private static final String COL_DEFAULTS = "defaults";
    751         private static final String COL_FLAGS = "flags";
    752         private static final String COL_PRIORITY = "priority";
    753         private static final String COL_CATEGORY = "category";
    754         private static final String COL_ACTION_COUNT = "action_count";
    755         private static final String COL_POSTTIME_MS = "posttime_ms";
    756         private static final String COL_AIRTIME_MS = "airtime_ms";
    757         private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
    758         private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
    759         private static final String COL_EXPAND_COUNT = "expansion_count";
    760 
    761 
    762         private static final int EVENT_TYPE_POST = 1;
    763         private static final int EVENT_TYPE_CLICK = 2;
    764         private static final int EVENT_TYPE_REMOVE = 3;
    765         private static final int EVENT_TYPE_DISMISS = 4;
    766 
    767         private static long sLastPruneMs;
    768         private static long sNumWrites;
    769 
    770         private final SQLiteOpenHelper mHelper;
    771         private final Handler mWriteHandler;
    772 
    773         private static final long DAY_MS = 24 * 60 * 60 * 1000;
    774 
    775         public SQLiteLog(Context context) {
    776             HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
    777                     android.os.Process.THREAD_PRIORITY_BACKGROUND);
    778             backgroundThread.start();
    779             mWriteHandler = new Handler(backgroundThread.getLooper()) {
    780                 @Override
    781                 public void handleMessage(Message msg) {
    782                     NotificationRecord r = (NotificationRecord) msg.obj;
    783                     long nowMs = System.currentTimeMillis();
    784                     switch (msg.what) {
    785                         case MSG_POST:
    786                             writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
    787                             break;
    788                         case MSG_CLICK:
    789                             writeEvent(nowMs, EVENT_TYPE_CLICK, r);
    790                             break;
    791                         case MSG_REMOVE:
    792                             writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
    793                             break;
    794                         case MSG_DISMISS:
    795                             writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
    796                             break;
    797                         default:
    798                             Log.wtf(TAG, "Unknown message type: " + msg.what);
    799                             break;
    800                     }
    801                 }
    802             };
    803             mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
    804                 @Override
    805                 public void onCreate(SQLiteDatabase db) {
    806                     db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
    807                             "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
    808                             COL_EVENT_USER_ID + " INT," +
    809                             COL_EVENT_TYPE + " INT," +
    810                             COL_EVENT_TIME + " INT," +
    811                             COL_KEY + " TEXT," +
    812                             COL_PKG + " TEXT," +
    813                             COL_NOTIFICATION_ID + " INT," +
    814                             COL_TAG + " TEXT," +
    815                             COL_WHEN_MS + " INT," +
    816                             COL_DEFAULTS + " INT," +
    817                             COL_FLAGS + " INT," +
    818                             COL_PRIORITY + " INT," +
    819                             COL_CATEGORY + " TEXT," +
    820                             COL_ACTION_COUNT + " INT," +
    821                             COL_POSTTIME_MS + " INT," +
    822                             COL_AIRTIME_MS + " INT," +
    823                             COL_FIRST_EXPANSIONTIME_MS + " INT," +
    824                             COL_AIRTIME_EXPANDED_MS + " INT," +
    825                             COL_EXPAND_COUNT + " INT" +
    826                             ")");
    827                 }
    828 
    829                 @Override
    830                 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    831                     if (oldVersion <= 3) {
    832                         // Version 3 creation left 'log' in a weird state. Just reset for now.
    833                         db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
    834                         onCreate(db);
    835                     }
    836                 }
    837             };
    838         }
    839 
    840         public void logPosted(NotificationRecord notification) {
    841             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
    842         }
    843 
    844         public void logClicked(NotificationRecord notification) {
    845             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
    846         }
    847 
    848         public void logRemoved(NotificationRecord notification) {
    849             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
    850         }
    851 
    852         public void logDismissed(NotificationRecord notification) {
    853             mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
    854         }
    855 
    856         private JSONArray JsonPostFrequencies(DumpFilter filter) throws JSONException {
    857             JSONArray frequencies = new JSONArray();
    858             SQLiteDatabase db = mHelper.getReadableDatabase();
    859             long midnight = getMidnightMs();
    860             String q = "SELECT " +
    861                     COL_EVENT_USER_ID + ", " +
    862                     COL_PKG + ", " +
    863                     // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
    864                     "CAST(((" + midnight + " - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
    865                     "AS day, " +
    866                     "COUNT(*) AS cnt " +
    867                     "FROM " + TAB_LOG + " " +
    868                     "WHERE " +
    869                     COL_EVENT_TYPE + "=" + EVENT_TYPE_POST +
    870                     " AND " + COL_EVENT_TIME + " > " + filter.since +
    871                     " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
    872             Cursor cursor = db.rawQuery(q, null);
    873             try {
    874                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
    875                     int userId = cursor.getInt(0);
    876                     String pkg = cursor.getString(1);
    877                     if (filter != null && !filter.matches(pkg)) continue;
    878                     int day = cursor.getInt(2);
    879                     int count = cursor.getInt(3);
    880                     JSONObject row = new JSONObject();
    881                     row.put("user_id", userId);
    882                     row.put("package", pkg);
    883                     row.put("day", day);
    884                     row.put("count", count);
    885                     frequencies.put(row);
    886                 }
    887             } finally {
    888                 cursor.close();
    889             }
    890             return frequencies;
    891         }
    892 
    893         public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
    894             SQLiteDatabase db = mHelper.getReadableDatabase();
    895             long midnight = getMidnightMs();
    896             String q = "SELECT " +
    897                     COL_EVENT_USER_ID + ", " +
    898                     COL_PKG + ", " +
    899                     // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
    900                     "CAST(((" + midnight + " - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
    901                         "AS day, " +
    902                     "COUNT(*) AS cnt " +
    903                     "FROM " + TAB_LOG + " " +
    904                     "WHERE " +
    905                     COL_EVENT_TYPE + "=" + EVENT_TYPE_POST + " " +
    906                     "GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
    907             Cursor cursor = db.rawQuery(q, null);
    908             try {
    909                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
    910                     int userId = cursor.getInt(0);
    911                     String pkg = cursor.getString(1);
    912                     if (filter != null && !filter.matches(pkg)) continue;
    913                     int day = cursor.getInt(2);
    914                     int count = cursor.getInt(3);
    915                     pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
    916                             ",day=" + day + ",count=" + count + "}");
    917                 }
    918             } finally {
    919                 cursor.close();
    920             }
    921         }
    922 
    923         private long getMidnightMs() {
    924             GregorianCalendar midnight = new GregorianCalendar();
    925             midnight.set(midnight.get(Calendar.YEAR), midnight.get(Calendar.MONTH),
    926                     midnight.get(Calendar.DATE), 23, 59, 59);
    927             return midnight.getTimeInMillis();
    928         }
    929 
    930         private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
    931             ContentValues cv = new ContentValues();
    932             cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
    933             cv.put(COL_EVENT_TIME, eventTimeMs);
    934             cv.put(COL_EVENT_TYPE, eventType);
    935             putNotificationIdentifiers(r, cv);
    936             if (eventType == EVENT_TYPE_POST) {
    937                 putNotificationDetails(r, cv);
    938             } else {
    939                 putPosttimeVisibility(r, cv);
    940             }
    941             SQLiteDatabase db = mHelper.getWritableDatabase();
    942             if (db.insert(TAB_LOG, null, cv) < 0) {
    943                 Log.wtf(TAG, "Error while trying to insert values: " + cv);
    944             }
    945             sNumWrites++;
    946             pruneIfNecessary(db);
    947         }
    948 
    949         private void pruneIfNecessary(SQLiteDatabase db) {
    950             // Prune if we haven't in a while.
    951             long nowMs = System.currentTimeMillis();
    952             if (sNumWrites > PRUNE_MIN_WRITES ||
    953                     nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
    954                 sNumWrites = 0;
    955                 sLastPruneMs = nowMs;
    956                 long horizonStartMs = nowMs - HORIZON_MS;
    957                 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
    958                         new String[] { String.valueOf(horizonStartMs) });
    959                 Log.d(TAG, "Pruned event entries: " + deletedRows);
    960             }
    961         }
    962 
    963         private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
    964             outCv.put(COL_KEY, r.sbn.getKey());
    965             outCv.put(COL_PKG, r.sbn.getPackageName());
    966         }
    967 
    968         private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
    969             outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
    970             if (r.sbn.getTag() != null) {
    971                 outCv.put(COL_TAG, r.sbn.getTag());
    972             }
    973             outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
    974             outCv.put(COL_FLAGS, r.getNotification().flags);
    975             outCv.put(COL_PRIORITY, r.getNotification().priority);
    976             if (r.getNotification().category != null) {
    977                 outCv.put(COL_CATEGORY, r.getNotification().category);
    978             }
    979             outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
    980                     r.getNotification().actions.length : 0);
    981         }
    982 
    983         private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
    984             outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
    985             outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
    986             outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
    987             outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
    988             outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
    989         }
    990 
    991         public void dump(PrintWriter pw, String indent, DumpFilter filter) {
    992             printPostFrequencies(pw, indent, filter);
    993         }
    994 
    995         public JSONObject dumpJson(DumpFilter filter) {
    996             JSONObject dump = new JSONObject();
    997             try {
    998                 dump.put("post_frequency", JsonPostFrequencies(filter));
    999             } catch (JSONException e) {
   1000                 // pass
   1001             }
   1002             return dump;
   1003         }
   1004     }
   1005 }
   1006