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