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