blob: 34c52833fbfdede7394008de38d39260cac69a92 [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 /**
Chris Wren888b7a82016-06-17 15:47:19 -0400117 * Called when a notification is tentatively enqueued by an app, before rate checking.
118 */
119 public synchronized void registerEnqueuedByApp(String packageName) {
120 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
121 for (AggregatedStats stats : aggregatedStatsArray) {
122 stats.numEnqueuedByApp++;
123 }
124 releaseAggregatedStatsLocked(aggregatedStatsArray);
125 }
126
127 /**
Chris Wrenc8673a82016-05-17 17:11:29 -0400128 * Called when a notification has been posted.
129 */
Christoph Studer546bec82014-03-14 12:17:12 +0100130 public synchronized void registerPostedByApp(NotificationRecord notification) {
Chris Wrenc8673a82016-05-17 17:11:29 -0400131 final long now = SystemClock.elapsedRealtime();
132 notification.stats.posttimeElapsedMs = now;
Chris Wren5eab2b72015-06-16 13:56:22 -0400133
134 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
135 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100136 stats.numPostedByApp++;
Chris Wrenc8673a82016-05-17 17:11:29 -0400137 stats.updateInterarrivalEstimate(now);
Chris Wren93abac42015-06-23 11:23:37 -0400138 stats.countApiUse(notification);
Christoph Studer546bec82014-03-14 12:17:12 +0100139 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400140 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer05e28842014-05-27 16:55:57 +0200141 if (ENABLE_SQLITE_LOG) {
142 mSQLiteLog.logPosted(notification);
143 }
Christoph Studer546bec82014-03-14 12:17:12 +0100144 }
145
146 /**
147 * Called when a notification has been updated.
148 */
Chris Wrene0aaeaa2016-06-21 14:38:04 -0400149 public synchronized void registerUpdatedByApp(NotificationRecord notification,
150 NotificationRecord old) {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500151 notification.stats.updateFrom(old.stats);
Chris Wren5eab2b72015-06-16 13:56:22 -0400152 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
153 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100154 stats.numUpdatedByApp++;
Chris Wrenc8673a82016-05-17 17:11:29 -0400155 stats.updateInterarrivalEstimate(SystemClock.elapsedRealtime());
Chris Wren93abac42015-06-23 11:23:37 -0400156 stats.countApiUse(notification);
Christoph Studer546bec82014-03-14 12:17:12 +0100157 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400158 releaseAggregatedStatsLocked(aggregatedStatsArray);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500159 if (ENABLE_SQLITE_LOG) {
160 mSQLiteLog.logPosted(notification);
161 }
Christoph Studer546bec82014-03-14 12:17:12 +0100162 }
163
164 /**
165 * Called when the originating app removed the notification programmatically.
166 */
167 public synchronized void registerRemovedByApp(NotificationRecord notification) {
Christoph Studerffeb0c32014-05-07 22:23:56 +0200168 notification.stats.onRemoved();
Chris Wren5eab2b72015-06-16 13:56:22 -0400169 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
170 for (AggregatedStats stats : aggregatedStatsArray) {
Christoph Studer546bec82014-03-14 12:17:12 +0100171 stats.numRemovedByApp++;
Christoph Studer546bec82014-03-14 12:17:12 +0100172 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400173 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer05e28842014-05-27 16:55:57 +0200174 if (ENABLE_SQLITE_LOG) {
175 mSQLiteLog.logRemoved(notification);
176 }
Christoph Studer546bec82014-03-14 12:17:12 +0100177 }
178
179 /**
180 * Called when the user dismissed the notification via the UI.
181 */
182 public synchronized void registerDismissedByUser(NotificationRecord notification) {
Chris Wren723aa762015-04-09 15:35:23 -0400183 MetricsLogger.histogram(mContext, "note_dismiss_longevity",
184 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
Christoph Studer546bec82014-03-14 12:17:12 +0100185 notification.stats.onDismiss();
Christoph Studer05e28842014-05-27 16:55:57 +0200186 if (ENABLE_SQLITE_LOG) {
187 mSQLiteLog.logDismissed(notification);
188 }
Christoph Studer546bec82014-03-14 12:17:12 +0100189 }
190
191 /**
192 * Called when the user clicked the notification in the UI.
193 */
194 public synchronized void registerClickedByUser(NotificationRecord notification) {
Chris Wren723aa762015-04-09 15:35:23 -0400195 MetricsLogger.histogram(mContext, "note_click_longevity",
196 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
Christoph Studer546bec82014-03-14 12:17:12 +0100197 notification.stats.onClick();
Christoph Studer05e28842014-05-27 16:55:57 +0200198 if (ENABLE_SQLITE_LOG) {
199 mSQLiteLog.logClicked(notification);
200 }
Christoph Studer546bec82014-03-14 12:17:12 +0100201 }
202
Chris Wren5eab2b72015-06-16 13:56:22 -0400203 public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
204 boolean starred, boolean cached) {
205 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
206 for (AggregatedStats stats : aggregatedStatsArray) {
207 if (valid) {
208 stats.numWithValidPeople++;
209 }
210 if (starred) {
211 stats.numWithStaredPeople++;
212 }
213 if (cached) {
214 stats.numPeopleCacheHit++;
215 } else {
216 stats.numPeopleCacheMiss++;
217 }
Christoph Studer546bec82014-03-14 12:17:12 +0100218 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400219 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer546bec82014-03-14 12:17:12 +0100220 }
221
Chris Wren5eab2b72015-06-16 13:56:22 -0400222 public synchronized void registerBlocked(NotificationRecord notification) {
223 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
224 for (AggregatedStats stats : aggregatedStatsArray) {
225 stats.numBlocked++;
Christoph Studer546bec82014-03-14 12:17:12 +0100226 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400227 releaseAggregatedStatsLocked(aggregatedStatsArray);
Christoph Studer546bec82014-03-14 12:17:12 +0100228 }
229
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000230 public synchronized void registerSuspendedByAdmin(NotificationRecord notification) {
231 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
232 for (AggregatedStats stats : aggregatedStatsArray) {
233 stats.numSuspendedByAdmin++;
234 }
235 releaseAggregatedStatsLocked(aggregatedStatsArray);
236 }
237
Chris Wrenc8673a82016-05-17 17:11:29 -0400238 public synchronized void registerOverRateQuota(String packageName) {
239 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
240 for (AggregatedStats stats : aggregatedStatsArray) {
241 stats.numRateViolations++;
242 }
243 }
244
245 public synchronized void registerOverCountQuota(String packageName) {
246 AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
247 for (AggregatedStats stats : aggregatedStatsArray) {
248 stats.numQuotaViolations++;
249 }
250 }
251
Christoph Studer546bec82014-03-14 12:17:12 +0100252 // Locked by this.
253 private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
Chris Wrenc8673a82016-05-17 17:11:29 -0400254 return getAggregatedStatsLocked(record.sbn.getPackageName());
255 }
256
257 // Locked by this.
258 private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
Christoph Studer856b2b82014-08-22 20:45:28 +0200259 if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
260 return EMPTY_AGGREGATED_STATS;
261 }
262
Chris Wren5eab2b72015-06-16 13:56:22 -0400263 AggregatedStats[] array = mStatsArrays.poll();
264 if (array == null) {
Chris Wrenc8673a82016-05-17 17:11:29 -0400265 array = new AggregatedStats[2];
Chris Wren5eab2b72015-06-16 13:56:22 -0400266 }
267 array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
Chris Wrenc8673a82016-05-17 17:11:29 -0400268 array[1] = getOrCreateAggregatedStatsLocked(packageName);
Chris Wren5eab2b72015-06-16 13:56:22 -0400269 return array;
270 }
Christoph Studer546bec82014-03-14 12:17:12 +0100271
Chris Wren5eab2b72015-06-16 13:56:22 -0400272 // Locked by this.
273 private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
274 for(int i = 0; i < array.length; i++) {
275 array[i] = null;
276 }
277 mStatsArrays.offer(array);
Christoph Studer546bec82014-03-14 12:17:12 +0100278 }
279
280 // Locked by this.
281 private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
282 AggregatedStats result = mStats.get(key);
283 if (result == null) {
Chris Wren5eab2b72015-06-16 13:56:22 -0400284 result = new AggregatedStats(mContext, key);
Christoph Studer546bec82014-03-14 12:17:12 +0100285 mStats.put(key, result);
286 }
Chris Wrenc8673a82016-05-17 17:11:29 -0400287 result.mLastAccessTime = SystemClock.elapsedRealtime();
Christoph Studer546bec82014-03-14 12:17:12 +0100288 return result;
289 }
290
Chris Wrene4b38802015-07-07 15:54:19 -0400291 public synchronized JSONObject dumpJson(DumpFilter filter) {
292 JSONObject dump = new JSONObject();
293 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
294 try {
295 JSONArray aggregatedStats = new JSONArray();
296 for (AggregatedStats as : mStats.values()) {
297 if (filter != null && !filter.matches(as.key))
298 continue;
299 aggregatedStats.put(as.dumpJson());
300 }
301 dump.put("current", aggregatedStats);
302 } catch (JSONException e) {
303 // pass
304 }
305 }
306 if (ENABLE_SQLITE_LOG) {
307 try {
308 dump.put("historical", mSQLiteLog.dumpJson(filter));
309 } catch (JSONException e) {
310 // pass
311 }
312 }
313 return dump;
314 }
315
John Spurlock25e2d242014-06-27 13:58:23 -0400316 public synchronized void dump(PrintWriter pw, String indent, DumpFilter filter) {
Christoph Studer856b2b82014-08-22 20:45:28 +0200317 if (ENABLE_AGGREGATED_IN_MEMORY_STATS) {
318 for (AggregatedStats as : mStats.values()) {
319 if (filter != null && !filter.matches(as.key))
320 continue;
321 as.dump(pw, indent);
322 }
Chris Wren5eab2b72015-06-16 13:56:22 -0400323 pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
Chris Wrenc8673a82016-05-17 17:11:29 -0400324 pw.println(indent + "mStats.size(): " + mStats.size());
Christoph Studer546bec82014-03-14 12:17:12 +0100325 }
Christoph Studer05e28842014-05-27 16:55:57 +0200326 if (ENABLE_SQLITE_LOG) {
John Spurlock25e2d242014-06-27 13:58:23 -0400327 mSQLiteLog.dump(pw, indent, filter);
Christoph Studer05e28842014-05-27 16:55:57 +0200328 }
Christoph Studer546bec82014-03-14 12:17:12 +0100329 }
330
Chris Wren5eab2b72015-06-16 13:56:22 -0400331 public synchronized void emit() {
Chris Wren5eab2b72015-06-16 13:56:22 -0400332 AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
333 stats.emit();
Chris Wren5eab2b72015-06-16 13:56:22 -0400334 mHandler.removeMessages(MSG_EMIT);
335 mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
Chris Wrenc8673a82016-05-17 17:11:29 -0400336 for(String key: mStats.keySet()) {
337 if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
338 mStatExpiredkeys.add(key);
339 }
340 }
341 for(String key: mStatExpiredkeys) {
342 mStats.remove(key);
343 }
344 mStatExpiredkeys.clear();
345 mLastEmitTime = SystemClock.elapsedRealtime();
Chris Wren5eab2b72015-06-16 13:56:22 -0400346 }
347
Christoph Studer546bec82014-03-14 12:17:12 +0100348 /**
349 * Aggregated notification stats.
350 */
351 private static class AggregatedStats {
Chris Wren5eab2b72015-06-16 13:56:22 -0400352
353 private final Context mContext;
Christoph Studer546bec82014-03-14 12:17:12 +0100354 public final String key;
Chris Wrene4b38802015-07-07 15:54:19 -0400355 private final long mCreated;
Chris Wren93abac42015-06-23 11:23:37 -0400356 private AggregatedStats mPrevious;
Christoph Studer546bec82014-03-14 12:17:12 +0100357
358 // ---- Updated as the respective events occur.
Chris Wren888b7a82016-06-17 15:47:19 -0400359 public int numEnqueuedByApp;
Christoph Studer546bec82014-03-14 12:17:12 +0100360 public int numPostedByApp;
361 public int numUpdatedByApp;
362 public int numRemovedByApp;
Chris Wren5eab2b72015-06-16 13:56:22 -0400363 public int numPeopleCacheHit;
364 public int numPeopleCacheMiss;;
365 public int numWithStaredPeople;
366 public int numWithValidPeople;
367 public int numBlocked;
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000368 public int numSuspendedByAdmin;
Chris Wren93abac42015-06-23 11:23:37 -0400369 public int numWithActions;
370 public int numPrivate;
371 public int numSecret;
Chris Wren93abac42015-06-23 11:23:37 -0400372 public int numWithBigText;
373 public int numWithBigPicture;
374 public int numForegroundService;
375 public int numOngoing;
376 public int numAutoCancel;
377 public int numWithLargeIcon;
378 public int numWithInbox;
379 public int numWithMediaSession;
380 public int numWithTitle;
381 public int numWithText;
382 public int numWithSubText;
383 public int numWithInfoText;
384 public int numInterrupt;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500385 public ImportanceHistogram noisyImportance;
386 public ImportanceHistogram quietImportance;
387 public ImportanceHistogram finalImportance;
Chris Wrenc8673a82016-05-17 17:11:29 -0400388 public RateEstimator enqueueRate;
389 public int numRateViolations;
390 public int numQuotaViolations;
391 public long mLastAccessTime;
Christoph Studer546bec82014-03-14 12:17:12 +0100392
Chris Wren5eab2b72015-06-16 13:56:22 -0400393 public AggregatedStats(Context context, String key) {
Christoph Studer546bec82014-03-14 12:17:12 +0100394 this.key = key;
Chris Wren5eab2b72015-06-16 13:56:22 -0400395 mContext = context;
Chris Wrene4b38802015-07-07 15:54:19 -0400396 mCreated = SystemClock.elapsedRealtime();
Chris Wrencdee8cd2016-01-25 17:10:30 -0500397 noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
398 quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
399 finalImportance = new ImportanceHistogram(context, "note_importance_");
Tony Makfd303322016-05-24 18:57:50 +0100400 enqueueRate = new RateEstimator();
Christoph Studer546bec82014-03-14 12:17:12 +0100401 }
402
Chris Wren7c4c87b2016-03-15 12:47:26 -0400403 public AggregatedStats getPrevious() {
404 if (mPrevious == null) {
405 mPrevious = new AggregatedStats(mContext, key);
406 }
407 return mPrevious;
408 }
409
Chris Wren93abac42015-06-23 11:23:37 -0400410 public void countApiUse(NotificationRecord record) {
411 final Notification n = record.getNotification();
412 if (n.actions != null) {
413 numWithActions++;
414 }
415
416 if ((n.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
417 numForegroundService++;
418 }
419
420 if ((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
421 numOngoing++;
422 }
423
424 if ((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) {
425 numAutoCancel++;
426 }
427
428 if ((n.defaults & Notification.DEFAULT_SOUND) != 0 ||
429 (n.defaults & Notification.DEFAULT_VIBRATE) != 0 ||
430 n.sound != null || n.vibrate != null) {
431 numInterrupt++;
432 }
433
434 switch (n.visibility) {
435 case Notification.VISIBILITY_PRIVATE:
436 numPrivate++;
437 break;
438 case Notification.VISIBILITY_SECRET:
439 numSecret++;
440 break;
441 }
442
Chris Wrencdee8cd2016-01-25 17:10:30 -0500443 if (record.stats.isNoisy) {
444 noisyImportance.increment(record.stats.requestedImportance);
445 } else {
446 quietImportance.increment(record.stats.requestedImportance);
Chris Wren93abac42015-06-23 11:23:37 -0400447 }
Chris Wrencdee8cd2016-01-25 17:10:30 -0500448 finalImportance.increment(record.getImportance());
Chris Wren93abac42015-06-23 11:23:37 -0400449
Chris Wren2667e482015-10-30 14:50:22 -0400450 final Set<String> names = n.extras.keySet();
451 if (names.contains(Notification.EXTRA_BIG_TEXT)) {
452 numWithBigText++;
453 }
454 if (names.contains(Notification.EXTRA_PICTURE)) {
455 numWithBigPicture++;
456 }
457 if (names.contains(Notification.EXTRA_LARGE_ICON)) {
458 numWithLargeIcon++;
459 }
460 if (names.contains(Notification.EXTRA_TEXT_LINES)) {
461 numWithInbox++;
462 }
463 if (names.contains(Notification.EXTRA_MEDIA_SESSION)) {
464 numWithMediaSession++;
465 }
466 if (names.contains(Notification.EXTRA_TITLE) &&
467 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TITLE))) {
468 numWithTitle++;
469 }
470 if (names.contains(Notification.EXTRA_TEXT) &&
471 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_TEXT))) {
472 numWithText++;
473 }
474 if (names.contains(Notification.EXTRA_SUB_TEXT) &&
475 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
476 numWithSubText++;
477 }
478 if (names.contains(Notification.EXTRA_INFO_TEXT) &&
479 !TextUtils.isEmpty(n.extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
480 numWithInfoText++;
Chris Wren93abac42015-06-23 11:23:37 -0400481 }
482 }
483
Chris Wren5eab2b72015-06-16 13:56:22 -0400484 public void emit() {
Chris Wren7c4c87b2016-03-15 12:47:26 -0400485 AggregatedStats previous = getPrevious();
Chris Wren888b7a82016-06-17 15:47:19 -0400486 maybeCount("note_enqueued", (numEnqueuedByApp - previous.numEnqueuedByApp));
Chris Wren7c4c87b2016-03-15 12:47:26 -0400487 maybeCount("note_post", (numPostedByApp - previous.numPostedByApp));
488 maybeCount("note_update", (numUpdatedByApp - previous.numUpdatedByApp));
489 maybeCount("note_remove", (numRemovedByApp - previous.numRemovedByApp));
490 maybeCount("note_with_people", (numWithValidPeople - previous.numWithValidPeople));
491 maybeCount("note_with_stars", (numWithStaredPeople - previous.numWithStaredPeople));
492 maybeCount("people_cache_hit", (numPeopleCacheHit - previous.numPeopleCacheHit));
493 maybeCount("people_cache_miss", (numPeopleCacheMiss - previous.numPeopleCacheMiss));
494 maybeCount("note_blocked", (numBlocked - previous.numBlocked));
495 maybeCount("note_suspended", (numSuspendedByAdmin - previous.numSuspendedByAdmin));
496 maybeCount("note_with_actions", (numWithActions - previous.numWithActions));
497 maybeCount("note_private", (numPrivate - previous.numPrivate));
498 maybeCount("note_secret", (numSecret - previous.numSecret));
499 maybeCount("note_interupt", (numInterrupt - previous.numInterrupt));
500 maybeCount("note_big_text", (numWithBigText - previous.numWithBigText));
501 maybeCount("note_big_pic", (numWithBigPicture - previous.numWithBigPicture));
502 maybeCount("note_fg", (numForegroundService - previous.numForegroundService));
503 maybeCount("note_ongoing", (numOngoing - previous.numOngoing));
504 maybeCount("note_auto", (numAutoCancel - previous.numAutoCancel));
505 maybeCount("note_large_icon", (numWithLargeIcon - previous.numWithLargeIcon));
506 maybeCount("note_inbox", (numWithInbox - previous.numWithInbox));
507 maybeCount("note_media", (numWithMediaSession - previous.numWithMediaSession));
508 maybeCount("note_title", (numWithTitle - previous.numWithTitle));
509 maybeCount("note_text", (numWithText - previous.numWithText));
510 maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
511 maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
Chris Wrenc8673a82016-05-17 17:11:29 -0400512 maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
513 maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
Chris Wren7c4c87b2016-03-15 12:47:26 -0400514 noisyImportance.maybeCount(previous.noisyImportance);
515 quietImportance.maybeCount(previous.quietImportance);
516 finalImportance.maybeCount(previous.finalImportance);
Chris Wren5eab2b72015-06-16 13:56:22 -0400517
Chris Wren888b7a82016-06-17 15:47:19 -0400518 previous.numEnqueuedByApp = numEnqueuedByApp;
Chris Wren7c4c87b2016-03-15 12:47:26 -0400519 previous.numPostedByApp = numPostedByApp;
520 previous.numUpdatedByApp = numUpdatedByApp;
521 previous.numRemovedByApp = numRemovedByApp;
522 previous.numPeopleCacheHit = numPeopleCacheHit;
523 previous.numPeopleCacheMiss = numPeopleCacheMiss;
524 previous.numWithStaredPeople = numWithStaredPeople;
525 previous.numWithValidPeople = numWithValidPeople;
526 previous.numBlocked = numBlocked;
527 previous.numSuspendedByAdmin = numSuspendedByAdmin;
528 previous.numWithActions = numWithActions;
529 previous.numPrivate = numPrivate;
530 previous.numSecret = numSecret;
531 previous.numInterrupt = numInterrupt;
532 previous.numWithBigText = numWithBigText;
533 previous.numWithBigPicture = numWithBigPicture;
534 previous.numForegroundService = numForegroundService;
535 previous.numOngoing = numOngoing;
536 previous.numAutoCancel = numAutoCancel;
537 previous.numWithLargeIcon = numWithLargeIcon;
538 previous.numWithInbox = numWithInbox;
539 previous.numWithMediaSession = numWithMediaSession;
540 previous.numWithTitle = numWithTitle;
541 previous.numWithText = numWithText;
542 previous.numWithSubText = numWithSubText;
543 previous.numWithInfoText = numWithInfoText;
Chris Wrenc8673a82016-05-17 17:11:29 -0400544 previous.numRateViolations = numRateViolations;
545 previous.numQuotaViolations = numQuotaViolations;
Chris Wren7c4c87b2016-03-15 12:47:26 -0400546 noisyImportance.update(previous.noisyImportance);
547 quietImportance.update(previous.quietImportance);
548 finalImportance.update(previous.finalImportance);
Chris Wren5eab2b72015-06-16 13:56:22 -0400549 }
550
551 void maybeCount(String name, int value) {
552 if (value > 0) {
553 MetricsLogger.count(mContext, name, value);
Chris Wren78403d72014-07-28 10:23:24 +0100554 }
Christoph Studer546bec82014-03-14 12:17:12 +0100555 }
556
557 public void dump(PrintWriter pw, String indent) {
558 pw.println(toStringWithIndent(indent));
559 }
560
561 @Override
562 public String toString() {
563 return toStringWithIndent("");
564 }
565
Chris Wrenc8673a82016-05-17 17:11:29 -0400566 /** @return the enqueue rate if there were a new enqueue event right now. */
567 public float getEnqueueRate() {
568 return getEnqueueRate(SystemClock.elapsedRealtime());
569 }
570
571 public float getEnqueueRate(long now) {
572 return enqueueRate.getRate(now);
573 }
574
575 public void updateInterarrivalEstimate(long now) {
576 enqueueRate.update(now);
577 }
578
Christoph Studer546bec82014-03-14 12:17:12 +0100579 private String toStringWithIndent(String indent) {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500580 StringBuilder output = new StringBuilder();
581 output.append(indent).append("AggregatedStats{\n");
582 String indentPlusTwo = indent + " ";
583 output.append(indentPlusTwo);
584 output.append("key='").append(key).append("',\n");
585 output.append(indentPlusTwo);
Chris Wren888b7a82016-06-17 15:47:19 -0400586 output.append("numEnqueuedByApp=").append(numEnqueuedByApp).append(",\n");
587 output.append(indentPlusTwo);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500588 output.append("numPostedByApp=").append(numPostedByApp).append(",\n");
589 output.append(indentPlusTwo);
590 output.append("numUpdatedByApp=").append(numUpdatedByApp).append(",\n");
591 output.append(indentPlusTwo);
592 output.append("numRemovedByApp=").append(numRemovedByApp).append(",\n");
593 output.append(indentPlusTwo);
594 output.append("numPeopleCacheHit=").append(numPeopleCacheHit).append(",\n");
595 output.append(indentPlusTwo);
596 output.append("numWithStaredPeople=").append(numWithStaredPeople).append(",\n");
597 output.append(indentPlusTwo);
598 output.append("numWithValidPeople=").append(numWithValidPeople).append(",\n");
599 output.append(indentPlusTwo);
600 output.append("numPeopleCacheMiss=").append(numPeopleCacheMiss).append(",\n");
601 output.append(indentPlusTwo);
602 output.append("numBlocked=").append(numBlocked).append(",\n");
603 output.append(indentPlusTwo);
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000604 output.append("numSuspendedByAdmin=").append(numSuspendedByAdmin).append(",\n");
605 output.append(indentPlusTwo);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500606 output.append("numWithActions=").append(numWithActions).append(",\n");
607 output.append(indentPlusTwo);
608 output.append("numPrivate=").append(numPrivate).append(",\n");
609 output.append(indentPlusTwo);
610 output.append("numSecret=").append(numSecret).append(",\n");
611 output.append(indentPlusTwo);
612 output.append("numInterrupt=").append(numInterrupt).append(",\n");
613 output.append(indentPlusTwo);
614 output.append("numWithBigText=").append(numWithBigText).append(",\n");
615 output.append(indentPlusTwo);
616 output.append("numWithBigPicture=").append(numWithBigPicture).append("\n");
617 output.append(indentPlusTwo);
618 output.append("numForegroundService=").append(numForegroundService).append("\n");
619 output.append(indentPlusTwo);
620 output.append("numOngoing=").append(numOngoing).append("\n");
621 output.append(indentPlusTwo);
622 output.append("numAutoCancel=").append(numAutoCancel).append("\n");
623 output.append(indentPlusTwo);
624 output.append("numWithLargeIcon=").append(numWithLargeIcon).append("\n");
625 output.append(indentPlusTwo);
626 output.append("numWithInbox=").append(numWithInbox).append("\n");
627 output.append(indentPlusTwo);
628 output.append("numWithMediaSession=").append(numWithMediaSession).append("\n");
629 output.append(indentPlusTwo);
630 output.append("numWithTitle=").append(numWithTitle).append("\n");
631 output.append(indentPlusTwo);
632 output.append("numWithText=").append(numWithText).append("\n");
633 output.append(indentPlusTwo);
634 output.append("numWithSubText=").append(numWithSubText).append("\n");
635 output.append(indentPlusTwo);
636 output.append("numWithInfoText=").append(numWithInfoText).append("\n");
Chris Wrenc8673a82016-05-17 17:11:29 -0400637 output.append("numRateViolations=").append(numRateViolations).append("\n");
638 output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
Chris Wrencdee8cd2016-01-25 17:10:30 -0500639 output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
640 output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
641 output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
642 output.append(indent).append("}");
643 return output.toString();
Christoph Studer546bec82014-03-14 12:17:12 +0100644 }
Chris Wrene4b38802015-07-07 15:54:19 -0400645
646 public JSONObject dumpJson() throws JSONException {
Chris Wren7c4c87b2016-03-15 12:47:26 -0400647 AggregatedStats previous = getPrevious();
Chris Wrene4b38802015-07-07 15:54:19 -0400648 JSONObject dump = new JSONObject();
649 dump.put("key", key);
650 dump.put("duration", SystemClock.elapsedRealtime() - mCreated);
Chris Wren888b7a82016-06-17 15:47:19 -0400651 maybePut(dump, "numEnqueuedByApp", numEnqueuedByApp);
Chris Wrene4b38802015-07-07 15:54:19 -0400652 maybePut(dump, "numPostedByApp", numPostedByApp);
653 maybePut(dump, "numUpdatedByApp", numUpdatedByApp);
654 maybePut(dump, "numRemovedByApp", numRemovedByApp);
655 maybePut(dump, "numPeopleCacheHit", numPeopleCacheHit);
656 maybePut(dump, "numPeopleCacheMiss", numPeopleCacheMiss);
657 maybePut(dump, "numWithStaredPeople", numWithStaredPeople);
658 maybePut(dump, "numWithValidPeople", numWithValidPeople);
659 maybePut(dump, "numBlocked", numBlocked);
Andrei Stingaceanu0122f6512016-01-22 15:33:03 +0000660 maybePut(dump, "numSuspendedByAdmin", numSuspendedByAdmin);
Chris Wrene4b38802015-07-07 15:54:19 -0400661 maybePut(dump, "numWithActions", numWithActions);
662 maybePut(dump, "numPrivate", numPrivate);
663 maybePut(dump, "numSecret", numSecret);
Chris Wrene4b38802015-07-07 15:54:19 -0400664 maybePut(dump, "numInterrupt", numInterrupt);
665 maybePut(dump, "numWithBigText", numWithBigText);
666 maybePut(dump, "numWithBigPicture", numWithBigPicture);
667 maybePut(dump, "numForegroundService", numForegroundService);
668 maybePut(dump, "numOngoing", numOngoing);
669 maybePut(dump, "numAutoCancel", numAutoCancel);
670 maybePut(dump, "numWithLargeIcon", numWithLargeIcon);
671 maybePut(dump, "numWithInbox", numWithInbox);
672 maybePut(dump, "numWithMediaSession", numWithMediaSession);
673 maybePut(dump, "numWithTitle", numWithTitle);
674 maybePut(dump, "numWithText", numWithText);
675 maybePut(dump, "numWithSubText", numWithSubText);
676 maybePut(dump, "numWithInfoText", numWithInfoText);
Chris Wrenc8673a82016-05-17 17:11:29 -0400677 maybePut(dump, "numRateViolations", numRateViolations);
678 maybePut(dump, "numQuotaLViolations", numQuotaViolations);
679 maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
Chris Wren7c4c87b2016-03-15 12:47:26 -0400680 noisyImportance.maybePut(dump, previous.noisyImportance);
681 quietImportance.maybePut(dump, previous.quietImportance);
682 finalImportance.maybePut(dump, previous.finalImportance);
Chris Wrencdee8cd2016-01-25 17:10:30 -0500683
Chris Wrene4b38802015-07-07 15:54:19 -0400684 return dump;
685 }
686
687 private void maybePut(JSONObject dump, String name, int value) throws JSONException {
688 if (value > 0) {
689 dump.put(name, value);
690 }
691 }
Chris Wrenc8673a82016-05-17 17:11:29 -0400692
693 private void maybePut(JSONObject dump, String name, float value) throws JSONException {
694 if (value > 0.0) {
695 dump.put(name, value);
696 }
697 }
Christoph Studer546bec82014-03-14 12:17:12 +0100698 }
699
Chris Wrencdee8cd2016-01-25 17:10:30 -0500700 private static class ImportanceHistogram {
701 // TODO define these somewhere else
Julia Reynoldsf0f629f2016-02-25 09:34:04 -0500702 private static final int NUM_IMPORTANCES = 6;
703 private static final String[] IMPORTANCE_NAMES =
704 {"none", "min", "low", "default", "high", "max"};
Chris Wrencdee8cd2016-01-25 17:10:30 -0500705 private final Context mContext;
706 private final String[] mCounterNames;
707 private final String mPrefix;
708 private int[] mCount;
709
710 ImportanceHistogram(Context context, String prefix) {
711 mContext = context;
712 mCount = new int[NUM_IMPORTANCES];
713 mCounterNames = new String[NUM_IMPORTANCES];
714 mPrefix = prefix;
715 for (int i = 0; i < NUM_IMPORTANCES; i++) {
716 mCounterNames[i] = mPrefix + IMPORTANCE_NAMES[i];
717 }
718 }
719
720 void increment(int imp) {
721 imp = imp < 0 ? 0 : imp > NUM_IMPORTANCES ? NUM_IMPORTANCES : imp;
722 mCount[imp] ++;
723 }
724
725 void maybeCount(ImportanceHistogram prev) {
726 for (int i = 0; i < NUM_IMPORTANCES; i++) {
727 final int value = mCount[i] - prev.mCount[i];
728 if (value > 0) {
729 MetricsLogger.count(mContext, mCounterNames[i], value);
730 }
731 }
732 }
733
734 void update(ImportanceHistogram that) {
735 for (int i = 0; i < NUM_IMPORTANCES; i++) {
736 mCount[i] = that.mCount[i];
737 }
738 }
739
740 public void maybePut(JSONObject dump, ImportanceHistogram prev)
741 throws JSONException {
742 dump.put(mPrefix, new JSONArray(mCount));
743 }
744
745 @Override
746 public String toString() {
747 StringBuilder output = new StringBuilder();
748 output.append(mPrefix).append(": [");
749 for (int i = 0; i < NUM_IMPORTANCES; i++) {
750 output.append(mCount[i]);
751 if (i < (NUM_IMPORTANCES-1)) {
752 output.append(", ");
753 }
754 }
755 output.append("]");
756 return output.toString();
757 }
758 }
759
Christoph Studer546bec82014-03-14 12:17:12 +0100760 /**
761 * Tracks usage of an individual notification that is currently active.
762 */
763 public static class SingleNotificationStats {
Chris Wren78403d72014-07-28 10:23:24 +0100764 private boolean isVisible = false;
765 private boolean isExpanded = false;
Christoph Studer546bec82014-03-14 12:17:12 +0100766 /** SystemClock.elapsedRealtime() when the notification was posted. */
767 public long posttimeElapsedMs = -1;
768 /** Elapsed time since the notification was posted until it was first clicked, or -1. */
769 public long posttimeToFirstClickMs = -1;
770 /** Elpased time since the notification was posted until it was dismissed by the user. */
771 public long posttimeToDismissMs = -1;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200772 /** Number of times the notification has been made visible. */
773 public long airtimeCount = 0;
774 /** Time in ms between the notification was posted and first shown; -1 if never shown. */
775 public long posttimeToFirstAirtimeMs = -1;
776 /**
777 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
778 * visible; -1 otherwise.
779 */
780 public long currentAirtimeStartElapsedMs = -1;
781 /** Accumulated visible time. */
782 public long airtimeMs = 0;
Chris Wren78403d72014-07-28 10:23:24 +0100783 /**
784 * Time in ms between the notification being posted and when it first
785 * became visible and expanded; -1 if it was never visibly expanded.
786 */
787 public long posttimeToFirstVisibleExpansionMs = -1;
788 /**
789 * If currently visible, SystemClock.elapsedRealtime() when the notification was made
790 * visible; -1 otherwise.
791 */
792 public long currentAirtimeExpandedStartElapsedMs = -1;
793 /** Accumulated visible expanded time. */
794 public long airtimeExpandedMs = 0;
795 /** Number of times the notification has been expanded by the user. */
796 public long userExpansionCount = 0;
Chris Wrencdee8cd2016-01-25 17:10:30 -0500797 /** Importance directly requested by the app. */
798 public int requestedImportance;
799 /** Did the app include sound or vibration on the notificaiton. */
800 public boolean isNoisy;
801 /** Importance after initial filtering for noise and other features */
802 public int naturalImportance;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200803
804 public long getCurrentPosttimeMs() {
805 if (posttimeElapsedMs < 0) {
806 return 0;
807 }
808 return SystemClock.elapsedRealtime() - posttimeElapsedMs;
809 }
810
811 public long getCurrentAirtimeMs() {
812 long result = airtimeMs;
813 // Add incomplete airtime if currently shown.
814 if (currentAirtimeStartElapsedMs >= 0) {
Chris Wren78403d72014-07-28 10:23:24 +0100815 result += (SystemClock.elapsedRealtime() - currentAirtimeStartElapsedMs);
816 }
817 return result;
818 }
819
820 public long getCurrentAirtimeExpandedMs() {
821 long result = airtimeExpandedMs;
822 // Add incomplete expanded airtime if currently shown.
823 if (currentAirtimeExpandedStartElapsedMs >= 0) {
824 result += (SystemClock.elapsedRealtime() - currentAirtimeExpandedStartElapsedMs);
Christoph Studerffeb0c32014-05-07 22:23:56 +0200825 }
826 return result;
827 }
Christoph Studer546bec82014-03-14 12:17:12 +0100828
829 /**
830 * Called when the user clicked the notification.
831 */
832 public void onClick() {
833 if (posttimeToFirstClickMs < 0) {
834 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
835 }
836 }
837
838 /**
839 * Called when the user removed the notification.
840 */
841 public void onDismiss() {
842 if (posttimeToDismissMs < 0) {
843 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
844 }
Christoph Studerffeb0c32014-05-07 22:23:56 +0200845 finish();
846 }
847
848 public void onCancel() {
849 finish();
850 }
851
852 public void onRemoved() {
853 finish();
854 }
855
856 public void onVisibilityChanged(boolean visible) {
857 long elapsedNowMs = SystemClock.elapsedRealtime();
Chris Wren78403d72014-07-28 10:23:24 +0100858 final boolean wasVisible = isVisible;
859 isVisible = visible;
Christoph Studerffeb0c32014-05-07 22:23:56 +0200860 if (visible) {
861 if (currentAirtimeStartElapsedMs < 0) {
862 airtimeCount++;
863 currentAirtimeStartElapsedMs = elapsedNowMs;
864 }
865 if (posttimeToFirstAirtimeMs < 0) {
866 posttimeToFirstAirtimeMs = elapsedNowMs - posttimeElapsedMs;
867 }
868 } else {
869 if (currentAirtimeStartElapsedMs >= 0) {
870 airtimeMs += (elapsedNowMs - currentAirtimeStartElapsedMs);
871 currentAirtimeStartElapsedMs = -1;
872 }
873 }
Chris Wren78403d72014-07-28 10:23:24 +0100874
875 if (wasVisible != isVisible) {
876 updateVisiblyExpandedStats();
877 }
878 }
879
880 public void onExpansionChanged(boolean userAction, boolean expanded) {
881 isExpanded = expanded;
882 if (isExpanded && userAction) {
883 userExpansionCount++;
884 }
885 updateVisiblyExpandedStats();
886 }
887
888 private void updateVisiblyExpandedStats() {
889 long elapsedNowMs = SystemClock.elapsedRealtime();
890 if (isExpanded && isVisible) {
891 // expanded and visible
892 if (currentAirtimeExpandedStartElapsedMs < 0) {
893 currentAirtimeExpandedStartElapsedMs = elapsedNowMs;
894 }
895 if (posttimeToFirstVisibleExpansionMs < 0) {
896 posttimeToFirstVisibleExpansionMs = elapsedNowMs - posttimeElapsedMs;
897 }
898 } else {
899 // not-expanded or not-visible
900 if (currentAirtimeExpandedStartElapsedMs >= 0) {
901 airtimeExpandedMs += (elapsedNowMs - currentAirtimeExpandedStartElapsedMs);
902 currentAirtimeExpandedStartElapsedMs = -1;
903 }
904 }
Christoph Studerffeb0c32014-05-07 22:23:56 +0200905 }
906
907 /** The notification is leaving the system. Finalize. */
908 public void finish() {
909 onVisibilityChanged(false);
Christoph Studer546bec82014-03-14 12:17:12 +0100910 }
911
912 @Override
913 public String toString() {
Chris Wrencdee8cd2016-01-25 17:10:30 -0500914 StringBuilder output = new StringBuilder();
915 output.append("SingleNotificationStats{");
916
917 output.append("posttimeElapsedMs=").append(posttimeElapsedMs).append(", ");
918 output.append("posttimeToFirstClickMs=").append(posttimeToFirstClickMs).append(", ");
919 output.append("posttimeToDismissMs=").append(posttimeToDismissMs).append(", ");
920 output.append("airtimeCount=").append(airtimeCount).append(", ");
921 output.append("airtimeMs=").append(airtimeMs).append(", ");
922 output.append("currentAirtimeStartElapsedMs=").append(currentAirtimeStartElapsedMs)
923 .append(", ");
924 output.append("airtimeExpandedMs=").append(airtimeExpandedMs).append(", ");
925 output.append("posttimeToFirstVisibleExpansionMs=")
926 .append(posttimeToFirstVisibleExpansionMs).append(", ");
927 output.append("currentAirtimeExpandedStartElapsedMs=")
928 .append(currentAirtimeExpandedStartElapsedMs).append(", ");
929 output.append("requestedImportance=").append(requestedImportance).append(", ");
930 output.append("naturalImportance=").append(naturalImportance).append(", ");
931 output.append("isNoisy=").append(isNoisy);
932 output.append('}');
933 return output.toString();
934 }
935
936 /** Copy useful information out of the stats from the pre-update notifications. */
937 public void updateFrom(SingleNotificationStats old) {
938 posttimeElapsedMs = old.posttimeElapsedMs;
939 posttimeToFirstClickMs = old.posttimeToFirstClickMs;
940 airtimeCount = old.airtimeCount;
941 posttimeToFirstAirtimeMs = old.posttimeToFirstAirtimeMs;
942 currentAirtimeStartElapsedMs = old.currentAirtimeStartElapsedMs;
943 airtimeMs = old.airtimeMs;
944 posttimeToFirstVisibleExpansionMs = old.posttimeToFirstVisibleExpansionMs;
945 currentAirtimeExpandedStartElapsedMs = old.currentAirtimeExpandedStartElapsedMs;
946 airtimeExpandedMs = old.airtimeExpandedMs;
947 userExpansionCount = old.userExpansionCount;
Christoph Studer546bec82014-03-14 12:17:12 +0100948 }
949 }
950
951 /**
952 * Aggregates long samples to sum and averages.
953 */
954 public static class Aggregate {
955 long numSamples;
Chris Wren51103972014-04-01 13:57:34 -0400956 double avg;
957 double sum2;
958 double var;
Christoph Studer546bec82014-03-14 12:17:12 +0100959
960 public void addSample(long sample) {
Chris Wren51103972014-04-01 13:57:34 -0400961 // Welford's "Method for Calculating Corrected Sums of Squares"
962 // http://www.jstor.org/stable/1266577?seq=2
Christoph Studer546bec82014-03-14 12:17:12 +0100963 numSamples++;
Chris Wren51103972014-04-01 13:57:34 -0400964 final double n = numSamples;
965 final double delta = sample - avg;
966 avg += (1.0 / n) * delta;
967 sum2 += ((n - 1) / n) * delta * delta;
968 final double divisor = numSamples == 1 ? 1.0 : n - 1.0;
969 var = sum2 / divisor;
Christoph Studer546bec82014-03-14 12:17:12 +0100970 }
971
972 @Override
973 public String toString() {
974 return "Aggregate{" +
975 "numSamples=" + numSamples +
Christoph Studer546bec82014-03-14 12:17:12 +0100976 ", avg=" + avg +
Chris Wren51103972014-04-01 13:57:34 -0400977 ", var=" + var +
Christoph Studer546bec82014-03-14 12:17:12 +0100978 '}';
979 }
980 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200981
982 private static class SQLiteLog {
983 private static final String TAG = "NotificationSQLiteLog";
984
985 // Message types passed to the background handler.
986 private static final int MSG_POST = 1;
987 private static final int MSG_CLICK = 2;
988 private static final int MSG_REMOVE = 3;
989 private static final int MSG_DISMISS = 4;
990
991 private static final String DB_NAME = "notification_log.db";
Chris Wrencdee8cd2016-01-25 17:10:30 -0500992 private static final int DB_VERSION = 5;
Christoph Studer1c3f81f2014-04-16 15:05:56 +0200993
994 /** Age in ms after which events are pruned from the DB. */
995 private static final long HORIZON_MS = 7 * 24 * 60 * 60 * 1000L; // 1 week
996 /** Delay between pruning the DB. Used to throttle pruning. */
997 private static final long PRUNE_MIN_DELAY_MS = 6 * 60 * 60 * 1000L; // 6 hours
998 /** Mininum number of writes between pruning the DB. Used to throttle pruning. */
999 private static final long PRUNE_MIN_WRITES = 1024;
1000
1001 // Table 'log'
1002 private static final String TAB_LOG = "log";
1003 private static final String COL_EVENT_USER_ID = "event_user_id";
1004 private static final String COL_EVENT_TYPE = "event_type";
1005 private static final String COL_EVENT_TIME = "event_time_ms";
1006 private static final String COL_KEY = "key";
1007 private static final String COL_PKG = "pkg";
1008 private static final String COL_NOTIFICATION_ID = "nid";
1009 private static final String COL_TAG = "tag";
1010 private static final String COL_WHEN_MS = "when_ms";
1011 private static final String COL_DEFAULTS = "defaults";
1012 private static final String COL_FLAGS = "flags";
Chris Wrencdee8cd2016-01-25 17:10:30 -05001013 private static final String COL_IMPORTANCE_REQ = "importance_request";
1014 private static final String COL_IMPORTANCE_FINAL = "importance_final";
1015 private static final String COL_NOISY = "noisy";
1016 private static final String COL_MUTED = "muted";
1017 private static final String COL_DEMOTED = "demoted";
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001018 private static final String COL_CATEGORY = "category";
1019 private static final String COL_ACTION_COUNT = "action_count";
Christoph Studerffeb0c32014-05-07 22:23:56 +02001020 private static final String COL_POSTTIME_MS = "posttime_ms";
1021 private static final String COL_AIRTIME_MS = "airtime_ms";
Chris Wren78403d72014-07-28 10:23:24 +01001022 private static final String COL_FIRST_EXPANSIONTIME_MS = "first_expansion_time_ms";
1023 private static final String COL_AIRTIME_EXPANDED_MS = "expansion_airtime_ms";
1024 private static final String COL_EXPAND_COUNT = "expansion_count";
1025
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001026
1027 private static final int EVENT_TYPE_POST = 1;
1028 private static final int EVENT_TYPE_CLICK = 2;
1029 private static final int EVENT_TYPE_REMOVE = 3;
1030 private static final int EVENT_TYPE_DISMISS = 4;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001031 private static long sLastPruneMs;
Chris Wrencdee8cd2016-01-25 17:10:30 -05001032
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001033 private static long sNumWrites;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001034 private final SQLiteOpenHelper mHelper;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001035
Chris Wrencdee8cd2016-01-25 17:10:30 -05001036 private final Handler mWriteHandler;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001037 private static final long DAY_MS = 24 * 60 * 60 * 1000;
Chris Wrencdee8cd2016-01-25 17:10:30 -05001038 private static final String STATS_QUERY = "SELECT " +
1039 COL_EVENT_USER_ID + ", " +
1040 COL_PKG + ", " +
1041 // Bucket by day by looking at 'floor((midnight - eventTimeMs) / dayMs)'
1042 "CAST(((%d - " + COL_EVENT_TIME + ") / " + DAY_MS + ") AS int) " +
1043 "AS day, " +
1044 "COUNT(*) AS cnt, " +
1045 "SUM(" + COL_MUTED + ") as muted, " +
1046 "SUM(" + COL_NOISY + ") as noisy, " +
1047 "SUM(" + COL_DEMOTED + ") as demoted " +
1048 "FROM " + TAB_LOG + " " +
1049 "WHERE " +
1050 COL_EVENT_TYPE + "=" + EVENT_TYPE_POST +
1051 " AND " + COL_EVENT_TIME + " > %d " +
1052 " GROUP BY " + COL_EVENT_USER_ID + ", day, " + COL_PKG;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001053
1054 public SQLiteLog(Context context) {
1055 HandlerThread backgroundThread = new HandlerThread("notification-sqlite-log",
1056 android.os.Process.THREAD_PRIORITY_BACKGROUND);
1057 backgroundThread.start();
1058 mWriteHandler = new Handler(backgroundThread.getLooper()) {
1059 @Override
1060 public void handleMessage(Message msg) {
1061 NotificationRecord r = (NotificationRecord) msg.obj;
1062 long nowMs = System.currentTimeMillis();
1063 switch (msg.what) {
1064 case MSG_POST:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001065 writeEvent(r.sbn.getPostTime(), EVENT_TYPE_POST, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001066 break;
1067 case MSG_CLICK:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001068 writeEvent(nowMs, EVENT_TYPE_CLICK, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001069 break;
1070 case MSG_REMOVE:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001071 writeEvent(nowMs, EVENT_TYPE_REMOVE, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001072 break;
1073 case MSG_DISMISS:
Christoph Studerffeb0c32014-05-07 22:23:56 +02001074 writeEvent(nowMs, EVENT_TYPE_DISMISS, r);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001075 break;
1076 default:
1077 Log.wtf(TAG, "Unknown message type: " + msg.what);
1078 break;
1079 }
1080 }
1081 };
1082 mHelper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
1083 @Override
1084 public void onCreate(SQLiteDatabase db) {
1085 db.execSQL("CREATE TABLE " + TAB_LOG + " (" +
1086 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1087 COL_EVENT_USER_ID + " INT," +
1088 COL_EVENT_TYPE + " INT," +
1089 COL_EVENT_TIME + " INT," +
1090 COL_KEY + " TEXT," +
1091 COL_PKG + " TEXT," +
1092 COL_NOTIFICATION_ID + " INT," +
1093 COL_TAG + " TEXT," +
1094 COL_WHEN_MS + " INT," +
1095 COL_DEFAULTS + " INT," +
1096 COL_FLAGS + " INT," +
Chris Wrencdee8cd2016-01-25 17:10:30 -05001097 COL_IMPORTANCE_REQ + " INT," +
1098 COL_IMPORTANCE_FINAL + " INT," +
1099 COL_NOISY + " INT," +
1100 COL_MUTED + " INT," +
1101 COL_DEMOTED + " INT," +
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001102 COL_CATEGORY + " TEXT," +
Christoph Studerffeb0c32014-05-07 22:23:56 +02001103 COL_ACTION_COUNT + " INT," +
1104 COL_POSTTIME_MS + " INT," +
Christoph Studer1ad856e2014-08-04 14:31:25 +02001105 COL_AIRTIME_MS + " INT," +
1106 COL_FIRST_EXPANSIONTIME_MS + " INT," +
1107 COL_AIRTIME_EXPANDED_MS + " INT," +
Chris Wren78403d72014-07-28 10:23:24 +01001108 COL_EXPAND_COUNT + " INT" +
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001109 ")");
1110 }
1111
1112 @Override
1113 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Chris Wrencdee8cd2016-01-25 17:10:30 -05001114 if (oldVersion != newVersion) {
Christoph Studer1ad856e2014-08-04 14:31:25 +02001115 db.execSQL("DROP TABLE IF EXISTS " + TAB_LOG);
1116 onCreate(db);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001117 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001118 }
1119 };
1120 }
1121
1122 public void logPosted(NotificationRecord notification) {
1123 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_POST, notification));
1124 }
1125
1126 public void logClicked(NotificationRecord notification) {
1127 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_CLICK, notification));
1128 }
1129
1130 public void logRemoved(NotificationRecord notification) {
1131 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_REMOVE, notification));
1132 }
1133
1134 public void logDismissed(NotificationRecord notification) {
1135 mWriteHandler.sendMessage(mWriteHandler.obtainMessage(MSG_DISMISS, notification));
1136 }
1137
Chris Wrencdee8cd2016-01-25 17:10:30 -05001138 private JSONArray jsonPostFrequencies(DumpFilter filter) throws JSONException {
Chris Wrene4b38802015-07-07 15:54:19 -04001139 JSONArray frequencies = new JSONArray();
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001140 SQLiteDatabase db = mHelper.getReadableDatabase();
Chris Wrene4b38802015-07-07 15:54:19 -04001141 long midnight = getMidnightMs();
Chris Wrencdee8cd2016-01-25 17:10:30 -05001142 String q = String.format(STATS_QUERY, midnight, filter.since);
Chris Wrene4b38802015-07-07 15:54:19 -04001143 Cursor cursor = db.rawQuery(q, null);
1144 try {
1145 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1146 int userId = cursor.getInt(0);
1147 String pkg = cursor.getString(1);
1148 if (filter != null && !filter.matches(pkg)) continue;
1149 int day = cursor.getInt(2);
1150 int count = cursor.getInt(3);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001151 int muted = cursor.getInt(4);
1152 int noisy = cursor.getInt(5);
1153 int demoted = cursor.getInt(6);
Chris Wrene4b38802015-07-07 15:54:19 -04001154 JSONObject row = new JSONObject();
1155 row.put("user_id", userId);
1156 row.put("package", pkg);
1157 row.put("day", day);
1158 row.put("count", count);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001159 row.put("noisy", noisy);
1160 row.put("muted", muted);
1161 row.put("demoted", demoted);
Chris Wrene4b38802015-07-07 15:54:19 -04001162 frequencies.put(row);
1163 }
1164 } finally {
1165 cursor.close();
1166 }
1167 return frequencies;
1168 }
1169
1170 public void printPostFrequencies(PrintWriter pw, String indent, DumpFilter filter) {
1171 SQLiteDatabase db = mHelper.getReadableDatabase();
1172 long midnight = getMidnightMs();
Chris Wrencdee8cd2016-01-25 17:10:30 -05001173 String q = String.format(STATS_QUERY, midnight, filter.since);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001174 Cursor cursor = db.rawQuery(q, null);
1175 try {
1176 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
1177 int userId = cursor.getInt(0);
1178 String pkg = cursor.getString(1);
John Spurlock25e2d242014-06-27 13:58:23 -04001179 if (filter != null && !filter.matches(pkg)) continue;
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001180 int day = cursor.getInt(2);
1181 int count = cursor.getInt(3);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001182 int muted = cursor.getInt(4);
1183 int noisy = cursor.getInt(5);
1184 int demoted = cursor.getInt(6);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001185 pw.println(indent + "post_frequency{user_id=" + userId + ",pkg=" + pkg +
Chris Wrencdee8cd2016-01-25 17:10:30 -05001186 ",day=" + day + ",count=" + count + ",muted=" + muted + "/" + noisy +
1187 ",demoted=" + demoted + "}");
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001188 }
1189 } finally {
1190 cursor.close();
1191 }
1192 }
1193
Chris Wrene4b38802015-07-07 15:54:19 -04001194 private long getMidnightMs() {
1195 GregorianCalendar midnight = new GregorianCalendar();
1196 midnight.set(midnight.get(Calendar.YEAR), midnight.get(Calendar.MONTH),
1197 midnight.get(Calendar.DATE), 23, 59, 59);
1198 return midnight.getTimeInMillis();
1199 }
1200
Christoph Studerffeb0c32014-05-07 22:23:56 +02001201 private void writeEvent(long eventTimeMs, int eventType, NotificationRecord r) {
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001202 ContentValues cv = new ContentValues();
1203 cv.put(COL_EVENT_USER_ID, r.sbn.getUser().getIdentifier());
1204 cv.put(COL_EVENT_TIME, eventTimeMs);
1205 cv.put(COL_EVENT_TYPE, eventType);
1206 putNotificationIdentifiers(r, cv);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001207 if (eventType == EVENT_TYPE_POST) {
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001208 putNotificationDetails(r, cv);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001209 } else {
Chris Wren78403d72014-07-28 10:23:24 +01001210 putPosttimeVisibility(r, cv);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001211 }
1212 SQLiteDatabase db = mHelper.getWritableDatabase();
1213 if (db.insert(TAB_LOG, null, cv) < 0) {
1214 Log.wtf(TAG, "Error while trying to insert values: " + cv);
1215 }
1216 sNumWrites++;
1217 pruneIfNecessary(db);
1218 }
1219
1220 private void pruneIfNecessary(SQLiteDatabase db) {
1221 // Prune if we haven't in a while.
1222 long nowMs = System.currentTimeMillis();
1223 if (sNumWrites > PRUNE_MIN_WRITES ||
1224 nowMs - sLastPruneMs > PRUNE_MIN_DELAY_MS) {
1225 sNumWrites = 0;
1226 sLastPruneMs = nowMs;
1227 long horizonStartMs = nowMs - HORIZON_MS;
1228 int deletedRows = db.delete(TAB_LOG, COL_EVENT_TIME + " < ?",
1229 new String[] { String.valueOf(horizonStartMs) });
1230 Log.d(TAG, "Pruned event entries: " + deletedRows);
1231 }
1232 }
1233
1234 private static void putNotificationIdentifiers(NotificationRecord r, ContentValues outCv) {
1235 outCv.put(COL_KEY, r.sbn.getKey());
1236 outCv.put(COL_PKG, r.sbn.getPackageName());
1237 }
1238
1239 private static void putNotificationDetails(NotificationRecord r, ContentValues outCv) {
1240 outCv.put(COL_NOTIFICATION_ID, r.sbn.getId());
1241 if (r.sbn.getTag() != null) {
1242 outCv.put(COL_TAG, r.sbn.getTag());
1243 }
1244 outCv.put(COL_WHEN_MS, r.sbn.getPostTime());
1245 outCv.put(COL_FLAGS, r.getNotification().flags);
Chris Wrencdee8cd2016-01-25 17:10:30 -05001246 final int before = r.stats.requestedImportance;
1247 final int after = r.getImportance();
1248 final boolean noisy = r.stats.isNoisy;
1249 outCv.put(COL_IMPORTANCE_REQ, before);
1250 outCv.put(COL_IMPORTANCE_FINAL, after);
1251 outCv.put(COL_DEMOTED, after < before ? 1 : 0);
1252 outCv.put(COL_NOISY, noisy);
1253 if (noisy && after < IMPORTANCE_HIGH) {
1254 outCv.put(COL_MUTED, 1);
1255 } else {
1256 outCv.put(COL_MUTED, 0);
1257 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001258 if (r.getNotification().category != null) {
1259 outCv.put(COL_CATEGORY, r.getNotification().category);
1260 }
1261 outCv.put(COL_ACTION_COUNT, r.getNotification().actions != null ?
1262 r.getNotification().actions.length : 0);
1263 }
1264
Chris Wren78403d72014-07-28 10:23:24 +01001265 private static void putPosttimeVisibility(NotificationRecord r, ContentValues outCv) {
Christoph Studerffeb0c32014-05-07 22:23:56 +02001266 outCv.put(COL_POSTTIME_MS, r.stats.getCurrentPosttimeMs());
1267 outCv.put(COL_AIRTIME_MS, r.stats.getCurrentAirtimeMs());
Chris Wren78403d72014-07-28 10:23:24 +01001268 outCv.put(COL_EXPAND_COUNT, r.stats.userExpansionCount);
1269 outCv.put(COL_AIRTIME_EXPANDED_MS, r.stats.getCurrentAirtimeExpandedMs());
1270 outCv.put(COL_FIRST_EXPANSIONTIME_MS, r.stats.posttimeToFirstVisibleExpansionMs);
Christoph Studerffeb0c32014-05-07 22:23:56 +02001271 }
1272
John Spurlock25e2d242014-06-27 13:58:23 -04001273 public void dump(PrintWriter pw, String indent, DumpFilter filter) {
1274 printPostFrequencies(pw, indent, filter);
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001275 }
Chris Wrene4b38802015-07-07 15:54:19 -04001276
1277 public JSONObject dumpJson(DumpFilter filter) {
1278 JSONObject dump = new JSONObject();
1279 try {
Chris Wrencdee8cd2016-01-25 17:10:30 -05001280 dump.put("post_frequency", jsonPostFrequencies(filter));
1281 dump.put("since", filter.since);
1282 dump.put("now", System.currentTimeMillis());
Chris Wrene4b38802015-07-07 15:54:19 -04001283 } catch (JSONException e) {
1284 // pass
1285 }
1286 return dump;
1287 }
Christoph Studer1c3f81f2014-04-16 15:05:56 +02001288 }
Christoph Studer546bec82014-03-14 12:17:12 +01001289}