2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.server.notification;
19 import static android.app.NotificationManager.IMPORTANCE_HIGH;
21 import android.app.Notification;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteOpenHelper;
27 import android.os.Handler;
28 import android.os.HandlerThread;
29 import android.os.Message;
30 import android.os.SystemClock;
31 import android.text.TextUtils;
32 import android.util.ArraySet;
33 import android.util.Log;
35 import com.android.internal.logging.MetricsLogger;
36 import com.android.server.notification.NotificationManagerService.DumpFilter;
38 import org.json.JSONArray;
39 import org.json.JSONException;
40 import org.json.JSONObject;
42 import java.io.PrintWriter;
43 import java.lang.Math;
44 import java.util.ArrayDeque;
45 import java.util.Calendar;
46 import java.util.GregorianCalendar;
47 import java.util.HashMap;
52 * Keeps track of notification activity, display, and user interaction.
54 * <p>This class receives signals from NoMan and keeps running stats of
55 * notification usage. Some metrics are updated as events occur. Others, namely
56 * those involving durations, are updated as the notification is canceled.</p>
58 * <p>This class is thread-safe.</p>
62 public class NotificationUsageStats {
63 private static final String TAG = "NotificationUsageStats";
65 private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
66 private static final boolean ENABLE_SQLITE_LOG = true;
67 private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
68 private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
69 private static final int MSG_EMIT = 1;
71 private static final boolean DEBUG = false;
72 public static final int TEN_SECONDS = 1000 * 10;
73 public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
74 private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
76 // Guarded by synchronized(this).
77 private final Map<String, AggregatedStats> mStats = new HashMap<>();
78 private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
79 private ArraySet<String> mStatExpiredkeys = new ArraySet<>();
80 private final SQLiteLog mSQLiteLog;
81 private final Context mContext;
82 private final Handler mHandler;
83 private long mLastEmitTime;
85 public NotificationUsageStats(Context context) {
87 mLastEmitTime = SystemClock.elapsedRealtime();
88 mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
89 mHandler = new Handler(mContext.getMainLooper()) {
91 public void handleMessage(Message msg) {
97 Log.wtf(TAG, "Unknown message type: " + msg.what);
102 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
106 * Called when a notification has been posted.
108 public synchronized float getAppEnqueueRate(String packageName) {
109 AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
111 return stats.getEnqueueRate(SystemClock.elapsedRealtime());
118 * Called when a notification wants to alert.
120 public synchronized boolean isAlertRateLimited(String packageName) {
121 AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
123 return stats.isAlertRateLimited();
130 * Called when a notification is tentatively enqueued by an app, before rate checking.
132 public synchronized void registerEnqueuedByApp(String packageName) {
133 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
134 for (AggregatedStats stats : aggregatedStatsArray) {
135 stats.numEnqueuedByApp++;
137 releaseAggregatedStatsLocked(aggregatedStatsArray);
141 * Called when a notification has been posted.
143 public synchronized void registerPostedByApp(NotificationRecord notification) {
144 final long now = SystemClock.elapsedRealtime();
145 notification.stats.posttimeElapsedMs = now;
147 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
148 for (AggregatedStats stats : aggregatedStatsArray) {
149 stats.numPostedByApp++;
150 stats.updateInterarrivalEstimate(now);
151 stats.countApiUse(notification);
153 releaseAggregatedStatsLocked(aggregatedStatsArray);
154 if (ENABLE_SQLITE_LOG) {
155 mSQLiteLog.logPosted(notification);
160 * Called when a notification has been updated.
162 public synchronized void registerUpdatedByApp(NotificationRecord notification,
163 NotificationRecord old) {
164 notification.stats.updateFrom(old.stats);
165 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
166 for (AggregatedStats stats : aggregatedStatsArray) {
167 stats.numUpdatedByApp++;
168 stats.updateInterarrivalEstimate(SystemClock.elapsedRealtime());
169 stats.countApiUse(notification);
171 releaseAggregatedStatsLocked(aggregatedStatsArray);
172 if (ENABLE_SQLITE_LOG) {
173 mSQLiteLog.logPosted(notification);
178 * Called when the originating app removed the notification programmatically.
180 public synchronized void registerRemovedByApp(NotificationRecord notification) {
181 notification.stats.onRemoved();
182 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
183 for (AggregatedStats stats : aggregatedStatsArray) {
184 stats.numRemovedByApp++;
186 releaseAggregatedStatsLocked(aggregatedStatsArray);
187 if (ENABLE_SQLITE_LOG) {
188 mSQLiteLog.logRemoved(notification);
193 * Called when the user dismissed the notification via the UI.
195 public synchronized void registerDismissedByUser(NotificationRecord notification) {
196 MetricsLogger.histogram(mContext, "note_dismiss_longevity",
197 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
198 notification.stats.onDismiss();
199 if (ENABLE_SQLITE_LOG) {
200 mSQLiteLog.logDismissed(notification);
205 * Called when the user clicked the notification in the UI.
207 public synchronized void registerClickedByUser(NotificationRecord notification) {
208 MetricsLogger.histogram(mContext, "note_click_longevity",
209 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
210 notification.stats.onClick();
211 if (ENABLE_SQLITE_LOG) {
212 mSQLiteLog.logClicked(notification);
216 public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
217 boolean starred, boolean cached) {
218 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
219 for (AggregatedStats stats : aggregatedStatsArray) {
221 stats.numWithValidPeople++;
224 stats.numWithStaredPeople++;
227 stats.numPeopleCacheHit++;
229 stats.numPeopleCacheMiss++;
232 releaseAggregatedStatsLocked(aggregatedStatsArray);
235 public synchronized void registerBlocked(NotificationRecord notification) {
236 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
237 for (AggregatedStats stats : aggregatedStatsArray) {
240 releaseAggregatedStatsLocked(aggregatedStatsArray);
243 public synchronized void registerSuspendedByAdmin(NotificationRecord notification) {
244 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
245 for (AggregatedStats stats : aggregatedStatsArray) {
246 stats.numSuspendedByAdmin++;
248 releaseAggregatedStatsLocked(aggregatedStatsArray);
251 public synchronized void registerOverRateQuota(String packageName) {
252 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
253 for (AggregatedStats stats : aggregatedStatsArray) {
254 stats.numRateViolations++;
258 public synchronized void registerOverCountQuota(String packageName) {
259 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
260 for (AggregatedStats stats : aggregatedStatsArray) {
261 stats.numQuotaViolations++;
266 private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
267 return getAggregatedStatsLocked(record.sbn.getPackageName());
271 private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
272 if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
273 return EMPTY_AGGREGATED_STATS;
276 AggregatedStats[] array = mStatsArrays.poll();
278 array = new AggregatedStats[2];
280 array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
281 array[1] = getOrCreateAggregatedStatsLocked(packageName);
286 private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
287 for(int i = 0; i < array.length; i++) {
290 mStatsArrays.offer(array);
294 private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
295 AggregatedStats result = mStats.get(key);
296 if (result == null) {
297 result = new AggregatedStats(mContext, key);
298 mStats.put(key, result);
300 result.mLastAccessTime = SystemClock.elapsedRealtime();
304 public synchronized JSONObject dumpJson(DumpFilter filter) {
305 JSONObject dump = new JSONObject();
306 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
308 JSONArray aggregatedStats = new JSONArray();
309 for (AggregatedStats as : mStats.values()) {
310 if (filter != null && !filter.matches(as.key))
312 aggregatedStats.put(as.dumpJson());
314 dump.put("current", aggregatedStats);
315 } catch (JSONException e) {
319 if (ENABLE_SQLITE_LOG) {
321 dump.put("historical", mSQLiteLog.dumpJson(filter));
322 } catch (JSONException e) {
329 public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
330 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
331 for (AggregatedStats as : mStats.values()) {
332 if (filter != null && !filter.matches(as.key))
336 pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
337 pw.println(indent + "mStats.size(): " + mStats.size());
339 if (ENABLE_SQLITE_LOG) {
340 mSQLiteLog.dump(pw, indent, filter);
344 public synchronized void emit() {
345 AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
347 mHandler.removeMessages(MSG_EMIT);
348 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
349 for(String key: mStats.keySet()) {
350 if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
351 mStatExpiredkeys.add(key);
354 for(String key: mStatExpiredkeys) {
357 mStatExpiredkeys.clear();
358 mLastEmitTime = SystemClock.elapsedRealtime();
362 * Aggregated notification stats.
364 private static class AggregatedStats {
366 private final Context mContext;
367 public final String key;
368 private final long mCreated;
369 private AggregatedStats mPrevious;
371 // ---- Updated as the respective events occur.
372 public int numEnqueuedByApp;
373 public int numPostedByApp;
374 public int numUpdatedByApp;
375 public int numRemovedByApp;
376 public int numPeopleCacheHit;
377 public int numPeopleCacheMiss;;
378 public int numWithStaredPeople;
379 public int numWithValidPeople;
380 public int numBlocked;
381 public int numSuspendedByAdmin;
382 public int numWithActions;
383 public int numPrivate;
384 public int numSecret;
385 public int numWithBigText;
386 public int numWithBigPicture;
387 public int numForegroundService;
388 public int numOngoing;
389 public int numAutoCancel;
390 public int numWithLargeIcon;
391 public int numWithInbox;
392 public int numWithMediaSession;
393 public int numWithTitle;
394 public int numWithText;
395 public int numWithSubText;
396 public int numWithInfoText;
397 public int numInterrupt;
398 public ImportanceHistogram noisyImportance;
399 public ImportanceHistogram quietImportance;
400 public ImportanceHistogram finalImportance;
401 public RateEstimator enqueueRate;
402 public AlertRateLimiter alertRate;
403 public int numRateViolations;
404 public int numAlertViolations;
405 public int numQuotaViolations;
406 public long mLastAccessTime;
408 public AggregatedStats(Context context, String key) {
411 mCreated = SystemClock.elapsedRealtime();
412 noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
413 quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
414 finalImportance = new ImportanceHistogram(context, "note_importance_");
415 enqueueRate = new RateEstimator();
416 alertRate = new AlertRateLimiter();
419 public AggregatedStats getPrevious() {
420 if (mPrevious == null) {
421 mPrevious = new AggregatedStats(mContext, key);
426 public void countApiUse(NotificationRecord record) {
427 final Notification n = record.getNotification();
428 if (n.actions != null) {
432 if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
433 numForegroundService++;
436 if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
440 if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
444 if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
445 (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
446 n.sound != null || n.vibrate != null) {
450 switch (n.visibility) {
451 case Notification.VISIBILITY_PRIVATE:
454 case Notification.VISIBILITY_SECRET:
459 if (record.stats.isNoisy) {
460 noisyImportance.increment(record.stats.requestedImportance);
462 quietImportance.increment(record.stats.requestedImportance);
464 finalImportance.increment(record.getImportance());
466 final Set<String> names = n.extras.keySet();
467 if (names.contains(Notification.EXTRA_BIG_TEXT)) {
470 if (names.contains(Notification.EXTRA_PICTURE)) {
473 if (names.contains(Notification.EXTRA_LARGE_ICON)) {
476 if (names.contains(Notification.EXTRA_TEXT_LINES)) {
479 if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
480 numWithMediaSession++;
482 if (names.contains(Notification.EXTRA_TITLE) &&
483 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
486 if (names.contains(Notification.EXTRA_TEXT) &&
487 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
490 if (names.contains(Notification.EXTRA_SUB_TEXT) &&
491 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
494 if (names.contains(Notification.EXTRA_INFO_TEXT) &&
495 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
501 AggregatedStats previous = getPrevious();
502 maybeCount("note_enqueued", (numEnqueuedByApp - previous.numEnqueuedByApp));
503 maybeCount("note_post", (numPostedByApp - previous.numPostedByApp));
504 maybeCount("note_update", (numUpdatedByApp - previous.numUpdatedByApp));
505 maybeCount("note_remove", (numRemovedByApp - previous.numRemovedByApp));
506 maybeCount("note_with_people", (numWithValidPeople - previous.numWithValidPeople));
507 maybeCount("note_with_stars", (numWithStaredPeople - previous.numWithStaredPeople));
508 maybeCount("people_cache_hit", (numPeopleCacheHit - previous.numPeopleCacheHit));
509 maybeCount("people_cache_miss", (numPeopleCacheMiss - previous.numPeopleCacheMiss));
510 maybeCount("note_blocked", (numBlocked - previous.numBlocked));
511 maybeCount("note_suspended", (numSuspendedByAdmin - previous.numSuspendedByAdmin));
512 maybeCount("note_with_actions", (numWithActions - previous.numWithActions));
513 maybeCount("note_private", (numPrivate - previous.numPrivate));
514 maybeCount("note_secret", (numSecret - previous.numSecret));
515 maybeCount("note_interupt", (numInterrupt - previous.numInterrupt));
516 maybeCount("note_big_text", (numWithBigText - previous.numWithBigText));
517 maybeCount("note_big_pic", (numWithBigPicture - previous.numWithBigPicture));
518 maybeCount("note_fg", (numForegroundService - previous.numForegroundService));
519 maybeCount("note_ongoing", (numOngoing - previous.numOngoing));
520 maybeCount("note_auto", (numAutoCancel - previous.numAutoCancel));
521 maybeCount("note_large_icon", (numWithLargeIcon - previous.numWithLargeIcon));
522 maybeCount("note_inbox", (numWithInbox - previous.numWithInbox));
523 maybeCount("note_media", (numWithMediaSession - previous.numWithMediaSession));
524 maybeCount("note_title", (numWithTitle - previous.numWithTitle));
525 maybeCount("note_text", (numWithText - previous.numWithText));
526 maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
527 maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
528 maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
529 maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations));
530 maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
531 noisyImportance.maybeCount(previous.noisyImportance);
532 quietImportance.maybeCount(previous.quietImportance);
533 finalImportance.maybeCount(previous.finalImportance);
535 previous.numEnqueuedByApp = numEnqueuedByApp;
536 previous.numPostedByApp = numPostedByApp;
537 previous.numUpdatedByApp = numUpdatedByApp;
538 previous.numRemovedByApp = numRemovedByApp;
539 previous.numPeopleCacheHit = numPeopleCacheHit;
540 previous.numPeopleCacheMiss = numPeopleCacheMiss;
541 previous.numWithStaredPeople = numWithStaredPeople;
542 previous.numWithValidPeople = numWithValidPeople;
543 previous.numBlocked = numBlocked;
544 previous.numSuspendedByAdmin = numSuspendedByAdmin;
545 previous.numWithActions = numWithActions;
546 previous.numPrivate = numPrivate;
547 previous.numSecret = numSecret;
548 previous.numInterrupt = numInterrupt;
549 previous.numWithBigText = numWithBigText;
550 previous.numWithBigPicture = numWithBigPicture;
551 previous.numForegroundService = numForegroundService;
552 previous.numOngoing = numOngoing;
553 previous.numAutoCancel = numAutoCancel;
554 previous.numWithLargeIcon = numWithLargeIcon;
555 previous.numWithInbox = numWithInbox;
556 previous.numWithMediaSession = numWithMediaSession;
557 previous.numWithTitle = numWithTitle;
558 previous.numWithText = numWithText;
559 previous.numWithSubText = numWithSubText;
560 previous.numWithInfoText = numWithInfoText;
561 previous.numRateViolations = numRateViolations;
562 previous.numAlertViolations = numAlertViolations;
563 previous.numQuotaViolations = numQuotaViolations;
564 noisyImportance.update(previous.noisyImportance);
565 quietImportance.update(previous.quietImportance);
566 finalImportance.update(previous.finalImportance);
569 void maybeCount(String name, int value) {
571 MetricsLogger.count(mContext, name, value);
575 public void dump(PrintWriter pw, String indent) {
576 pw.println(toStringWithIndent(indent));
580 public String toString() {
581 return toStringWithIndent("");
584 /** @return the enqueue rate if there were a new enqueue event right now. */
585 public float getEnqueueRate() {
586 return getEnqueueRate(SystemClock.elapsedRealtime());
589 public float getEnqueueRate(long now) {
590 return enqueueRate.getRate(now);
593 public void updateInterarrivalEstimate(long now) {
594 enqueueRate.update(now);
597 public boolean isAlertRateLimited() {
598 boolean limited = alertRate.shouldRateLimitAlert(SystemClock.elapsedRealtime());
600 numAlertViolations++;
605 private String toStringWithIndent(String indent) {
606 StringBuilder output = new StringBuilder();
607 output.append(indent).append("AggregatedStats{\n");
608 String indentPlusTwo = indent + " ";
609 output.append(indentPlusTwo);
610 output.append("key='").append(key).append("',\n");
611 output.append(indentPlusTwo);
612 output.append("numEnqueuedByApp=").append(numEnqueuedByApp).append(",\n");
613 output.append(indentPlusTwo);
614 output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
615 output.append(indentPlusTwo);
616 output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
617 output.append(indentPlusTwo);
618 output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
619 output.append(indentPlusTwo);
620 output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
621 output.append(indentPlusTwo);
622 output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
623 output.append(indentPlusTwo);
624 output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
625 output.append(indentPlusTwo);
626 output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
627 output.append(indentPlusTwo);
628 output.append("numBlocked=").append(numBlocked).append(",\n");
629 output.append(indentPlusTwo);
630 output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
631 output.append(indentPlusTwo);
632 output.append("numWithActions=").append(numWithActions).append(",\n");
633 output.append(indentPlusTwo);
634 output.append("numPrivate=").append(numPrivate).append(",\n");
635 output.append(indentPlusTwo);
636 output.append("numSecret=").append(numSecret).append(",\n");
637 output.append(indentPlusTwo);
638 output.append("numInterrupt=").append(numInterrupt).append(",\n");
639 output.append(indentPlusTwo);
640 output.append("numWithBigText=").append(numWithBigText).append(",\n");
641 output.append(indentPlusTwo);
642 output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
643 output.append(indentPlusTwo);
644 output.append("numForegroundService=").append(numForegroundService).append("\n");
645 output.append(indentPlusTwo);
646 output.append("numOngoing=").append(numOngoing).append("\n");
647 output.append(indentPlusTwo);
648 output.append("numAutoCancel=").append(numAutoCancel).append("\n");
649 output.append(indentPlusTwo);
650 output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
651 output.append(indentPlusTwo);
652 output.append("numWithInbox=").append(numWithInbox).append("\n");
653 output.append(indentPlusTwo);
654 output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
655 output.append(indentPlusTwo);
656 output.append("numWithTitle=").append(numWithTitle).append("\n");
657 output.append(indentPlusTwo);
658 output.append("numWithText=").append(numWithText).append("\n");
659 output.append(indentPlusTwo);
660 output.append("numWithSubText=").append(numWithSubText).append("\n");
661 output.append(indentPlusTwo);
662 output.append("numWithInfoText=").append(numWithInfoText).append("\n");
663 output.append(indentPlusTwo);
664 output.append("numRateViolations=").append(numRateViolations).append("\n");
665 output.append(indentPlusTwo);
666 output.append("numAlertViolations=").append(numAlertViolations).append("\n");
667 output.append(indentPlusTwo);
668 output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
669 output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
670 output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
671 output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
672 output.append(indent).append("}");
673 return output.toString();
676 public JSONObject dumpJson() throws JSONException {
677 AggregatedStats previous = getPrevious();
678 JSONObject dump = new JSONObject();
679 dump.put("key", key);
680 dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
681 maybePut(dump, "numEnqueuedByApp", numEnqueuedByApp);
682 maybePut(dump, "numPostedByApp", numPostedByApp);
683 maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
684 maybePut(dump, "numRemovedByApp", numRemovedByApp);
685 maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
686 maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
687 maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
688 maybePut(dump, "numWithValidPeople", numWithValidPeople);
689 maybePut(dump, "numBlocked", numBlocked);
690 maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
691 maybePut(dump, "numWithActions", numWithActions);
692 maybePut(dump, "numPrivate", numPrivate);
693 maybePut(dump, "numSecret", numSecret);
694 maybePut(dump, "numInterrupt", numInterrupt);
695 maybePut(dump, "numWithBigText", numWithBigText);
696 maybePut(dump, "numWithBigPicture", numWithBigPicture);
697 maybePut(dump, "numForegroundService", numForegroundService);
698 maybePut(dump, "numOngoing", numOngoing);
699 maybePut(dump, "numAutoCancel", numAutoCancel);
700 maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
701 maybePut(dump, "numWithInbox", numWithInbox);
702 maybePut(dump, "numWithMediaSession", numWithMediaSession);
703 maybePut(dump, "numWithTitle", numWithTitle);
704 maybePut(dump, "numWithText", numWithText);
705 maybePut(dump, "numWithSubText", numWithSubText);
706 maybePut(dump, "numWithInfoText", numWithInfoText);
707 maybePut(dump, "numRateViolations", numRateViolations);
708 maybePut(dump, "numQuotaLViolations", numQuotaViolations);
709 maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
710 maybePut(dump, "numAlertViolations", numAlertViolations);
711 noisyImportance.maybePut(dump, previous.noisyImportance);
712 quietImportance.maybePut(dump, previous.quietImportance);
713 finalImportance.maybePut(dump, previous.finalImportance);
718 private void maybePut(JSONObject dump, String name, int value) throws JSONException {
720 dump.put(name, value);
724 private void maybePut(JSONObject dump, String name, float value) throws JSONException {
726 dump.put(name, value);
731 private static class ImportanceHistogram {
732 // TODO define these somewhere else
733 private static final int NUM_IMPORTANCES = 6;
734 private static final String[] IMPORTANCE_NAMES =
735 {"none", "min", "low", "default", "high", "max"};
736 private final Context mContext;
737 private final String[] mCounterNames;
738 private final String mPrefix;
739 private int[] mCount;
741 ImportanceHistogram(Context context, String prefix) {
743 mCount = new int[NUM_IMPORTANCES];
744 mCounterNames = new String[NUM_IMPORTANCES];
746 for (int i = 0; i < NUM_IMPORTANCES; i++) {
747 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
751 void increment(int imp) {
752 imp = Math.max(0, Math.min(imp, mCount.length - 1));
756 void maybeCount(ImportanceHistogram prev) {
757 for (int i = 0; i < NUM_IMPORTANCES; i++) {
758 final int value = mCount[i] - prev.mCount[i];
760 MetricsLogger.count(mContext, mCounterNames[i], value);
765 void update(ImportanceHistogram that) {
766 for (int i = 0; i < NUM_IMPORTANCES; i++) {
767 mCount[i] = that.mCount[i];
771 public void maybePut(JSONObject dump, ImportanceHistogram prev)
772 throws JSONException {
773 dump.put(mPrefix, new JSONArray(mCount));
777 public String toString() {
778 StringBuilder output = new StringBuilder();
779 output.append(mPrefix).append(": [");
780 for (int i = 0; i < NUM_IMPORTANCES; i++) {
781 output.append(mCount[i]);
782 if (i < (NUM_IMPORTANCES-1)) {
787 return output.toString();
792 * Tracks usage of an individual notification that is currently active.
794 public static class SingleNotificationStats {
795 private boolean isVisible = false;
796 private boolean isExpanded = false;
797 /** SystemClock.elapsedRealtime() when the notification was posted. */
798 public long posttimeElapsedMs = -1;
799 /** Elapsed time since the notification was posted until it was first clicked, or -1. */
800 public long posttimeToFirstClickMs = -1;
801 /** Elpased time since the notification was posted until it was dismissed by the user. */
802 public long posttimeToDismissMs = -1;
803 /** Number of times the notification has been made visible. */
804 public long airtimeCount = 0;
805 /** Time in ms between the notification was posted and first shown; -1 if never shown. */
806 public long posttimeToFirstAirtimeMs = -1;
808 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
809 * visible; -1 otherwise.
811 public long currentAirtimeStartElapsedMs = -1;
812 /** Accumulated visible time. */
813 public long airtimeMs = 0;
815 * Time in ms between the notification being posted and when it first
816 * became visible and expanded; -1 if it was never visibly expanded.
818 public long posttimeToFirstVisibleExpansionMs = -1;
820 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
821 * visible; -1 otherwise.
823 public long currentAirtimeExpandedStartElapsedMs = -1;
824 /** Accumulated visible expanded time. */
825 public long airtimeExpandedMs = 0;
826 /** Number of times the notification has been expanded by the user. */
827 public long userExpansionCount = 0;
828 /** Importance directly requested by the app. */
829 public int requestedImportance;
830 /** Did the app include sound or vibration on the notificaiton. */
831 public boolean isNoisy;
832 /** Importance after initial filtering for noise and other features */
833 public int naturalImportance;
835 public long getCurrentPosttimeMs() {
836 if (posttimeElapsedMs < 0) {
839 return SystemClock.elapsedRealtime() - posttimeElapsedMs;
842 public long getCurrentAirtimeMs() {
843 long result = airtimeMs;
844 // Add incomplete airtime if currently shown.
845 if (currentAirtimeStartElapsedMs >= 0) {
846 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
851 public long getCurrentAirtimeExpandedMs() {
852 long result = airtimeExpandedMs;
853 // Add incomplete expanded airtime if currently shown.
854 if (currentAirtimeExpandedStartElapsedMs >= 0) {
855 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
861 * Called when the user clicked the notification.
863 public void onClick() {
864 if (posttimeToFirstClickMs < 0) {
865 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
870 * Called when the user removed the notification.
872 public void onDismiss() {
873 if (posttimeToDismissMs < 0) {
874 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
879 public void onCancel() {
883 public void onRemoved() {
887 public void onVisibilityChanged(boolean visible) {
888 long elapsedNowMs = SystemClock.elapsedRealtime();
889 final boolean wasVisible = isVisible;
892 if (currentAirtimeStartElapsedMs < 0) {
894 currentAirtimeStartElapsedMs = elapsedNowMs;
896 if (posttimeToFirstAirtimeMs < 0) {
897 posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
900 if (currentAirtimeStartElapsedMs >= 0) {
901 airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
902 currentAirtimeStartElapsedMs = -1;
906 if (wasVisible != isVisible) {
907 updateVisiblyExpandedStats();
911 public void onExpansionChanged(boolean userAction, boolean expanded) {
912 isExpanded = expanded;
913 if (isExpanded && userAction) {
914 userExpansionCount++;
916 updateVisiblyExpandedStats();
919 private void updateVisiblyExpandedStats() {
920 long elapsedNowMs = SystemClock.elapsedRealtime();
921 if (isExpanded && isVisible) {
922 // expanded and visible
923 if (currentAirtimeExpandedStartElapsedMs < 0) {
924 currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
926 if (posttimeToFirstVisibleExpansionMs < 0) {
927 posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
930 // not-expanded or not-visible
931 if (currentAirtimeExpandedStartElapsedMs >= 0) {
932 airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
933 currentAirtimeExpandedStartElapsedMs = -1;
938 /** The notification is leaving the system. Finalize. */
939 public void finish() {
940 onVisibilityChanged(false);
944 public String toString() {
945 StringBuilder output = new StringBuilder();
946 output.append("SingleNotificationStats{");
948 output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
949 output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
950 output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
951 output.append("airtimeCount=").append(airtimeCount).append(", ");
952 output.append("airtimeMs=").append(airtimeMs).append(", ");
953 output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
955 output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
956 output.append("posttimeToFirstVisibleExpansionMs=")
957 .append(posttimeToFirstVisibleExpansionMs).append(", ");
958 output.append("currentAirtimeExpandedStartElapsedMs=")
959 .append(currentAirtimeExpandedStartElapsedMs).append(", ");
960 output.append("requestedImportance=").append(requestedImportance).append(", ");
961 output.append("naturalImportance=").append(naturalImportance).append(", ");
962 output.append("isNoisy=").append(isNoisy);
964 return output.toString();
967 /** Copy useful information out of the stats from the pre-update notifications. */
968 public void updateFrom(SingleNotificationStats old) {
969 posttimeElapsedMs = old.posttimeElapsedMs;
970 posttimeToFirstClickMs = old.posttimeToFirstClickMs;
971 airtimeCount = old.airtimeCount;
972 posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
973 currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
974 airtimeMs = old.airtimeMs;
975 posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
976 currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
977 airtimeExpandedMs = old.airtimeExpandedMs;
978 userExpansionCount = old.userExpansionCount;
983 * Aggregates long samples to sum and averages.
985 public static class Aggregate {
991 public void addSample(long sample) {
992 // Welford's "Method for Calculating Corrected Sums of Squares"
993 // http://www.jstor.org/stable/1266577?seq=2
995 final double n = numSamples;
996 final double delta = sample - avg;
997 avg += (1.0 / n) * delta;
998 sum2 += ((n - 1) / n) * delta * delta;
999 final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
1000 var = sum2 / divisor;
1004 public String toString() {
1005 return "Aggregate{" +
1006 "numSamples=" + numSamples +
1013 private static class SQLiteLog {
1014 private static final String TAG = "NotificationSQLiteLog";
1016 // Message types passed to the background handler.
1017 private static final int MSG_POST = 1;
1018 private static final int MSG_CLICK = 2;
1019 private static final int MSG_REMOVE = 3;
1020 private static final int MSG_DISMISS = 4;
1022 private static final String DB_NAME = "notification_log.db";
1023 private static final int DB_VERSION = 5;
1025 /** Age in ms after which events are pruned from the DB. */
1026 private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L; // 1 week
1027 /** Delay between pruning the DB. Used to throttle pruning. */
1028 private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L; // 6 hours
1029 /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
1030 private static final long PRUNE_MIN_WRITES = 1024;
1033 private static final String TAB_LOG = "log";
1034 private static final String COL_EVENT_USER_ID = "event_user_id";
1035 private static final String COL_EVENT_TYPE = "event_type";
1036 private static final String COL_EVENT_TIME = "event_time_ms";
1037 private static final String COL_KEY = "key";
1038 private static final String COL_PKG = "pkg";
1039 private static final String COL_NOTIFICATION_ID = "nid";
1040 private static final String COL_TAG = "tag";
1041 private static final String COL_WHEN_MS = "when_ms";
1042 private static final String COL_DEFAULTS = "defaults";
1043 private static final String COL_FLAGS = "flags";
1044 private static final String COL_IMPORTANCE_REQ = "importance_request";
1045 private static final String COL_IMPORTANCE_FINAL = "importance_final";
1046 private static final String COL_NOISY = "noisy";
1047 private static final String COL_MUTED = "muted";
1048 private static final String COL_DEMOTED = "demoted";
1049 private static final String COL_CATEGORY = "category";
1050 private static final String COL_ACTION_COUNT = "action_count";
1051 private static final String COL_POSTTIME_MS = "posttime_ms";
1052 private static final String COL_AIRTIME_MS = "airtime_ms";
1053 private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
1054 private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
1055 private static final String COL_EXPAND_COUNT = "expansion_count";
1058 private static final int EVENT_TYPE_POST = 1;
1059 private static final int EVENT_TYPE_CLICK = 2;
1060 private static final int EVENT_TYPE_REMOVE = 3;
1061 private static final int EVENT_TYPE_DISMISS = 4;
1063 private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
1065 private static long sLastPruneMs;
1067 private static long sNumWrites;
1068 private final SQLiteOpenHelper mHelper;
1070 private final Handler mWriteHandler;
1071 private static final long DAY_MS = 24 * 60 * 60 * 1000;
1072 private static final String STATS_QUERY = "SELECT " +
1073 COL_EVENT_USER_ID + ", " +
1075 // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
1076 "CAST(((%d - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
1078 "COUNT(*) AS cnt, " +
1079 "SUM(" + COL_MUTED + ") as muted, " +
1080 "SUM(" + COL_NOISY + ") as noisy, " +
1081 "SUM(" + COL_DEMOTED + ") as demoted " +
1082 "FROM " + TAB_LOG + " " +
1084 COL_EVENT_TYPE + "=" + EVENT_TYPE_POST +
1085 " AND " + COL_EVENT_TIME + " > %d " +
1086 " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
1088 public SQLiteLog(Context context) {
1089 HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
1090 android.os.Process.THREAD_PRIORITY_BACKGROUND);
1091 backgroundThread.start();
1092 mWriteHandler = new Handler(backgroundThread.getLooper()) {
1094 public void handleMessage(Message msg) {
1095 NotificationRecord r = (NotificationRecord) msg.obj;
1096 long nowMs = System.currentTimeMillis();
1099 writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
1102 writeEvent(nowMs, EVENT_TYPE_CLICK, r);
1105 writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
1108 writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
1111 Log.wtf(TAG, "Unknown message type: " + msg.what);
1116 mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
1118 public void onCreate(SQLiteDatabase db) {
1119 db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
1120 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1121 COL_EVENT_USER_ID + " INT," +
1122 COL_EVENT_TYPE + " INT," +
1123 COL_EVENT_TIME + " INT," +
1124 COL_KEY + " TEXT," +
1125 COL_PKG + " TEXT," +
1126 COL_NOTIFICATION_ID + " INT," +
1127 COL_TAG + " TEXT," +
1128 COL_WHEN_MS + " INT," +
1129 COL_DEFAULTS + " INT," +
1130 COL_FLAGS + " INT," +
1131 COL_IMPORTANCE_REQ + " INT," +
1132 COL_IMPORTANCE_FINAL + " INT," +
1133 COL_NOISY + " INT," +
1134 COL_MUTED + " INT," +
1135 COL_DEMOTED + " INT," +
1136 COL_CATEGORY + " TEXT," +
1137 COL_ACTION_COUNT + " INT," +
1138 COL_POSTTIME_MS + " INT," +
1139 COL_AIRTIME_MS + " INT," +
1140 COL_FIRST_EXPANSIONTIME_MS + " INT," +
1141 COL_AIRTIME_EXPANDED_MS + " INT," +
1142 COL_EXPAND_COUNT + " INT" +
1147 public void onConfigure(SQLiteDatabase db) {
1148 // Memory optimization - close idle connections after 30s of inactivity
1149 setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
1153 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
1154 if (oldVersion != newVersion) {
1155 db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
1162 public void logPosted(NotificationRecord notification) {
1163 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
1166 public void logClicked(NotificationRecord notification) {
1167 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
1170 public void logRemoved(NotificationRecord notification) {
1171 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
1174 public void logDismissed(NotificationRecord notification) {
1175 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
1178 private JSONArray jsonPostFrequencies(DumpFilter filter) throws JSONException {
1179 JSONArray frequencies = new JSONArray();
1180 SQLiteDatabase db = mHelper.getReadableDatabase();
1181 long midnight = getMidnightMs();
1182 String q = String.format(STATS_QUERY, midnight, filter.since);
1183 Cursor cursor = db.rawQuery(q, null);
1185 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1186 int userId = cursor.getInt(0);
1187 String pkg = cursor.getString(1);
1188 if (filter != null && !filter.matches(pkg)) continue;
1189 int day = cursor.getInt(2);
1190 int count = cursor.getInt(3);
1191 int muted = cursor.getInt(4);
1192 int noisy = cursor.getInt(5);
1193 int demoted = cursor.getInt(6);
1194 JSONObject row = new JSONObject();
1195 row.put("user_id", userId);
1196 row.put("package", pkg);
1197 row.put("day", day);
1198 row.put("count", count);
1199 row.put("noisy", noisy);
1200 row.put("muted", muted);
1201 row.put("demoted", demoted);
1202 frequencies.put(row);
1210 public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
1211 SQLiteDatabase db = mHelper.getReadableDatabase();
1212 long midnight = getMidnightMs();
1213 String q = String.format(STATS_QUERY, midnight, filter.since);
1214 Cursor cursor = db.rawQuery(q, null);
1216 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1217 int userId = cursor.getInt(0);
1218 String pkg = cursor.getString(1);
1219 if (filter != null && !filter.matches(pkg)) continue;
1220 int day = cursor.getInt(2);
1221 int count = cursor.getInt(3);
1222 int muted = cursor.getInt(4);
1223 int noisy = cursor.getInt(5);
1224 int demoted = cursor.getInt(6);
1225 pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
1226 ",day=" + day + ",count=" + count + ",muted=" + muted + "/" + noisy +
1227 ",demoted=" + demoted + "}");
1234 private long getMidnightMs() {
1235 GregorianCalendar midnight = new GregorianCalendar();
1236 midnight.set(midnight.get(Calendar.YEAR), midnight.get(Calendar.MONTH),
1237 midnight.get(Calendar.DATE), 23, 59, 59);
1238 return midnight.getTimeInMillis();
1241 private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
1242 ContentValues cv = new ContentValues();
1243 cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
1244 cv.put(COL_EVENT_TIME, eventTimeMs);
1245 cv.put(COL_EVENT_TYPE, eventType);
1246 putNotificationIdentifiers(r, cv);
1247 if (eventType == EVENT_TYPE_POST) {
1248 putNotificationDetails(r, cv);
1250 putPosttimeVisibility(r, cv);
1252 SQLiteDatabase db = mHelper.getWritableDatabase();
1253 if (db.insert(TAB_LOG, null, cv) < 0) {
1254 Log.wtf(TAG, "Error while trying to insert values: " + cv);
1257 pruneIfNecessary(db);
1260 private void pruneIfNecessary(SQLiteDatabase db) {
1261 // Prune if we haven't in a while.
1262 long nowMs = System.currentTimeMillis();
1263 if (sNumWrites > PRUNE_MIN_WRITES ||
1264 nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
1266 sLastPruneMs = nowMs;
1267 long horizonStartMs = nowMs - HORIZON_MS;
1268 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
1269 new String[] { String.valueOf(horizonStartMs) });
1270 Log.d(TAG, "Pruned event entries: " + deletedRows);
1274 private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
1275 outCv.put(COL_KEY, r.sbn.getKey());
1276 outCv.put(COL_PKG, r.sbn.getPackageName());
1279 private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
1280 outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
1281 if (r.sbn.getTag() != null) {
1282 outCv.put(COL_TAG, r.sbn.getTag());
1284 outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
1285 outCv.put(COL_FLAGS, r.getNotification().flags);
1286 final int before = r.stats.requestedImportance;
1287 final int after = r.getImportance();
1288 final boolean noisy = r.stats.isNoisy;
1289 outCv.put(COL_IMPORTANCE_REQ, before);
1290 outCv.put(COL_IMPORTANCE_FINAL, after);
1291 outCv.put(COL_DEMOTED, after < before ? 1 : 0);
1292 outCv.put(COL_NOISY, noisy);
1293 if (noisy && after < IMPORTANCE_HIGH) {
1294 outCv.put(COL_MUTED, 1);
1296 outCv.put(COL_MUTED, 0);
1298 if (r.getNotification().category != null) {
1299 outCv.put(COL_CATEGORY, r.getNotification().category);
1301 outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
1302 r.getNotification().actions.length : 0);
1305 private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
1306 outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
1307 outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
1308 outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
1309 outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
1310 outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
1313 public void dump(PrintWriter pw, String indent, DumpFilter filter) {
1314 printPostFrequencies(pw, indent, filter);
1317 public JSONObject dumpJson(DumpFilter filter) {
1318 JSONObject dump = new JSONObject();
1320 dump.put("post_frequency", jsonPostFrequencies(filter));
1321 dump.put("since", filter.since);
1322 dump.put("now", System.currentTimeMillis());
1323 } catch (JSONException e) {