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