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