NotificationUsageStats.java revision 546bec8ebf2cf865e88d02cc8cb29563ad224967
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;
23import android.util.Log;
24
25import java.io.PrintWriter;
26import java.util.HashMap;
27import java.util.Map;
28
29/**
30 * Keeps track of notification activity, display, and user interaction.
31 *
32 * <p>This class receives signals from NoMan and keeps running stats of
33 * notification usage. Some metrics are updated as events occur. Others, namely
34 * those involving durations, are updated as the notification is canceled.</p>
35 *
36 * <p>This class is thread-safe.</p>
37 *
38 * {@hide}
39 */
40public class NotificationUsageStats {
41
42    // Guarded by synchronized(this).
43    private final Map<String, AggregatedStats> mStats = new HashMap<String, AggregatedStats>();
44
45    /**
46     * Called when a notification has been posted.
47     */
48    public synchronized void registerPostedByApp(NotificationRecord notification) {
49        notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
50        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
51            stats.numPostedByApp++;
52        }
53    }
54
55    /**
56     * Called when a notification has been updated.
57     */
58    public void registerUpdatedByApp(NotificationRecord notification) {
59        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
60            stats.numUpdatedByApp++;
61        }
62    }
63
64    /**
65     * Called when the originating app removed the notification programmatically.
66     */
67    public synchronized void registerRemovedByApp(NotificationRecord notification) {
68        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
69            stats.numRemovedByApp++;
70            stats.collect(notification.stats);
71        }
72    }
73
74    /**
75     * Called when the user dismissed the notification via the UI.
76     */
77    public synchronized void registerDismissedByUser(NotificationRecord notification) {
78        notification.stats.onDismiss();
79        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
80            stats.numDismissedByUser++;
81            stats.collect(notification.stats);
82        }
83    }
84
85    /**
86     * Called when the user clicked the notification in the UI.
87     */
88    public synchronized void registerClickedByUser(NotificationRecord notification) {
89        notification.stats.onClick();
90        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
91            stats.numClickedByUser++;
92        }
93    }
94
95    /**
96     * Called when the notification is canceled because the user clicked it.
97     *
98     * <p>Called after {@link #registerClickedByUser(NotificationRecord)}.</p>
99     */
100    public synchronized void registerCancelDueToClick(NotificationRecord notification) {
101        // No explicit stats for this (the click has already been registered in
102        // registerClickedByUser), just make sure the single notification stats
103        // are folded up into aggregated stats.
104        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
105            stats.collect(notification.stats);
106        }
107    }
108
109    /**
110     * Called when the notification is canceled due to unknown reasons.
111     *
112     * <p>Called for notifications of apps being uninstalled, for example.</p>
113     */
114    public synchronized void registerCancelUnknown(NotificationRecord notification) {
115        // Fold up individual stats.
116        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
117            stats.collect(notification.stats);
118        }
119    }
120
121    // Locked by this.
122    private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
123        StatusBarNotification n = record.sbn;
124
125        String user = String.valueOf(n.getUserId());
126        String userPackage = user + ":" + n.getPackageName();
127
128        // TODO: Use pool of arrays.
129        return new AggregatedStats[] {
130                getOrCreateAggregatedStatsLocked(user),
131                getOrCreateAggregatedStatsLocked(userPackage),
132                getOrCreateAggregatedStatsLocked(n.getKey()),
133        };
134    }
135
136    // Locked by this.
137    private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
138        AggregatedStats result = mStats.get(key);
139        if (result == null) {
140            result = new AggregatedStats(key);
141            mStats.put(key, result);
142        }
143        return result;
144    }
145
146    public synchronized void dump(PrintWriter pw, String indent) {
147        for (AggregatedStats as : mStats.values()) {
148            as.dump(pw, indent);
149        }
150    }
151
152    /**
153     * Aggregated notification stats.
154     */
155    private static class AggregatedStats {
156        public final String key;
157
158        // ---- Updated as the respective events occur.
159        public int numPostedByApp;
160        public int numUpdatedByApp;
161        public int numRemovedByApp;
162        public int numClickedByUser;
163        public int numDismissedByUser;
164
165        // ----  Updated when a notification is canceled.
166        public final Aggregate posttimeMs = new Aggregate();
167        public final Aggregate posttimeToDismissMs = new Aggregate();
168        public final Aggregate posttimeToFirstClickMs = new Aggregate();
169
170        public AggregatedStats(String key) {
171            this.key = key;
172        }
173
174        public void collect(SingleNotificationStats singleNotificationStats) {
175            posttimeMs.addSample(
176	            SystemClock.elapsedRealtime() - singleNotificationStats.posttimeElapsedMs);
177            if (singleNotificationStats.posttimeToDismissMs >= 0) {
178                posttimeToDismissMs.addSample(singleNotificationStats.posttimeToDismissMs);
179            }
180            if (singleNotificationStats.posttimeToFirstClickMs >= 0) {
181                posttimeToFirstClickMs.addSample(singleNotificationStats.posttimeToFirstClickMs);
182            }
183        }
184
185        public void dump(PrintWriter pw, String indent) {
186            pw.println(toStringWithIndent(indent));
187        }
188
189        @Override
190        public String toString() {
191            return toStringWithIndent("");
192        }
193
194        private String toStringWithIndent(String indent) {
195            return indent + "AggregatedStats{\n" +
196                    indent + "  key='" + key + "',\n" +
197                    indent + "  numPostedByApp=" + numPostedByApp + ",\n" +
198                    indent + "  numUpdatedByApp=" + numUpdatedByApp + ",\n" +
199                    indent + "  numRemovedByApp=" + numRemovedByApp + ",\n" +
200                    indent + "  numClickedByUser=" + numClickedByUser + ",\n" +
201                    indent + "  numDismissedByUser=" + numDismissedByUser + ",\n" +
202                    indent + "  posttimeMs=" + posttimeMs + ",\n" +
203                    indent + "  posttimeToDismissMs=" + posttimeToDismissMs + ",\n" +
204                    indent + "  posttimeToFirstClickMs=" + posttimeToFirstClickMs + ",\n" +
205                    indent + "}";
206        }
207    }
208
209    /**
210     * Tracks usage of an individual notification that is currently active.
211     */
212    public static class SingleNotificationStats {
213        /** SystemClock.elapsedRealtime() when the notification was posted. */
214        public long posttimeElapsedMs = -1;
215        /** Elapsed time since the notification was posted until it was first clicked, or -1. */
216        public long posttimeToFirstClickMs = -1;
217        /** Elpased time since the notification was posted until it was dismissed by the user. */
218        public long posttimeToDismissMs = -1;
219
220        /**
221         * Called when the user clicked the notification.
222         */
223        public void onClick() {
224            if (posttimeToFirstClickMs < 0) {
225                posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
226            }
227        }
228
229        /**
230         * Called when the user removed the notification.
231         */
232        public void onDismiss() {
233            if (posttimeToDismissMs < 0) {
234                posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
235            }
236        }
237
238        @Override
239        public String toString() {
240            return "SingleNotificationStats{" +
241                    "posttimeElapsedMs=" + posttimeElapsedMs +
242                    ", posttimeToFirstClickMs=" + posttimeToFirstClickMs +
243                    ", posttimeToDismissMs=" + posttimeToDismissMs +
244                    '}';
245        }
246    }
247
248    /**
249     * Aggregates long samples to sum and averages.
250     */
251    public static class Aggregate {
252        long numSamples;
253        long sum;
254        long avg;
255
256        public void addSample(long sample) {
257            numSamples++;
258            sum += sample;
259            avg = sum / numSamples;
260        }
261
262        @Override
263        public String toString() {
264            return "Aggregate{" +
265                    "numSamples=" + numSamples +
266                    ", sum=" + sum +
267                    ", avg=" + avg +
268                    '}';
269        }
270    }
271}
272