high-frequency notification stats.

Aggregate and then periodically report stats that are high-frequency
because they are driven by app behavior, not user behavior.

Reuse the NotificationUsageStats facility.
Remove redundant stats.
Lessen memory foot print.
Enable in-memeory aggregates with small, bounded memory footprint.

Bug: 20258744
Change-Id: I87e391419c53917fa13c68a56f8cdb40a7c8e548
diff --git a/services/core/java/com/android/server/notification/NotificationIntrusivenessExtractor.java b/services/core/java/com/android/server/notification/NotificationIntrusivenessExtractor.java
index 22cdd58..d4fadcf 100644
--- a/services/core/java/com/android/server/notification/NotificationIntrusivenessExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationIntrusivenessExtractor.java
@@ -33,7 +33,7 @@
     the top of the ranking order, before it falls back to its natural position. */
     private static final long HANG_TIME_MS = 10000;
 
-    public void initialize(Context ctx) {
+    public void initialize(Context ctx, NotificationUsageStats usageStats) {
         if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 90e912d..9c288be 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -856,8 +856,10 @@
         } catch (Resources.NotFoundException e) {
             extractorNames = new String[0];
         }
+        mUsageStats = new NotificationUsageStats(getContext());
         mRankingHelper = new RankingHelper(getContext(),
                 new RankingWorkerHandler(mRankingThread.getLooper()),
+                mUsageStats,
                 extractorNames);
         mConditionProviders = new ConditionProviders(getContext(), mHandler, mUserProfiles);
         mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders);
@@ -882,7 +884,6 @@
         });
         final File systemDir = new File(Environment.getDataDirectory(), "system");
         mPolicyFile = new AtomicFile(new File(systemDir, "notification_policy.xml"));
-        mUsageStats = new NotificationUsageStats(getContext());
 
         importOldBlockDb();
 
@@ -2071,6 +2072,7 @@
                             r.score = JUNK_SCORE;
                             Slog.e(TAG, "Suppressing notification from package " + pkg
                                     + " by user request.");
+                            mUsageStats.registerBlocked(r);
                         }
                     }
 
@@ -2736,12 +2738,6 @@
             case REASON_NOMAN_CANCEL_ALL:
                 mUsageStats.registerRemovedByApp(r);
                 break;
-            case REASON_DELEGATE_CLICK:
-                mUsageStats.registerCancelDueToClick(r);
-                break;
-            default:
-                mUsageStats.registerCancelUnknown(r);
-                break;
         }
 
         mNotificationsByKey.remove(r.sbn.getKey());
diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
index 43d05d0..31f8b27 100644
--- a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
@@ -26,7 +26,7 @@
 public interface NotificationSignalExtractor {
 
     /** One-time initialization. */
-    public void initialize(Context context);
+    public void initialize(Context context, NotificationUsageStats usageStats);
 
     /**
      * Called once per notification that is posted or updated.
diff --git a/services/core/java/com/android/server/notification/NotificationUsageStats.java b/services/core/java/com/android/server/notification/NotificationUsageStats.java
index 4696771..2d5c199 100644
--- a/services/core/java/com/android/server/notification/NotificationUsageStats.java
+++ b/services/core/java/com/android/server/notification/NotificationUsageStats.java
@@ -25,13 +25,13 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.os.SystemClock;
-import android.service.notification.StatusBarNotification;
 import android.util.Log;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 
 import java.io.PrintWriter;
+import java.util.ArrayDeque;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -47,21 +47,45 @@
  * {@hide}
  */
 public class NotificationUsageStats {
-    // WARNING: Aggregated stats can grow unboundedly with pkg+id+tag.
-    // Don't enable on production builds.
-    private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = false;
-    private static final boolean ENABLE_SQLITE_LOG = true;
+    private static final String TAG = "NotificationUsageStats";
 
+    private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
+    private static final boolean ENABLE_SQLITE_LOG = true;
     private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
+    private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
+    private static final int MSG_EMIT = 1;
+
+    private static final boolean DEBUG = false;
+    public static final int TEN_SECONDS = 1000 * 10;
+    public static final int ONE_HOUR = 1000 * 60 * 60;
+    private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : ONE_HOUR;
 
     // Guarded by synchronized(this).
-    private final Map<String, AggregatedStats> mStats = new HashMap<String, AggregatedStats>();
+    private final Map<String, AggregatedStats> mStats = new HashMap<>();
+    private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
     private final SQLiteLog mSQLiteLog;
     private final Context mContext;
+    private final Handler mHandler;
+    private long mLastEmitTime;
 
     public NotificationUsageStats(Context context) {
         mContext = context;
+        mLastEmitTime = SystemClock.elapsedRealtime();
         mSQLiteLog = ENABLE_SQLITE_LOG ? new SQLiteLog(context) : null;
+        mHandler = new Handler(mContext.getMainLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_EMIT:
+                        emit();
+                        break;
+                    default:
+                        Log.wtf(TAG, "Unknown message type: " + msg.what);
+                        break;
+                }
+            }
+        };
+        mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
     }
 
     /**
@@ -70,9 +94,12 @@
     public synchronized void registerPostedByApp(NotificationRecord notification) {
         notification.stats = new SingleNotificationStats();
         notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
+
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
+        for (AggregatedStats stats : aggregatedStatsArray) {
             stats.numPostedByApp++;
         }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
         if (ENABLE_SQLITE_LOG) {
             mSQLiteLog.logPosted(notification);
         }
@@ -83,9 +110,11 @@
      */
     public void registerUpdatedByApp(NotificationRecord notification, NotificationRecord old) {
         notification.stats = old.stats;
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
+        for (AggregatedStats stats : aggregatedStatsArray) {
             stats.numUpdatedByApp++;
         }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
     }
 
     /**
@@ -93,10 +122,11 @@
      */
     public synchronized void registerRemovedByApp(NotificationRecord notification) {
         notification.stats.onRemoved();
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
+        for (AggregatedStats stats : aggregatedStatsArray) {
             stats.numRemovedByApp++;
-            stats.collect(notification.stats);
         }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
         if (ENABLE_SQLITE_LOG) {
             mSQLiteLog.logRemoved(notification);
         }
@@ -109,10 +139,6 @@
         MetricsLogger.histogram(mContext, "note_dismiss_longevity",
                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
         notification.stats.onDismiss();
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
-            stats.numDismissedByUser++;
-            stats.collect(notification.stats);
-        }
         if (ENABLE_SQLITE_LOG) {
             mSQLiteLog.logDismissed(notification);
         }
@@ -125,36 +151,36 @@
         MetricsLogger.histogram(mContext, "note_click_longevity",
                 (int) (System.currentTimeMillis() - notification.getRankingTimeMs()) / (60 * 1000));
         notification.stats.onClick();
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
-            stats.numClickedByUser++;
-        }
         if (ENABLE_SQLITE_LOG) {
             mSQLiteLog.logClicked(notification);
         }
     }
 
