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