am 9fca9ecb: Merge "notification ranking infrastructure"

* commit '9fca9ecbf966372d42210f229005de43bf2d3159':
  notification ranking infrastructure
diff --git a/api/current.txt b/api/current.txt
index 5d9feeb..a9ccd79 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -25222,16 +25222,24 @@
     method public final deprecated void cancelNotification(java.lang.String, java.lang.String, int);
     method public final void cancelNotification(java.lang.String);
     method public final void cancelNotifications(java.lang.String[]);
-    method public java.lang.String[] getActiveNotificationKeys();
     method public android.service.notification.StatusBarNotification[] getActiveNotifications();
     method public android.service.notification.StatusBarNotification[] getActiveNotifications(java.lang.String[]);
+    method public java.lang.String[] getOrderedNotificationKeys();
     method public android.os.IBinder onBind(android.content.Intent);
     method public void onListenerConnected(java.lang.String[]);
+    method public void onNotificationOrderUpdate();
     method public abstract void onNotificationPosted(android.service.notification.StatusBarNotification);
     method public abstract void onNotificationRemoved(android.service.notification.StatusBarNotification);
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationListenerService";
   }
 
+  public class NotificationOrderUpdate implements android.os.Parcelable {
+    ctor public NotificationOrderUpdate(android.os.Parcel);
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+  }
+
   public class StatusBarNotification implements android.os.Parcelable {
     ctor public StatusBarNotification(java.lang.String, java.lang.String, int, java.lang.String, int, int, int, android.app.Notification, android.os.UserHandle, long);
     ctor public StatusBarNotification(android.os.Parcel);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index bba6caf..76a6a8e 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -659,8 +659,8 @@
 
     /**
      * @hide
-     * Extra added by NotificationManagerService to indicate whether a NotificationScorer
-     * modified the Notifications's score.
+     * Extra added by NotificationManagerService to indicate whether
+     * the Notifications's score has been modified.
      */
     public static final String EXTRA_SCORE_MODIFIED = "android.scoreModified";
 
diff --git a/core/java/android/service/notification/INotificationListener.aidl b/core/java/android/service/notification/INotificationListener.aidl
index d4b29d8..d4919eb 100644
--- a/core/java/android/service/notification/INotificationListener.aidl
+++ b/core/java/android/service/notification/INotificationListener.aidl
@@ -17,11 +17,15 @@
 package android.service.notification;
 
 import android.service.notification.StatusBarNotification;
+import android.service.notification.NotificationOrderUpdate;
 
 /** @hide */
 oneway interface INotificationListener
 {
-    void onListenerConnected(in String[] notificationKeys);
-    void onNotificationPosted(in StatusBarNotification notification);
-    void onNotificationRemoved(in StatusBarNotification notification);
+    void onListenerConnected(in NotificationOrderUpdate update);
+    void onNotificationPosted(in StatusBarNotification notification,
+            in NotificationOrderUpdate update);
+    void onNotificationRemoved(in StatusBarNotification notification,
+            in NotificationOrderUpdate update);
+    void onNotificationOrderUpdate(in NotificationOrderUpdate update);
 }
\ No newline at end of file
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 3673f03..a94f45a 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -22,10 +22,13 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.UserHandle;
 import android.util.Log;
 
+import java.util.Comparator;
+import java.util.HashMap;
+
 /**
  * A service that receives calls from the system when new notifications are posted or removed.
  * <p>To extend this class, you must declare the service in your manifest file with
@@ -46,6 +49,7 @@
             + "[" + getClass().getSimpleName() + "]";
 
     private INotificationListenerWrapper mWrapper = null;
+    private String[] mNotificationKeys;
 
     private INotificationManager mNoMan;
 
@@ -95,6 +99,15 @@
         // optional
     }
 
+    /**
+     * Implement this method to be notified when the notification order cahnges.
+     *
+     * Call {@link #getOrderedNotificationKeys()} to retrieve the new order.
+     */
+    public void onNotificationOrderUpdate() {
+        // optional
+    }
+
     private final INotificationManager getNotificationInterface() {
         if (mNoMan == null) {
             mNoMan = INotificationManager.Stub.asInterface(
@@ -202,7 +215,7 @@
      * Request the list of outstanding notifications (that is, those that are visible to the
      * current user). Useful when you don't know what's already been posted.
      *
-     * @return An array of active notifications.
+     * @return An array of active notifications, sorted in natural order.
      */
     public StatusBarNotification[] getActiveNotifications() {
         return getActiveNotifications(null /*all*/);
@@ -213,7 +226,8 @@
      * current user). Useful when you don't know what's already been posted.
      *
      * @param keys A specific list of notification keys, or {@code null} for all.
-     * @return An array of active notifications.
+     * @return An array of active notifications, sorted in natural order
+     *   if {@code keys} is {@code null}.
      */
     public StatusBarNotification[] getActiveNotifications(String[] keys) {
         if (!isBound()) return null;
@@ -226,21 +240,15 @@
     }
 
     /**
-     * Request the list of outstanding notification keys(that is, those that are visible to the
-     * current user).  You can use the notification keys for subsequent retrieval via
+     * Request the list of notification keys in their current natural order.
+     * You can use the notification keys for subsequent retrieval via
      * {@link #getActiveNotifications(String[]) or dismissal via
      * {@link #cancelNotifications(String[]).
      *
-     * @return An array of active notification keys.
+     * @return An array of active notification keys, in their natural order.
      */
-    public String[] getActiveNotificationKeys() {
-        if (!isBound()) return null;
-        try {
-            return getNotificationInterface().getActiveNotificationKeysFromListener(mWrapper);
-        } catch (android.os.RemoteException ex) {
-            Log.v(TAG, "Unable to contact notification manager", ex);
-        }
-        return null;
+    public String[] getOrderedNotificationKeys() {
+        return mNotificationKeys;
     }
 
     @Override
@@ -261,28 +269,60 @@
 
     private class INotificationListenerWrapper extends INotificationListener.Stub {
         @Override
-        public void onNotificationPosted(StatusBarNotification sbn) {
+        public void onNotificationPosted(StatusBarNotification sbn,
+                NotificationOrderUpdate update) {
             try {
-                NotificationListenerService.this.onNotificationPosted(sbn);
+                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+                synchronized (mWrapper) {
+                    updateNotificationKeys(update);
+                    NotificationListenerService.this.onNotificationPosted(sbn);
+                }
             } catch (Throwable t) {
-                Log.w(TAG, "Error running onNotificationPosted", t);
+                Log.w(TAG, "Error running onOrderedNotificationPosted", t);
             }
         }
         @Override
-        public void onNotificationRemoved(StatusBarNotification sbn) {
+        public void onNotificationRemoved(StatusBarNotification sbn,
+                NotificationOrderUpdate update) {
             try {
-                NotificationListenerService.this.onNotificationRemoved(sbn);
+                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+                synchronized (mWrapper) {
+                    updateNotificationKeys(update);
+                    NotificationListenerService.this.onNotificationRemoved(sbn);
+                }
             } catch (Throwable t) {
                 Log.w(TAG, "Error running onNotificationRemoved", t);
             }
         }
         @Override
-        public void onListenerConnected(String[] notificationKeys) {
+        public void onListenerConnected(NotificationOrderUpdate update) {
             try {
-                NotificationListenerService.this.onListenerConnected(notificationKeys);
+                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+                synchronized (mWrapper) {
+                    updateNotificationKeys(update);
+                    NotificationListenerService.this.onListenerConnected(mNotificationKeys);
+                }
             } catch (Throwable t) {
                 Log.w(TAG, "Error running onListenerConnected", t);
             }
         }
+        @Override
+        public void onNotificationOrderUpdate(NotificationOrderUpdate update)
+                throws RemoteException {
+            try {
+                // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+                synchronized (mWrapper) {
+                    updateNotificationKeys(update);
+                    NotificationListenerService.this.onNotificationOrderUpdate();
+                }
+            } catch (Throwable t) {
+                Log.w(TAG, "Error running onNotificationOrderUpdate", t);
+            }
+        }
+    }
+
+    private void updateNotificationKeys(NotificationOrderUpdate update) {
+        // TODO: avoid garbage by comparing the lists
+        mNotificationKeys = update.getOrderedKeys();
     }
 }
diff --git a/core/java/android/service/notification/NotificationOrderUpdate.aidl b/core/java/android/service/notification/NotificationOrderUpdate.aidl
new file mode 100644
index 0000000..5d50641
--- /dev/null
+++ b/core/java/android/service/notification/NotificationOrderUpdate.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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 android.service.notification;
+
+parcelable NotificationOrderUpdate;
diff --git a/core/java/android/service/notification/NotificationOrderUpdate.java b/core/java/android/service/notification/NotificationOrderUpdate.java
new file mode 100644
index 0000000..20e19a3
--- /dev/null
+++ b/core/java/android/service/notification/NotificationOrderUpdate.java
@@ -0,0 +1,62 @@
+/*
+ * 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 android.service.notification;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class NotificationOrderUpdate implements Parcelable {
+    // TODO replace this with an update instead of the whole array
+    private final String[] mKeys;
+
+    /** @hide */
+    public NotificationOrderUpdate(String[] keys) {
+        this.mKeys = keys;
+    }
+
+    public NotificationOrderUpdate(Parcel in) {
+        this.mKeys = in.readStringArray();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeStringArray(this.mKeys);
+    }
+
+    public static final Parcelable.Creator<NotificationOrderUpdate> CREATOR
+            = new Parcelable.Creator<NotificationOrderUpdate>() {
+        public NotificationOrderUpdate createFromParcel(Parcel parcel) {
+            return new NotificationOrderUpdate(parcel);
+        }
+
+        public NotificationOrderUpdate[] newArray(int size) {
+            return new NotificationOrderUpdate[size];
+        }
+    };
+
+    /**
+     * @hide
+     * @return ordered list of keys
+     */
+    String[] getOrderedKeys() {
+        return mKeys;
+    }
+}
diff --git a/core/java/com/android/internal/notification/DemoContactNotificationScorer.java b/core/java/com/android/internal/notification/DemoContactNotificationScorer.java
deleted file mode 100644
index f484724..0000000
--- a/core/java/com/android/internal/notification/DemoContactNotificationScorer.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
-* Copyright (C) 2013 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.internal.notification;
-
-import android.app.Notification;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.ContactsContract;
-import android.provider.Settings;
-import android.text.SpannableString;
-import android.util.Slog;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * This NotificationScorer bumps up the priority of notifications that contain references to the
- * display names of starred contacts. The references it picks up are spannable strings which, in
- * their entirety, match the display name of some starred contact. The magnitude of the bump ranges
- * from 0 to 15 (assuming NOTIFICATION_PRIORITY_MULTIPLIER = 10) depending on the initial score, and
- * the mapping is defined by priorityBumpMap. In a production version of this scorer, a notification
- * extra will be used to specify contact identifiers.
- */
-
-public class DemoContactNotificationScorer implements NotificationScorer {
-    private static final String TAG = "DemoContactNotificationScorer";
-    private static final boolean DBG = false;
-
-    protected static final boolean ENABLE_CONTACT_SCORER = true;
-    private static final String SETTING_ENABLE_SCORER = "contact_scorer_enabled";
-    protected boolean mEnabled;
-
-    // see NotificationManagerService
-    private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
-
-    private Context mContext;
-
-    private static final List<String> RELEVANT_KEYS_LIST = Arrays.asList(
-            Notification.EXTRA_INFO_TEXT, Notification.EXTRA_TEXT, Notification.EXTRA_TEXT_LINES,
-            Notification.EXTRA_SUB_TEXT, Notification.EXTRA_TITLE
-    );
-
-    private static final String[] PROJECTION = new String[] {
-            ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME
-    };
-
-    private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI;
-
-    private static List<String> extractSpannedStrings(CharSequence charSequence) {
-        if (charSequence == null) return Collections.emptyList();
-        if (!(charSequence instanceof SpannableString)) {
-            return Arrays.asList(charSequence.toString());
-        }
-        SpannableString spannableString = (SpannableString)charSequence;
-        // get all spans
-        Object[] ssArr = spannableString.getSpans(0, spannableString.length(), Object.class);
-        // spanned string sequences
-        ArrayList<String> sss = new ArrayList<String>();
-        for (Object spanObj : ssArr) {
-            try {
-                sss.add(spannableString.subSequence(spannableString.getSpanStart(spanObj),
-                        spannableString.getSpanEnd(spanObj)).toString());
-            } catch(StringIndexOutOfBoundsException e) {
-                Slog.e(TAG, "Bad indices when extracting spanned subsequence", e);
-            }
-        }
-        return sss;
-    };
-
-    private static String getQuestionMarksInParens(int n) {
-        StringBuilder sb = new StringBuilder("(");
-        for (int i = 0; i < n; i++) {
-            if (sb.length() > 1) sb.append(',');
-            sb.append('?');
-        }
-        sb.append(")");
-        return sb.toString();
-    }
-
-    private boolean hasStarredContact(Bundle extras) {
-        if (extras == null) return false;
-        ArrayList<String> qStrings = new ArrayList<String>();
-        // build list to query against the database for display names.
-        for (String rk: RELEVANT_KEYS_LIST) {
-            if (extras.get(rk) == null) {
-                continue;
-            } else if (extras.get(rk) instanceof CharSequence) {
-                qStrings.addAll(extractSpannedStrings((CharSequence) extras.get(rk)));
-            } else if (extras.get(rk) instanceof CharSequence[]) {
-                // this is intended for Notification.EXTRA_TEXT_LINES
-                for (CharSequence line: (CharSequence[]) extras.get(rk)){
-                    qStrings.addAll(extractSpannedStrings(line));
-                }
-            } else {
-                Slog.w(TAG, "Strange, the extra " + rk + " is of unexpected type.");
-            }
-        }
-        if (qStrings.isEmpty()) return false;
-        String[] qStringsArr = qStrings.toArray(new String[qStrings.size()]);
-
-        String selection = ContactsContract.Contacts.DISPLAY_NAME + " IN "
-                + getQuestionMarksInParens(qStringsArr.length) + " AND "
-                + ContactsContract.Contacts.STARRED+" ='1'";
-
-        Cursor c = null;
-        try {
-            c = mContext.getContentResolver().query(
-                    CONTACTS_URI, PROJECTION, selection, qStringsArr, null);
-            if (c != null) return c.getCount() > 0;
-        } catch(Throwable t) {
-            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return false;
-    }
-
-    private final static int clamp(int x, int low, int high) {
-        return (x < low) ? low : ((x > high) ? high : x);
-    }
-
-    private static int priorityBumpMap(int incomingScore) {
-        //assumption is that scale runs from [-2*pm, 2*pm]
-        int pm = NOTIFICATION_PRIORITY_MULTIPLIER;
-        int theScore = incomingScore;
-        // enforce input in range
-        theScore = clamp(theScore, -2 * pm, 2 * pm);
-        if (theScore != incomingScore) return incomingScore;
-        // map -20 -> -20 and -10 -> 5 (when pm = 10)
-        if (theScore <= -pm) {
-            theScore += 1.5 * (theScore + 2 * pm);
-        } else {
-            // map 0 -> 10, 10 -> 15, 20 -> 20;
-            theScore += 0.5 * (2 * pm - theScore);
-        }
-        if (DBG) Slog.v(TAG, "priorityBumpMap: score before: " + incomingScore
-                + ", score after " + theScore + ".");
-        return theScore;
-    }
-
-    @Override
-    public void initialize(Context context) {
-        if (DBG) Slog.v(TAG, "Initializing  " + getClass().getSimpleName() + ".");
-        mContext = context;
-        mEnabled = ENABLE_CONTACT_SCORER && 1 == Settings.Global.getInt(
-                mContext.getContentResolver(), SETTING_ENABLE_SCORER, 0);
-    }
-
-    @Override
-    public int getScore(Notification notification, int score) {
-        if (notification == null || !mEnabled) {
-            if (DBG) Slog.w(TAG, "empty notification? scorer disabled?");
-            return score;
-        }
-        boolean hasStarredPriority = hasStarredContact(notification.extras);
-
-        if (DBG) {
-            if (hasStarredPriority) {
-                Slog.v(TAG, "Notification references starred contact. Promoted!");
-            } else {
-                Slog.v(TAG, "Notification lacks any starred contact reference. Not promoted!");
-            }
-        }
-        if (hasStarredPriority) score = priorityBumpMap(score);
-        return score;
-    }
-}
-
diff --git a/core/java/com/android/internal/notification/NotificationScorer.java b/core/java/com/android/internal/notification/NotificationScorer.java
deleted file mode 100644
index 863c08c..0000000
--- a/core/java/com/android/internal/notification/NotificationScorer.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
-* Copyright (C) 2013 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.internal.notification;
-
-import android.app.Notification;
-import android.content.Context;
-
-public interface NotificationScorer {
-
-    public void initialize(Context context);
-    public int getScore(Notification notification, int score);
-
-}
diff --git a/core/java/com/android/internal/notification/PeopleNotificationScorer.java b/core/java/com/android/internal/notification/PeopleNotificationScorer.java
deleted file mode 100644
index efb5f63..0000000
--- a/core/java/com/android/internal/notification/PeopleNotificationScorer.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
-* 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.internal.notification;
-
-import android.app.Notification;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.Contacts;
-import android.provider.Settings;
-import android.text.TextUtils;
-import android.util.LruCache;
-import android.util.Slog;
-
-/**
- * This {@link NotificationScorer} attempts to validate people references.
- * Also elevates the priority of real people.
- */
-public class PeopleNotificationScorer implements NotificationScorer {
-    private static final String TAG = "PeopleNotificationScorer";
-    private static final boolean DBG = false;
-
-    private static final boolean ENABLE_PEOPLE_SCORER = true;
-    private static final String SETTING_ENABLE_PEOPLE_SCORER = "people_scorer_enabled";
-    private static final String[] LOOKUP_PROJECTION = { Contacts._ID };
-    private static final int MAX_PEOPLE = 10;
-    private static final int PEOPLE_CACHE_SIZE = 200;
-    // see NotificationManagerService
-    private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
-
-    protected boolean mEnabled;
-    private Context mContext;
-
-    // maps raw person handle to resolved person object
-    private LruCache<String, LookupResult> mPeopleCache;
-
-    private float findMaxContactScore(Bundle extras) {
-        if (extras == null) {
-            return 0f;
-        }
-
-        final String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
-        if (people == null || people.length == 0) {
-            return 0f;
-        }
-
-        float rank = 0f;
-        for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
-            final String handle = people[personIdx];
-            if (TextUtils.isEmpty(handle)) continue;
-
-            LookupResult lookupResult = mPeopleCache.get(handle);
-            if (lookupResult == null || lookupResult.isExpired()) {
-                final Uri uri = Uri.parse(handle);
-                if ("tel".equals(uri.getScheme())) {
-                    if (DBG) Slog.w(TAG, "checking telephone URI: " + handle);
-                    lookupResult = lookupPhoneContact(handle, uri.getSchemeSpecificPart());
-                } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
-                    if (DBG) Slog.w(TAG, "checking lookup URI: " + handle);
-                    lookupResult = resolveContactsUri(handle, uri);
-                } else {
-                    if (DBG) Slog.w(TAG, "unsupported URI " + handle);
-                }
-            } else {
-                if (DBG) Slog.w(TAG, "using cached lookupResult: " + lookupResult.mId);
-            }
-            if (lookupResult != null) {
-                rank = Math.max(rank, lookupResult.getRank());
-            }
-        }
-        return rank;
-    }
-
-    private LookupResult lookupPhoneContact(final String handle, final String number) {
-        LookupResult lookupResult = null;
-        Cursor c = null;
-        try {
-            Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
-                    Uri.encode(number));
-            c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null);
-            if (c != null && c.getCount() > 0) {
-                c.moveToFirst();
-                final int idIdx = c.getColumnIndex(Contacts._ID);
-                final int id = c.getInt(idIdx);
-                if (DBG) Slog.w(TAG, "is valid: " + id);
-                lookupResult = new LookupResult(id);
-            }
-        } catch(Throwable t) {
-            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        if (lookupResult == null) {
-            lookupResult = new LookupResult(LookupResult.INVALID_ID);
-        }
-        mPeopleCache.put(handle, lookupResult);
-        return lookupResult;
-    }
-
-    private LookupResult resolveContactsUri(String handle, final Uri personUri) {
-        LookupResult lookupResult = null;
-        Cursor c = null;
-        try {
-            c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null);
-            if (c != null && c.getCount() > 0) {
-                c.moveToFirst();
-                final int idIdx = c.getColumnIndex(Contacts._ID);
-                final int id = c.getInt(idIdx);
-                if (DBG) Slog.w(TAG, "is valid: " + id);
-                lookupResult = new LookupResult(id);
-            }
-        } catch(Throwable t) {
-            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        if (lookupResult == null) {
-            lookupResult = new LookupResult(LookupResult.INVALID_ID);
-        }
-        mPeopleCache.put(handle, lookupResult);
-        return lookupResult;
-    }
-
-    private final static int clamp(int x, int low, int high) {
-        return (x < low) ? low : ((x > high) ? high : x);
-    }
-
-    // TODO: rework this function before shipping
-    private static int priorityBumpMap(int incomingScore) {
-        //assumption is that scale runs from [-2*pm, 2*pm]
-        int pm = NOTIFICATION_PRIORITY_MULTIPLIER;
-        int theScore = incomingScore;
-        // enforce input in range
-        theScore = clamp(theScore, -2 * pm, 2 * pm);
-        if (theScore != incomingScore) return incomingScore;
-        // map -20 -> -20 and -10 -> 5 (when pm = 10)
-        if (theScore <= -pm) {
-            theScore += 1.5 * (theScore + 2 * pm);
-        } else {
-            // map 0 -> 10, 10 -> 15, 20 -> 20;
-            theScore += 0.5 * (2 * pm - theScore);
-        }
-        if (DBG) Slog.v(TAG, "priorityBumpMap: score before: " + incomingScore
-                + ", score after " + theScore + ".");
-        return theScore;
-    }
-
-    @Override
-    public void initialize(Context context) {
-        if (DBG) Slog.v(TAG, "Initializing  " + getClass().getSimpleName() + ".");
-        mContext = context;
-        mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
-        mEnabled = ENABLE_PEOPLE_SCORER && 1 == Settings.Global.getInt(
-                mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_SCORER, 0);
-    }
-
-    @Override
-    public int getScore(Notification notification, int score) {
-        if (notification == null || !mEnabled) {
-            if (DBG) Slog.w(TAG, "empty notification? scorer disabled?");
-            return score;
-        }
-        float contactScore = findMaxContactScore(notification.extras);
-        if (contactScore > 0f) {
-            if (DBG) Slog.v(TAG, "Notification references a real contact. Promoted!");
-            score = priorityBumpMap(score);
-        } else {
-            if (DBG) Slog.v(TAG, "Notification lacks any valid contact reference. Not promoted!");
-        }
-        return score;
-    }
-
-    private static class LookupResult {
-        private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000;  // 1hr
-        public static final int INVALID_ID = -1;
-
-        private final long mExpireMillis;
-        private int mId;
-
-        public LookupResult(int id) {
-            mId = id;
-            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
-        }
-
-        public boolean isExpired() {
-            return mExpireMillis < System.currentTimeMillis();
-        }
-
-        public boolean isInvalid() {
-            return mId == INVALID_ID || isExpired();
-        }
-
-        public float getRank() {
-            if (isInvalid()) {
-                return 0f;
-            } else {
-                return 1f;  // TODO: finer grained score
-            }
-        }
-
-        public LookupResult setId(int id) {
-            mId = id;
-            return this;
-        }
-    }
-}
-
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f39155b..83cbb74 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1393,8 +1393,10 @@
         <item>com.android.inputmethod.latin</item>
     </string-array>
 
-    <string-array name="config_notificationScorers">
-        <item>com.android.internal.notification.PeopleNotificationScorer</item>
+    <!-- The list of classes that should be added to the notification ranking pipline.
+     See {@link com.android.server.notification.NotificationSignalExtractortor} -->
+    <string-array name="config_notificationSignalExtractors">
+        <item>com.android.server.notification.ValidateNotificationPeople</item>
     </string-array>
 
     <!-- Flag indicating that this device does not rotate and will always remain in its default
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5a78bfe..1057cc2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1655,7 +1655,7 @@
   <java-symbol type="id" name="button_always" />
   <java-symbol type="integer" name="config_globalActionsKeyTimeout" />
   <java-symbol type="integer" name="config_maxResolverActivityColumns" />
-  <java-symbol type="array" name="config_notificationScorers" />
+  <java-symbol type="array" name="config_notificationSignalExtractors" />
 
   <java-symbol type="layout" name="notification_quantum_action" />
   <java-symbol type="layout" name="notification_quantum_action_list" />
diff --git a/services/core/java/com/android/server/notification/NotificationComparator.java b/services/core/java/com/android/server/notification/NotificationComparator.java
new file mode 100644
index 0000000..c8b1ba0
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationComparator.java
@@ -0,0 +1,44 @@
+/*
+ * 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 java.util.Comparator;
+
+/**
+ * Sorts notificaitons into attention-relelvant order.
+ */
+public class NotificationComparator
+        implements Comparator<NotificationManagerService.NotificationRecord> {
+
+    @Override
+    public int compare(NotificationManagerService.NotificationRecord lhs,
+            NotificationManagerService.NotificationRecord rhs) {
+        final int leftScore = lhs.sbn.getScore();
+        final int rightScore = rhs.sbn.getScore();
+        if (leftScore != rightScore) {
+            // by priority, high to low
+            return -1 * Integer.compare(leftScore, rightScore);
+        }
+        final float leftPeple = lhs.getContactAffinity();
+        final float rightPeople = rhs.getContactAffinity();
+        if (leftPeple != rightPeople) {
+            // by contact proximity, close to far
+            return -1 * Float.compare(leftPeple, rightPeople);
+        }
+        // then break ties by time, most recent first
+        return -1 * Long.compare(lhs.sbn.getPostTime(), rhs.sbn.getPostTime());
+    }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 4698587..7a4f951 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -49,8 +49,10 @@
 import android.os.Binder;
 import android.os.Environment;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.IInterface;
+import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
@@ -61,6 +63,7 @@
 import android.service.notification.IConditionListener;
 import android.service.notification.IConditionProvider;
 import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationOrderUpdate;
 import android.service.notification.StatusBarNotification;
 import android.service.notification.Condition;
 import android.service.notification.ZenModeConfig;
@@ -76,7 +79,6 @@
 import android.widget.Toast;
 
 import com.android.internal.R;
-import com.android.internal.notification.NotificationScorer;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.server.EventLogTags;
 import com.android.server.SystemService;
@@ -104,9 +106,12 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 
 /** {@hide} */
 public class NotificationManagerService extends SystemService {
@@ -118,6 +123,8 @@
     // message codes
     static final int MESSAGE_TIMEOUT = 2;
     static final int MESSAGE_SAVE_POLICY_FILE = 3;
+    static final int MESSAGE_RECONSIDER_RANKING = 4;
+    static final int MESSAGE_SEND_RANKING_UPDATE = 5;
 
     static final int LONG_DELAY = 3500; // 3.5 seconds
     static final int SHORT_DELAY = 2000; // 2 seconds
@@ -147,6 +154,9 @@
 
     final IBinder mForegroundToken = new Binder();
     private WorkerHandler mHandler;
+    private final HandlerThread mRankingThread = new HandlerThread("ranker",
+            Process.THREAD_PRIORITY_BACKGROUND);
+    private Handler mRankingHandler = null;
 
     private Light mNotificationLight;
     Light mAttentionLight;
@@ -171,6 +181,7 @@
     // used as a mutex for access to all active notifications & listeners
     final ArrayList<NotificationRecord> mNotificationList =
             new ArrayList<NotificationRecord>();
+    final NotificationComparator mRankingComparator = new NotificationComparator();
     final ArrayMap<String, NotificationRecord> mNotificationsByKey =
             new ArrayMap<String, NotificationRecord>();
     final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
@@ -193,7 +204,7 @@
     private static final String TAG_PACKAGE = "package";
     private static final String ATTR_NAME = "name";
 
-    final ArrayList<NotificationScorer> mScorers = new ArrayList<NotificationScorer>();
+    final ArrayList<NotificationSignalExtractor> mSignalExtractors = new ArrayList<NotificationSignalExtractor>();
 
     private final UserProfiles mUserProfiles = new UserProfiles();
     private NotificationListeners mListeners;
@@ -446,6 +457,7 @@
         final StatusBarNotification sbn;
         SingleNotificationStats stats;
         IBinder statusBarKey;
+        private float mContactAffinity;
 
         NotificationRecord(StatusBarNotification sbn)
         {
@@ -528,6 +540,14 @@
                     this.sbn.getTag(), this.sbn.getScore(), this.sbn.getKey(),
                     this.sbn.getNotification());
         }
+
+        public void setContactAffinity(float contactAffinity) {
+            mContactAffinity = contactAffinity;
+        }
+
+        public float getContactAffinity() {
+            return mContactAffinity;
+        }
     }
 
     private static final class ToastRecord
@@ -707,7 +727,7 @@
             boolean queryRemove = false;
             boolean packageChanged = false;
             boolean cancelNotifications = true;
-            
+
             if (action.equals(Intent.ACTION_PACKAGE_ADDED)
                     || (queryRemove=action.equals(Intent.ACTION_PACKAGE_REMOVED))
                     || action.equals(Intent.ACTION_PACKAGE_RESTARTED)
@@ -849,6 +869,8 @@
         mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
 
         mHandler = new WorkerHandler();
+        mRankingThread.start();
+        mRankingHandler = new RankingWorkerHandler(mRankingThread.getLooper());
         mZenModeHelper = new ZenModeHelper(getContext(), mHandler);
         mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
             @Override
@@ -925,21 +947,22 @@
 
         mSettingsObserver = new SettingsObserver(mHandler);
 
-        // spin up NotificationScorers
-        String[] notificationScorerNames = resources.getStringArray(
-                R.array.config_notificationScorers);
-        for (String scorerName : notificationScorerNames) {
+        // spin up NotificationSignalExtractors
+        String[] extractorNames = resources.getStringArray(
+                R.array.config_notificationSignalExtractors);
+        for (String extractorName : extractorNames) {
             try {
-                Class<?> scorerClass = getContext().getClassLoader().loadClass(scorerName);
-                NotificationScorer scorer = (NotificationScorer) scorerClass.newInstance();
-                scorer.initialize(getContext());
-                mScorers.add(scorer);
+                Class<?> extractorClass = getContext().getClassLoader().loadClass(extractorName);
+                NotificationSignalExtractor extractor =
+                        (NotificationSignalExtractor) extractorClass.newInstance();
+                extractor.initialize(getContext());
+                mSignalExtractors.add(extractor);
             } catch (ClassNotFoundException e) {
-                Slog.w(TAG, "Couldn't find scorer " + scorerName + ".", e);
+                Slog.w(TAG, "Couldn't find extractor " + extractorName + ".", e);
             } catch (InstantiationException e) {
-                Slog.w(TAG, "Couldn't instantiate scorer " + scorerName + ".", e);
+                Slog.w(TAG, "Couldn't instantiate extractor " + extractorName + ".", e);
             } catch (IllegalAccessException e) {
-                Slog.w(TAG, "Problem accessing scorer " + scorerName + ".", e);
+                Slog.w(TAG, "Problem accessing extractor " + extractorName + ".", e);
             }
         }
 
@@ -1150,6 +1173,7 @@
          * System-only API for getting a list of current (i.e. not cleared) notifications.
          *
          * Requires ACCESS_NOTIFICATIONS which is signature|system.
+         * @returns A list of all the notifications, in natural order.
          */
         @Override
         public StatusBarNotification[] getActiveNotifications(String callingPkg) {
@@ -1306,6 +1330,9 @@
          * should be used.
          *
          * @param token The binder for the listener, to check that the caller is allowed
+         * @param keys the notification keys to fetch, or null for all active notifications.
+         * @returns The return value will contain the notifications specified in keys, in that
+         *      order, or if keys is null, all the notifications, in natural order.
          */
         @Override
         public StatusBarNotification[] getActiveNotificationsFromListener(
@@ -1337,7 +1364,7 @@
 
         @Override
         public String[] getActiveNotificationKeysFromListener(INotificationListener token) {
-            return NotificationManagerService.this.getActiveNotificationKeysFromListener(token);
+            return NotificationManagerService.this.getActiveNotificationKeys(token);
         }
 
         @Override
@@ -1409,19 +1436,21 @@
         }
     };
 
-    private String[] getActiveNotificationKeysFromListener(INotificationListener token) {
-        synchronized (mNotificationList) {
-            final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
-            final ArrayList<String> keys = new ArrayList<String>();
-            final int N = mNotificationList.size();
-            for (int i=0; i<N; i++) {
-                final StatusBarNotification sbn = mNotificationList.get(i).sbn;
-                if (info.enabledAndUserMatches(sbn.getUserId())) {
-                    keys.add(sbn.getKey());
+    private String[] getActiveNotificationKeys(INotificationListener token) {
+        final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+        final ArrayList<String> keys = new ArrayList<String>();
+        if (info.isEnabledForCurrentProfiles()) {
+            synchronized (mNotificationList) {
+                final int N = mNotificationList.size();
+                for (int i = 0; i < N; i++) {
+                    final StatusBarNotification sbn = mNotificationList.get(i).sbn;
+                    if (info.enabledAndUserMatches(sbn.getUserId())) {
+                        keys.add(sbn.getKey());
+                    }
                 }
             }
-            return keys.toArray(new String[keys.size()]);
         }
+        return keys.toArray(new String[keys.size()]);
     }
 
     void dumpImpl(PrintWriter pw) {
@@ -1578,26 +1607,23 @@
                 // 1. initial score: buckets of 10, around the app
                 int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20]
 
-                // 2. Consult external heuristics (TBD)
-
-                // 3. Apply local rules
-
-                int initialScore = score;
-                if (!mScorers.isEmpty()) {
-                    if (DBG) Slog.v(TAG, "Initial score is " + score + ".");
-                    for (NotificationScorer scorer : mScorers) {
+                // 2. extract ranking signals from the notification data
+                final StatusBarNotification n = new StatusBarNotification(
+                        pkg, opPkg, id, tag, callingUid, callingPid, score, notification,
+                        user);
+                NotificationRecord r = new NotificationRecord(n);
+                if (!mSignalExtractors.isEmpty()) {
+                    for (NotificationSignalExtractor extractor : mSignalExtractors) {
                         try {
-                            score = scorer.getScore(notification, score);
+                            RankingFuture future = extractor.process(r);
+                            scheduleRankingReconsideration(future);
                         } catch (Throwable t) {
-                            Slog.w(TAG, "Scorer threw on .getScore.", t);
+                            Slog.w(TAG, "NotificationSignalExtractor failed.", t);
                         }
                     }
-                    if (DBG) Slog.v(TAG, "Final score is " + score + ".");
                 }
 
-                // add extra to indicate score modified by NotificationScorer
-                notification.extras.putBoolean(Notification.EXTRA_SCORE_MODIFIED,
-                        score != initialScore);
+                // 3. Apply local rules
 
                 // blocked apps
                 if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) {
@@ -1608,10 +1634,6 @@
                     }
                 }
 
-                if (DBG) {
-                    Slog.v(TAG, "Assigned score=" + score + " to " + notification);
-                }
-
                 if (score < SCORE_DISPLAY_THRESHOLD) {
                     // Notification will be blocked because the score is too low.
                     return;
@@ -1626,12 +1648,7 @@
                 if (DBG || intercept) Slog.v(TAG,
                         "pkg=" + pkg + " canInterrupt=" + canInterrupt + " intercept=" + intercept);
                 synchronized (mNotificationList) {
-                    final StatusBarNotification n = new StatusBarNotification(
-                            pkg, opPkg, id, tag, callingUid, callingPid, score, notification,
-                            user);
-                    NotificationRecord r = new NotificationRecord(n);
                     NotificationRecord old = null;
-
                     int index = indexOfNotificationLocked(pkg, tag, id, userId);
                     if (index < 0) {
                         mNotificationList.add(r);
@@ -1651,6 +1668,8 @@
                     }
                     mNotificationsByKey.put(n.getKey(), r);
 
+                    Collections.sort(mNotificationList, mRankingComparator);
+
                     // Ensure if this is a foreground service that the proper additional
                     // flags are set.
                     if ((notification.flags&Notification.FLAG_FOREGROUND_SERVICE) != 0) {
@@ -1948,6 +1967,57 @@
         }
     }
 
+    private void scheduleRankingReconsideration(RankingFuture future) {
+        if (future != null) {
+            Message m = Message.obtain(mRankingHandler, MESSAGE_RECONSIDER_RANKING, future);
+            long delay = future.getDelay(TimeUnit.MILLISECONDS);
+            mRankingHandler.sendMessageDelayed(m, delay);
+        }
+    }
+
+    private void handleRankingReconsideration(Message message) {
+        if (!(message.obj instanceof RankingFuture)) return;
+
+        RankingFuture future = (RankingFuture) message.obj;
+        future.run();
+        try {
+            NotificationRecord record = future.get();
+            synchronized (mNotificationList) {
+                int before = mNotificationList.indexOf(record);
+                if (before != -1) {
+                    Collections.sort(mNotificationList, mRankingComparator);
+                    int after = mNotificationList.indexOf(record);
+
+                    if (before != after) {
+                        scheduleSendRankingUpdate();
+                    }
+                }
+            }
+        } catch (InterruptedException e) {
+            // we're running the future explicitly, so this should never happen
+        } catch (ExecutionException e) {
+            // we're running the future explicitly, so this should never happen
+        }
+    }
+
+    private void scheduleSendRankingUpdate() {
+        mHandler.removeMessages(MESSAGE_SEND_RANKING_UPDATE);
+        Message m = Message.obtain(mHandler, MESSAGE_SEND_RANKING_UPDATE);
+        mHandler.sendMessage(m);
+    }
+
+    private void handleSendRankingUpdate() {
+        synchronized (mNotificationList) {
+            final int N = mNotificationList.size();
+            ArrayList<StatusBarNotification> sbns =
+                    new ArrayList<StatusBarNotification>(N);
+            for (int i = 0; i < N; i++ ) {
+                sbns.add(mNotificationList.get(i).sbn);
+            }
+            mListeners.notifyOrderUpdateLocked(sbns);
+        }
+    }
+
     private final class WorkerHandler extends Handler
     {
         @Override
@@ -1961,11 +2031,30 @@
                 case MESSAGE_SAVE_POLICY_FILE:
                     handleSavePolicyFile();
                     break;
+                case MESSAGE_SEND_RANKING_UPDATE:
+                    handleSendRankingUpdate();
+                    break;
+            }
+        }
+
+    }
+
+    private final class RankingWorkerHandler extends Handler
+    {
+        public RankingWorkerHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MESSAGE_RECONSIDER_RANKING:
+                    handleRankingReconsideration(msg);
+                    break;
             }
         }
     }
 
-
     // Notifications
     // ============================================================================
     static int clamp(int x, int low, int high) {
@@ -2346,9 +2435,9 @@
         @Override
         public void onServiceAdded(ManagedServiceInfo info) {
             final INotificationListener listener = (INotificationListener) info.service;
-            final String[] keys = getActiveNotificationKeysFromListener(listener);
+            final String[] keys = getActiveNotificationKeys(listener);
             try {
-                listener.onListenerConnected(keys);
+                listener.onListenerConnected(new NotificationOrderUpdate(keys));
             } catch (RemoteException e) {
                 // we tried
             }
@@ -2361,12 +2450,18 @@
             // make a copy in case changes are made to the underlying Notification object
             final StatusBarNotification sbnClone = sbn.clone();
             for (final ManagedServiceInfo info : mServices) {
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        notifyPostedIfUserMatch(info, sbnClone);
+                if (info.isEnabledForCurrentProfiles()) {
+                    final INotificationListener listener = (INotificationListener) info.service;
+                    final String[] keys = getActiveNotificationKeys(listener);
+                    if (keys.length > 0) {
+                        mHandler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                notifyPostedIfUserMatch(info, sbnClone, keys);
+                            }
+                        });
                     }
-                });
+                }
             }
         }
 
@@ -2378,39 +2473,83 @@
             // NOTE: this copy is lightweight: it doesn't include heavyweight parts of the
             // notification
             final StatusBarNotification sbnLight = sbn.cloneLight();
-            for (ManagedServiceInfo serviceInfo : mServices) {
-                final ManagedServiceInfo info = (ManagedServiceInfo) serviceInfo;
+            for (final ManagedServiceInfo info : mServices) {
+                if (info.isEnabledForCurrentProfiles()) {
+                    final INotificationListener listener = (INotificationListener) info.service;
+                    final String[] keys = getActiveNotificationKeys(listener);
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            notifyRemovedIfUserMatch(info, sbnLight, keys);
+                        }
+                    });
+                }
+            }
+        }
+
+        /**
+         * asynchronously notify all listeners about a reordering of notifications
+         * @param sbns an array of {@link StatusBarNotification}s to consider.  This code
+         *             must not rely on mutable members of these objects, such as the
+         *             {@link Notification}.
+         */
+        public void notifyOrderUpdateLocked(final ArrayList<StatusBarNotification> sbns) {
+            for (final ManagedServiceInfo serviceInfo : mServices) {
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        notifyRemovedIfUserMatch(info, sbnLight);
+                        notifyOrderUpdateIfUserMatch(serviceInfo, sbns);
                     }
                 });
             }
         }
 
