NotificationUsageStats.java revision 1ad856e17f988482bbf37fc5b94cd55baee464ae
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 17package com.android.server.notification; 18 19import android.content.ContentValues; 20import android.content.Context; 21import android.database.Cursor; 22import android.database.sqlite.SQLiteDatabase; 23import android.database.sqlite.SQLiteOpenHelper; 24import android.os.Handler; 25import android.os.HandlerThread; 26import android.os.Message; 27import android.os.SystemClock; 28import android.service.notification.StatusBarNotification; 29import android.util.Log; 30 31import com.android.server.notification.NotificationManagerService.DumpFilter; 32 33import java.io.PrintWriter; 34import java.util.HashMap; 35import java.util.Map; 36 37/** 38 * Keeps track of notification activity, display, and user interaction. 39 * 40 * <p>This class receives signals from NoMan and keeps running stats of 41 * notification usage. Some metrics are updated as events occur. Others, namely 42 * those involving durations, are updated as the notification is canceled.</p> 43 * 44 * <p>This class is thread-safe.</p> 45 * 46 * {@hide} 47 */ 48public class NotificationUsageStats { 49 private static final boolean ENABLE_SQLITE_LOG = true; 50 51 // Guarded by synchronized(this). 52 private final Map<String, AggregatedStats> mStats = new HashMap<String, AggregatedStats>(); 53 private final SQLiteLog mSQLiteLog; 54 55 public NotificationUsageStats(Context context) { 56 mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null; 57 } 58 59 /** 60 * Called when a notification has been posted. 61 */ 62 public synchronized void registerPostedByApp(NotificationRecord notification) { 63 notification.stats = new SingleNotificationStats(); 64 notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime(); 65 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 66 stats.numPostedByApp++; 67 } 68 if (ENABLE_SQLITE_LOG) { 69 mSQLiteLog.logPosted(notification); 70 } 71 } 72 73 /** 74 * Called when a notification has been updated. 75 */ 76 public void registerUpdatedByApp(NotificationRecord notification, NotificationRecord old) { 77 notification.stats = old.stats; 78 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 79 stats.numUpdatedByApp++; 80 } 81 } 82 83 /** 84 * Called when the originating app removed the notification programmatically. 85 */ 86 public synchronized void registerRemovedByApp(NotificationRecord notification) { 87 notification.stats.onRemoved(); 88 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 89 stats.numRemovedByApp++; 90 stats.collect(notification.stats); 91 } 92 if (ENABLE_SQLITE_LOG) { 93 mSQLiteLog.logRemoved(notification); 94 } 95 } 96 97 /** 98 * Called when the user dismissed the notification via the UI. 99 */ 100 public synchronized void registerDismissedByUser(NotificationRecord notification) { 101 notification.stats.onDismiss(); 102 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 103 stats.numDismissedByUser++; 104 stats.collect(notification.stats); 105 } 106 if (ENABLE_SQLITE_LOG) { 107 mSQLiteLog.logDismissed(notification); 108 } 109 } 110 111 /** 112 * Called when the user clicked the notification in the UI. 113 */ 114 public synchronized void registerClickedByUser(NotificationRecord notification) { 115 notification.stats.onClick(); 116 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 117 stats.numClickedByUser++; 118 } 119 if (ENABLE_SQLITE_LOG) { 120 mSQLiteLog.logClicked(notification); 121 } 122 } 123 124 /** 125 * Called when the notification is canceled because the user clicked it. 126 * 127 * <p>Called after {@link #registerClickedByUser(NotificationRecord)}.</p> 128 */ 129 public synchronized void registerCancelDueToClick(NotificationRecord notification) { 130 notification.stats.onCancel(); 131 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 132 stats.collect(notification.stats); 133 } 134 } 135 136 /** 137 * Called when the notification is canceled due to unknown reasons. 138 * 139 * <p>Called for notifications of apps being uninstalled, for example.</p> 140 */ 141 public synchronized void registerCancelUnknown(NotificationRecord notification) { 142 notification.stats.onCancel(); 143 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 144 stats.collect(notification.stats); 145 } 146 } 147 148 // Locked by this. 149 private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) { 150 StatusBarNotification n = record.sbn; 151 152 String user = String.valueOf(n.getUserId()); 153 String userPackage = user + ":" + n.getPackageName(); 154 155 // TODO: Use pool of arrays. 156 return new AggregatedStats[] { 157 getOrCreateAggregatedStatsLocked(user), 158 getOrCreateAggregatedStatsLocked(userPackage), 159 getOrCreateAggregatedStatsLocked(n.getKey()), 160 }; 161 } 162 163 // Locked by this. 164 private AggregatedStats getOrCreateAggregatedStatsLocked(String key) { 165 AggregatedStats result = mStats.get(key); 166 if (result == null) { 167 result = new AggregatedStats(key); 168 mStats.put(key, result); 169 } 170 return result; 171 } 172 173 public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) { 174 for (AggregatedStats as : mStats.values()) { 175 if (filter != null && !filter.matches(as.key)) continue; 176 as.dump(pw, indent); 177 } 178 if (ENABLE_SQLITE_LOG) { 179 mSQLiteLog.dump(pw, indent, filter); 180 } 181 } 182 183 /** 184 * Aggregated notification stats. 185 */ 186 private static class AggregatedStats { 187 public final String key; 188 189 // ---- Updated as the respective events occur. 190 public int numPostedByApp; 191 public int numUpdatedByApp; 192 public int numRemovedByApp; 193 public int numClickedByUser; 194 public int numDismissedByUser; 195 196 // ---- Updated when a notification is canceled. 197 public final Aggregate posttimeMs = new Aggregate(); 198 public final Aggregate posttimeToDismissMs = new Aggregate(); 199 public final Aggregate posttimeToFirstClickMs = new Aggregate(); 200 public final Aggregate airtimeCount = new Aggregate(); 201 public final Aggregate airtimeMs = new Aggregate(); 202 public final Aggregate posttimeToFirstAirtimeMs = new Aggregate(); 203 public final Aggregate userExpansionCount = new Aggregate(); 204 public final Aggregate airtimeExpandedMs = new Aggregate(); 205 public final Aggregate posttimeToFirstVisibleExpansionMs = new Aggregate(); 206 207 public AggregatedStats(String key) { 208 this.key = key; 209 } 210 211 public void collect(SingleNotificationStats singleNotificationStats) { 212 posttimeMs.addSample( 213 SystemClock.elapsedRealtime() - singleNotificationStats.posttimeElapsedMs); 214 if (singleNotificationStats.posttimeToDismissMs >= 0) { 215 posttimeToDismissMs.addSample(singleNotificationStats.posttimeToDismissMs); 216 } 217 if (singleNotificationStats.posttimeToFirstClickMs >= 0) { 218 posttimeToFirstClickMs.addSample(singleNotificationStats.posttimeToFirstClickMs); 219 } 220 airtimeCount.addSample(singleNotificationStats.airtimeCount); 221 if (singleNotificationStats.airtimeMs >= 0) { 222 airtimeMs.addSample(singleNotificationStats.airtimeMs); 223 } 224 if (singleNotificationStats.posttimeToFirstAirtimeMs >= 0) { 225 posttimeToFirstAirtimeMs.addSample( 226 singleNotificationStats.posttimeToFirstAirtimeMs); 227 } 228 if (singleNotificationStats.posttimeToFirstVisibleExpansionMs >= 0) { 229 posttimeToFirstVisibleExpansionMs.addSample( 230 singleNotificationStats.posttimeToFirstVisibleExpansionMs); 231 } 232 userExpansionCount.addSample(singleNotificationStats.userExpansionCount); 233 if (singleNotificationStats.airtimeExpandedMs >= 0) { 234 airtimeExpandedMs.addSample(singleNotificationStats.airtimeExpandedMs); 235 } 236 } 237 238 public void dump(PrintWriter pw, String indent) { 239 pw.println(toStringWithIndent(indent)); 240 } 241 242 @Override 243 public String toString() { 244 return toStringWithIndent(""); 245 } 246 247 private String toStringWithIndent(String indent) { 248 return indent + "AggregatedStats{\n" + 249 indent + " key='" + key + "',\n" + 250 indent + " numPostedByApp=" + numPostedByApp + ",\n" + 251 indent + " numUpdatedByApp=" + numUpdatedByApp + ",\n" + 252 indent + " numRemovedByApp=" + numRemovedByApp + ",\n" + 253 indent + " numClickedByUser=" + numClickedByUser + ",\n" + 254 indent + " numDismissedByUser=" + numDismissedByUser + ",\n" + 255 indent + " posttimeMs=" + posttimeMs + ",\n" + 256 indent + " posttimeToDismissMs=" + posttimeToDismissMs + ",\n" + 257 indent + " posttimeToFirstClickMs=" + posttimeToFirstClickMs + ",\n" + 258 indent + " airtimeCount=" + airtimeCount + ",\n" + 259 indent + " airtimeMs=" + airtimeMs + ",\n" + 260 indent + " posttimeToFirstAirtimeMs=" + posttimeToFirstAirtimeMs + ",\n" + 261 indent + " userExpansionCount=" + userExpansionCount + ",\n" + 262 indent + " airtimeExpandedMs=" + airtimeExpandedMs + ",\n" + 263 indent + " posttimeToFVEMs=" + posttimeToFirstVisibleExpansionMs + ",\n" + 264 indent + "}"; 265 } 266 } 267 268 /** 269 * Tracks usage of an individual notification that is currently active. 270 */ 271 public static class SingleNotificationStats { 272 private boolean isVisible = false; 273 private boolean isExpanded = false; 274 /** SystemClock.elapsedRealtime() when the notification was posted. */ 275 public long posttimeElapsedMs = -1; 276 /** Elapsed time since the notification was posted until it was first clicked, or -1. */ 277 public long posttimeToFirstClickMs = -1; 278 /** Elpased time since the notification was posted until it was dismissed by the user. */ 279 public long posttimeToDismissMs = -1; 280 /** Number of times the notification has been made visible. */ 281 public long airtimeCount = 0; 282 /** Time in ms between the notification was posted and first shown; -1 if never shown. */ 283 public long posttimeToFirstAirtimeMs = -1; 284 /** 285 * If currently visible, SystemClock.elapsedRealtime() when the notification was made 286 * visible; -1 otherwise. 287 */ 288 public long currentAirtimeStartElapsedMs = -1; 289 /** Accumulated visible time. */ 290 public long airtimeMs = 0; 291 /** 292 * Time in ms between the notification being posted and when it first 293 * became visible and expanded; -1 if it was never visibly expanded. 294 */ 295 public long posttimeToFirstVisibleExpansionMs = -1; 296 /** 297 * If currently visible, SystemClock.elapsedRealtime() when the notification was made 298 * visible; -1 otherwise. 299 */ 300 public long currentAirtimeExpandedStartElapsedMs = -1; 301 /** Accumulated visible expanded time. */ 302 public long airtimeExpandedMs = 0; 303 /** Number of times the notification has been expanded by the user. */ 304 public long userExpansionCount = 0; 305 306 public long getCurrentPosttimeMs() { 307 if (posttimeElapsedMs < 0) { 308 return 0; 309 } 310 return SystemClock.elapsedRealtime() - posttimeElapsedMs; 311 } 312 313 public long getCurrentAirtimeMs() { 314 long result = airtimeMs; 315 // Add incomplete airtime if currently shown. 316 if (currentAirtimeStartElapsedMs >= 0) { 317 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs); 318 } 319 return result; 320 } 321 322 public long getCurrentAirtimeExpandedMs() { 323 long result = airtimeExpandedMs; 324 // Add incomplete expanded airtime if currently shown. 325 if (currentAirtimeExpandedStartElapsedMs >= 0) { 326 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs); 327 } 328 return result; 329 } 330 331 /** 332 * Called when the user clicked the notification. 333 */ 334 public void onClick() { 335 if (posttimeToFirstClickMs < 0) { 336 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs; 337 } 338 } 339 340 /** 341 * Called when the user removed the notification. 342 */ 343 public void onDismiss() { 344 if (posttimeToDismissMs < 0) { 345 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs; 346 } 347 finish(); 348 } 349 350 public void onCancel() { 351 finish(); 352 } 353 354 public void onRemoved() { 355 finish(); 356 } 357 358 public void onVisibilityChanged(boolean visible) { 359 long elapsedNowMs = SystemClock.elapsedRealtime(); 360 final boolean wasVisible = isVisible; 361 isVisible = visible; 362 if (visible) { 363 if (currentAirtimeStartElapsedMs < 0) { 364 airtimeCount++; 365 currentAirtimeStartElapsedMs = elapsedNowMs; 366 } 367 if (posttimeToFirstAirtimeMs < 0) { 368 posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs; 369 } 370 } else { 371 if (currentAirtimeStartElapsedMs >= 0) { 372 airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs); 373 currentAirtimeStartElapsedMs = -1; 374 } 375 } 376 377 if (wasVisible != isVisible) { 378 updateVisiblyExpandedStats(); 379 } 380 } 381 382 public void onExpansionChanged(boolean userAction, boolean expanded) { 383 isExpanded = expanded; 384 if (isExpanded && userAction) { 385 userExpansionCount++; 386 } 387 updateVisiblyExpandedStats(); 388 } 389 390 private void updateVisiblyExpandedStats() { 391 long elapsedNowMs = SystemClock.elapsedRealtime(); 392 if (isExpanded && isVisible) { 393 // expanded and visible 394 if (currentAirtimeExpandedStartElapsedMs < 0) { 395 currentAirtimeExpandedStartElapsedMs = elapsedNowMs; 396 } 397 if (posttimeToFirstVisibleExpansionMs < 0) { 398 posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs; 399 } 400 } else { 401 // not-expanded or not-visible 402 if (currentAirtimeExpandedStartElapsedMs >= 0) { 403 airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs); 404 currentAirtimeExpandedStartElapsedMs = -1; 405 } 406 } 407 } 408 409 /** The notification is leaving the system. Finalize. */ 410 public void finish() { 411 onVisibilityChanged(false); 412 } 413 414 @Override 415 public String toString() { 416 return "SingleNotificationStats{" + 417 "posttimeElapsedMs=" + posttimeElapsedMs + 418 ", posttimeToFirstClickMs=" + posttimeToFirstClickMs + 419 ", posttimeToDismissMs=" + posttimeToDismissMs + 420 ", airtimeCount=" + airtimeCount + 421 ", airtimeMs=" + airtimeMs + 422 ", currentAirtimeStartElapsedMs=" + currentAirtimeStartElapsedMs + 423 ", airtimeExpandedMs=" + airtimeExpandedMs + 424 ", posttimeToFirstVisibleExpansionMs=" + posttimeToFirstVisibleExpansionMs + 425 ", currentAirtimeExpandedSEMs=" + currentAirtimeExpandedStartElapsedMs + 426 '}'; 427 } 428 } 429 430 /** 431 * Aggregates long samples to sum and averages. 432 */ 433 public static class Aggregate { 434 long numSamples; 435 double avg; 436 double sum2; 437 double var; 438 439 public void addSample(long sample) { 440 // Welford's "Method for Calculating Corrected Sums of Squares" 441 // http://www.jstor.org/stable/1266577?seq=2 442 numSamples++; 443 final double n = numSamples; 444 final double delta = sample - avg; 445 avg += (1.0 / n) * delta; 446 sum2 += ((n - 1) / n) * delta * delta; 447 final double divisor = numSamples == 1 ? 1.0 : n - 1.0; 448 var = sum2 / divisor; 449 } 450 451 @Override 452 public String toString() { 453 return "Aggregate{" + 454 "numSamples=" + numSamples + 455 ", avg=" + avg + 456 ", var=" + var + 457 '}'; 458 } 459 } 460 461 private static class SQLiteLog { 462 private static final String TAG = "NotificationSQLiteLog"; 463 464 // Message types passed to the background handler. 465 private static final int MSG_POST = 1; 466 private static final int MSG_CLICK = 2; 467 private static final int MSG_REMOVE = 3; 468 private static final int MSG_DISMISS = 4; 469 470 private static final String DB_NAME = "notification_log.db"; 471 private static final int DB_VERSION = 4; 472 473 /** Age in ms after which events are pruned from the DB. */ 474 private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L; // 1 week 475 /** Delay between pruning the DB. Used to throttle pruning. */ 476 private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L; // 6 hours 477 /** Mininum number of writes between pruning the DB. Used to throttle pruning. */ 478 private static final long PRUNE_MIN_WRITES = 1024; 479 480 // Table 'log' 481 private static final String TAB_LOG = "log"; 482 private static final String COL_EVENT_USER_ID = "event_user_id"; 483 private static final String COL_EVENT_TYPE = "event_type"; 484 private static final String COL_EVENT_TIME = "event_time_ms"; 485 private static final String COL_KEY = "key"; 486 private static final String COL_PKG = "pkg"; 487 private static final String COL_NOTIFICATION_ID = "nid"; 488 private static final String COL_TAG = "tag"; 489 private static final String COL_WHEN_MS = "when_ms"; 490 private static final String COL_DEFAULTS = "defaults"; 491 private static final String COL_FLAGS = "flags"; 492 private static final String COL_PRIORITY = "priority"; 493 private static final String COL_CATEGORY = "category"; 494 private static final String COL_ACTION_COUNT = "action_count"; 495 private static final String COL_POSTTIME_MS = "posttime_ms"; 496 private static final String COL_AIRTIME_MS = "airtime_ms"; 497 private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms"; 498 private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms"; 499 private static final String COL_EXPAND_COUNT = "expansion_count"; 500 501 502 private static final int EVENT_TYPE_POST = 1; 503 private static final int EVENT_TYPE_CLICK = 2; 504 private static final int EVENT_TYPE_REMOVE = 3; 505 private static final int EVENT_TYPE_DISMISS = 4; 506 507 private static long sLastPruneMs; 508 private static long sNumWrites; 509 510 private final SQLiteOpenHelper mHelper; 511 private final Handler mWriteHandler; 512 513 private static final long DAY_MS = 24 * 60 * 60 * 1000; 514 515 public SQLiteLog(Context context) { 516 HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log", 517 android.os.Process.THREAD_PRIORITY_BACKGROUND); 518 backgroundThread.start(); 519 mWriteHandler = new Handler(backgroundThread.getLooper()) { 520 @Override 521 public void handleMessage(Message msg) { 522 NotificationRecord r = (NotificationRecord) msg.obj; 523 long nowMs = System.currentTimeMillis(); 524 switch (msg.what) { 525 case MSG_POST: 526 writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r); 527 break; 528 case MSG_CLICK: 529 writeEvent(nowMs, EVENT_TYPE_CLICK, r); 530 break; 531 case MSG_REMOVE: 532 writeEvent(nowMs, EVENT_TYPE_REMOVE, r); 533 break; 534 case MSG_DISMISS: 535 writeEvent(nowMs, EVENT_TYPE_DISMISS, r); 536 break; 537 default: 538 Log.wtf(TAG, "Unknown message type: " + msg.what); 539 break; 540 } 541 } 542 }; 543 mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { 544 @Override 545 public void onCreate(SQLiteDatabase db) { 546 db.execSQL("CREATE TABLE " + TAB_LOG + " (" + 547 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 548 COL_EVENT_USER_ID + " INT," + 549 COL_EVENT_TYPE + " INT," + 550 COL_EVENT_TIME + " INT," + 551 COL_KEY + " TEXT," + 552 COL_PKG + " TEXT," + 553 COL_NOTIFICATION_ID + " INT," + 554 COL_TAG + " TEXT," + 555 COL_WHEN_MS + " INT," + 556 COL_DEFAULTS + " INT," + 557 COL_FLAGS + " INT," + 558 COL_PRIORITY + " INT," + 559 COL_CATEGORY + " TEXT," + 560 COL_ACTION_COUNT + " INT," + 561 COL_POSTTIME_MS + " INT," + 562 COL_AIRTIME_MS + " INT," + 563 COL_FIRST_EXPANSIONTIME_MS + " INT," + 564 COL_AIRTIME_EXPANDED_MS + " INT," + 565 COL_EXPAND_COUNT + " INT" + 566 ")"); 567 } 568 569 @Override 570 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 571 if (oldVersion <= 3) { 572 // Version 3 creation left 'log' in a weird state. Just reset for now. 573 db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG); 574 onCreate(db); 575 } 576 } 577 }; 578 } 579 580 public void logPosted(NotificationRecord notification) { 581 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification)); 582 } 583 584 public void logClicked(NotificationRecord notification) { 585 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification)); 586 } 587 588 public void logRemoved(NotificationRecord notification) { 589 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification)); 590 } 591 592 public void logDismissed(NotificationRecord notification) { 593 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification)); 594 } 595 596 public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) { 597 SQLiteDatabase db = mHelper.getReadableDatabase(); 598 long nowMs = System.currentTimeMillis(); 599 String q = "SELECT " + 600 COL_EVENT_USER_ID + ", " + 601 COL_PKG + ", " + 602 // Bucket by day by looking at 'floor((nowMs - eventTimeMs) / dayMs)' 603 "CAST(((" + nowMs + " - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " + 604 "AS day, " + 605 "COUNT(*) AS cnt " + 606 "FROM " + TAB_LOG + " " + 607 "WHERE " + 608 COL_EVENT_TYPE + "=" + EVENT_TYPE_POST + " " + 609 "GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG; 610 Cursor cursor = db.rawQuery(q, null); 611 try { 612 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 613 int userId = cursor.getInt(0); 614 String pkg = cursor.getString(1); 615 if (filter != null && !filter.matches(pkg)) continue; 616 int day = cursor.getInt(2); 617 int count = cursor.getInt(3); 618 pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg + 619 ",day=" + day + ",count=" + count + "}"); 620 } 621 } finally { 622 cursor.close(); 623 } 624 } 625 626 private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) { 627 ContentValues cv = new ContentValues(); 628 cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier()); 629 cv.put(COL_EVENT_TIME, eventTimeMs); 630 cv.put(COL_EVENT_TYPE, eventType); 631 putNotificationIdentifiers(r, cv); 632 if (eventType == EVENT_TYPE_POST) { 633 putNotificationDetails(r, cv); 634 } else { 635 putPosttimeVisibility(r, cv); 636 } 637 SQLiteDatabase db = mHelper.getWritableDatabase(); 638 if (db.insert(TAB_LOG, null, cv) < 0) { 639 Log.wtf(TAG, "Error while trying to insert values: " + cv); 640 } 641 sNumWrites++; 642 pruneIfNecessary(db); 643 } 644 645 private void pruneIfNecessary(SQLiteDatabase db) { 646 // Prune if we haven't in a while. 647 long nowMs = System.currentTimeMillis(); 648 if (sNumWrites > PRUNE_MIN_WRITES || 649 nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) { 650 sNumWrites = 0; 651 sLastPruneMs = nowMs; 652 long horizonStartMs = nowMs - HORIZON_MS; 653 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?", 654 new String[] { String.valueOf(horizonStartMs) }); 655 Log.d(TAG, "Pruned event entries: " + deletedRows); 656 } 657 } 658 659 private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) { 660 outCv.put(COL_KEY, r.sbn.getKey()); 661 outCv.put(COL_PKG, r.sbn.getPackageName()); 662 } 663 664 private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) { 665 outCv.put(COL_NOTIFICATION_ID, r.sbn.getId()); 666 if (r.sbn.getTag() != null) { 667 outCv.put(COL_TAG, r.sbn.getTag()); 668 } 669 outCv.put(COL_WHEN_MS, r.sbn.getPostTime()); 670 outCv.put(COL_FLAGS, r.getNotification().flags); 671 outCv.put(COL_PRIORITY, r.getNotification().priority); 672 if (r.getNotification().category != null) { 673 outCv.put(COL_CATEGORY, r.getNotification().category); 674 } 675 outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ? 676 r.getNotification().actions.length : 0); 677 } 678 679 private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) { 680 outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs()); 681 outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs()); 682 outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount); 683 outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs()); 684 outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs); 685 } 686 687 public void dump(PrintWriter pw, String indent, DumpFilter filter) { 688 printPostFrequencies(pw, indent, filter); 689 } 690 } 691} 692