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