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