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