am 2bb8dde4: Merge "support the people extra on the notification object"

* commit '2bb8dde44be10212ec453d435aaad27177714f5e':
  support the people extra on the notification object
diff --git a/api/current.txt b/api/current.txt
index b24cd55..7f9602e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -4453,6 +4453,7 @@
     ctor public Notification.Builder(android.content.Context);
     method public android.app.Notification.Builder addAction(int, java.lang.CharSequence, android.app.PendingIntent);
     method public android.app.Notification.Builder addExtras(android.os.Bundle);
+    method public android.app.Notification.Builder addPerson(java.lang.String);
     method public android.app.Notification build();
     method public android.os.Bundle getExtras();
     method public deprecated android.app.Notification getNotification();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 36d2635..fe629f6 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1311,6 +1311,7 @@
         private Notification mPublicVersion = null;
         private boolean mQuantumTheme;
         private final LegacyNotificationUtil mLegacyNotificationUtil;
+        private ArrayList<String> mPeople;
 
         /**
          * Constructs a new Builder with the defaults:
@@ -1338,6 +1339,7 @@
             mWhen = System.currentTimeMillis();
             mAudioStreamType = STREAM_DEFAULT;
             mPriority = PRIORITY_DEFAULT;
+            mPeople = new ArrayList<String>();
 
             // TODO: Decide on targetSdk from calling app whether to use quantum theme.
             mQuantumTheme = true;
@@ -1723,6 +1725,16 @@
         }
 
         /**
+         * Add a person that is relevant to this notification.
+         *
+         * @see Notification#EXTRA_PEOPLE
+         */
+        public Builder addPerson(String handle) {
+            mPeople.add(handle);
+            return this;
+        }
+
+        /**
          * Merge additional metadata into this notification.
          *
          * <p>Values within the Bundle will replace existing extras values in this Builder.
@@ -2149,6 +2161,9 @@
             if (mLargeIcon != null) {
                 extras.putParcelable(EXTRA_LARGE_ICON, mLargeIcon);
             }
+            if (!mPeople.isEmpty()) {
+                extras.putStringArray(EXTRA_PEOPLE, mPeople.toArray(new String[mPeople.size()]));
+            }
         }
 
         /**
diff --git a/core/java/com/android/internal/notification/PeopleNotificationScorer.java b/core/java/com/android/internal/notification/PeopleNotificationScorer.java
new file mode 100644
index 0000000..efb5f63
--- /dev/null
+++ b/core/java/com/android/internal/notification/PeopleNotificationScorer.java
@@ -0,0 +1,227 @@
+/*
+* 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 fbe066a..d5e78a5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1370,7 +1370,7 @@
     </string-array>
 
     <string-array name="config_notificationScorers">
-        <item>com.android.internal.notification.DemoContactNotificationScorer</item>
+        <item>com.android.internal.notification.PeopleNotificationScorer</item>
     </string-array>
 
     <!-- Flag indicating that this device does not rotate and will always remain in its default