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