blob: 00d7a7b9e0cb5c9e4bd9327cb033875861402b6d [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;
Chris Wrenc8673a82016-05-17 17:11:29 -040032import android.util.ArraySet;
Christoph Studer1c3f81f2014-04-16 15:05:56 +020033import android.util.Log;
Christoph Studer546bec82014-03-14 12:17:12 +010034
Chris Wren723aa762015-04-09 15:35:23 -040035import com.android.internal.logging.MetricsLogger;
John Spurlock25e2d242014-06-27 13:58:23 -040036import com.android.server.notification.NotificationManagerService.DumpFilter;
37
Chris Wrene4b38802015-07-07 15:54:19 -040038import org.json.JSONArray;
39import org.json.JSONException;
40import org.json.JSONObject;
41
Christoph Studer546bec82014-03-14 12:17:12 +010042import java.io.PrintWriter;
Chris Wren5eab2b72015-06-16 13:56:22 -040043import java.util.ArrayDeque;
Chris Wrene4b38802015-07-07 15:54:19 -040044import java.util.Calendar;
45import java.util.GregorianCalendar;
Christoph Studer546bec82014-03-14 12:17:12 +010046import java.util.HashMap;
47import java.util.Map;
Chris Wren2667e482015-10-30 14:50:22 -040048import java.util.Set;
Christoph Studer546bec82014-03-14 12:17:12 +010049
50/**
51 * Keeps track of notification activity, display, and user interaction.
52 *
53 * <p>This class receives signals from NoMan and keeps running stats of
54 * notification usage. Some metrics are updated as events occur. Others, namely
55 * those involving durations, are updated as the notification is canceled.</p>
56 *
57 * <p>This class is thread-safe.</p>
58 *
59 * {@hide}
60 */
61public class NotificationUsageStats {
Chris Wren5eab2b72015-06-16 13:56:22 -040062 private static final String TAG = "NotificationUsageStats";
Christoph Studer05e28842014-05-27 16:55:57 +020063
Chris Wren5eab2b72015-06-16 13:56:22 -040064 private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
65 private static final boolean ENABLE_SQLITE_LOG = true;
Christoph Studer856b2b82014-08-22 20:45:28 +020066 private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
Chris Wren5eab2b72015-06-16 13:56:22 -040067 private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
68 private static final int MSG_EMIT = 1;
69
70 private static final boolean DEBUG = false;
71 public static final int TEN_SECONDS = 1000 * 10;
Chris Wren93abac42015-06-23 11:23:37 -040072 public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
73 private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
Christoph Studer856b2b82014-08-22 20:45:28 +020074
Christoph Studer546bec82014-03-14 12:17:12 +010075 // Guarded by synchronized(this).
Chris Wren5eab2b72015-06-16 13:56:22 -040076 private final Map<String, AggregatedStats> mStats = new HashMap<>();
77 private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
Chris Wrenc8673a82016-05-17 17:11:29 -040078 private ArraySet<String> mStatExpiredkeys = new ArraySet<>();
Christoph Studer1c3f81f2014-04-16 15:05:56 +020079 private final SQLiteLog mSQLiteLog;
Chris Wren723aa762015-04-09 15:35:23 -040080 private final Context mContext;
Chris Wren5eab2b72015-06-16 13:56:22 -040081 private final Handler mHandler;
82 private long mLastEmitTime;
Christoph Studer1c3f81f2014-04-16 15:05:56 +020083
84 public NotificationUsageStats(Context context) {
Chris Wren723aa762015-04-09 15:35:23 -040085 mContext = context;
Chris Wren5eab2b72015-06-16 13:56:22 -040086 mLastEmitTime = SystemClock.elapsedRealtime();
Christoph Studer05e28842014-05-27 16:55:57 +020087 mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
Chris Wren5eab2b72015-06-16 13:56:22 -040088 mHandler = new Handler(mContext.getMainLooper()) {
89 @Override
90 public void handleMessage(Message msg) {
91 switch (msg.what) {
92 case MSG_EMIT:
93 emit();
94 break;
95 default:
96 Log.wtf(TAG, "Unknown message type: " + msg.what);
97 break;
98 }
99 }
100 };
101 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200102 }
Christoph Studer546bec82014-03-14 12:17:12 +0100103
104 /**
105 * Called when a notification has been posted.
106 */
Chris Wrenc8673a82016-05-17 17:11:29 -0400107 public synchronized float getAppEnqueueRate(String packageName) {
108 AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
109 if (stats != null) {
110 return stats.getEnqueueRate(SystemClock.elapsedRealtime());
111 } else {
112 return 0f;
113 }
114 }
115
116 /**
117 * Called when a notification has been posted.
118 */
Christoph Studer546bec82014-03-14 12:17:12 +0100119 public synchronized void registerPostedByApp(NotificationRecord notification) {
Chris Wrenc8673a82016-05-17 17:11:29 -0400120 final long now = SystemClock.elapsedRealtime();
121 notification.stats.posttimeElapsedMs = now;
Chris Wren5eab2b72015-06-16 13:56:22 -0400122
123 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
124 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100125 stats.numPostedByApp++;
Chris Wrenc8673a82016-05-17 17:11:29 -0400126 stats.updateInterarrivalEstimate(now);
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);
Christoph Studer05e28842014-05-27 16:55:57 +0200130 if (ENABLE_SQLITE_LOG) {
131 mSQLiteLog.logPosted(notification);
132 }
Christoph Studer546bec82014-03-14 12:17:12 +0100133 }
134
135 /**
136 * Called when a notification has been updated.
137 */
Christoph Studer061dee22014-05-09 12:28:55 +0200138 public void registerUpdatedByApp(NotificationRecord notification, NotificationRecord old) {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500139 notification.stats.updateFrom(old.stats);
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.numUpdatedByApp++;
Chris Wrenc8673a82016-05-17 17:11:29 -0400143 stats.updateInterarrivalEstimate(SystemClock.elapsedRealtime());
Chris Wren93abac42015-06-23 11:23:37 -0400144 stats.countApiUse(notification);
Christoph Studer546bec82014-03-14 12:17:12 +0100145 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400146 releaseAggregatedStatsLocked(aggregatedStatsArray);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500147 if (ENABLE_SQLITE_LOG) {
148 mSQLiteLog.logPosted(notification);
149 }
Christoph Studer546bec82014-03-14 12:17:12 +0100150 }
151
152 /**
153 * Called when the originating app removed the notification programmatically.
154 */
155 public synchronized void registerRemovedByApp(NotificationRecord notification) {
Christoph Studerffeb0c32014-05-07 22:23:56 +0200156 notification.stats.onRemoved();
Chris Wren5eab2b72015-06-16 13:56:22 -0400157 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
158 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100159 stats.numRemovedByApp++;
Christoph Studer546bec82014-03-14 12:17:12 +0100160 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400161 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer05e28842014-05-27 16:55:57 +0200162 if (ENABLE_SQLITE_LOG) {
163 mSQLiteLog.logRemoved(notification);
164 }
Christoph Studer546bec82014-03-14 12:17:12 +0100165 }
166
167 /**
168 * Called when the user dismissed the notification via the UI.
169 */
170 public synchronized void registerDismissedByUser(NotificationRecord notification) {
Chris Wren723aa762015-04-09 15:35:23 -0400171 MetricsLogger.histogram(mContext, "note_dismiss_longevity",
172 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
Christoph Studer546bec82014-03-14 12:17:12 +0100173 notification.stats.onDismiss();
Christoph Studer05e28842014-05-27 16:55:57 +0200174 if (ENABLE_SQLITE_LOG) {
175 mSQLiteLog.logDismissed(notification);
176 }
Christoph Studer546bec82014-03-14 12:17:12 +0100177 }
178
179 /**
180 * Called when the user clicked the notification in the UI.
181 */
182 public synchronized void registerClickedByUser(NotificationRecord notification) {
Chris Wren723aa762015-04-09 15:35:23 -0400183 MetricsLogger.histogram(mContext, "note_click_longevity",
184 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
Christoph Studer546bec82014-03-14 12:17:12 +0100185 notification.stats.onClick();
Christoph Studer05e28842014-05-27 16:55:57 +0200186 if (ENABLE_SQLITE_LOG) {
187 mSQLiteLog.logClicked(notification);
188 }
Christoph Studer546bec82014-03-14 12:17:12 +0100189 }
190
Chris Wren5eab2b72015-06-16 13:56:22 -0400191 public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
192 boolean starred, boolean cached) {
193 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
194 for (AggregatedStats stats : aggregatedStatsArray) {
195 if (valid) {
196 stats.numWithValidPeople++;
197 }
198 if (starred) {
199 stats.numWithStaredPeople++;
200 }
201 if (cached) {
202 stats.numPeopleCacheHit++;
203 } else {
204 stats.numPeopleCacheMiss++;
205 }
Christoph Studer546bec82014-03-14 12:17:12 +0100206 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400207 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer546bec82014-03-14 12:17:12 +0100208 }
209
Chris Wren5eab2b72015-06-16 13:56:22 -0400210 public synchronized void registerBlocked(NotificationRecord notification) {
211 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
212 for (AggregatedStats stats : aggregatedStatsArray) {
213 stats.numBlocked++;
Christoph Studer546bec82014-03-14 12:17:12 +0100214 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400215 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer546bec82014-03-14 12:17:12 +0100216 }
217
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000218 public synchronized void registerSuspendedByAdmin(NotificationRecord notification) {
219 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
220 for (AggregatedStats stats : aggregatedStatsArray) {
221 stats.numSuspendedByAdmin++;
222 }
223 releaseAggregatedStatsLocked(aggregatedStatsArray);
224 }
225
Chris Wrenc8673a82016-05-17 17:11:29 -0400226 public synchronized void registerOverRateQuota(String packageName) {
227 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
228 for (AggregatedStats stats : aggregatedStatsArray) {
229 stats.numRateViolations++;
230 }
231 }
232
233 public synchronized void registerOverCountQuota(String packageName) {
234 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
235 for (AggregatedStats stats : aggregatedStatsArray) {
236 stats.numQuotaViolations++;
237 }
238 }
239
Christoph Studer546bec82014-03-14 12:17:12 +0100240 // Locked by this.
241 private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
Chris Wrenc8673a82016-05-17 17:11:29 -0400242 return getAggregatedStatsLocked(record.sbn.getPackageName());
243 }
244
245 // Locked by this.
246 private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
Christoph Studer856b2b82014-08-22 20:45:28 +0200247 if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
248 return EMPTY_AGGREGATED_STATS;
249 }
250
Chris Wren5eab2b72015-06-16 13:56:22 -0400251 AggregatedStats[] array = mStatsArrays.poll();
252 if (array == null) {
Chris Wrenc8673a82016-05-17 17:11:29 -0400253 array = new AggregatedStats[2];
Chris Wren5eab2b72015-06-16 13:56:22 -0400254 }
255 array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
Chris Wrenc8673a82016-05-17 17:11:29 -0400256 array[1] = getOrCreateAggregatedStatsLocked(packageName);
Chris Wren5eab2b72015-06-16 13:56:22 -0400257 return array;
258 }
Christoph Studer546bec82014-03-14 12:17:12 +0100259
Chris Wren5eab2b72015-06-16 13:56:22 -0400260 // Locked by this.
261 private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
262 for(int i = 0; i < array.length; i++) {
263 array[i] = null;
264 }
265 mStatsArrays.offer(array);
Christoph Studer546bec82014-03-14 12:17:12 +0100266 }
267
268 // Locked by this.
269 private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
270 AggregatedStats result = mStats.get(key);
271 if (result == null) {
Chris Wren5eab2b72015-06-16 13:56:22 -0400272 result = new AggregatedStats(mContext, key);
Christoph Studer546bec82014-03-14 12:17:12 +0100273 mStats.put(key, result);
274 }
Chris Wrenc8673a82016-05-17 17:11:29 -0400275 result.mLastAccessTime = SystemClock.elapsedRealtime();
Christoph Studer546bec82014-03-14 12:17:12 +0100276 return result;
277 }
278
Chris Wrene4b38802015-07-07 15:54:19 -0400279 public synchronized JSONObject dumpJson(DumpFilter filter) {
280 JSONObject dump = new JSONObject();
281 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
282 try {
283 JSONArray aggregatedStats = new JSONArray();
284 for (AggregatedStats as : mStats.values()) {
285 if (filter != null && !filter.matches(as.key))
286 continue;
287 aggregatedStats.put(as.dumpJson());
288 }
289 dump.put("current", aggregatedStats);
290 } catch (JSONException e) {
291 // pass
292 }
293 }
294 if (ENABLE_SQLITE_LOG) {
295 try {
296 dump.put("historical", mSQLiteLog.dumpJson(filter));
297 } catch (JSONException e) {
298 // pass
299 }
300 }
301 return dump;
302 }
303
John Spurlock25e2d242014-06-27 13:58:23 -0400304 public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
Christoph Studer856b2b82014-08-22 20:45:28 +0200305 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
306 for (AggregatedStats as : mStats.values()) {
307 if (filter != null && !filter.matches(as.key))
308 continue;
309 as.dump(pw, indent);
310 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400311 pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
Chris Wrenc8673a82016-05-17 17:11:29 -0400312 pw.println(indent + "mStats.size(): " + mStats.size());
Christoph Studer546bec82014-03-14 12:17:12 +0100313 }
Christoph Studer05e28842014-05-27 16:55:57 +0200314 if (ENABLE_SQLITE_LOG) {
John Spurlock25e2d242014-06-27 13:58:23 -0400315 mSQLiteLog.dump(pw, indent, filter);
Christoph Studer05e28842014-05-27 16:55:57 +0200316 }
Christoph Studer546bec82014-03-14 12:17:12 +0100317 }
318
Chris Wren5eab2b72015-06-16 13:56:22 -0400319 public synchronized void emit() {
Chris Wren5eab2b72015-06-16 13:56:22 -0400320 AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
321 stats.emit();
Chris Wren5eab2b72015-06-16 13:56:22 -0400322 mHandler.removeMessages(MSG_EMIT);
323 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
Chris Wrenc8673a82016-05-17 17:11:29 -0400324 for(String key: mStats.keySet()) {
325 if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
326 mStatExpiredkeys.add(key);
327 }
328 }
329 for(String key: mStatExpiredkeys) {
330 mStats.remove(key);
331 }
332 mStatExpiredkeys.clear();
333 mLastEmitTime = SystemClock.elapsedRealtime();
Chris Wren5eab2b72015-06-16 13:56:22 -0400334 }
335
Christoph Studer546bec82014-03-14 12:17:12 +0100336 /**
337 * Aggregated notification stats.
338 */
339 private static class AggregatedStats {
Chris Wren5eab2b72015-06-16 13:56:22 -0400340
341 private final Context mContext;
Christoph Studer546bec82014-03-14 12:17:12 +0100342 public final String key;
Chris Wrene4b38802015-07-07 15:54:19 -0400343 private final long mCreated;
Chris Wren93abac42015-06-23 11:23:37 -0400344 private AggregatedStats mPrevious;
Christoph Studer546bec82014-03-14 12:17:12 +0100345
346 // ---- Updated as the respective events occur.
347 public int numPostedByApp;
348 public int numUpdatedByApp;
349 public int numRemovedByApp;
Chris Wren5eab2b72015-06-16 13:56:22 -0400350 public int numPeopleCacheHit;
351 public int numPeopleCacheMiss;;
352 public int numWithStaredPeople;
353 public int numWithValidPeople;
354 public int numBlocked;
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000355 public int numSuspendedByAdmin;
Chris Wren93abac42015-06-23 11:23:37 -0400356 public int numWithActions;
357 public int numPrivate;
358 public int numSecret;
Chris Wren93abac42015-06-23 11:23:37 -0400359 public int numWithBigText;
360 public int numWithBigPicture;
361 public int numForegroundService;
362 public int numOngoing;
363 public int numAutoCancel;
364 public int numWithLargeIcon;
365 public int numWithInbox;
366 public int numWithMediaSession;
367 public int numWithTitle;
368 public int numWithText;
369 public int numWithSubText;
370 public int numWithInfoText;
371 public int numInterrupt;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500372 public ImportanceHistogram noisyImportance;
373 public ImportanceHistogram quietImportance;
374 public ImportanceHistogram finalImportance;
Chris Wrenc8673a82016-05-17 17:11:29 -0400375 public RateEstimator enqueueRate;
376 public int numRateViolations;
377 public int numQuotaViolations;
378 public long mLastAccessTime;
Christoph Studer546bec82014-03-14 12:17:12 +0100379
Chris Wren5eab2b72015-06-16 13:56:22 -0400380 public AggregatedStats(Context context, String key) {
Christoph Studer546bec82014-03-14 12:17:12 +0100381 this.key = key;
Chris Wren5eab2b72015-06-16 13:56:22 -0400382 mContext = context;
Chris Wrene4b38802015-07-07 15:54:19 -0400383 mCreated = SystemClock.elapsedRealtime();
Chris Wrencdee8cd2016-01-25 17:10:30 -0500384 noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
385 quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
386 finalImportance = new ImportanceHistogram(context, "note_importance_");
Chris Wrenc8673a82016-05-17 17:11:29 -0400387 enqueueRate = new RateEstimator(mCreated);
Christoph Studer546bec82014-03-14 12:17:12 +0100388 }
389
Chris Wren7c4c87b2016-03-15 12:47:26 -0400390 public AggregatedStats getPrevious() {
391 if (mPrevious == null) {
392 mPrevious = new AggregatedStats(mContext, key);
393 }
394 return mPrevious;
395 }
396
Chris Wren93abac42015-06-23 11:23:37 -0400397 public void countApiUse(NotificationRecord record) {
398 final Notification n = record.getNotification();
399 if (n.actions != null) {
400 numWithActions++;
401 }
402
403 if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
404 numForegroundService++;
405 }
406
407 if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
408 numOngoing++;
409 }
410
411 if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
412 numAutoCancel++;
413 }
414
415 if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
416 (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
417 n.sound != null || n.vibrate != null) {
418 numInterrupt++;
419 }
420
421 switch (n.visibility) {
422 case Notification.VISIBILITY_PRIVATE:
423 numPrivate++;
424 break;
425 case Notification.VISIBILITY_SECRET:
426 numSecret++;
427 break;
428 }
429
Chris Wrencdee8cd2016-01-25 17:10:30 -0500430 if (record.stats.isNoisy) {
431 noisyImportance.increment(record.stats.requestedImportance);
432 } else {
433 quietImportance.increment(record.stats.requestedImportance);
Chris Wren93abac42015-06-23 11:23:37 -0400434 }
Chris Wrencdee8cd2016-01-25 17:10:30 -0500435 finalImportance.increment(record.getImportance());
Chris Wren93abac42015-06-23 11:23:37 -0400436
Chris Wren2667e482015-10-30 14:50:22 -0400437 final Set<String> names = n.extras.keySet();
438 if (names.contains(Notification.EXTRA_BIG_TEXT)) {
439 numWithBigText++;
440 }
441 if (names.contains(Notification.EXTRA_PICTURE)) {
442 numWithBigPicture++;
443 }
444 if (names.contains(Notification.EXTRA_LARGE_ICON)) {
445 numWithLargeIcon++;
446 }
447 if (names.contains(Notification.EXTRA_TEXT_LINES)) {
448 numWithInbox++;
449 }
450 if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
451 numWithMediaSession++;
452 }
453 if (names.contains(Notification.EXTRA_TITLE) &&
454 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
455 numWithTitle++;
456 }
457 if (names.contains(Notification.EXTRA_TEXT) &&
458 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
459 numWithText++;
460 }
461 if (names.contains(Notification.EXTRA_SUB_TEXT) &&
462 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
463 numWithSubText++;
464 }
465 if (names.contains(Notification.EXTRA_INFO_TEXT) &&
466 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
467 numWithInfoText++;
Chris Wren93abac42015-06-23 11:23:37 -0400468 }
469 }
470
Chris Wren5eab2b72015-06-16 13:56:22 -0400471 public void emit() {
Chris Wren7c4c87b2016-03-15 12:47:26 -0400472 AggregatedStats previous = getPrevious();
473 maybeCount("note_post", (numPostedByApp - previous.numPostedByApp));
474 maybeCount("note_update", (numUpdatedByApp - previous.numUpdatedByApp));
475 maybeCount("note_remove", (numRemovedByApp - previous.numRemovedByApp));
476 maybeCount("note_with_people", (numWithValidPeople - previous.numWithValidPeople));
477 maybeCount("note_with_stars", (numWithStaredPeople - previous.numWithStaredPeople));
478 maybeCount("people_cache_hit", (numPeopleCacheHit - previous.numPeopleCacheHit));
479 maybeCount("people_cache_miss", (numPeopleCacheMiss - previous.numPeopleCacheMiss));
480 maybeCount("note_blocked", (numBlocked - previous.numBlocked));
481 maybeCount("note_suspended", (numSuspendedByAdmin - previous.numSuspendedByAdmin));
482 maybeCount("note_with_actions", (numWithActions - previous.numWithActions));
483 maybeCount("note_private", (numPrivate - previous.numPrivate));
484 maybeCount("note_secret", (numSecret - previous.numSecret));
485 maybeCount("note_interupt", (numInterrupt - previous.numInterrupt));
486 maybeCount("note_big_text", (numWithBigText - previous.numWithBigText));
487 maybeCount("note_big_pic", (numWithBigPicture - previous.numWithBigPicture));
488 maybeCount("note_fg", (numForegroundService - previous.numForegroundService));
489 maybeCount("note_ongoing", (numOngoing - previous.numOngoing));
490 maybeCount("note_auto", (numAutoCancel - previous.numAutoCancel));
491 maybeCount("note_large_icon", (numWithLargeIcon - previous.numWithLargeIcon));
492 maybeCount("note_inbox", (numWithInbox - previous.numWithInbox));
493 maybeCount("note_media", (numWithMediaSession - previous.numWithMediaSession));
494 maybeCount("note_title", (numWithTitle - previous.numWithTitle));
495 maybeCount("note_text", (numWithText - previous.numWithText));
496 maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
497 maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
Chris Wrenc8673a82016-05-17 17:11:29 -0400498 maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
499 maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
Chris Wren7c4c87b2016-03-15 12:47:26 -0400500 noisyImportance.maybeCount(previous.noisyImportance);
501 quietImportance.maybeCount(previous.quietImportance);
502 finalImportance.maybeCount(previous.finalImportance);
Chris Wren5eab2b72015-06-16 13:56:22 -0400503
Chris Wren7c4c87b2016-03-15 12:47:26 -0400504 previous.numPostedByApp = numPostedByApp;
505 previous.numUpdatedByApp = numUpdatedByApp;
506 previous.numRemovedByApp = numRemovedByApp;
507 previous.numPeopleCacheHit = numPeopleCacheHit;
508 previous.numPeopleCacheMiss = numPeopleCacheMiss;
509 previous.numWithStaredPeople = numWithStaredPeople;
510 previous.numWithValidPeople = numWithValidPeople;
511 previous.numBlocked = numBlocked;
512 previous.numSuspendedByAdmin = numSuspendedByAdmin;
513 previous.numWithActions = numWithActions;
514 previous.numPrivate = numPrivate;
515 previous.numSecret = numSecret;
516 previous.numInterrupt = numInterrupt;
517 previous.numWithBigText = numWithBigText;
518 previous.numWithBigPicture = numWithBigPicture;
519 previous.numForegroundService = numForegroundService;
520 previous.numOngoing = numOngoing;
521 previous.numAutoCancel = numAutoCancel;
522 previous.numWithLargeIcon = numWithLargeIcon;
523 previous.numWithInbox = numWithInbox;
524 previous.numWithMediaSession = numWithMediaSession;
525 previous.numWithTitle = numWithTitle;
526 previous.numWithText = numWithText;
527 previous.numWithSubText = numWithSubText;
528 previous.numWithInfoText = numWithInfoText;
Chris Wrenc8673a82016-05-17 17:11:29 -0400529 previous.numRateViolations = numRateViolations;
530 previous.numQuotaViolations = numQuotaViolations;
Chris Wren7c4c87b2016-03-15 12:47:26 -0400531 noisyImportance.update(previous.noisyImportance);
532 quietImportance.update(previous.quietImportance);
533 finalImportance.update(previous.finalImportance);
Chris Wren5eab2b72015-06-16 13:56:22 -0400534 }
535
536 void maybeCount(String name, int value) {
537 if (value > 0) {
538 MetricsLogger.count(mContext, name, value);
Chris Wren78403d72014-07-28 10:23:24 +0100539 }
Christoph Studer546bec82014-03-14 12:17:12 +0100540 }
541
542 public void dump(PrintWriter pw, String indent) {
543 pw.println(toStringWithIndent(indent));
544 }
545
546 @Override
547 public String toString() {
548 return toStringWithIndent("");
549 }
550
Chris Wrenc8673a82016-05-17 17:11:29 -0400551 /** @return the enqueue rate if there were a new enqueue event right now. */
552 public float getEnqueueRate() {
553 return getEnqueueRate(SystemClock.elapsedRealtime());
554 }
555
556 public float getEnqueueRate(long now) {
557 return enqueueRate.getRate(now);
558 }
559
560 public void updateInterarrivalEstimate(long now) {
561 enqueueRate.update(now);
562 }
563
Christoph Studer546bec82014-03-14 12:17:12 +0100564 private String toStringWithIndent(String indent) {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500565 StringBuilder output = new StringBuilder();
566 output.append(indent).append("AggregatedStats{\n");
567 String indentPlusTwo = indent + " ";
568 output.append(indentPlusTwo);
569 output.append("key='").append(key).append("',\n");
570 output.append(indentPlusTwo);
571 output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
572 output.append(indentPlusTwo);
573 output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
574 output.append(indentPlusTwo);
575 output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
576 output.append(indentPlusTwo);
577 output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
578 output.append(indentPlusTwo);
579 output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
580 output.append(indentPlusTwo);
581 output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
582 output.append(indentPlusTwo);
583 output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
584 output.append(indentPlusTwo);
585 output.append("numBlocked=").append(numBlocked).append(",\n");
586 output.append(indentPlusTwo);
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000587 output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
588 output.append(indentPlusTwo);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500589 output.append("numWithActions=").append(numWithActions).append(",\n");
590 output.append(indentPlusTwo);
591 output.append("numPrivate=").append(numPrivate).append(",\n");
592 output.append(indentPlusTwo);
593 output.append("numSecret=").append(numSecret).append(",\n");
594 output.append(indentPlusTwo);
595 output.append("numInterrupt=").append(numInterrupt).append(",\n");
596 output.append(indentPlusTwo);
597 output.append("numWithBigText=").append(numWithBigText).append(",\n");
598 output.append(indentPlusTwo);
599 output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
600 output.append(indentPlusTwo);
601 output.append("numForegroundService=").append(numForegroundService).append("\n");
602 output.append(indentPlusTwo);
603 output.append("numOngoing=").append(numOngoing).append("\n");
604 output.append(indentPlusTwo);
605 output.append("numAutoCancel=").append(numAutoCancel).append("\n");
606 output.append(indentPlusTwo);
607 output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
608 output.append(indentPlusTwo);
609 output.append("numWithInbox=").append(numWithInbox).append("\n");
610 output.append(indentPlusTwo);
611 output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
612 output.append(indentPlusTwo);
613 output.append("numWithTitle=").append(numWithTitle).append("\n");
614 output.append(indentPlusTwo);
615 output.append("numWithText=").append(numWithText).append("\n");
616 output.append(indentPlusTwo);
617 output.append("numWithSubText=").append(numWithSubText).append("\n");
618 output.append(indentPlusTwo);
619 output.append("numWithInfoText=").append(numWithInfoText).append("\n");
Chris Wrenc8673a82016-05-17 17:11:29 -0400620 output.append("numRateViolations=").append(numRateViolations).append("\n");
621 output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
Chris Wrencdee8cd2016-01-25 17:10:30 -0500622 output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
623 output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
624 output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
625 output.append(indent).append("}");
626 return output.toString();
Christoph Studer546bec82014-03-14 12:17:12 +0100627 }
Chris Wrene4b38802015-07-07 15:54:19 -0400628
629 public JSONObject dumpJson() throws JSONException {
Chris Wren7c4c87b2016-03-15 12:47:26 -0400630 AggregatedStats previous = getPrevious();
Chris Wrene4b38802015-07-07 15:54:19 -0400631 JSONObject dump = new JSONObject();
632 dump.put("key", key);
633 dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
634 maybePut(dump, "numPostedByApp", numPostedByApp);
635 maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
636 maybePut(dump, "numRemovedByApp", numRemovedByApp);
637 maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
638 maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
639 maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
640 maybePut(dump, "numWithValidPeople", numWithValidPeople);
641 maybePut(dump, "numBlocked", numBlocked);
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000642 maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
Chris Wrene4b38802015-07-07 15:54:19 -0400643 maybePut(dump, "numWithActions", numWithActions);
644 maybePut(dump, "numPrivate", numPrivate);
645 maybePut(dump, "numSecret", numSecret);
Chris Wrene4b38802015-07-07 15:54:19 -0400646 maybePut(dump, "numInterrupt", numInterrupt);
647 maybePut(dump, "numWithBigText", numWithBigText);
648 maybePut(dump, "numWithBigPicture", numWithBigPicture);
649 maybePut(dump, "numForegroundService", numForegroundService);
650 maybePut(dump, "numOngoing", numOngoing);
651 maybePut(dump, "numAutoCancel", numAutoCancel);
652 maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
653 maybePut(dump, "numWithInbox", numWithInbox);
654 maybePut(dump, "numWithMediaSession", numWithMediaSession);
655 maybePut(dump, "numWithTitle", numWithTitle);
656 maybePut(dump, "numWithText", numWithText);
657 maybePut(dump, "numWithSubText", numWithSubText);
658 maybePut(dump, "numWithInfoText", numWithInfoText);
Chris Wrenc8673a82016-05-17 17:11:29 -0400659 maybePut(dump, "numRateViolations", numRateViolations);
660 maybePut(dump, "numQuotaLViolations", numQuotaViolations);
661 maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
Chris Wren7c4c87b2016-03-15 12:47:26 -0400662 noisyImportance.maybePut(dump, previous.noisyImportance);
663 quietImportance.maybePut(dump, previous.quietImportance);
664 finalImportance.maybePut(dump, previous.finalImportance);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500665
Chris Wrene4b38802015-07-07 15:54:19 -0400666 return dump;
667 }
668
669 private void maybePut(JSONObject dump, String name, int value) throws JSONException {
670 if (value > 0) {
671 dump.put(name, value);
672 }
673 }
Chris Wrenc8673a82016-05-17 17:11:29 -0400674
675 private void maybePut(JSONObject dump, String name, float value) throws JSONException {
676 if (value > 0.0) {
677 dump.put(name, value);
678 }
679 }
Christoph Studer546bec82014-03-14 12:17:12 +0100680 }
681
Chris Wrencdee8cd2016-01-25 17:10:30 -0500682 private static class ImportanceHistogram {
683 // TODO define these somewhere else
Julia Reynoldsf0f629f2016-02-25 09:34:04 -0500684 private static final int NUM_IMPORTANCES = 6;
685 private static final String[] IMPORTANCE_NAMES =
686 {"none", "min", "low", "default", "high", "max"};
Chris Wrencdee8cd2016-01-25 17:10:30 -0500687 private final Context mContext;
688 private final String[] mCounterNames;
689 private final String mPrefix;
690 private int[] mCount;
691
692 ImportanceHistogram(Context context, String prefix) {
693 mContext = context;
694 mCount = new int[NUM_IMPORTANCES];
695 mCounterNames = new String[NUM_IMPORTANCES];
696 mPrefix = prefix;
697 for (int i = 0; i < NUM_IMPORTANCES; i++) {
698 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
699 }
700 }
701
702 void increment(int imp) {
703 imp = imp < 0 ? 0 : imp > NUM_IMPORTANCES ? NUM_IMPORTANCES : imp;
704 mCount[imp] ++;
705 }
706
707 void maybeCount(ImportanceHistogram prev) {
708 for (int i = 0; i < NUM_IMPORTANCES; i++) {
709 final int value = mCount[i] - prev.mCount[i];
710 if (value > 0) {
711 MetricsLogger.count(mContext, mCounterNames[i], value);
712 }
713 }
714 }
715
716 void update(ImportanceHistogram that) {
717 for (int i = 0; i < NUM_IMPORTANCES; i++) {
718 mCount[i] = that.mCount[i];
719 }
720 }
721
722 public void maybePut(JSONObject dump, ImportanceHistogram prev)
723 throws JSONException {
724 dump.put(mPrefix, new JSONArray(mCount));
725 }
726
727 @Override
728 public String toString() {
729 StringBuilder output = new StringBuilder();
730 output.append(mPrefix).append(": [");
731 for (int i = 0; i < NUM_IMPORTANCES; i++) {
732 output.append(mCount[i]);
733 if (i < (NUM_IMPORTANCES-1)) {
734 output.append(", ");
735 }
736 }
737 output.append("]");
738 return output.toString();
739 }
740 }
741
Christoph Studer546bec82014-03-14 12:17:12 +0100742 /**
743 * Tracks usage of an individual notification that is currently active.
744 */
745 public static class SingleNotificationStats {
Chris Wren78403d72014-07-28 10:23:24 +0100746 private boolean isVisible = false;
747 private boolean isExpanded = false;
Christoph Studer546bec82014-03-14 12:17:12 +0100748 /** SystemClock.elapsedRealtime() when the notification was posted. */
749 public long posttimeElapsedMs = -1;
750 /** Elapsed time since the notification was posted until it was first clicked, or -1. */
751 public long posttimeToFirstClickMs = -1;
752 /** Elpased time since the notification was posted until it was dismissed by the user. */
753 public long posttimeToDismissMs = -1;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200754 /** Number of times the notification has been made visible. */
755 public long airtimeCount = 0;
756 /** Time in ms between the notification was posted and first shown; -1 if never shown. */
757 public long posttimeToFirstAirtimeMs = -1;
758 /**
759 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
760 * visible; -1 otherwise.
761 */
762 public long currentAirtimeStartElapsedMs = -1;
763 /** Accumulated visible time. */
764 public long airtimeMs = 0;
Chris Wren78403d72014-07-28 10:23:24 +0100765 /**
766 * Time in ms between the notification being posted and when it first
767 * became visible and expanded; -1 if it was never visibly expanded.
768 */
769 public long posttimeToFirstVisibleExpansionMs = -1;
770 /**
771 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
772 * visible; -1 otherwise.
773 */
774 public long currentAirtimeExpandedStartElapsedMs = -1;
775 /** Accumulated visible expanded time. */
776 public long airtimeExpandedMs = 0;
777 /** Number of times the notification has been expanded by the user. */
778 public long userExpansionCount = 0;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500779 /** Importance directly requested by the app. */
780 public int requestedImportance;
781 /** Did the app include sound or vibration on the notificaiton. */
782 public boolean isNoisy;
783 /** Importance after initial filtering for noise and other features */
784 public int naturalImportance;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200785
786 public long getCurrentPosttimeMs() {
787 if (posttimeElapsedMs < 0) {
788 return 0;
789 }
790 return SystemClock.elapsedRealtime() - posttimeElapsedMs;
791 }
792
793 public long getCurrentAirtimeMs() {
794 long result = airtimeMs;
795 // Add incomplete airtime if currently shown.
796 if (currentAirtimeStartElapsedMs >= 0) {
Chris Wren78403d72014-07-28 10:23:24 +0100797 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
798 }
799 return result;
800 }
801
802 public long getCurrentAirtimeExpandedMs() {
803 long result = airtimeExpandedMs;
804 // Add incomplete expanded airtime if currently shown.
805 if (currentAirtimeExpandedStartElapsedMs >= 0) {
806 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
Christoph Studerffeb0c32014-05-07 22:23:56 +0200807 }
808 return result;
809 }
Christoph Studer546bec82014-03-14 12:17:12 +0100810
811 /**
812 * Called when the user clicked the notification.
813 */
814 public void onClick() {
815 if (posttimeToFirstClickMs < 0) {
816 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
817 }
818 }
819
820 /**
821 * Called when the user removed the notification.
822 */
823 public void onDismiss() {
824 if (posttimeToDismissMs < 0) {
825 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
826 }
Christoph Studerffeb0c32014-05-07 22:23:56 +0200827 finish();
828 }
829
830 public void onCancel() {
831 finish();
832 }
833
834 public void onRemoved() {
835 finish();
836 }
837
838 public void onVisibilityChanged(boolean visible) {
839 long elapsedNowMs = SystemClock.elapsedRealtime();
Chris Wren78403d72014-07-28 10:23:24 +0100840 final boolean wasVisible = isVisible;
841 isVisible = visible;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200842 if (visible) {
843 if (currentAirtimeStartElapsedMs < 0) {
844 airtimeCount++;
845 currentAirtimeStartElapsedMs = elapsedNowMs;
846 }
847 if (posttimeToFirstAirtimeMs < 0) {
848 posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
849 }
850 } else {
851 if (currentAirtimeStartElapsedMs >= 0) {
852 airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
853 currentAirtimeStartElapsedMs = -1;
854 }
855 }
Chris Wren78403d72014-07-28 10:23:24 +0100856
857 if (wasVisible != isVisible) {
858 updateVisiblyExpandedStats();
859 }
860 }
861
862 public void onExpansionChanged(boolean userAction, boolean expanded) {
863 isExpanded = expanded;
864 if (isExpanded && userAction) {
865 userExpansionCount++;
866 }
867 updateVisiblyExpandedStats();
868 }
869
870 private void updateVisiblyExpandedStats() {
871 long elapsedNowMs = SystemClock.elapsedRealtime();
872 if (isExpanded && isVisible) {
873 // expanded and visible
874 if (currentAirtimeExpandedStartElapsedMs < 0) {
875 currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
876 }
877 if (posttimeToFirstVisibleExpansionMs < 0) {
878 posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
879 }
880 } else {
881 // not-expanded or not-visible
882 if (currentAirtimeExpandedStartElapsedMs >= 0) {
883 airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
884 currentAirtimeExpandedStartElapsedMs = -1;
885 }
886 }
Christoph Studerffeb0c32014-05-07 22:23:56 +0200887 }
888
889 /** The notification is leaving the system. Finalize. */
890 public void finish() {
891 onVisibilityChanged(false);
Christoph Studer546bec82014-03-14 12:17:12 +0100892 }
893
894 @Override
895 public String toString() {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500896 StringBuilder output = new StringBuilder();
897 output.append("SingleNotificationStats{");
898
899 output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
900 output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
901 output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
902 output.append("airtimeCount=").append(airtimeCount).append(", ");
903 output.append("airtimeMs=").append(airtimeMs).append(", ");
904 output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
905 .append(", ");
906 output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
907 output.append("posttimeToFirstVisibleExpansionMs=")
908 .append(posttimeToFirstVisibleExpansionMs).append(", ");
909 output.append("currentAirtimeExpandedStartElapsedMs=")
910 .append(currentAirtimeExpandedStartElapsedMs).append(", ");
911 output.append("requestedImportance=").append(requestedImportance).append(", ");
912 output.append("naturalImportance=").append(naturalImportance).append(", ");
913 output.append("isNoisy=").append(isNoisy);
914 output.append('}');
915 return output.toString();
916 }
917
918 /** Copy useful information out of the stats from the pre-update notifications. */
919 public void updateFrom(SingleNotificationStats old) {
920 posttimeElapsedMs = old.posttimeElapsedMs;
921 posttimeToFirstClickMs = old.posttimeToFirstClickMs;
922 airtimeCount = old.airtimeCount;
923 posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
924 currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
925 airtimeMs = old.airtimeMs;
926 posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
927 currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
928 airtimeExpandedMs = old.airtimeExpandedMs;
929 userExpansionCount = old.userExpansionCount;
Christoph Studer546bec82014-03-14 12:17:12 +0100930 }
931 }
932
933 /**
934 * Aggregates long samples to sum and averages.
935 */
936 public static class Aggregate {
937 long numSamples;
Chris Wren51103972014-04-01 13:57:34 -0400938 double avg;
939 double sum2;
940 double var;
Christoph Studer546bec82014-03-14 12:17:12 +0100941
942 public void addSample(long sample) {
Chris Wren51103972014-04-01 13:57:34 -0400943 // Welford's "Method for Calculating Corrected Sums of Squares"
944 // http://www.jstor.org/stable/1266577?seq=2
Christoph Studer546bec82014-03-14 12:17:12 +0100945 numSamples++;
Chris Wren51103972014-04-01 13:57:34 -0400946 final double n = numSamples;
947 final double delta = sample - avg;
948 avg += (1.0 / n) * delta;
949 sum2 += ((n - 1) / n) * delta * delta;
950 final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
951 var = sum2 / divisor;
Christoph Studer546bec82014-03-14 12:17:12 +0100952 }
953
954 @Override
955 public String toString() {
956 return "Aggregate{" +
957 "numSamples=" + numSamples +
Christoph Studer546bec82014-03-14 12:17:12 +0100958 ", avg=" + avg +
Chris Wren51103972014-04-01 13:57:34 -0400959 ", var=" + var +
Christoph Studer546bec82014-03-14 12:17:12 +0100960 '}';
961 }
962 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200963
964 private static class SQLiteLog {
965 private static final String TAG = "NotificationSQLiteLog";
966
967 // Message types passed to the background handler.
968 private static final int MSG_POST = 1;
969 private static final int MSG_CLICK = 2;
970 private static final int MSG_REMOVE = 3;
971 private static final int MSG_DISMISS = 4;
972
973 private static final String DB_NAME = "notification_log.db";
Chris Wrencdee8cd2016-01-25 17:10:30 -0500974 private static final int DB_VERSION = 5;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200975
976 /** Age in ms after which events are pruned from the DB. */
977 private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L; // 1 week
978 /** Delay between pruning the DB. Used to throttle pruning. */
979 private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L; // 6 hours
980 /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
981 private static final long PRUNE_MIN_WRITES = 1024;
982
983 // Table 'log'
984 private static final String TAB_LOG = "log";
985 private static final String COL_EVENT_USER_ID = "event_user_id";
986 private static final String COL_EVENT_TYPE = "event_type";
987 private static final String COL_EVENT_TIME = "event_time_ms";
988 private static final String COL_KEY = "key";
989 private static final String COL_PKG = "pkg";
990 private static final String COL_NOTIFICATION_ID = "nid";
991 private static final String COL_TAG = "tag";
992 private static final String COL_WHEN_MS = "when_ms";
993 private static final String COL_DEFAULTS = "defaults";
994 private static final String COL_FLAGS = "flags";
Chris Wrencdee8cd2016-01-25 17:10:30 -0500995 private static final String COL_IMPORTANCE_REQ = "importance_request";
996 private static final String COL_IMPORTANCE_FINAL = "importance_final";
997 private static final String COL_NOISY = "noisy";
998 private static final String COL_MUTED = "muted";
999 private static final String COL_DEMOTED = "demoted";
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001000 private static final String COL_CATEGORY = "category";
1001 private static final String COL_ACTION_COUNT = "action_count";
Christoph Studerffeb0c32014-05-07 22:23:56 +02001002 private static final String COL_POSTTIME_MS = "posttime_ms";
1003 private static final String COL_AIRTIME_MS = "airtime_ms";
Chris Wren78403d72014-07-28 10:23:24 +01001004 private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
1005 private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
1006 private static final String COL_EXPAND_COUNT = "expansion_count";
1007
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001008
1009 private static final int EVENT_TYPE_POST = 1;
1010 private static final int EVENT_TYPE_CLICK = 2;
1011 private static final int EVENT_TYPE_REMOVE = 3;
1012 private static final int EVENT_TYPE_DISMISS = 4;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001013 private static long sLastPruneMs;
Chris Wrencdee8cd2016-01-25 17:10:30 -05001014
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001015 private static long sNumWrites;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001016 private final SQLiteOpenHelper mHelper;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001017
Chris Wrencdee8cd2016-01-25 17:10:30 -05001018 private final Handler mWriteHandler;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001019 private static final long DAY_MS = 24 * 60 * 60 * 1000;
Chris Wrencdee8cd2016-01-25 17:10:30 -05001020 private static final String STATS_QUERY = "SELECT " +
1021 COL_EVENT_USER_ID + ", " +
1022 COL_PKG + ", " +
1023 // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
1024 "CAST(((%d - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
1025 "AS day, " +
1026 "COUNT(*) AS cnt, " +
1027 "SUM(" + COL_MUTED + ") as muted, " +
1028 "SUM(" + COL_NOISY + ") as noisy, " +
1029 "SUM(" + COL_DEMOTED + ") as demoted " +
1030 "FROM " + TAB_LOG + " " +
1031 "WHERE " +
1032 COL_EVENT_TYPE + "=" + EVENT_TYPE_POST +
1033 " AND " + COL_EVENT_TIME + " > %d " +
1034 " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001035
1036 public SQLiteLog(Context context) {
1037 HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
1038 android.os.Process.THREAD_PRIORITY_BACKGROUND);
1039 backgroundThread.start();
1040 mWriteHandler = new Handler(backgroundThread.getLooper()) {
1041 @Override
1042 public void handleMessage(Message msg) {
1043 NotificationRecord r = (NotificationRecord) msg.obj;
1044 long nowMs = System.currentTimeMillis();
1045 switch (msg.what) {
1046 case MSG_POST:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001047 writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001048 break;
1049 case MSG_CLICK:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001050 writeEvent(nowMs, EVENT_TYPE_CLICK, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001051 break;
1052 case MSG_REMOVE:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001053 writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001054 break;
1055 case MSG_DISMISS:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001056 writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001057 break;
1058 default:
1059 Log.wtf(TAG, "Unknown message type: " + msg.what);
1060 break;
1061 }
1062 }
1063 };
1064 mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
1065 @Override
1066 public void onCreate(SQLiteDatabase db) {
1067 db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
1068 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1069 COL_EVENT_USER_ID + " INT," +
1070 COL_EVENT_TYPE + " INT," +
1071 COL_EVENT_TIME + " INT," +
1072 COL_KEY + " TEXT," +
1073 COL_PKG + " TEXT," +
1074 COL_NOTIFICATION_ID + " INT," +
1075 COL_TAG + " TEXT," +
1076 COL_WHEN_MS + " INT," +
1077 COL_DEFAULTS + " INT," +
1078 COL_FLAGS + " INT," +
Chris Wrencdee8cd2016-01-25 17:10:30 -05001079 COL_IMPORTANCE_REQ + " INT," +
1080 COL_IMPORTANCE_FINAL + " INT," +
1081 COL_NOISY + " INT," +
1082 COL_MUTED + " INT," +
1083 COL_DEMOTED + " INT," +
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001084 COL_CATEGORY + " TEXT," +
Christoph Studerffeb0c32014-05-07 22:23:56 +02001085 COL_ACTION_COUNT + " INT," +
1086 COL_POSTTIME_MS + " INT," +
Christoph Studer1ad856e2014-08-04 14:31:25 +02001087 COL_AIRTIME_MS + " INT," +
1088 COL_FIRST_EXPANSIONTIME_MS + " INT," +
1089 COL_AIRTIME_EXPANDED_MS + " INT," +
Chris Wren78403d72014-07-28 10:23:24 +01001090 COL_EXPAND_COUNT + " INT" +
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001091 ")");
1092 }
1093
1094 @Override
1095 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Chris Wrencdee8cd2016-01-25 17:10:30 -05001096 if (oldVersion != newVersion) {
Christoph Studer1ad856e2014-08-04 14:31:25 +02001097 db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
1098 onCreate(db);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001099 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001100 }
1101 };
1102 }
1103
1104 public void logPosted(NotificationRecord notification) {
1105 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
1106 }
1107
1108 public void logClicked(NotificationRecord notification) {
1109 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
1110 }
1111
1112 public void logRemoved(NotificationRecord notification) {
1113 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
1114 }
1115
1116 public void logDismissed(NotificationRecord notification) {
1117 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
1118 }
1119
Chris Wrencdee8cd2016-01-25 17:10:30 -05001120 private JSONArray jsonPostFrequencies(DumpFilter filter) throws JSONException {
Chris Wrene4b38802015-07-07 15:54:19 -04001121 JSONArray frequencies = new JSONArray();
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001122 SQLiteDatabase db = mHelper.getReadableDatabase();
Chris Wrene4b38802015-07-07 15:54:19 -04001123 long midnight = getMidnightMs();
Chris Wrencdee8cd2016-01-25 17:10:30 -05001124 String q = String.format(STATS_QUERY, midnight, filter.since);
Chris Wrene4b38802015-07-07 15:54:19 -04001125 Cursor cursor = db.rawQuery(q, null);
1126 try {
1127 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1128 int userId = cursor.getInt(0);
1129 String pkg = cursor.getString(1);
1130 if (filter != null && !filter.matches(pkg)) continue;
1131 int day = cursor.getInt(2);
1132 int count = cursor.getInt(3);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001133 int muted = cursor.getInt(4);
1134 int noisy = cursor.getInt(5);
1135 int demoted = cursor.getInt(6);
Chris Wrene4b38802015-07-07 15:54:19 -04001136 JSONObject row = new JSONObject();
1137 row.put("user_id", userId);
1138 row.put("package", pkg);
1139 row.put("day", day);
1140 row.put("count", count);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001141 row.put("noisy", noisy);
1142 row.put("muted", muted);
1143 row.put("demoted", demoted);
Chris Wrene4b38802015-07-07 15:54:19 -04001144 frequencies.put(row);
1145 }
1146 } finally {
1147 cursor.close();
1148 }
1149 return frequencies;
1150 }
1151
1152 public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
1153 SQLiteDatabase db = mHelper.getReadableDatabase();
1154 long midnight = getMidnightMs();
Chris Wrencdee8cd2016-01-25 17:10:30 -05001155 String q = String.format(STATS_QUERY, midnight, filter.since);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001156 Cursor cursor = db.rawQuery(q, null);
1157 try {
1158 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1159 int userId = cursor.getInt(0);
1160 String pkg = cursor.getString(1);
John Spurlock25e2d242014-06-27 13:58:23 -04001161 if (filter != null && !filter.matches(pkg)) continue;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001162 int day = cursor.getInt(2);
1163 int count = cursor.getInt(3);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001164 int muted = cursor.getInt(4);
1165 int noisy = cursor.getInt(5);
1166 int demoted = cursor.getInt(6);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001167 pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
Chris Wrencdee8cd2016-01-25 17:10:30 -05001168 ",day=" + day + ",count=" + count + ",muted=" + muted + "/" + noisy +
1169 ",demoted=" + demoted + "}");
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001170 }
1171 } finally {
1172 cursor.close();
1173 }
1174 }
1175
Chris Wrene4b38802015-07-07 15:54:19 -04001176 private long getMidnightMs() {
1177 GregorianCalendar midnight = new GregorianCalendar();
1178 midnight.set(midnight.get(Calendar.YEAR), midnight.get(Calendar.MONTH),
1179 midnight.get(Calendar.DATE), 23, 59, 59);
1180 return midnight.getTimeInMillis();
1181 }
1182
Christoph Studerffeb0c32014-05-07 22:23:56 +02001183 private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001184 ContentValues cv = new ContentValues();
1185 cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
1186 cv.put(COL_EVENT_TIME, eventTimeMs);
1187 cv.put(COL_EVENT_TYPE, eventType);
1188 putNotificationIdentifiers(r, cv);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001189 if (eventType == EVENT_TYPE_POST) {
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001190 putNotificationDetails(r, cv);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001191 } else {
Chris Wren78403d72014-07-28 10:23:24 +01001192 putPosttimeVisibility(r, cv);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001193 }
1194 SQLiteDatabase db = mHelper.getWritableDatabase();
1195 if (db.insert(TAB_LOG, null, cv) < 0) {
1196 Log.wtf(TAG, "Error while trying to insert values: " + cv);
1197 }
1198 sNumWrites++;
1199 pruneIfNecessary(db);
1200 }
1201
1202 private void pruneIfNecessary(SQLiteDatabase db) {
1203 // Prune if we haven't in a while.
1204 long nowMs = System.currentTimeMillis();
1205 if (sNumWrites > PRUNE_MIN_WRITES ||
1206 nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
1207 sNumWrites = 0;
1208 sLastPruneMs = nowMs;
1209 long horizonStartMs = nowMs - HORIZON_MS;
1210 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
1211 new String[] { String.valueOf(horizonStartMs) });
1212 Log.d(TAG, "Pruned event entries: " + deletedRows);
1213 }
1214 }
1215
1216 private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
1217 outCv.put(COL_KEY, r.sbn.getKey());
1218 outCv.put(COL_PKG, r.sbn.getPackageName());
1219 }
1220
1221 private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
1222 outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
1223 if (r.sbn.getTag() != null) {
1224 outCv.put(COL_TAG, r.sbn.getTag());
1225 }
1226 outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
1227 outCv.put(COL_FLAGS, r.getNotification().flags);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001228 final int before = r.stats.requestedImportance;
1229 final int after = r.getImportance();
1230 final boolean noisy = r.stats.isNoisy;
1231 outCv.put(COL_IMPORTANCE_REQ, before);
1232 outCv.put(COL_IMPORTANCE_FINAL, after);
1233 outCv.put(COL_DEMOTED, after < before ? 1 : 0);
1234 outCv.put(COL_NOISY, noisy);
1235 if (noisy && after < IMPORTANCE_HIGH) {
1236 outCv.put(COL_MUTED, 1);
1237 } else {
1238 outCv.put(COL_MUTED, 0);
1239 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001240 if (r.getNotification().category != null) {
1241 outCv.put(COL_CATEGORY, r.getNotification().category);
1242 }
1243 outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
1244 r.getNotification().actions.length : 0);
1245 }
1246
Chris Wren78403d72014-07-28 10:23:24 +01001247 private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
Christoph Studerffeb0c32014-05-07 22:23:56 +02001248 outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
1249 outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
Chris Wren78403d72014-07-28 10:23:24 +01001250 outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
1251 outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
1252 outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001253 }
1254
John Spurlock25e2d242014-06-27 13:58:23 -04001255 public void dump(PrintWriter pw, String indent, DumpFilter filter) {
1256 printPostFrequencies(pw, indent, filter);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001257 }
Chris Wrene4b38802015-07-07 15:54:19 -04001258
1259 public JSONObject dumpJson(DumpFilter filter) {
1260 JSONObject dump = new JSONObject();
1261 try {
Chris Wrencdee8cd2016-01-25 17:10:30 -05001262 dump.put("post_frequency", jsonPostFrequencies(filter));
1263 dump.put("since", filter.since);
1264 dump.put("now", System.currentTimeMillis());
Chris Wrene4b38802015-07-07 15:54:19 -04001265 } catch (JSONException e) {
1266 // pass
1267 }
1268 return dump;
1269 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001270 }
Christoph Studer546bec82014-03-14 12:17:12 +01001271}