-        private void notifyPostedIfUserMatch(ManagedServiceInfo info, StatusBarNotification sbn) {
+        private void notifyPostedIfUserMatch(final ManagedServiceInfo info,
+                final StatusBarNotification sbn, String[] keys) {
             if (!info.enabledAndUserMatches(sbn.getUserId())) {
                 return;
             }
             final INotificationListener listener = (INotificationListener)info.service;
             try {
-                listener.onNotificationPosted(sbn);
+                listener.onNotificationPosted(sbn, new NotificationOrderUpdate(keys));
             } catch (RemoteException ex) {
                 Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
             }
         }
 
-        private void notifyRemovedIfUserMatch(ManagedServiceInfo info, StatusBarNotification sbn) {
+        private void notifyRemovedIfUserMatch(ManagedServiceInfo info, StatusBarNotification sbn,
+                String[] keys) {
             if (!info.enabledAndUserMatches(sbn.getUserId())) {
                 return;
             }
             final INotificationListener listener = (INotificationListener)info.service;
             try {
-                listener.onNotificationRemoved(sbn);
+                listener.onNotificationRemoved(sbn, new NotificationOrderUpdate(keys));
             } catch (RemoteException ex) {
                 Log.e(TAG, "unable to notify listener (removed): " + listener, ex);
             }
         }
+
+        /**
+         * @param sbns an array of {@link StatusBarNotification}s to consider.  This code
+         *             must not rely on mutable members of these objects, such as the
+         *             {@link Notification}.
+         */
+        public void notifyOrderUpdateIfUserMatch(ManagedServiceInfo info,
+                ArrayList<StatusBarNotification> sbns) {
+            ArrayList<String> keys = new ArrayList<String>(sbns.size());
+            for (StatusBarNotification sbn: sbns) {
+                if (info.enabledAndUserMatches(sbn.getUserId())) {
+                    keys.add(sbn.getKey());
+                }
+            }
+            final INotificationListener listener = (INotificationListener)info.service;
+            try {
+                listener.onNotificationOrderUpdate(
+                        new NotificationOrderUpdate(keys.toArray(new String[keys.size()])));
+            } catch (RemoteException ex) {
+                Log.e(TAG, "unable to notify listener (ranking update): " + listener, ex);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
new file mode 100644
index 0000000..a41fdfe
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
@@ -0,0 +1,41 @@
+/*
+* 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 android.content.Context;
+
+/**
+ * Extracts signals that will be useful to the {@link NotificationComparator} and caches them
+ *  on the {@link NotificationManagerService.NotificationRecord} object. These annotations will
+ *  not be passed on to {@link android.service.notification.NotificationListenerService}s.
+ */
+public interface NotificationSignalExtractor {
+
+    /** One-time initialization. */
+    public void initialize(Context context);
+
+    /**
+     * Called once per notification that is posted or updated.
+     *
+     * @return null if the work is done, or a future if there is more to do. The
+     * {@link RankingFuture} will be run on a worker thread, and if notifications are re-ordered
+     * by that execution, the {@link NotificationManagerService} may send order update
+     * events to the {@link android.service.notification.NotificationListenerService}s.
+     */
+    public RankingFuture process(NotificationManagerService.NotificationRecord notification);
+
+}
diff --git a/services/core/java/com/android/server/notification/RankingFuture.java b/services/core/java/com/android/server/notification/RankingFuture.java
new file mode 100644
index 0000000..33aad8d
--- /dev/null
+++ b/services/core/java/com/android/server/notification/RankingFuture.java
@@ -0,0 +1,118 @@
+/*
+ * 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 java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public abstract class RankingFuture
+        implements ScheduledFuture<NotificationManagerService.NotificationRecord> {
+    private static final long IMMEDIATE = 0l;
+
+    private static final int START = 0;
+    private static final int RUNNING = 1;
+    private static final int DONE = 2;
+    private static final int CANCELLED = 3;
+
+    private int mState;
+    private long mDelay;
+    protected NotificationManagerService.NotificationRecord mRecord;
+
+    public RankingFuture(NotificationManagerService.NotificationRecord record) {
+        this(record, IMMEDIATE);
+    }
+
+    public RankingFuture(NotificationManagerService.NotificationRecord record, long delay) {
+        mDelay = delay;
+        mRecord = record;
+        mState = START;
+    }
+
+    public void run() {
+        if (mState == START) {
+            mState = RUNNING;
+
+            work();
+
+            mState = DONE;
+            synchronized (this) {
+                notifyAll();
+            }
+        }
+    }
+
+    @Override
+    public long getDelay(TimeUnit unit) {
+        return unit.convert(mDelay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public int compareTo(Delayed another) {
+        return Long.compare(getDelay(TimeUnit.MICROSECONDS),
+                another.getDelay(TimeUnit.MICROSECONDS));
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        if (mState == START) {  // can't cancel if running or done
+            mState = CANCELLED;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return mState == CANCELLED;
+    }
+
+    @Override
+    public boolean isDone() {
+        return mState == DONE;
+    }
+
+    @Override
+    public NotificationManagerService.NotificationRecord get()
+            throws InterruptedException, ExecutionException {
+        while (!isDone()) {
+            synchronized (this) {
+                this.wait();
+            }
+        }
+        return mRecord;
+    }
+
+    @Override
+    public NotificationManagerService.NotificationRecord get(long timeout, TimeUnit unit)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        long timeoutMillis = unit.convert(timeout, TimeUnit.MILLISECONDS);
+        long start = System.currentTimeMillis();
+        long now = System.currentTimeMillis();
+        while (!isDone() && (now - start) < timeoutMillis) {
+            try {
+                wait(timeoutMillis - (now - start));
+            } catch (InterruptedException e) {
+                now = System.currentTimeMillis();
+            }
+        }
+        return mRecord;
+    }
+
+    public abstract void work();
+}
diff --git a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
new file mode 100644
index 0000000..8cd2f9b2
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
@@ -0,0 +1,298 @@
+/*
+* 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 android.app.Notification;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.LruCache;
+import android.util.Slog;
+
+import com.android.server.notification.NotificationManagerService.NotificationRecord;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+/**
+ * This {@link NotificationSignalExtractor} attempts to validate
+ * people references. Also elevates the priority of real people.
+ */
+public class ValidateNotificationPeople implements NotificationSignalExtractor {
+    private static final String TAG = "ValidateNotificationPeople";
+    private static final boolean INFO = true;
+    private static final boolean DEBUG = false;
+
+    private static final boolean ENABLE_PEOPLE_VALIDATOR = true;
+    private static final String SETTING_ENABLE_PEOPLE_VALIDATOR =
+            "validate_notification_people_enabled";
+    private static final String[] LOOKUP_PROJECTION = { Contacts._ID };
+    private static final int MAX_PEOPLE = 10;
+    private static final int PEOPLE_CACHE_SIZE = 200;
+
+    private static final float NONE = 0f;
+    private static final float VALID_CONTACT = 0.5f;
+    // TODO private static final float STARRED_CONTACT = 1f;
+
+    protected boolean mEnabled;
+    private Context mContext;
+
+    // maps raw person handle to resolved person object
+    private LruCache<String, LookupResult> mPeopleCache;
+
+    private RankingFuture validatePeople(NotificationRecord record) {
+        float affinity = NONE;
+        Bundle extras = record.getNotification().extras;
+        if (extras == null) {
+            return null;
+        }
+
+        final String[] people = getExtraPeople(extras);
+        if (people == null || people.length == 0) {
+            return null;
+        }
+
+        if (INFO) Slog.i(TAG, "Validating: " + record.sbn.getKey());
+        final LinkedList<String> pendingLookups = new LinkedList<String>();
+        for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
+            final String handle = people[personIdx];
+            if (TextUtils.isEmpty(handle)) continue;
+
+            synchronized (mPeopleCache) {
+                LookupResult lookupResult = mPeopleCache.get(handle);
+                if (lookupResult == null || lookupResult.isExpired()) {
+                    pendingLookups.add(handle);
+                } else {
+                    if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId);
+                }
+                if (lookupResult != null) {
+                    affinity = Math.max(affinity, lookupResult.getAffinity());
+                }
+            }
+        }
+
+        // record the best available data, so far:
+        record.setContactAffinity(affinity);
+
+        if (pendingLookups.isEmpty()) {
+            if (INFO) Slog.i(TAG, "final affinity: " + affinity);
+            return null;
+        }
+
+        if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey());
+        return new RankingFuture(record) {
+            @Override
+            public void work() {
+                if (INFO) Slog.i(TAG, "Executing: validation for: " + mRecord.sbn.getKey());
+                float affinity = NONE;
+                LookupResult lookupResult = null;
+                for (final String handle: pendingLookups) {
+                    final Uri uri = Uri.parse(handle);
+                    if ("tel".equals(uri.getScheme())) {
+                        if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
+                        lookupResult = resolvePhoneContact(handle, uri.getSchemeSpecificPart());
+                    } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
+                        if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
+                        lookupResult = resolveContactsUri(handle, uri);
+                    } else {
+                        Slog.w(TAG, "unsupported URI " + handle);
+                    }
+                }
+                if (lookupResult != null) {
+                    affinity = Math.max(affinity, lookupResult.getAffinity());
+                }
+
+                float affinityBound = mRecord.getContactAffinity();
+                affinity = Math.max(affinity, affinityBound);
+                mRecord.setContactAffinity(affinity);
+                if (INFO) Slog.i(TAG, "final affinity: " + affinity);
+            }
+        };
+    }
+
+    private String[] getExtraPeople(Bundle extras) {
+        String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
+        if (people != null) {
+            return people;
+        }
+
+        ArrayList<String> stringArray = extras.getStringArrayList(Notification.EXTRA_PEOPLE);
+        if (stringArray != null) {
+            return (String[]) stringArray.toArray();
+        }
+
+        String string = extras.getString(Notification.EXTRA_PEOPLE);
+        if (string != null) {
+            people = new String[1];
+            people[0] = string;
+            return people;
+        }
+        char[] charArray = extras.getCharArray(Notification.EXTRA_PEOPLE);
+        if (charArray != null) {
+            people = new String[1];
+            people[0] = new String(charArray);
+            return people;
+        }
+
+        CharSequence charSeq = extras.getCharSequence(Notification.EXTRA_PEOPLE);
+        if (charSeq != null) {
+            people = new String[1];
+            people[0] = charSeq.toString();
+            return people;
+        }
+
+        CharSequence[] charSeqArray = extras.getCharSequenceArray(Notification.EXTRA_PEOPLE);
+        if (charSeqArray != null) {
+            final int N = charSeqArray.length;
+            people = new String[N];
+            for (int i = 0; i < N; i++) {
+                people[i] = charSeqArray[i].toString();
+            }
+            return people;
+        }
+
+        ArrayList<CharSequence> charSeqList =
+                extras.getCharSequenceArrayList(Notification.EXTRA_PEOPLE);
+        if (charSeqList != null) {
+            final int N = charSeqList.size();
+            people = new String[N];
+            for (int i = 0; i < N; i++) {
+                people[i] = charSeqList.get(i).toString();
+            }
+            return people;
+        }
+        return null;
+    }
+
+    private LookupResult resolvePhoneContact(final String handle, final String number) {
+        LookupResult lookupResult = null;
+        Cursor c = null;
+        try {
+            Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
+                    Uri.encode(number));
+            c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null);
+            if (c != null && c.getCount() > 0) {
+                c.moveToFirst();
+                final int idIdx = c.getColumnIndex(Contacts._ID);
+                final int id = c.getInt(idIdx);
+                if (DEBUG) Slog.d(TAG, "is valid: " + id);
+                lookupResult = new LookupResult(id);
+            }
+        } catch(Throwable t) {
+            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        if (lookupResult == null) {
+            lookupResult = new LookupResult(LookupResult.INVALID_ID);
+        }
+        synchronized (mPeopleCache) {
+            mPeopleCache.put(handle, lookupResult);
+        }
+        return lookupResult;
+    }
+
+    private LookupResult resolveContactsUri(String handle, final Uri personUri) {
+        LookupResult lookupResult = null;
+        Cursor c = null;
+        try {
+            c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null);
+            if (c != null && c.getCount() > 0) {
+                c.moveToFirst();
+                final int idIdx = c.getColumnIndex(Contacts._ID);
+                final int id = c.getInt(idIdx);
+                if (DEBUG) Slog.d(TAG, "is valid: " + id);
+                lookupResult = new LookupResult(id);
+            }
+        } catch(Throwable t) {
+            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        if (lookupResult == null) {
+            lookupResult = new LookupResult(LookupResult.INVALID_ID);
+        }
+        synchronized (mPeopleCache) {
+            mPeopleCache.put(handle, lookupResult);
+        }
+        return lookupResult;
+    }
+
+    public void initialize(Context context) {
+        if (DEBUG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
+        mContext = context;
+        mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
+        mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
+                mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
+    }
+
+    public RankingFuture process(NotificationManagerService.NotificationRecord record) {
+        if (!mEnabled) {
+            if (INFO) Slog.i(TAG, "disabled");
+            return null;
+        }
+        if (record == null || record.getNotification() == null) {
+            if (INFO) Slog.i(TAG, "skipping empty notification");
+            return null;
+        }
+        return validatePeople(record);
+    }
+
+    private static class LookupResult {
+        private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000;  // 1hr
+        public static final int INVALID_ID = -1;
+
+        private final long mExpireMillis;
+        private int mId;
+
+        public LookupResult(int id) {
+            mId = id;
+            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
+        }
+
+        public boolean isExpired() {
+            return mExpireMillis < System.currentTimeMillis();
+        }
+
+        public boolean isInvalid() {
+            return mId == INVALID_ID || isExpired();
+        }
+
+        public float getAffinity() {
+            if (isInvalid()) {
+                return NONE;
+            } else {
+                return VALID_CONTACT;  // TODO: finer grained result: stars
+            }
+        }
+
+        public LookupResult setId(int id) {
+            mId = id;
+            return this;
+        }
+    }
+}
+