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

Commit 9a05b31a authored by Scott Greenwald's avatar Scott Greenwald
Browse files

Add support for notification scorers.

This CL adds an interface and classes for scoring notifications.
The NotificationManagerService initializes an array of scorers
specified as a resource. When a Notification is enqueued, the
getScore() method is called successively on the scorers, each
getting the Notification to be scored, and the score outputted
by the previous scorer. At present there is a single scorer
which prioritizes Notifications that mention the display name of
a starred contact.

To turn off the StarredContactNotificationScorer:
  adb shell settings put global contact_scorer_enabled 0

Change-Id: Ic16c80952e7c85bdde292ebb3f7900efb01f2e29
parent b1f5585f
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -456,6 +456,13 @@ public class Notification implements Parcelable
    // extras keys for other interesting pieces of information
    public static final String EXTRA_PEOPLE = "android.people";

    /**
     * @hide
     * Extra added by NotificationManagerService to indicate whether a NotificationScorer
     * modified the Notifications's score.
     */
    public static final String EXTRA_SCORE_MODIFIED = "android.scoreModified";

    /**
     * Notification extra to specify heads up display preference.
     * @hide
+188 −0
Original line number Diff line number Diff line
/*
* 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 = "StarredContactScoring";
    private static final boolean DBG = true;

    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, 1);
    }

    @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;
    }
}
+27 −0
Original line number Diff line number Diff line
/*
* 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);

}
+4 −0
Original line number Diff line number Diff line
@@ -1153,4 +1153,8 @@
        <item>com.android.inputmethod.latin</item>
    </string-array>

    <string-array name="config_notificationScorers">
        <item>com.android.internal.notification.DemoContactNotificationScorer</item>
    </string-array>

</resources>
+1 −0
Original line number Diff line number Diff line
@@ -1570,6 +1570,7 @@
  <java-symbol type="id" name="button_once" />
  <java-symbol type="id" name="button_always" />
  <java-symbol type="integer" name="config_maxResolverActivityColumns" />
  <java-symbol type="array" name="config_notificationScorers" />

  <!-- From SystemUI -->
  <java-symbol type="anim" name="push_down_in" />
Loading