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

Commit dde75307 authored by Chris Wren's avatar Chris Wren
Browse files

support the people extra on the notification object

currently supported URIs:
  content://com.android.contacts/contacts/lookup/*
  tel:*

Change-Id: I833eee3f2ee40aa8cb2cfad135bdd6cb2c0eb792
parent edd6d9e3
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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();
+15 −0
Original line number Diff line number Diff line
@@ -1311,6 +1311,7 @@ public class Notification implements Parcelable
        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 @@ public class Notification implements Parcelable
            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;
@@ -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()]));
            }
        }

        /**
+227 −0
Original line number Diff line number Diff line
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.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;
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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