NotificationUsageStats.java revision 511039748872e1cc469d20c3db52bec4e0d66d46
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.server.notification; 18 19import com.android.server.notification.NotificationManagerService.NotificationRecord; 20 21import android.os.SystemClock; 22import android.service.notification.StatusBarNotification; 23 24import java.io.PrintWriter; 25import java.util.HashMap; 26import java.util.Map; 27 28/** 29 * Keeps track of notification activity, display, and user interaction. 30 * 31 * <p>This class receives signals from NoMan and keeps running stats of 32 * notification usage. Some metrics are updated as events occur. Others, namely 33 * those involving durations, are updated as the notification is canceled.</p> 34 * 35 * <p>This class is thread-safe.</p> 36 * 37 * {@hide} 38 */ 39public class NotificationUsageStats { 40 41 // Guarded by synchronized(this). 42 private final Map<String, AggregatedStats> mStats = new HashMap<String, AggregatedStats>(); 43 44 /** 45 * Called when a notification has been posted. 46 */ 47 public synchronized void registerPostedByApp(NotificationRecord notification) { 48 notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime(); 49 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 50 stats.numPostedByApp++; 51 } 52 } 53 54 /** 55 * Called when a notification has been updated. 56 */ 57 public void registerUpdatedByApp(NotificationRecord notification) { 58 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 59 stats.numUpdatedByApp++; 60 } 61 } 62 63 /** 64 * Called when the originating app removed the notification programmatically. 65 */ 66 public synchronized void registerRemovedByApp(NotificationRecord notification) { 67 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 68 stats.numRemovedByApp++; 69 stats.collect(notification.stats); 70 } 71 } 72 73 /** 74 * Called when the user dismissed the notification via the UI. 75 */ 76 public synchronized void registerDismissedByUser(NotificationRecord notification) { 77 notification.stats.onDismiss(); 78 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 79 stats.numDismissedByUser++; 80 stats.collect(notification.stats); 81 } 82 } 83 84 /** 85 * Called when the user clicked the notification in the UI. 86 */ 87 public synchronized void registerClickedByUser(NotificationRecord notification) { 88 notification.stats.onClick(); 89 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 90 stats.numClickedByUser++; 91 } 92 } 93 94 /** 95 * Called when the notification is canceled because the user clicked it. 96 * 97 * <p>Called after {@link #registerClickedByUser(NotificationRecord)}.</p> 98 */ 99 public synchronized void registerCancelDueToClick(NotificationRecord notification) { 100 // No explicit stats for this (the click has already been registered in 101 // registerClickedByUser), just make sure the single notification stats 102 // are folded up into aggregated stats. 103 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 104 stats.collect(notification.stats); 105 } 106 } 107 108 /** 109 * Called when the notification is canceled due to unknown reasons. 110 * 111 * <p>Called for notifications of apps being uninstalled, for example.</p> 112 */ 113 public synchronized void registerCancelUnknown(NotificationRecord notification) { 114 // Fold up individual stats. 115 for (AggregatedStats stats : getAggregatedStatsLocked(notification)) { 116 stats.collect(notification.stats); 117 } 118 } 119 120 // Locked by this. 121 private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) { 122 StatusBarNotification n = record.sbn; 123 124 String user = String.valueOf(n.getUserId()); 125 String userPackage = user + ":" + n.getPackageName(); 126 127 // TODO: Use pool of arrays. 128 return new AggregatedStats[] { 129 getOrCreateAggregatedStatsLocked(user), 130 getOrCreateAggregatedStatsLocked(userPackage), 131 getOrCreateAggregatedStatsLocked(n.getKey()), 132 }; 133 } 134 135 // Locked by this. 136 private AggregatedStats getOrCreateAggregatedStatsLocked(String key) { 137 AggregatedStats result = mStats.get(key); 138 if (result == null) { 139 result = new AggregatedStats(key); 140 mStats.put(key, result); 141 } 142 return result; 143 } 144 145 public synchronized void dump(PrintWriter pw, String indent) { 146 for (AggregatedStats as : mStats.values()) { 147 as.dump(pw, indent); 148 } 149 } 150 151 /** 152 * Aggregated notification stats. 153 */ 154 private static class AggregatedStats { 155 public final String key; 156 157 // ---- Updated as the respective events occur. 158 public int numPostedByApp; 159 public int numUpdatedByApp; 160 public int numRemovedByApp; 161 public int numClickedByUser; 162 public int numDismissedByUser; 163 164 // ---- Updated when a notification is canceled. 165 public final Aggregate posttimeMs = new Aggregate(); 166 public final Aggregate posttimeToDismissMs = new Aggregate(); 167 public final Aggregate posttimeToFirstClickMs = new Aggregate(); 168 169 public AggregatedStats(String key) { 170 this.key = key; 171 } 172 173 public void collect(SingleNotificationStats singleNotificationStats) { 174 posttimeMs.addSample( 175 SystemClock.elapsedRealtime() - singleNotificationStats.posttimeElapsedMs); 176 if (singleNotificationStats.posttimeToDismissMs >= 0) { 177 posttimeToDismissMs.addSample(singleNotificationStats.posttimeToDismissMs); 178 } 179 if (singleNotificationStats.posttimeToFirstClickMs >= 0) { 180 posttimeToFirstClickMs.addSample(singleNotificationStats.posttimeToFirstClickMs); 181 } 182 } 183 184 public void dump(PrintWriter pw, String indent) { 185 pw.println(toStringWithIndent(indent)); 186 } 187 188 @Override 189 public String toString() { 190 return toStringWithIndent(""); 191 } 192 193 private String toStringWithIndent(String indent) { 194 return indent + "AggregatedStats{\n" + 195 indent + " key='" + key + "',\n" + 196 indent + " numPostedByApp=" + numPostedByApp + ",\n" + 197 indent + " numUpdatedByApp=" + numUpdatedByApp + ",\n" + 198 indent + " numRemovedByApp=" + numRemovedByApp + ",\n" + 199 indent + " numClickedByUser=" + numClickedByUser + ",\n" + 200 indent + " numDismissedByUser=" + numDismissedByUser + ",\n" + 201 indent + " posttimeMs=" + posttimeMs + ",\n" + 202 indent + " posttimeToDismissMs=" + posttimeToDismissMs + ",\n" + 203 indent + " posttimeToFirstClickMs=" + posttimeToFirstClickMs + ",\n" + 204 indent + "}"; 205 } 206 } 207 208 /** 209 * Tracks usage of an individual notification that is currently active. 210 */ 211 public static class SingleNotificationStats { 212 /** SystemClock.elapsedRealtime() when the notification was posted. */ 213 public long posttimeElapsedMs = -1; 214 /** Elapsed time since the notification was posted until it was first clicked, or -1. */ 215 public long posttimeToFirstClickMs = -1; 216 /** Elpased time since the notification was posted until it was dismissed by the user. */ 217 public long posttimeToDismissMs = -1; 218 219 /** 220 * Called when the user clicked the notification. 221 */ 222 public void onClick() { 223 if (posttimeToFirstClickMs < 0) { 224 posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs; 225 } 226 } 227 228 /** 229 * Called when the user removed the notification. 230 */ 231 public void onDismiss() { 232 if (posttimeToDismissMs < 0) { 233 posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs; 234 } 235 } 236 237 @Override 238 public String toString() { 239 return "SingleNotificationStats{" + 240 "posttimeElapsedMs=" + posttimeElapsedMs + 241 ", posttimeToFirstClickMs=" + posttimeToFirstClickMs + 242 ", posttimeToDismissMs=" + posttimeToDismissMs + 243 '}'; 244 } 245 } 246 247 /** 248 * Aggregates long samples to sum and averages. 249 */ 250 public static class Aggregate { 251 long numSamples; 252 double avg; 253 double sum2; 254 double var; 255 256 public void addSample(long sample) { 257 // Welford's "Method for Calculating Corrected Sums of Squares" 258 // http://www.jstor.org/stable/1266577?seq=2 259 numSamples++; 260 final double n = numSamples; 261 final double delta = sample - avg; 262 avg += (1.0 / n) * delta; 263 sum2 += ((n - 1) / n) * delta * delta; 264 final double divisor = numSamples == 1 ? 1.0 : n - 1.0; 265 var = sum2 / divisor; 266 } 267 268 @Override 269 public String toString() { 270 return "Aggregate{" + 271 "numSamples=" + numSamples + 272 ", avg=" + avg + 273 ", var=" + var + 274 '}'; 275 } 276 } 277} 278