Notification usage stats
First cut of gathering implicit notification signals and computing running stats. Tracks: 1. Post, update, remove by apps 2. Click, dismissal by users Stats are aggregated on user, user+pkg, and getKey() levels. Current stats are printed as part of 'dumpsys notification'. Change-Id: I06ecbf76e517509895f2f9eea5b9d19bf9a34975
This commit is contained in:
@@ -78,6 +78,7 @@ import android.widget.Toast;
|
|||||||
import com.android.internal.R;
|
import com.android.internal.R;
|
||||||
import com.android.internal.notification.NotificationScorer;
|
import com.android.internal.notification.NotificationScorer;
|
||||||
import com.android.server.EventLogTags;
|
import com.android.server.EventLogTags;
|
||||||
|
import com.android.server.notification.NotificationUsageStats.SingleNotificationStats;
|
||||||
import com.android.server.statusbar.StatusBarManagerInternal;
|
import com.android.server.statusbar.StatusBarManagerInternal;
|
||||||
import com.android.server.SystemService;
|
import com.android.server.SystemService;
|
||||||
import com.android.server.lights.Light;
|
import com.android.server.lights.Light;
|
||||||
@@ -205,6 +206,8 @@ public class NotificationManagerService extends SystemService {
|
|||||||
|
|
||||||
final ArrayList<NotificationScorer> mScorers = new ArrayList<NotificationScorer>();
|
final ArrayList<NotificationScorer> mScorers = new ArrayList<NotificationScorer>();
|
||||||
|
|
||||||
|
private final NotificationUsageStats mUsageStats = new NotificationUsageStats();
|
||||||
|
|
||||||
private int mZenMode;
|
private int mZenMode;
|
||||||
// temporary, until we update apps to provide metadata
|
// temporary, until we update apps to provide metadata
|
||||||
private static final Set<String> CALL_PACKAGES = new HashSet<String>(Arrays.asList(
|
private static final Set<String> CALL_PACKAGES = new HashSet<String>(Arrays.asList(
|
||||||
@@ -791,6 +794,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
public static final class NotificationRecord
|
public static final class NotificationRecord
|
||||||
{
|
{
|
||||||
final StatusBarNotification sbn;
|
final StatusBarNotification sbn;
|
||||||
|
final SingleNotificationStats stats = new SingleNotificationStats();
|
||||||
IBinder statusBarKey;
|
IBinder statusBarKey;
|
||||||
|
|
||||||
NotificationRecord(StatusBarNotification sbn)
|
NotificationRecord(StatusBarNotification sbn)
|
||||||
@@ -861,6 +865,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
}
|
}
|
||||||
pw.println(prefix + " }");
|
pw.println(prefix + " }");
|
||||||
}
|
}
|
||||||
|
pw.println(prefix + " stats=" + stats.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1758,6 +1763,9 @@ public class NotificationManagerService extends SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw.println("\n Usage Stats:");
|
||||||
|
mUsageStats.dump(pw, " ");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1905,9 +1913,11 @@ public class NotificationManagerService extends SystemService {
|
|||||||
int index = indexOfNotificationLocked(pkg, tag, id, userId);
|
int index = indexOfNotificationLocked(pkg, tag, id, userId);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
mNotificationList.add(r);
|
mNotificationList.add(r);
|
||||||
|
mUsageStats.registerPostedByApp(r);
|
||||||
} else {
|
} else {
|
||||||
old = mNotificationList.remove(index);
|
old = mNotificationList.remove(index);
|
||||||
mNotificationList.add(index, r);
|
mNotificationList.add(index, r);
|
||||||
|
mUsageStats.registerUpdatedByApp(r);
|
||||||
// Make sure we don't lose the foreground service state.
|
// Make sure we don't lose the foreground service state.
|
||||||
if (old != null) {
|
if (old != null) {
|
||||||
notification.flags |=
|
notification.flags |=
|
||||||
@@ -2300,7 +2310,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
manager.sendAccessibilityEvent(event);
|
manager.sendAccessibilityEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete) {
|
private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) {
|
||||||
// tell the app
|
// tell the app
|
||||||
if (sendDelete) {
|
if (sendDelete) {
|
||||||
if (r.getNotification().deleteIntent != null) {
|
if (r.getNotification().deleteIntent != null) {
|
||||||
@@ -2359,6 +2369,26 @@ public class NotificationManagerService extends SystemService {
|
|||||||
mLedNotification = null;
|
mLedNotification = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record usage stats
|
||||||
|
switch (reason) {
|
||||||
|
case REASON_DELEGATE_CANCEL:
|
||||||
|
case REASON_DELEGATE_CANCEL_ALL:
|
||||||
|
case REASON_LISTENER_CANCEL:
|
||||||
|
case REASON_LISTENER_CANCEL_ALL:
|
||||||
|
mUsageStats.registerDismissedByUser(r);
|
||||||
|
break;
|
||||||
|
case REASON_NOMAN_CANCEL:
|
||||||
|
case REASON_NOMAN_CANCEL_ALL:
|
||||||
|
mUsageStats.registerRemovedByApp(r);
|
||||||
|
break;
|
||||||
|
case REASON_DELEGATE_CLICK:
|
||||||
|
mUsageStats.registerCancelDueToClick(r);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mUsageStats.registerCancelUnknown(r);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Save it for users of getHistoricalNotifications()
|
// Save it for users of getHistoricalNotifications()
|
||||||
mArchive.record(r.sbn);
|
mArchive.record(r.sbn);
|
||||||
}
|
}
|
||||||
@@ -2387,6 +2417,12 @@ public class NotificationManagerService extends SystemService {
|
|||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
NotificationRecord r = mNotificationList.get(index);
|
NotificationRecord r = mNotificationList.get(index);
|
||||||
|
|
||||||
|
// Ideally we'd do this in the caller of this method. However, that would
|
||||||
|
// require the caller to also find the notification.
|
||||||
|
if (reason == REASON_DELEGATE_CLICK) {
|
||||||
|
mUsageStats.registerClickedByUser(r);
|
||||||
|
}
|
||||||
|
|
||||||
if ((r.getNotification().flags & mustHaveFlags) != mustHaveFlags) {
|
if ((r.getNotification().flags & mustHaveFlags) != mustHaveFlags) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2397,7 +2433,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
mNotificationList.remove(index);
|
mNotificationList.remove(index);
|
||||||
mNotificationsByKey.remove(r.sbn.getKey());
|
mNotificationsByKey.remove(r.sbn.getKey());
|
||||||
|
|
||||||
cancelNotificationLocked(r, sendDelete);
|
cancelNotificationLocked(r, sendDelete, reason);
|
||||||
updateLightsLocked();
|
updateLightsLocked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2469,7 +2505,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
}
|
}
|
||||||
mNotificationList.remove(i);
|
mNotificationList.remove(i);
|
||||||
mNotificationsByKey.remove(r.sbn.getKey());
|
mNotificationsByKey.remove(r.sbn.getKey());
|
||||||
cancelNotificationLocked(r, false);
|
cancelNotificationLocked(r, false, reason);
|
||||||
}
|
}
|
||||||
if (canceledSomething) {
|
if (canceledSomething) {
|
||||||
updateLightsLocked();
|
updateLightsLocked();
|
||||||
@@ -2521,6 +2557,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
EventLogTags.writeNotificationCancelAll(callingUid, callingPid,
|
EventLogTags.writeNotificationCancelAll(callingUid, callingPid,
|
||||||
null, userId, 0, 0, reason,
|
null, userId, 0, 0, reason,
|
||||||
listener == null ? null : listener.component.toShortString());
|
listener == null ? null : listener.component.toShortString());
|
||||||
|
|
||||||
final int N = mNotificationList.size();
|
final int N = mNotificationList.size();
|
||||||
for (int i=N-1; i>=0; i--) {
|
for (int i=N-1; i>=0; i--) {
|
||||||
NotificationRecord r = mNotificationList.get(i);
|
NotificationRecord r = mNotificationList.get(i);
|
||||||
@@ -2532,7 +2569,7 @@ public class NotificationManagerService extends SystemService {
|
|||||||
| Notification.FLAG_NO_CLEAR)) == 0) {
|
| Notification.FLAG_NO_CLEAR)) == 0) {
|
||||||
mNotificationList.remove(i);
|
mNotificationList.remove(i);
|
||||||
mNotificationsByKey.remove(r.sbn.getKey());
|
mNotificationsByKey.remove(r.sbn.getKey());
|
||||||
cancelNotificationLocked(r, true);
|
cancelNotificationLocked(r, true, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateLightsLocked();
|
updateLightsLocked();
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.server.notification;
|
||||||
|
|
||||||
|
import com.android.server.notification.NotificationManagerService.NotificationRecord;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.service.notification.StatusBarNotification;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of notification activity, display, and user interaction.
|
||||||
|
*
|
||||||
|
* <p>This class receives signals from NoMan and keeps running stats of
|
||||||
|
* notification usage. Some metrics are updated as events occur. Others, namely
|
||||||
|
* those involving durations, are updated as the notification is canceled.</p>
|
||||||
|
*
|
||||||
|
* <p>This class is thread-safe.</p>
|
||||||
|
*
|
||||||
|
* {@hide}
|
||||||
|
*/
|
||||||
|
public class NotificationUsageStats {
|
||||||
|
|
||||||
|
// Guarded by synchronized(this).
|
||||||
|
private final Map<String, AggregatedStats> mStats = new HashMap<String, AggregatedStats>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a notification has been posted.
|
||||||
|
*/
|
||||||
|
public synchronized void registerPostedByApp(NotificationRecord notification) {
|
||||||
|
notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.numPostedByApp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a notification has been updated.
|
||||||
|
*/
|
||||||
|
public void registerUpdatedByApp(NotificationRecord notification) {
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.numUpdatedByApp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the originating app removed the notification programmatically.
|
||||||
|
*/
|
||||||
|
public synchronized void registerRemovedByApp(NotificationRecord notification) {
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.numRemovedByApp++;
|
||||||
|
stats.collect(notification.stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user dismissed the notification via the UI.
|
||||||
|
*/
|
||||||
|
public synchronized void registerDismissedByUser(NotificationRecord notification) {
|
||||||
|
notification.stats.onDismiss();
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.numDismissedByUser++;
|
||||||
|
stats.collect(notification.stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user clicked the notification in the UI.
|
||||||
|
*/
|
||||||
|
public synchronized void registerClickedByUser(NotificationRecord notification) {
|
||||||
|
notification.stats.onClick();
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.numClickedByUser++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the notification is canceled because the user clicked it.
|
||||||
|
*
|
||||||
|
* <p>Called after {@link #registerClickedByUser(NotificationRecord)}.</p>
|
||||||
|
*/
|
||||||
|
public synchronized void registerCancelDueToClick(NotificationRecord notification) {
|
||||||
|
// No explicit stats for this (the click has already been registered in
|
||||||
|
// registerClickedByUser), just make sure the single notification stats
|
||||||
|
// are folded up into aggregated stats.
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.collect(notification.stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the notification is canceled due to unknown reasons.
|
||||||
|
*
|
||||||
|
* <p>Called for notifications of apps being uninstalled, for example.</p>
|
||||||
|
*/
|
||||||
|
public synchronized void registerCancelUnknown(NotificationRecord notification) {
|
||||||
|
// Fold up individual stats.
|
||||||
|
for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
|
||||||
|
stats.collect(notification.stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locked by this.
|
||||||
|
private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
|
||||||
|
StatusBarNotification n = record.sbn;
|
||||||
|
|
||||||
|
String user = String.valueOf(n.getUserId());
|
||||||
|
String userPackage = user + ":" + n.getPackageName();
|
||||||
|
|
||||||
|
// TODO: Use pool of arrays.
|
||||||
|
return new AggregatedStats[] {
|
||||||
|
getOrCreateAggregatedStatsLocked(user),
|
||||||
|
getOrCreateAggregatedStatsLocked(userPackage),
|
||||||
|
getOrCreateAggregatedStatsLocked(n.getKey()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locked by this.
|
||||||
|
private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
|
||||||
|
AggregatedStats result = mStats.get(key);
|
||||||
|
if (result == null) {
|
||||||
|
result = new AggregatedStats(key);
|
||||||
|
mStats.put(key, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void dump(PrintWriter pw, String indent) {
|
||||||
|
for (AggregatedStats as : mStats.values()) {
|
||||||
|
as.dump(pw, indent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregated notification stats.
|
||||||
|
*/
|
||||||
|
private static class AggregatedStats {
|
||||||
|
public final String key;
|
||||||
|
|
||||||
|
// ---- Updated as the respective events occur.
|
||||||
|
public int numPostedByApp;
|
||||||
|
public int numUpdatedByApp;
|
||||||
|
public int numRemovedByApp;
|
||||||
|
public int numClickedByUser;
|
||||||
|
public int numDismissedByUser;
|
||||||
|
|
||||||
|
// ---- Updated when a notification is canceled.
|
||||||
|
public final Aggregate posttimeMs = new Aggregate();
|
||||||
|
public final Aggregate posttimeToDismissMs = new Aggregate();
|
||||||
|
public final Aggregate posttimeToFirstClickMs = new Aggregate();
|
||||||
|
|
||||||
|
public AggregatedStats(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collect(SingleNotificationStats singleNotificationStats) {
|
||||||
|
posttimeMs.addSample(
|
||||||
|
SystemClock.elapsedRealtime() - singleNotificationStats.posttimeElapsedMs);
|
||||||
|
if (singleNotificationStats.posttimeToDismissMs >= 0) {
|
||||||
|
posttimeToDismissMs.addSample(singleNotificationStats.posttimeToDismissMs);
|
||||||
|
}
|
||||||
|
if (singleNotificationStats.posttimeToFirstClickMs >= 0) {
|
||||||
|
posttimeToFirstClickMs.addSample(singleNotificationStats.posttimeToFirstClickMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dump(PrintWriter pw, String indent) {
|
||||||
|
pw.println(toStringWithIndent(indent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return toStringWithIndent("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toStringWithIndent(String indent) {
|
||||||
|
return indent + "AggregatedStats{\n" +
|
||||||
|
indent + " key='" + key + "',\n" +
|
||||||
|
indent + " numPostedByApp=" + numPostedByApp + ",\n" +
|
||||||
|
indent + " numUpdatedByApp=" + numUpdatedByApp + ",\n" +
|
||||||
|
indent + " numRemovedByApp=" + numRemovedByApp + ",\n" +
|
||||||
|
indent + " numClickedByUser=" + numClickedByUser + ",\n" +
|
||||||
|
indent + " numDismissedByUser=" + numDismissedByUser + ",\n" +
|
||||||
|
indent + " posttimeMs=" + posttimeMs + ",\n" +
|
||||||
|
indent + " posttimeToDismissMs=" + posttimeToDismissMs + ",\n" +
|
||||||
|
indent + " posttimeToFirstClickMs=" + posttimeToFirstClickMs + ",\n" +
|
||||||
|
indent + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks usage of an individual notification that is currently active.
|
||||||
|
*/
|
||||||
|
public static class SingleNotificationStats {
|
||||||
|
/** SystemClock.elapsedRealtime() when the notification was posted. */
|
||||||
|
public long posttimeElapsedMs = -1;
|
||||||
|
/** Elapsed time since the notification was posted until it was first clicked, or -1. */
|
||||||
|
public long posttimeToFirstClickMs = -1;
|
||||||
|
/** Elpased time since the notification was posted until it was dismissed by the user. */
|
||||||
|
public long posttimeToDismissMs = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user clicked the notification.
|
||||||
|
*/
|
||||||
|
public void onClick() {
|
||||||
|
if (posttimeToFirstClickMs < 0) {
|
||||||
|
posttimeToFirstClickMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user removed the notification.
|
||||||
|
*/
|
||||||
|
public void onDismiss() {
|
||||||
|
if (posttimeToDismissMs < 0) {
|
||||||
|
posttimeToDismissMs = SystemClock.elapsedRealtime() - posttimeElapsedMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SingleNotificationStats{" +
|
||||||
|
"posttimeElapsedMs=" + posttimeElapsedMs +
|
||||||
|
", posttimeToFirstClickMs=" + posttimeToFirstClickMs +
|
||||||
|
", posttimeToDismissMs=" + posttimeToDismissMs +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates long samples to sum and averages.
|
||||||
|
*/
|
||||||
|
public static class Aggregate {
|
||||||
|
long numSamples;
|
||||||
|
long sum;
|
||||||
|
long avg;
|
||||||
|
|
||||||
|
public void addSample(long sample) {
|
||||||
|
numSamples++;
|
||||||
|
sum += sample;
|
||||||
|
avg = sum / numSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Aggregate{" +
|
||||||
|
"numSamples=" + numSamples +
|
||||||
|
", sum=" + sum +
|
||||||
|
", avg=" + avg +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user