blob: 027285087de3dd4c458ad185e1eaedfe3c9637f8 [file] [log] [blame]
Christoph Studer546bec82014-03-14 12:17:12 +01001/*
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
Chris Wrencdee8cd2016-01-25 17:10:30 -050019import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_HIGH;
20
Chris Wren93abac42015-06-23 11:23:37 -040021import android.app.Notification;
Christoph Studer1c3f81f2014-04-16 15:05:56 +020022import android.content.ContentValues;
23import android.content.Context;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.os.Handler;
28import android.os.HandlerThread;
29import android.os.Message;
Christoph Studer546bec82014-03-14 12:17:12 +010030import android.os.SystemClock;
Chris Wren2667e482015-10-30 14:50:22 -040031import android.text.TextUtils;
Christoph Studer1c3f81f2014-04-16 15:05:56 +020032import android.util.Log;
Christoph Studer546bec82014-03-14 12:17:12 +010033
Chris Wren723aa762015-04-09 15:35:23 -040034import com.android.internal.logging.MetricsLogger;
John Spurlock25e2d242014-06-27 13:58:23 -040035import com.android.server.notification.NotificationManagerService.DumpFilter;
36
Chris Wrene4b38802015-07-07 15:54:19 -040037import org.json.JSONArray;
38import org.json.JSONException;
39import org.json.JSONObject;
40
Christoph Studer546bec82014-03-14 12:17:12 +010041import java.io.PrintWriter;
Chris Wren5eab2b72015-06-16 13:56:22 -040042import java.util.ArrayDeque;
Chris Wrene4b38802015-07-07 15:54:19 -040043import java.util.Calendar;
44import java.util.GregorianCalendar;
Christoph Studer546bec82014-03-14 12:17:12 +010045import java.util.HashMap;
46import java.util.Map;
Chris Wren2667e482015-10-30 14:50:22 -040047import java.util.Set;
Christoph Studer546bec82014-03-14 12:17:12 +010048
49/**
50 * Keeps track of notification activity, display, and user interaction.
51 *
52 * <p>This class receives signals from NoMan and keeps running stats of
53 * notification usage. Some metrics are updated as events occur. Others, namely
54 * those involving durations, are updated as the notification is canceled.</p>
55 *
56 * <p>This class is thread-safe.</p>
57 *
58 * {@hide}
59 */
60public class NotificationUsageStats {
Chris Wren5eab2b72015-06-16 13:56:22 -040061 private static final String TAG = "NotificationUsageStats";
Christoph Studer05e28842014-05-27 16:55:57 +020062
Chris Wren5eab2b72015-06-16 13:56:22 -040063 private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
64 private static final boolean ENABLE_SQLITE_LOG = true;
Christoph Studer856b2b82014-08-22 20:45:28 +020065 private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
Chris Wren5eab2b72015-06-16 13:56:22 -040066 private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
67 private static final int MSG_EMIT = 1;
68
69 private static final boolean DEBUG = false;
70 public static final int TEN_SECONDS = 1000 * 10;
Chris Wren93abac42015-06-23 11:23:37 -040071 public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
72 private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
Christoph Studer856b2b82014-08-22 20:45:28 +020073
Christoph Studer546bec82014-03-14 12:17:12 +010074 // Guarded by synchronized(this).
Chris Wren5eab2b72015-06-16 13:56:22 -040075 private final Map<String, AggregatedStats> mStats = new HashMap<>();
76 private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
Christoph Studer1c3f81f2014-04-16 15:05:56 +020077 private final SQLiteLog mSQLiteLog;
Chris Wren723aa762015-04-09 15:35:23 -040078 private final Context mContext;
Chris Wren5eab2b72015-06-16 13:56:22 -040079 private final Handler mHandler;
80 private long mLastEmitTime;
Christoph Studer1c3f81f2014-04-16 15:05:56 +020081
82 public NotificationUsageStats(Context context) {
Chris Wren723aa762015-04-09 15:35:23 -040083 mContext = context;
Chris Wren5eab2b72015-06-16 13:56:22 -040084 mLastEmitTime = SystemClock.elapsedRealtime();
Christoph Studer05e28842014-05-27 16:55:57 +020085 mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
Chris Wren5eab2b72015-06-16 13:56:22 -040086 mHandler = new Handler(mContext.getMainLooper()) {
87 @Override
88 public void handleMessage(Message msg) {
89 switch (msg.what) {
90 case MSG_EMIT:
91 emit();
92 break;
93 default:
94 Log.wtf(TAG, "Unknown message type: " + msg.what);
95 break;
96 }
97 }
98 };
99 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200100 }
Christoph Studer546bec82014-03-14 12:17:12 +0100101
102 /**
103 * Called when a notification has been posted.
104 */
105 public synchronized void registerPostedByApp(NotificationRecord notification) {
106 notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
Chris Wren5eab2b72015-06-16 13:56:22 -0400107
108 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
109 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100110 stats.numPostedByApp++;
Chris Wren93abac42015-06-23 11:23:37 -0400111 stats.countApiUse(notification);
Christoph Studer546bec82014-03-14 12:17:12 +0100112 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400113 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer05e28842014-05-27 16:55:57 +0200114 if (ENABLE_SQLITE_LOG) {
115 mSQLiteLog.logPosted(notification);
116 }
Christoph Studer546bec82014-03-14 12:17:12 +0100117 }
118
119 /**
120 * Called when a notification has been updated.
121 */
Christoph Studer061dee22014-05-09 12:28:55 +0200122 public void registerUpdatedByApp(NotificationRecord notification, NotificationRecord old) {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500123 notification.stats.updateFrom(old.stats);
Chris Wren5eab2b72015-06-16 13:56:22 -0400124 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
125 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100126 stats.numUpdatedByApp++;
Chris Wren93abac42015-06-23 11:23:37 -0400127 stats.countApiUse(notification);
Christoph Studer546bec82014-03-14 12:17:12 +0100128 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400129 releaseAggregatedStatsLocked(aggregatedStatsArray);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500130 if (ENABLE_SQLITE_LOG) {
131 mSQLiteLog.logPosted(notification);
132 }
Christoph Studer546bec82014-03-14 12:17:12 +0100133 }
134
135 /**
136 * Called when the originating app removed the notification programmatically.
137 */
138 public synchronized void registerRemovedByApp(NotificationRecord notification) {
Christoph Studerffeb0c32014-05-07 22:23:56 +0200139 notification.stats.onRemoved();
Chris Wren5eab2b72015-06-16 13:56:22 -0400140 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
141 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100142 stats.numRemovedByApp++;
Christoph Studer546bec82014-03-14 12:17:12 +0100143 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400144 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer05e28842014-05-27 16:55:57 +0200145 if (ENABLE_SQLITE_LOG) {
146 mSQLiteLog.logRemoved(notification);
147 }
Christoph Studer546bec82014-03-14 12:17:12 +0100148 }
149
150 /**
151 * Called when the user dismissed the notification via the UI.
152 */
153 public synchronized void registerDismissedByUser(NotificationRecord notification) {
Chris Wren723aa762015-04-09 15:35:23 -0400154 MetricsLogger.histogram(mContext, "note_dismiss_longevity",
155 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
Christoph Studer546bec82014-03-14 12:17:12 +0100156 notification.stats.onDismiss();
Christoph Studer05e28842014-05-27 16:55:57 +0200157 if (ENABLE_SQLITE_LOG) {
158 mSQLiteLog.logDismissed(notification);
159 }
Christoph Studer546bec82014-03-14 12:17:12 +0100160 }
161
162 /**
163 * Called when the user clicked the notification in the UI.
164 */
165 public synchronized void registerClickedByUser(NotificationRecord notification) {
Chris Wren723aa762015-04-09 15:35:23 -0400166 MetricsLogger.histogram(mContext, "note_click_longevity",
167 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
Christoph Studer546bec82014-03-14 12:17:12 +0100168 notification.stats.onClick();
Christoph Studer05e28842014-05-27 16:55:57 +0200169 if (ENABLE_SQLITE_LOG) {
170 mSQLiteLog.logClicked(notification);
171 }
Christoph Studer546bec82014-03-14 12:17:12 +0100172 }
173
Chris Wren5eab2b72015-06-16 13:56:22 -0400174 public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
175 boolean starred, boolean cached) {
176 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
177 for (AggregatedStats stats : aggregatedStatsArray) {
178 if (valid) {
179 stats.numWithValidPeople++;
180 }
181 if (starred) {
182 stats.numWithStaredPeople++;
183 }
184 if (cached) {
185 stats.numPeopleCacheHit++;
186 } else {
187 stats.numPeopleCacheMiss++;
188 }
Christoph Studer546bec82014-03-14 12:17:12 +0100189 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400190 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer546bec82014-03-14 12:17:12 +0100191 }
192
Chris Wren5eab2b72015-06-16 13:56:22 -0400193 public synchronized void registerBlocked(NotificationRecord notification) {
194 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
195 for (AggregatedStats stats : aggregatedStatsArray) {
196 stats.numBlocked++;
Christoph Studer546bec82014-03-14 12:17:12 +0100197 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400198 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer546bec82014-03-14 12:17:12 +0100199 }
200
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000201 public synchronized void registerSuspendedByAdmin(NotificationRecord notification) {
202 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
203 for (AggregatedStats stats : aggregatedStatsArray) {
204 stats.numSuspendedByAdmin++;
205 }
206 releaseAggregatedStatsLocked(aggregatedStatsArray);
207 }
208
Christoph Studer546bec82014-03-14 12:17:12 +0100209 // Locked by this.
210 private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
Christoph Studer856b2b82014-08-22 20:45:28 +0200211 if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
212 return EMPTY_AGGREGATED_STATS;
213 }
214
Chris Wren5eab2b72015-06-16 13:56:22 -0400215 // TODO: expand to package-level counts in the future.
216 AggregatedStats[] array = mStatsArrays.poll();
217 if (array == null) {
218 array = new AggregatedStats[1];
219 }
220 array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
221 return array;
222 }
Christoph Studer546bec82014-03-14 12:17:12 +0100223
Chris Wren5eab2b72015-06-16 13:56:22 -0400224 // Locked by this.
225 private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
226 for(int i = 0; i < array.length; i++) {
227 array[i] = null;
228 }
229 mStatsArrays.offer(array);
Christoph Studer546bec82014-03-14 12:17:12 +0100230 }
231
232 // Locked by this.
233 private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
234 AggregatedStats result = mStats.get(key);
235 if (result == null) {
Chris Wren5eab2b72015-06-16 13:56:22 -0400236 result = new AggregatedStats(mContext, key);
Christoph Studer546bec82014-03-14 12:17:12 +0100237 mStats.put(key, result);
238 }
239 return result;
240 }
241
Chris Wrene4b38802015-07-07 15:54:19 -0400242 public synchronized JSONObject dumpJson(DumpFilter filter) {
243 JSONObject dump = new JSONObject();
244 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
245 try {
246 JSONArray aggregatedStats = new JSONArray();
247 for (AggregatedStats as : mStats.values()) {
248 if (filter != null && !filter.matches(as.key))
249 continue;
250 aggregatedStats.put(as.dumpJson());
251 }
252 dump.put("current", aggregatedStats);
253 } catch (JSONException e) {
254 // pass
255 }
256 }
257 if (ENABLE_SQLITE_LOG) {
258 try {
259 dump.put("historical", mSQLiteLog.dumpJson(filter));
260 } catch (JSONException e) {
261 // pass
262 }
263 }
264 return dump;
265 }
266
John Spurlock25e2d242014-06-27 13:58:23 -0400267 public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
Christoph Studer856b2b82014-08-22 20:45:28 +0200268 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
269 for (AggregatedStats as : mStats.values()) {
270 if (filter != null && !filter.matches(as.key))
271 continue;
272 as.dump(pw, indent);
273 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400274 pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
Christoph Studer546bec82014-03-14 12:17:12 +0100275 }
Christoph Studer05e28842014-05-27 16:55:57 +0200276 if (ENABLE_SQLITE_LOG) {
John Spurlock25e2d242014-06-27 13:58:23 -0400277 mSQLiteLog.dump(pw, indent, filter);
Christoph Studer05e28842014-05-27 16:55:57 +0200278 }
Christoph Studer546bec82014-03-14 12:17:12 +0100279 }
280
Chris Wren5eab2b72015-06-16 13:56:22 -0400281 public synchronized void emit() {
282 // TODO: expand to package-level counts in the future.
283 AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
284 stats.emit();
285 mLastEmitTime = SystemClock.elapsedRealtime();
286 mHandler.removeMessages(MSG_EMIT);
287 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
288 }
289
Christoph Studer546bec82014-03-14 12:17:12 +0100290 /**
291 * Aggregated notification stats.
292 */
293 private static class AggregatedStats {
Chris Wren5eab2b72015-06-16 13:56:22 -0400294
295 private final Context mContext;
Christoph Studer546bec82014-03-14 12:17:12 +0100296 public final String key;
Chris Wrene4b38802015-07-07 15:54:19 -0400297 private final long mCreated;
Chris Wren93abac42015-06-23 11:23:37 -0400298 private AggregatedStats mPrevious;
Christoph Studer546bec82014-03-14 12:17:12 +0100299
300 // ---- Updated as the respective events occur.
301 public int numPostedByApp;
302 public int numUpdatedByApp;
303 public int numRemovedByApp;
Chris Wren5eab2b72015-06-16 13:56:22 -0400304 public int numPeopleCacheHit;
305 public int numPeopleCacheMiss;;
306 public int numWithStaredPeople;
307 public int numWithValidPeople;
308 public int numBlocked;
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000309 public int numSuspendedByAdmin;
Chris Wren93abac42015-06-23 11:23:37 -0400310 public int numWithActions;
311 public int numPrivate;
312 public int numSecret;
Chris Wren93abac42015-06-23 11:23:37 -0400313 public int numWithBigText;
314 public int numWithBigPicture;
315 public int numForegroundService;
316 public int numOngoing;
317 public int numAutoCancel;
318 public int numWithLargeIcon;
319 public int numWithInbox;
320 public int numWithMediaSession;
321 public int numWithTitle;
322 public int numWithText;
323 public int numWithSubText;
324 public int numWithInfoText;
325 public int numInterrupt;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500326 public ImportanceHistogram noisyImportance;
327 public ImportanceHistogram quietImportance;
328 public ImportanceHistogram finalImportance;
Christoph Studer546bec82014-03-14 12:17:12 +0100329
Chris Wren5eab2b72015-06-16 13:56:22 -0400330 public AggregatedStats(Context context, String key) {
Christoph Studer546bec82014-03-14 12:17:12 +0100331 this.key = key;
Chris Wren5eab2b72015-06-16 13:56:22 -0400332 mContext = context;
Chris Wrene4b38802015-07-07 15:54:19 -0400333 mCreated = SystemClock.elapsedRealtime();
Chris Wrencdee8cd2016-01-25 17:10:30 -0500334 noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
335 quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
336 finalImportance = new ImportanceHistogram(context, "note_importance_");
Christoph Studer546bec82014-03-14 12:17:12 +0100337 }
338
Chris Wren93abac42015-06-23 11:23:37 -0400339 public void countApiUse(NotificationRecord record) {
340 final Notification n = record.getNotification();
341 if (n.actions != null) {
342 numWithActions++;
343 }
344
345 if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
346 numForegroundService++;
347 }
348
349 if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
350 numOngoing++;
351 }
352
353 if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
354 numAutoCancel++;
355 }
356
357 if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
358 (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
359 n.sound != null || n.vibrate != null) {
360 numInterrupt++;
361 }
362
363 switch (n.visibility) {
364 case Notification.VISIBILITY_PRIVATE:
365 numPrivate++;
366 break;
367 case Notification.VISIBILITY_SECRET:
368 numSecret++;
369 break;
370 }
371
Chris Wrencdee8cd2016-01-25 17:10:30 -0500372 if (record.stats.isNoisy) {
373 noisyImportance.increment(record.stats.requestedImportance);
374 } else {
375 quietImportance.increment(record.stats.requestedImportance);
Chris Wren93abac42015-06-23 11:23:37 -0400376 }
Chris Wrencdee8cd2016-01-25 17:10:30 -0500377 finalImportance.increment(record.getImportance());
Chris Wren93abac42015-06-23 11:23:37 -0400378
Chris Wren2667e482015-10-30 14:50:22 -0400379 final Set<String> names = n.extras.keySet();
380 if (names.contains(Notification.EXTRA_BIG_TEXT)) {
381 numWithBigText++;
382 }
383 if (names.contains(Notification.EXTRA_PICTURE)) {
384 numWithBigPicture++;
385 }
386 if (names.contains(Notification.EXTRA_LARGE_ICON)) {
387 numWithLargeIcon++;
388 }
389 if (names.contains(Notification.EXTRA_TEXT_LINES)) {
390 numWithInbox++;
391 }
392 if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
393 numWithMediaSession++;
394 }
395 if (names.contains(Notification.EXTRA_TITLE) &&
396 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
397 numWithTitle++;
398 }
399 if (names.contains(Notification.EXTRA_TEXT) &&
400 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
401 numWithText++;
402 }
403 if (names.contains(Notification.EXTRA_SUB_TEXT) &&
404 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
405 numWithSubText++;
406 }
407 if (names.contains(Notification.EXTRA_INFO_TEXT) &&
408 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
409 numWithInfoText++;
Chris Wren93abac42015-06-23 11:23:37 -0400410 }
411 }
412
Chris Wren5eab2b72015-06-16 13:56:22 -0400413 public void emit() {
414 if (mPrevious == null) {
415 mPrevious = new AggregatedStats(null, key);
Christoph Studer546bec82014-03-14 12:17:12 +0100416 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400417
418 maybeCount("note_post", (numPostedByApp - mPrevious.numPostedByApp));
419 maybeCount("note_update", (numUpdatedByApp - mPrevious.numUpdatedByApp));
420 maybeCount("note_remove", (numRemovedByApp - mPrevious.numRemovedByApp));
421 maybeCount("note_with_people", (numWithValidPeople - mPrevious.numWithValidPeople));
422 maybeCount("note_with_stars", (numWithStaredPeople - mPrevious.numWithStaredPeople));
423 maybeCount("people_cache_hit", (numPeopleCacheHit - mPrevious.numPeopleCacheHit));
424 maybeCount("people_cache_miss", (numPeopleCacheMiss - mPrevious.numPeopleCacheMiss));
425 maybeCount("note_blocked", (numBlocked - mPrevious.numBlocked));
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000426 maybeCount("note_suspended", (numSuspendedByAdmin - mPrevious.numSuspendedByAdmin));
Chris Wren93abac42015-06-23 11:23:37 -0400427 maybeCount("note_with_actions", (numWithActions - mPrevious.numWithActions));
428 maybeCount("note_private", (numPrivate - mPrevious.numPrivate));
429 maybeCount("note_secret", (numSecret - mPrevious.numSecret));
Chris Wren93abac42015-06-23 11:23:37 -0400430 maybeCount("note_interupt", (numInterrupt - mPrevious.numInterrupt));
431 maybeCount("note_big_text", (numWithBigText - mPrevious.numWithBigText));
432 maybeCount("note_big_pic", (numWithBigPicture - mPrevious.numWithBigPicture));
433 maybeCount("note_fg", (numForegroundService - mPrevious.numForegroundService));
434 maybeCount("note_ongoing", (numOngoing - mPrevious.numOngoing));
435 maybeCount("note_auto", (numAutoCancel - mPrevious.numAutoCancel));
436 maybeCount("note_large_icon", (numWithLargeIcon - mPrevious.numWithLargeIcon));
437 maybeCount("note_inbox", (numWithInbox - mPrevious.numWithInbox));
438 maybeCount("note_media", (numWithMediaSession - mPrevious.numWithMediaSession));
439 maybeCount("note_title", (numWithTitle - mPrevious.numWithTitle));
440 maybeCount("note_text", (numWithText - mPrevious.numWithText));
441 maybeCount("note_sub_text", (numWithSubText - mPrevious.numWithSubText));
442 maybeCount("note_info_text", (numWithInfoText - mPrevious.numWithInfoText));
Chris Wrencdee8cd2016-01-25 17:10:30 -0500443 noisyImportance.maybeCount(mPrevious.noisyImportance);
444 quietImportance.maybeCount(mPrevious.quietImportance);
445 finalImportance.maybeCount(mPrevious.finalImportance);
Chris Wren5eab2b72015-06-16 13:56:22 -0400446
447 mPrevious.numPostedByApp = numPostedByApp;
448 mPrevious.numUpdatedByApp = numUpdatedByApp;
449 mPrevious.numRemovedByApp = numRemovedByApp;
450 mPrevious.numPeopleCacheHit = numPeopleCacheHit;
451 mPrevious.numPeopleCacheMiss = numPeopleCacheMiss;
452 mPrevious.numWithStaredPeople = numWithStaredPeople;
453 mPrevious.numWithValidPeople = numWithValidPeople;
454 mPrevious.numBlocked = numBlocked;
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000455 mPrevious.numSuspendedByAdmin = numSuspendedByAdmin;
Chris Wren93abac42015-06-23 11:23:37 -0400456 mPrevious.numWithActions = numWithActions;
457 mPrevious.numPrivate = numPrivate;
458 mPrevious.numSecret = numSecret;
Chris Wren93abac42015-06-23 11:23:37 -0400459 mPrevious.numInterrupt = numInterrupt;
460 mPrevious.numWithBigText = numWithBigText;
461 mPrevious.numWithBigPicture = numWithBigPicture;
462 mPrevious.numForegroundService = numForegroundService;
463 mPrevious.numOngoing = numOngoing;
464 mPrevious.numAutoCancel = numAutoCancel;
465 mPrevious.numWithLargeIcon = numWithLargeIcon;
466 mPrevious.numWithInbox = numWithInbox;
467 mPrevious.numWithMediaSession = numWithMediaSession;
468 mPrevious.numWithTitle = numWithTitle;
469 mPrevious.numWithText = numWithText;
470 mPrevious.numWithSubText = numWithSubText;
471 mPrevious.numWithInfoText = numWithInfoText;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500472 noisyImportance.update(mPrevious.noisyImportance);
473 quietImportance.update(mPrevious.quietImportance);
474 finalImportance.update(mPrevious.finalImportance);
Chris Wren5eab2b72015-06-16 13:56:22 -0400475 }
476
477 void maybeCount(String name, int value) {
478 if (value > 0) {
479 MetricsLogger.count(mContext, name, value);
Chris Wren78403d72014-07-28 10:23:24 +0100480 }
Christoph Studer546bec82014-03-14 12:17:12 +0100481 }
482
483 public void dump(PrintWriter pw, String indent) {
484 pw.println(toStringWithIndent(indent));
485 }
486
487 @Override
488 public String toString() {
489 return toStringWithIndent("");
490 }
491
492 private String toStringWithIndent(String indent) {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500493 StringBuilder output = new StringBuilder();
494 output.append(indent).append("AggregatedStats{\n");
495 String indentPlusTwo = indent + " ";
496 output.append(indentPlusTwo);
497 output.append("key='").append(key).append("',\n");
498 output.append(indentPlusTwo);
499 output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
500 output.append(indentPlusTwo);
501 output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
502 output.append(indentPlusTwo);
503 output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
504 output.append(indentPlusTwo);
505 output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
506 output.append(indentPlusTwo);
507 output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
508 output.append(indentPlusTwo);
509 output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
510 output.append(indentPlusTwo);
511 output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
512 output.append(indentPlusTwo);
513 output.append("numBlocked=").append(numBlocked).append(",\n");
514 output.append(indentPlusTwo);
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000515 output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
516 output.append(indentPlusTwo);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500517 output.append("numWithActions=").append(numWithActions).append(",\n");
518 output.append(indentPlusTwo);
519 output.append("numPrivate=").append(numPrivate).append(",\n");
520 output.append(indentPlusTwo);
521 output.append("numSecret=").append(numSecret).append(",\n");
522 output.append(indentPlusTwo);
523 output.append("numInterrupt=").append(numInterrupt).append(",\n");
524 output.append(indentPlusTwo);
525 output.append("numWithBigText=").append(numWithBigText).append(",\n");
526 output.append(indentPlusTwo);
527 output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
528 output.append(indentPlusTwo);
529 output.append("numForegroundService=").append(numForegroundService).append("\n");
530 output.append(indentPlusTwo);
531 output.append("numOngoing=").append(numOngoing).append("\n");
532 output.append(indentPlusTwo);
533 output.append("numAutoCancel=").append(numAutoCancel).append("\n");
534 output.append(indentPlusTwo);
535 output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
536 output.append(indentPlusTwo);
537 output.append("numWithInbox=").append(numWithInbox).append("\n");
538 output.append(indentPlusTwo);
539 output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
540 output.append(indentPlusTwo);
541 output.append("numWithTitle=").append(numWithTitle).append("\n");
542 output.append(indentPlusTwo);
543 output.append("numWithText=").append(numWithText).append("\n");
544 output.append(indentPlusTwo);
545 output.append("numWithSubText=").append(numWithSubText).append("\n");
546 output.append(indentPlusTwo);
547 output.append("numWithInfoText=").append(numWithInfoText).append("\n");
548 output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
549 output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
550 output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
551 output.append(indent).append("}");
552 return output.toString();
Christoph Studer546bec82014-03-14 12:17:12 +0100553 }
Chris Wrene4b38802015-07-07 15:54:19 -0400554
555 public JSONObject dumpJson() throws JSONException {
556 JSONObject dump = new JSONObject();
557 dump.put("key", key);
558 dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
559 maybePut(dump, "numPostedByApp", numPostedByApp);
560 maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
561 maybePut(dump, "numRemovedByApp", numRemovedByApp);
562 maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
563 maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
564 maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
565 maybePut(dump, "numWithValidPeople", numWithValidPeople);
566 maybePut(dump, "numBlocked", numBlocked);
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000567 maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
Chris Wrene4b38802015-07-07 15:54:19 -0400568 maybePut(dump, "numWithActions", numWithActions);
569 maybePut(dump, "numPrivate", numPrivate);
570 maybePut(dump, "numSecret", numSecret);
Chris Wrene4b38802015-07-07 15:54:19 -0400571 maybePut(dump, "numInterrupt", numInterrupt);
572 maybePut(dump, "numWithBigText", numWithBigText);
573 maybePut(dump, "numWithBigPicture", numWithBigPicture);
574 maybePut(dump, "numForegroundService", numForegroundService);
575 maybePut(dump, "numOngoing", numOngoing);
576 maybePut(dump, "numAutoCancel", numAutoCancel);
577 maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
578 maybePut(dump, "numWithInbox", numWithInbox);
579 maybePut(dump, "numWithMediaSession", numWithMediaSession);
580 maybePut(dump, "numWithTitle", numWithTitle);
581 maybePut(dump, "numWithText", numWithText);
582 maybePut(dump, "numWithSubText", numWithSubText);
583 maybePut(dump, "numWithInfoText", numWithInfoText);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500584 noisyImportance.maybePut(dump, mPrevious.noisyImportance);
585 quietImportance.maybePut(dump, mPrevious.quietImportance);
586 finalImportance.maybePut(dump, mPrevious.finalImportance);
587
Chris Wrene4b38802015-07-07 15:54:19 -0400588 return dump;
589 }
590
591 private void maybePut(JSONObject dump, String name, int value) throws JSONException {
592 if (value > 0) {
593 dump.put(name, value);
594 }
595 }
Christoph Studer546bec82014-03-14 12:17:12 +0100596 }
597
Chris Wrencdee8cd2016-01-25 17:10:30 -0500598 private static class ImportanceHistogram {
599 // TODO define these somewhere else
600 private static final int NUM_IMPORTANCES = 5;
601 private static final String[] IMPORTANCE_NAMES = {"none", "low", "default", "high", "max"};
602 private final Context mContext;
603 private final String[] mCounterNames;
604 private final String mPrefix;
605 private int[] mCount;
606
607 ImportanceHistogram(Context context, String prefix) {
608 mContext = context;
609 mCount = new int[NUM_IMPORTANCES];
610 mCounterNames = new String[NUM_IMPORTANCES];
611 mPrefix = prefix;
612 for (int i = 0; i < NUM_IMPORTANCES; i++) {
613 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
614 }
615 }
616
617 void increment(int imp) {
618 imp = imp < 0 ? 0 : imp > NUM_IMPORTANCES ? NUM_IMPORTANCES : imp;
619 mCount[imp] ++;
620 }
621
622 void maybeCount(ImportanceHistogram prev) {
623 for (int i = 0; i < NUM_IMPORTANCES; i++) {
624 final int value = mCount[i] - prev.mCount[i];
625 if (value > 0) {
626 MetricsLogger.count(mContext, mCounterNames[i], value);
627 }
628 }
629 }
630
631 void update(ImportanceHistogram that) {
632 for (int i = 0; i < NUM_IMPORTANCES; i++) {
633 mCount[i] = that.mCount[i];
634 }
635 }
636
637 public void maybePut(JSONObject dump, ImportanceHistogram prev)
638 throws JSONException {
639 dump.put(mPrefix, new JSONArray(mCount));
640 }
641
642 @Override
643 public String toString() {
644 StringBuilder output = new StringBuilder();
645 output.append(mPrefix).append(": [");
646 for (int i = 0; i < NUM_IMPORTANCES; i++) {
647 output.append(mCount[i]);
648 if (i < (NUM_IMPORTANCES-1)) {
649 output.append(", ");
650 }
651 }
652 output.append("]");
653 return output.toString();
654 }
655 }
656
Christoph Studer546bec82014-03-14 12:17:12 +0100657 /**
658 * Tracks usage of an individual notification that is currently active.
659 */
660 public static class SingleNotificationStats {
Chris Wren78403d72014-07-28 10:23:24 +0100661 private boolean isVisible = false;
662 private boolean isExpanded = false;
Christoph Studer546bec82014-03-14 12:17:12 +0100663 /** SystemClock.elapsedRealtime() when the notification was posted. */
664 public long posttimeElapsedMs = -1;
665 /** Elapsed time since the notification was posted until it was first clicked, or -1. */
666 public long posttimeToFirstClickMs = -1;
667 /** Elpased time since the notification was posted until it was dismissed by the user. */
668 public long posttimeToDismissMs = -1;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200669 /** Number of times the notification has been made visible. */
670 public long airtimeCount = 0;
671 /** Time in ms between the notification was posted and first shown; -1 if never shown. */
672 public long posttimeToFirstAirtimeMs = -1;
673 /**
674 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
675 * visible; -1 otherwise.
676 */
677 public long currentAirtimeStartElapsedMs = -1;
678 /** Accumulated visible time. */
679 public long airtimeMs = 0;
Chris Wren78403d72014-07-28 10:23:24 +0100680 /**
681 * Time in ms between the notification being posted and when it first
682 * became visible and expanded; -1 if it was never visibly expanded.
683 */
684 public long posttimeToFirstVisibleExpansionMs = -1;
685 /**
686 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
687 * visible; -1 otherwise.
688 */
689 public long currentAirtimeExpandedStartElapsedMs = -1;
690 /** Accumulated visible expanded time. */
691 public long airtimeExpandedMs = 0;
692 /** Number of times the notification has been expanded by the user. */
693 public long userExpansionCount = 0;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500694 /** Importance directly requested by the app. */
695 public int requestedImportance;
696 /** Did the app include sound or vibration on the notificaiton. */
697 public boolean isNoisy;
698 /** Importance after initial filtering for noise and other features */
699 public int naturalImportance;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200700
701 public long getCurrentPosttimeMs() {
702 if (posttimeElapsedMs < 0) {
703 return 0;
704 }
705 return SystemClock.elapsedRealtime() - posttimeElapsedMs;
706 }
707
708 public long getCurrentAirtimeMs() {
709 long result = airtimeMs;
710 // Add incomplete airtime if currently shown.
711 if (currentAirtimeStartElapsedMs >= 0) {
Chris Wren78403d72014-07-28 10:23:24 +0100712 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
713 }
714 return result;
715 }
716
717 public long getCurrentAirtimeExpandedMs() {
718 long result = airtimeExpandedMs;
719 // Add incomplete expanded airtime if currently shown.
720 if (currentAirtimeExpandedStartElapsedMs >= 0) {
721 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
Christoph Studerffeb0c32014-05-07 22:23:56 +0200722 }
723 return result;
724 }
Christoph Studer546bec82014-03-14 12:17:12 +0100725
726 /**
727 * Called when the user clicked the notification.
728 */
729 public void onClick() {
730 if (posttimeToFirstClickMs < 0) {
731 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
732 }
733 }
734
735 /**
736 * Called when the user removed the notification.
737 */
738 public void onDismiss() {
739 if (posttimeToDismissMs < 0) {
740 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
741 }
Christoph Studerffeb0c32014-05-07 22:23:56 +0200742 finish();
743 }
744
745 public void onCancel() {
746 finish();
747 }
748
749 public void onRemoved() {
750 finish();
751 }
752
753 public void onVisibilityChanged(boolean visible) {
754 long elapsedNowMs = SystemClock.elapsedRealtime();
Chris Wren78403d72014-07-28 10:23:24 +0100755 final boolean wasVisible = isVisible;
756 isVisible = visible;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200757 if (visible) {
758 if (currentAirtimeStartElapsedMs < 0) {
759 airtimeCount++;
760 currentAirtimeStartElapsedMs = elapsedNowMs;
761 }
762 if (posttimeToFirstAirtimeMs < 0) {
763 posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
764 }
765 } else {
766 if (currentAirtimeStartElapsedMs >= 0) {
767 airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
768 currentAirtimeStartElapsedMs = -1;
769 }
770 }
Chris Wren78403d72014-07-28 10:23:24 +0100771
772 if (wasVisible != isVisible) {
773 updateVisiblyExpandedStats();
774 }
775 }
776
777 public void onExpansionChanged(boolean userAction, boolean expanded) {
778 isExpanded = expanded;
779 if (isExpanded && userAction) {
780 userExpansionCount++;
781 }
782 updateVisiblyExpandedStats();
783 }
784
785 private void updateVisiblyExpandedStats() {
786 long elapsedNowMs = SystemClock.elapsedRealtime();
787 if (isExpanded && isVisible) {
788 // expanded and visible
789 if (currentAirtimeExpandedStartElapsedMs < 0) {
790 currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
791 }
792 if (posttimeToFirstVisibleExpansionMs < 0) {
793 posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
794 }
795 } else {
796 // not-expanded or not-visible
797 if (currentAirtimeExpandedStartElapsedMs >= 0) {
798 airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
799 currentAirtimeExpandedStartElapsedMs = -1;
800 }
801 }
Christoph Studerffeb0c32014-05-07 22:23:56 +0200802 }
803
804 /** The notification is leaving the system. Finalize. */
805 public void finish() {
806 onVisibilityChanged(false);
Christoph Studer546bec82014-03-14 12:17:12 +0100807 }
808
809 @Override
810 public String toString() {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500811 StringBuilder output = new StringBuilder();
812 output.append("SingleNotificationStats{");
813
814 output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
815 output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
816 output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
817 output.append("airtimeCount=").append(airtimeCount).append(", ");
818 output.append("airtimeMs=").append(airtimeMs).append(", ");
819 output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
820 .append(", ");
821 output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
822 output.append("posttimeToFirstVisibleExpansionMs=")
823 .append(posttimeToFirstVisibleExpansionMs).append(", ");
824 output.append("currentAirtimeExpandedStartElapsedMs=")
825 .append(currentAirtimeExpandedStartElapsedMs).append(", ");
826 output.append("requestedImportance=").append(requestedImportance).append(", ");
827 output.append("naturalImportance=").append(naturalImportance).append(", ");
828 output.append("isNoisy=").append(isNoisy);
829 output.append('}');
830 return output.toString();
831 }
832
833 /** Copy useful information out of the stats from the pre-update notifications. */
834 public void updateFrom(SingleNotificationStats old) {
835 posttimeElapsedMs = old.posttimeElapsedMs;
836 posttimeToFirstClickMs = old.posttimeToFirstClickMs;
837 airtimeCount = old.airtimeCount;
838 posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
839 currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
840 airtimeMs = old.airtimeMs;
841 posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
842 currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
843 airtimeExpandedMs = old.airtimeExpandedMs;
844 userExpansionCount = old.userExpansionCount;
Christoph Studer546bec82014-03-14 12:17:12 +0100845 }
846 }
847
848 /**
849 * Aggregates long samples to sum and averages.
850 */
851 public static class Aggregate {
852 long numSamples;
Chris Wren51103972014-04-01 13:57:34 -0400853 double avg;
854 double sum2;
855 double var;
Christoph Studer546bec82014-03-14 12:17:12 +0100856
857 public void addSample(long sample) {
Chris Wren51103972014-04-01 13:57:34 -0400858 // Welford's "Method for Calculating Corrected Sums of Squares"
859 // http://www.jstor.org/stable/1266577?seq=2
Christoph Studer546bec82014-03-14 12:17:12 +0100860 numSamples++;
Chris Wren51103972014-04-01 13:57:34 -0400861 final double n = numSamples;
862 final double delta = sample - avg;
863 avg += (1.0 / n) * delta;
864 sum2 += ((n - 1) / n) * delta * delta;
865 final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
866 var = sum2 / divisor;
Christoph Studer546bec82014-03-14 12:17:12 +0100867 }
868
869 @Override
870 public String toString() {
871 return "Aggregate{" +
872 "numSamples=" + numSamples +
Christoph Studer546bec82014-03-14 12:17:12 +0100873 ", avg=" + avg +
Chris Wren51103972014-04-01 13:57:34 -0400874 ", var=" + var +
Christoph Studer546bec82014-03-14 12:17:12 +0100875 '}';
876 }
877 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200878
879 private static class SQLiteLog {
880 private static final String TAG = "NotificationSQLiteLog";
881
882 // Message types passed to the background handler.
883 private static final int MSG_POST = 1;
884 private static final int MSG_CLICK = 2;
885 private static final int MSG_REMOVE = 3;
886 private static final int MSG_DISMISS = 4;
887
888 private static final String DB_NAME = "notification_log.db";
Chris Wrencdee8cd2016-01-25 17:10:30 -0500889 private static final int DB_VERSION = 5;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200890
891 /** Age in ms after which events are pruned from the DB. */
892 private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L; // 1 week
893 /** Delay between pruning the DB. Used to throttle pruning. */
894 private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L; // 6 hours
895 /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
896 private static final long PRUNE_MIN_WRITES = 1024;
897
898 // Table 'log'
899 private static final String TAB_LOG = "log";
900 private static final String COL_EVENT_USER_ID = "event_user_id";
901 private static final String COL_EVENT_TYPE = "event_type";
902 private static final String COL_EVENT_TIME = "event_time_ms";
903 private static final String COL_KEY = "key";
904 private static final String COL_PKG = "pkg";
905 private static final String COL_NOTIFICATION_ID = "nid";
906 private static final String COL_TAG = "tag";
907 private static final String COL_WHEN_MS = "when_ms";
908 private static final String COL_DEFAULTS = "defaults";
909 private static final String COL_FLAGS = "flags";
Chris Wrencdee8cd2016-01-25 17:10:30 -0500910 private static final String COL_IMPORTANCE_REQ = "importance_request";
911 private static final String COL_IMPORTANCE_FINAL = "importance_final";
912 private static final String COL_NOISY = "noisy";
913 private static final String COL_MUTED = "muted";
914 private static final String COL_DEMOTED = "demoted";
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200915 private static final String COL_CATEGORY = "category";
916 private static final String COL_ACTION_COUNT = "action_count";
Christoph Studerffeb0c32014-05-07 22:23:56 +0200917 private static final String COL_POSTTIME_MS = "posttime_ms";
918 private static final String COL_AIRTIME_MS = "airtime_ms";
Chris Wren78403d72014-07-28 10:23:24 +0100919 private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
920 private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
921 private static final String COL_EXPAND_COUNT = "expansion_count";
922
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200923
924 private static final int EVENT_TYPE_POST = 1;
925 private static final int EVENT_TYPE_CLICK = 2;
926 private static final int EVENT_TYPE_REMOVE = 3;
927 private static final int EVENT_TYPE_DISMISS = 4;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200928 private static long sLastPruneMs;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500929
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200930 private static long sNumWrites;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200931 private final SQLiteOpenHelper mHelper;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200932
Chris Wrencdee8cd2016-01-25 17:10:30 -0500933 private final Handler mWriteHandler;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200934 private static final long DAY_MS = 24 * 60 * 60 * 1000;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500935 private static final String STATS_QUERY = "SELECT " +
936 COL_EVENT_USER_ID + ", " +
937 COL_PKG + ", " +
938 // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
939 "CAST(((%d - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
940 "AS day, " +
941 "COUNT(*) AS cnt, " +
942 "SUM(" + COL_MUTED + ") as muted, " +
943 "SUM(" + COL_NOISY + ") as noisy, " +
944 "SUM(" + COL_DEMOTED + ") as demoted " +
945 "FROM " + TAB_LOG + " " +
946 "WHERE " +
947 COL_EVENT_TYPE + "=" + EVENT_TYPE_POST +
948 " AND " + COL_EVENT_TIME + " > %d " +
949 " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200950
951 public SQLiteLog(Context context) {
952 HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
953 android.os.Process.THREAD_PRIORITY_BACKGROUND);
954 backgroundThread.start();
955 mWriteHandler = new Handler(backgroundThread.getLooper()) {
956 @Override
957 public void handleMessage(Message msg) {
958 NotificationRecord r = (NotificationRecord) msg.obj;
959 long nowMs = System.currentTimeMillis();
960 switch (msg.what) {
961 case MSG_POST:
Christoph Studerffeb0c32014-05-07 22:23:56 +0200962 writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200963 break;
964 case MSG_CLICK:
Christoph Studerffeb0c32014-05-07 22:23:56 +0200965 writeEvent(nowMs, EVENT_TYPE_CLICK, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200966 break;
967 case MSG_REMOVE:
Christoph Studerffeb0c32014-05-07 22:23:56 +0200968 writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200969 break;
970 case MSG_DISMISS:
Christoph Studerffeb0c32014-05-07 22:23:56 +0200971 writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200972 break;
973 default:
974 Log.wtf(TAG, "Unknown message type: " + msg.what);
975 break;
976 }
977 }
978 };
979 mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
980 @Override
981 public void onCreate(SQLiteDatabase db) {
982 db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
983 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
984 COL_EVENT_USER_ID + " INT," +
985 COL_EVENT_TYPE + " INT," +
986 COL_EVENT_TIME + " INT," +
987 COL_KEY + " TEXT," +
988 COL_PKG + " TEXT," +
989 COL_NOTIFICATION_ID + " INT," +
990 COL_TAG + " TEXT," +
991 COL_WHEN_MS + " INT," +
992 COL_DEFAULTS + " INT," +
993 COL_FLAGS + " INT," +
Chris Wrencdee8cd2016-01-25 17:10:30 -0500994 COL_IMPORTANCE_REQ + " INT," +
995 COL_IMPORTANCE_FINAL + " INT," +
996 COL_NOISY + " INT," +
997 COL_MUTED + " INT," +
998 COL_DEMOTED + " INT," +
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200999 COL_CATEGORY + " TEXT," +
Christoph Studerffeb0c32014-05-07 22:23:56 +02001000 COL_ACTION_COUNT + " INT," +
1001 COL_POSTTIME_MS + " INT," +
Christoph Studer1ad856e2014-08-04 14:31:25 +02001002 COL_AIRTIME_MS + " INT," +
1003 COL_FIRST_EXPANSIONTIME_MS + " INT," +
1004 COL_AIRTIME_EXPANDED_MS + " INT," +
Chris Wren78403d72014-07-28 10:23:24 +01001005 COL_EXPAND_COUNT + " INT" +
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001006 ")");
1007 }
1008
1009 @Override
1010 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Chris Wrencdee8cd2016-01-25 17:10:30 -05001011 if (oldVersion != newVersion) {
Christoph Studer1ad856e2014-08-04 14:31:25 +02001012 db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
1013 onCreate(db);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001014 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001015 }
1016 };
1017 }
1018
1019 public void logPosted(NotificationRecord notification) {
1020 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
1021 }
1022
1023 public void logClicked(NotificationRecord notification) {
1024 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
1025 }
1026
1027 public void logRemoved(NotificationRecord notification) {
1028 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
1029 }
1030
1031 public void logDismissed(NotificationRecord notification) {
1032 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
1033 }
1034
Chris Wrencdee8cd2016-01-25 17:10:30 -05001035 private JSONArray jsonPostFrequencies(DumpFilter filter) throws JSONException {
Chris Wrene4b38802015-07-07 15:54:19 -04001036 JSONArray frequencies = new JSONArray();
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001037 SQLiteDatabase db = mHelper.getReadableDatabase();
Chris Wrene4b38802015-07-07 15:54:19 -04001038 long midnight = getMidnightMs();
Chris Wrencdee8cd2016-01-25 17:10:30 -05001039 String q = String.format(STATS_QUERY, midnight, filter.since);
Chris Wrene4b38802015-07-07 15:54:19 -04001040 Cursor cursor = db.rawQuery(q, null);
1041 try {
1042 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1043 int userId = cursor.getInt(0);
1044 String pkg = cursor.getString(1);
1045 if (filter != null && !filter.matches(pkg)) continue;
1046 int day = cursor.getInt(2);
1047 int count = cursor.getInt(3);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001048 int muted = cursor.getInt(4);
1049 int noisy = cursor.getInt(5);
1050 int demoted = cursor.getInt(6);
Chris Wrene4b38802015-07-07 15:54:19 -04001051 JSONObject row = new JSONObject();
1052 row.put("user_id", userId);
1053 row.put("package", pkg);
1054 row.put("day", day);
1055 row.put("count", count);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001056 row.put("noisy", noisy);
1057 row.put("muted", muted);
1058 row.put("demoted", demoted);
Chris Wrene4b38802015-07-07 15:54:19 -04001059 frequencies.put(row);
1060 }
1061 } finally {
1062 cursor.close();
1063 }
1064 return frequencies;
1065 }
1066
1067 public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
1068 SQLiteDatabase db = mHelper.getReadableDatabase();
1069 long midnight = getMidnightMs();
Chris Wrencdee8cd2016-01-25 17:10:30 -05001070 String q = String.format(STATS_QUERY, midnight, filter.since);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001071 Cursor cursor = db.rawQuery(q, null);
1072 try {
1073 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1074 int userId = cursor.getInt(0);
1075 String pkg = cursor.getString(1);
John Spurlock25e2d242014-06-27 13:58:23 -04001076 if (filter != null && !filter.matches(pkg)) continue;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001077 int day = cursor.getInt(2);
1078 int count = cursor.getInt(3);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001079 int muted = cursor.getInt(4);
1080 int noisy = cursor.getInt(5);
1081 int demoted = cursor.getInt(6);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001082 pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
Chris Wrencdee8cd2016-01-25 17:10:30 -05001083 ",day=" + day + ",count=" + count + ",muted=" + muted + "/" + noisy +
1084 ",demoted=" + demoted + "}");
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001085 }
1086 } finally {
1087 cursor.close();
1088 }
1089 }
1090
Chris Wrene4b38802015-07-07 15:54:19 -04001091 private long getMidnightMs() {
1092 GregorianCalendar midnight = new GregorianCalendar();
1093 midnight.set(midnight.get(Calendar.YEAR), midnight.get(Calendar.MONTH),
1094 midnight.get(Calendar.DATE), 23, 59, 59);
1095 return midnight.getTimeInMillis();
1096 }
1097
Christoph Studerffeb0c32014-05-07 22:23:56 +02001098 private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001099 ContentValues cv = new ContentValues();
1100 cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
1101 cv.put(COL_EVENT_TIME, eventTimeMs);
1102 cv.put(COL_EVENT_TYPE, eventType);
1103 putNotificationIdentifiers(r, cv);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001104 if (eventType == EVENT_TYPE_POST) {
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001105 putNotificationDetails(r, cv);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001106 } else {
Chris Wren78403d72014-07-28 10:23:24 +01001107 putPosttimeVisibility(r, cv);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001108 }
1109 SQLiteDatabase db = mHelper.getWritableDatabase();
1110 if (db.insert(TAB_LOG, null, cv) < 0) {
1111 Log.wtf(TAG, "Error while trying to insert values: " + cv);
1112 }
1113 sNumWrites++;
1114 pruneIfNecessary(db);
1115 }
1116
1117 private void pruneIfNecessary(SQLiteDatabase db) {
1118 // Prune if we haven't in a while.
1119 long nowMs = System.currentTimeMillis();
1120 if (sNumWrites > PRUNE_MIN_WRITES ||
1121 nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
1122 sNumWrites = 0;
1123 sLastPruneMs = nowMs;
1124 long horizonStartMs = nowMs - HORIZON_MS;
1125 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
1126 new String[] { String.valueOf(horizonStartMs) });
1127 Log.d(TAG, "Pruned event entries: " + deletedRows);
1128 }
1129 }
1130
1131 private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
1132 outCv.put(COL_KEY, r.sbn.getKey());
1133 outCv.put(COL_PKG, r.sbn.getPackageName());
1134 }
1135
1136 private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
1137 outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
1138 if (r.sbn.getTag() != null) {
1139 outCv.put(COL_TAG, r.sbn.getTag());
1140 }
1141 outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
1142 outCv.put(COL_FLAGS, r.getNotification().flags);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001143 final int before = r.stats.requestedImportance;
1144 final int after = r.getImportance();
1145 final boolean noisy = r.stats.isNoisy;
1146 outCv.put(COL_IMPORTANCE_REQ, before);
1147 outCv.put(COL_IMPORTANCE_FINAL, after);
1148 outCv.put(COL_DEMOTED, after < before ? 1 : 0);
1149 outCv.put(COL_NOISY, noisy);
1150 if (noisy && after < IMPORTANCE_HIGH) {
1151 outCv.put(COL_MUTED, 1);
1152 } else {
1153 outCv.put(COL_MUTED, 0);
1154 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001155 if (r.getNotification().category != null) {
1156 outCv.put(COL_CATEGORY, r.getNotification().category);
1157 }
1158 outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
1159 r.getNotification().actions.length : 0);
1160 }
1161
Chris Wren78403d72014-07-28 10:23:24 +01001162 private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
Christoph Studerffeb0c32014-05-07 22:23:56 +02001163 outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
1164 outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
Chris Wren78403d72014-07-28 10:23:24 +01001165 outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
1166 outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
1167 outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001168 }
1169
John Spurlock25e2d242014-06-27 13:58:23 -04001170 public void dump(PrintWriter pw, String indent, DumpFilter filter) {
1171 printPostFrequencies(pw, indent, filter);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001172 }
Chris Wrene4b38802015-07-07 15:54:19 -04001173
1174 public JSONObject dumpJson(DumpFilter filter) {
1175 JSONObject dump = new JSONObject();
1176 try {
Chris Wrencdee8cd2016-01-25 17:10:30 -05001177 dump.put("post_frequency", jsonPostFrequencies(filter));
1178 dump.put("since", filter.since);
1179 dump.put("now", System.currentTimeMillis());
Chris Wrene4b38802015-07-07 15:54:19 -04001180 } catch (JSONException e) {
1181 // pass
1182 }
1183 return dump;
1184 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001185 }
Christoph Studer546bec82014-03-14 12:17:12 +01001186}