Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 546bec8e authored by Christoph Studer's avatar Christoph Studer
Browse files

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
parent 37109819
Loading
Loading
Loading
Loading
+41 −4
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.notification.NotificationScorer;
import com.android.server.EventLogTags;
import com.android.server.notification.NotificationUsageStats.SingleNotificationStats;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.SystemService;
import com.android.server.lights.Light;
@@ -205,6 +206,8 @@ public class NotificationManagerService extends SystemService {

    final ArrayList<NotificationScorer> mScorers = new ArrayList<NotificationScorer>();

    private final NotificationUsageStats mUsageStats = new NotificationUsageStats();

    private int mZenMode;
    // temporary, until we update apps to provide metadata
    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
    {
        final StatusBarNotification sbn;
        final SingleNotificationStats stats = new SingleNotificationStats();
        IBinder statusBarKey;

        NotificationRecord(StatusBarNotification sbn)
@@ -861,6 +865,7 @@ public class NotificationManagerService extends SystemService {
                }
                pw.println(prefix + "  }");
            }
            pw.println(prefix + "  stats=" + stats.toString());
        }

        @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);
                    if (index < 0) {
                        mNotificationList.add(r);
                        mUsageStats.registerPostedByApp(r);
                    } else {
                        old = mNotificationList.remove(index);
                        mNotificationList.add(index, r);
                        mUsageStats.registerUpdatedByApp(r);
                        // Make sure we don't lose the foreground service state.
                        if (old != null) {
                            notification.flags |=
@@ -2300,7 +2310,7 @@ public class NotificationManagerService extends SystemService {
        manager.sendAccessibilityEvent(event);
    }

    private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete) {
    private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) {
        // tell the app
        if (sendDelete) {
            if (r.getNotification().deleteIntent != null) {
@@ -2359,6 +2369,26 @@ public class NotificationManagerService extends SystemService {
            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()
        mArchive.record(r.sbn);
    }
@@ -2387,6 +2417,12 @@ public class NotificationManagerService extends SystemService {
                    if (index >= 0) {
                        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) {
                            return;
                        }
@@ -2397,7 +2433,7 @@ public class NotificationManagerService extends SystemService {
                        mNotificationList.remove(index);
                        mNotificationsByKey.remove(r.sbn.getKey());

                        cancelNotificationLocked(r, sendDelete);
                        cancelNotificationLocked(r, sendDelete, reason);
                        updateLightsLocked();
                    }
                }
@@ -2469,7 +2505,7 @@ public class NotificationManagerService extends SystemService {
                }
                mNotificationList.remove(i);
                mNotificationsByKey.remove(r.sbn.getKey());
                cancelNotificationLocked(r, false);
                cancelNotificationLocked(r, false, reason);
            }
            if (canceledSomething) {
                updateLightsLocked();
@@ -2521,6 +2557,7 @@ public class NotificationManagerService extends SystemService {
        EventLogTags.writeNotificationCancelAll(callingUid, callingPid,
                null, userId, 0, 0, reason,
                listener == null ? null : listener.component.toShortString());

        final int N = mNotificationList.size();
        for (int i=N-1; i>=0; i--) {
            NotificationRecord r = mNotificationList.get(i);
@@ -2532,7 +2569,7 @@ public class NotificationManagerService extends SystemService {
                            | Notification.FLAG_NO_CLEAR)) == 0) {
                mNotificationList.remove(i);
                mNotificationsByKey.remove(r.sbn.getKey());
                cancelNotificationLocked(r, true);
                cancelNotificationLocked(r, true, reason);
            }
        }
        updateLightsLocked();
+271 −0
Original line number Diff line number Diff line
/*
 * 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 +
                    '}';
        }
    }
}