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