diff --git a/api/current.txt b/api/current.txt index 15351dee1464d..47e7c484d81d0 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4453,6 +4453,7 @@ package android.app { 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 36d26359675af..fe629f6742859 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1311,6 +1311,7 @@ public class Notification implements Parcelable private Notification mPublicVersion = null; private boolean mQuantumTheme; private final LegacyNotificationUtil mLegacyNotificationUtil; + private ArrayList mPeople; /** * Constructs a new Builder with the defaults: @@ -1338,6 +1339,7 @@ public class Notification implements Parcelable mWhen = System.currentTimeMillis(); mAudioStreamType = STREAM_DEFAULT; mPriority = PRIORITY_DEFAULT; + mPeople = new ArrayList(); // TODO: Decide on targetSdk from calling app whether to use quantum theme. mQuantumTheme = true; @@ -1722,6 +1724,16 @@ public class Notification implements Parcelable return this; } + /** + * 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. * @@ -2149,6 +2161,9 @@ public class Notification implements Parcelable 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 0000000000000..efb5f6368a3fe --- /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 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(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 fbe066a42213e..d5e78a564e7c0 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1370,7 +1370,7 @@ - com.android.internal.notification.DemoContactNotificationScorer + com.android.internal.notification.PeopleNotificationScorer