-    /**
-     * Called when the notification is canceled because the user clicked it.
-     *
-     * <p>Called after {@link #registerClickedByUser(NotificationRecord)}.</p>
-     */
-    public synchronized void registerCancelDueToClick(NotificationRecord notification) {
-        notification.stats.onCancel();
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
-            stats.collect(notification.stats);
+    public synchronized void registerPeopleAffinity(NotificationRecord notification, boolean valid,
+            boolean starred, boolean cached) {
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
+        for (AggregatedStats stats : aggregatedStatsArray) {
+            if (valid) {
+                stats.numWithValidPeople++;
+            }
+            if (starred) {
+                stats.numWithStaredPeople++;
+            }
+            if (cached) {
+                stats.numPeopleCacheHit++;
+            } else {
+                stats.numPeopleCacheMiss++;
+            }
         }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
     }
 
-    /**
-     * 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) {
-        notification.stats.onCancel();
-        for (AggregatedStats stats : getAggregatedStatsLocked(notification)) {
-            stats.collect(notification.stats);
+    public synchronized void registerBlocked(NotificationRecord notification) {
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
+        for (AggregatedStats stats : aggregatedStatsArray) {
+            stats.numBlocked++;
         }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
     }
 
     // Locked by this.
@@ -163,24 +189,28 @@
             return EMPTY_AGGREGATED_STATS;
         }
 
-        StatusBarNotification n = record.sbn;
+        // TODO: expand to package-level counts in the future.
+        AggregatedStats[] array = mStatsArrays.poll();
+        if (array == null) {
+            array = new AggregatedStats[1];
+        }
+        array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
+        return array;
+    }
 
-        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 void releaseAggregatedStatsLocked(AggregatedStats[] array) {
+        for(int i = 0; i < array.length; i++) {
+            array[i] = null;
+        }
+        mStatsArrays.offer(array);
     }
 
     // Locked by this.
     private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
         AggregatedStats result = mStats.get(key);
         if (result == null) {
-            result = new AggregatedStats(key);
+            result = new AggregatedStats(mContext, key);
             mStats.put(key, result);
         }
         return result;
@@ -193,64 +223,74 @@
                     continue;
                 as.dump(pw, indent);
             }
+            pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
         }
         if (ENABLE_SQLITE_LOG) {
             mSQLiteLog.dump(pw, indent, filter);
         }
     }
 
+    public synchronized void emit() {
+        // TODO: expand to package-level counts in the future.
+        AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
+        stats.emit();
+        mLastEmitTime = SystemClock.elapsedRealtime();
+        mHandler.removeMessages(MSG_EMIT);
+        mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
+    }
+
     /**
      * Aggregated notification stats.
      */
     private static class AggregatedStats {
+
+        private final Context mContext;
         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;
+        public int numPeopleCacheHit;
+        public int numPeopleCacheMiss;;
+        public int numWithStaredPeople;
+        public int numWithValidPeople;
+        public int numBlocked;
 
-        // ----  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 final Aggregate airtimeCount = new Aggregate();
-        public final Aggregate airtimeMs = new Aggregate();
-        public final Aggregate posttimeToFirstAirtimeMs = new Aggregate();
-        public final Aggregate userExpansionCount = new Aggregate();
-        public final Aggregate airtimeExpandedMs = new Aggregate();
-        public final Aggregate posttimeToFirstVisibleExpansionMs = new Aggregate();
+        private AggregatedStats mPrevious;
 
-        public AggregatedStats(String key) {
+        public AggregatedStats(Context context, String key) {
             this.key = key;
+            mContext = context;
         }
 
-        public void collect(SingleNotificationStats singleNotificationStats) {
-            posttimeMs.addSample(
-                    SystemClock.elapsedRealtime() - singleNotificationStats.posttimeElapsedMs);
-            if (singleNotificationStats.posttimeToDismissMs >= 0) {
-                posttimeToDismissMs.addSample(singleNotificationStats.posttimeToDismissMs);
+        public void emit() {
+            if (mPrevious == null) {
+                mPrevious = new AggregatedStats(null, key);
             }
-            if (singleNotificationStats.posttimeToFirstClickMs >= 0) {
-                posttimeToFirstClickMs.addSample(singleNotificationStats.posttimeToFirstClickMs);
-            }
-            airtimeCount.addSample(singleNotificationStats.airtimeCount);
-            if (singleNotificationStats.airtimeMs >= 0) {
-                airtimeMs.addSample(singleNotificationStats.airtimeMs);
-            }
-            if (singleNotificationStats.posttimeToFirstAirtimeMs >= 0) {
-                posttimeToFirstAirtimeMs.addSample(
-                        singleNotificationStats.posttimeToFirstAirtimeMs);
-            }
-            if (singleNotificationStats.posttimeToFirstVisibleExpansionMs >= 0) {
-                posttimeToFirstVisibleExpansionMs.addSample(
-                        singleNotificationStats.posttimeToFirstVisibleExpansionMs);
-            }
-            userExpansionCount.addSample(singleNotificationStats.userExpansionCount);
-            if (singleNotificationStats.airtimeExpandedMs >= 0) {
-                airtimeExpandedMs.addSample(singleNotificationStats.airtimeExpandedMs);
+
+            maybeCount("note_post", (numPostedByApp - mPrevious.numPostedByApp));
+            maybeCount("note_update", (numUpdatedByApp - mPrevious.numUpdatedByApp));
+            maybeCount("note_remove", (numRemovedByApp - mPrevious.numRemovedByApp));
+            maybeCount("note_with_people", (numWithValidPeople - mPrevious.numWithValidPeople));
+            maybeCount("note_with_stars", (numWithStaredPeople - mPrevious.numWithStaredPeople));
+            maybeCount("people_cache_hit", (numPeopleCacheHit - mPrevious.numPeopleCacheHit));
+            maybeCount("people_cache_miss", (numPeopleCacheMiss - mPrevious.numPeopleCacheMiss));
+            maybeCount("note_blocked", (numBlocked - mPrevious.numBlocked));
+
+            mPrevious.numPostedByApp = numPostedByApp;
+            mPrevious.numUpdatedByApp = numUpdatedByApp;
+            mPrevious.numRemovedByApp = numRemovedByApp;
+            mPrevious.numPeopleCacheHit = numPeopleCacheHit;
+            mPrevious.numPeopleCacheMiss = numPeopleCacheMiss;
+            mPrevious.numWithStaredPeople = numWithStaredPeople;
+            mPrevious.numWithValidPeople = numWithValidPeople;
+            mPrevious.numBlocked = numBlocked;
+        }
+
+        void maybeCount(String name, int value) {
+            if (value > 0) {
+                MetricsLogger.count(mContext, name, value);
             }
         }
 
@@ -269,17 +309,11 @@
                     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 + "  airtimeCount=" + airtimeCount + ",\n" +
-                    indent + "  airtimeMs=" + airtimeMs + ",\n" +
-                    indent + "  posttimeToFirstAirtimeMs=" + posttimeToFirstAirtimeMs + ",\n" +
-                    indent + "  userExpansionCount=" + userExpansionCount + ",\n" +
-                    indent + "  airtimeExpandedMs=" + airtimeExpandedMs + ",\n" +
-                    indent + "  posttimeToFVEMs=" + posttimeToFirstVisibleExpansionMs + ",\n" +
+                    indent + "  numPeopleCacheHit=" + numPeopleCacheHit + ",\n" +
+                    indent + "  numWithStaredPeople=" + numWithStaredPeople + ",\n" +
+                    indent + "  numWithValidPeople=" + numWithValidPeople + ",\n" +
+                    indent + "  numPeopleCacheMiss=" + numPeopleCacheMiss + ",\n" +
+                    indent + "  numBlocked=" + numBlocked + ",\n" +
                     indent + "}";
         }
     }
diff --git a/services/core/java/com/android/server/notification/PackagePriorityExtractor.java b/services/core/java/com/android/server/notification/PackagePriorityExtractor.java
index a13e54a..6beed9c 100644
--- a/services/core/java/com/android/server/notification/PackagePriorityExtractor.java
+++ b/services/core/java/com/android/server/notification/PackagePriorityExtractor.java
@@ -24,7 +24,7 @@
 
     private RankingConfig mConfig;
 
-    public void initialize(Context ctx) {
+    public void initialize(Context ctx, NotificationUsageStats usageStats) {
         if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
     }
 
diff --git a/services/core/java/com/android/server/notification/PackageVisibilityExtractor.java b/services/core/java/com/android/server/notification/PackageVisibilityExtractor.java
index f720f9f..af99db7 100644
--- a/services/core/java/com/android/server/notification/PackageVisibilityExtractor.java
+++ b/services/core/java/com/android/server/notification/PackageVisibilityExtractor.java
@@ -24,7 +24,7 @@
 
     private RankingConfig mConfig;
 
-    public void initialize(Context ctx) {
+    public void initialize(Context ctx, NotificationUsageStats usageStats) {
         if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
     }
 
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index e503ac8..a089518 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -68,7 +68,8 @@
     private final Context mContext;
     private final Handler mRankingHandler;
 
-    public RankingHelper(Context context, Handler rankingHandler, String[] extractorNames) {
+    public RankingHelper(Context context, Handler rankingHandler, NotificationUsageStats usageStats,
+            String[] extractorNames) {
         mContext = context;
         mRankingHandler = rankingHandler;
 
@@ -79,7 +80,7 @@
                 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
                 NotificationSignalExtractor extractor =
                         (NotificationSignalExtractor) extractorClass.newInstance();
-                extractor.initialize(mContext);
+                extractor.initialize(mContext, usageStats);
                 extractor.setConfig(this);
                 mSignalExtractors[i] = extractor;
             } catch (ClassNotFoundException e) {
diff --git a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
index 4af7d4e..0420269 100644
--- a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
+++ b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
@@ -34,7 +34,6 @@
 import android.util.Log;
 import android.util.LruCache;
 import android.util.Slog;
-import com.android.internal.logging.MetricsLogger;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -85,11 +84,13 @@
     private Handler mHandler;
     private ContentObserver mObserver;
     private int mEvictionCount;
+    private NotificationUsageStats mUsageStats;
 
-    public void initialize(Context context) {
+    public void initialize(Context context, NotificationUsageStats usageStats) {
         if (DEBUG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
         mUserToContextMap = new ArrayMap<>();
         mBaseContext = context;
+        mUsageStats = usageStats;
         mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
         mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
                 mBaseContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
@@ -203,8 +204,15 @@
         final String key = record.getKey();
         final Bundle extras = record.getNotification().extras;
         final float[] affinityOut = new float[1];
-        final RankingReconsideration rr = validatePeople(context, key, extras, affinityOut);
-        record.setContactAffinity(affinityOut[0]);
+        final PeopleRankingReconsideration rr = validatePeople(context, key, extras, affinityOut);
+        final float affinity = affinityOut[0];
+        record.setContactAffinity(affinity);
+        if (rr == null) {
+            mUsageStats.registerPeopleAffinity(record, affinity > NONE, affinity == STARRED_CONTACT,
+                    true /* cached */);
+        } else {
+            rr.setRecord(record);
+        }
         return rr;
     }
 
@@ -245,7 +253,6 @@
 
         if (pendingLookups.isEmpty()) {
             if (VERBOSE) Slog.i(TAG, "final affinity: " + affinity);
-            if (affinity != NONE) MetricsLogger.count(mBaseContext, "note_with_people", 1);
             return null;
         }
 
@@ -413,6 +420,7 @@
         private final Context mContext;
 
         private float mContactAffinity = NONE;
+        private NotificationRecord mRecord;
 
         private PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups) {
             super(key);
@@ -456,7 +464,10 @@
                         "ms");
             }
 
-            if (mContactAffinity != NONE) MetricsLogger.count(mBaseContext, "note_with_people", 1);
+            if (mRecord != null) {
+                mUsageStats.registerPeopleAffinity(mRecord, mContactAffinity > NONE,
+                        mContactAffinity == STARRED_CONTACT, false /* cached */);
+            }
         }
 
         @Override
@@ -469,6 +480,10 @@
         public float getContactAffinity() {
             return mContactAffinity;
         }
+
+        public void setRecord(NotificationRecord record) {
+            mRecord = record;
+        }
     }
 }
 
diff --git a/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java
index 3cc04e8..b40fd068 100644
--- a/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -15,6 +15,9 @@
  */
 package com.android.server.notification;
 
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
 import android.app.Notification;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
@@ -24,6 +27,7 @@
 import java.util.ArrayList;
 
 public class RankingHelperTest extends AndroidTestCase {
+    @Mock NotificationUsageStats mUsageStats;
 
     private Notification mNotiGroupGSortA;
     private Notification mNotiGroupGSortB;
@@ -39,9 +43,10 @@
 
     @Override
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         UserHandle user = UserHandle.ALL;
 
-        mHelper = new RankingHelper(getContext(), null, new String[0]);
+        mHelper = new RankingHelper(getContext(), null, mUsageStats, new String[0]);
 
         mNotiGroupGSortA = new Notification.Builder(getContext())
                 .setContentTitle("A")