1/*
2* Copyright (C) 2013 The Android Open Source Project
3*
4* Licensed under the Apache License, Version 2.0 (the "License");
5* you may not use this file except in compliance with the License.
6* You may obtain a copy of the License at
7*
8*      http://www.apache.org/licenses/LICENSE-2.0
9*
10* Unless required by applicable law or agreed to in writing, software
11* distributed under the License is distributed on an "AS IS" BASIS,
12* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13* See the License for the specific language governing permissions and
14* limitations under the License.
15*/
16
17package com.android.internal.notification;
18
19import android.app.Notification;
20import android.content.Context;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.Bundle;
24import android.provider.ContactsContract;
25import android.provider.Settings;
26import android.text.SpannableString;
27import android.util.Slog;
28
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Collections;
32import java.util.List;
33
34/**
35 * This NotificationScorer bumps up the priority of notifications that contain references to the
36 * display names of starred contacts. The references it picks up are spannable strings which, in
37 * their entirety, match the display name of some starred contact. The magnitude of the bump ranges
38 * from 0 to 15 (assuming NOTIFICATION_PRIORITY_MULTIPLIER = 10) depending on the initial score, and
39 * the mapping is defined by priorityBumpMap. In a production version of this scorer, a notification
40 * extra will be used to specify contact identifiers.
41 */
42
43public class DemoContactNotificationScorer implements NotificationScorer {
44    private static final String TAG = "DemoContactNotificationScorer";
45    private static final boolean DBG = false;
46
47    protected static final boolean ENABLE_CONTACT_SCORER = true;
48    private static final String SETTING_ENABLE_SCORER = "contact_scorer_enabled";
49    protected boolean mEnabled;
50
51    // see NotificationManagerService
52    private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
53
54    private Context mContext;
55
56    private static final List<String> RELEVANT_KEYS_LIST = Arrays.asList(
57            Notification.EXTRA_INFO_TEXT, Notification.EXTRA_TEXT, Notification.EXTRA_TEXT_LINES,
58            Notification.EXTRA_SUB_TEXT, Notification.EXTRA_TITLE
59    );
60
61    private static final String[] PROJECTION = new String[] {
62            ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME
63    };
64
65    private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI;
66
67    private static List<String> extractSpannedStrings(CharSequence charSequence) {
68        if (charSequence == null) return Collections.emptyList();
69        if (!(charSequence instanceof SpannableString)) {
70            return Arrays.asList(charSequence.toString());
71        }
72        SpannableString spannableString = (SpannableString)charSequence;
73        // get all spans
74        Object[] ssArr = spannableString.getSpans(0, spannableString.length(), Object.class);
75        // spanned string sequences
76        ArrayList<String> sss = new ArrayList<String>();
77        for (Object spanObj : ssArr) {
78            try {
79                sss.add(spannableString.subSequence(spannableString.getSpanStart(spanObj),
80                        spannableString.getSpanEnd(spanObj)).toString());
81            } catch(StringIndexOutOfBoundsException e) {
82                Slog.e(TAG, "Bad indices when extracting spanned subsequence", e);
83            }
84        }
85        return sss;
86    };
87
88    private static String getQuestionMarksInParens(int n) {
89        StringBuilder sb = new StringBuilder("(");
90        for (int i = 0; i < n; i++) {
91            if (sb.length() > 1) sb.append(',');
92            sb.append('?');
93        }
94        sb.append(")");
95        return sb.toString();
96    }
97
98    private boolean hasStarredContact(Bundle extras) {
99        if (extras == null) return false;
100        ArrayList<String> qStrings = new ArrayList<String>();
101        // build list to query against the database for display names.
102        for (String rk: RELEVANT_KEYS_LIST) {
103            if (extras.get(rk) == null) {
104                continue;
105            } else if (extras.get(rk) instanceof CharSequence) {
106                qStrings.addAll(extractSpannedStrings((CharSequence) extras.get(rk)));
107            } else if (extras.get(rk) instanceof CharSequence[]) {
108                // this is intended for Notification.EXTRA_TEXT_LINES
109                for (CharSequence line: (CharSequence[]) extras.get(rk)){
110                    qStrings.addAll(extractSpannedStrings(line));
111                }
112            } else {
113                Slog.w(TAG, "Strange, the extra " + rk + " is of unexpected type.");
114            }
115        }
116        if (qStrings.isEmpty()) return false;
117        String[] qStringsArr = qStrings.toArray(new String[qStrings.size()]);
118
119        String selection = ContactsContract.Contacts.DISPLAY_NAME + " IN "
120                + getQuestionMarksInParens(qStringsArr.length) + " AND "
121                + ContactsContract.Contacts.STARRED+" ='1'";
122
123        Cursor c = null;
124        try {
125            c = mContext.getContentResolver().query(
126                    CONTACTS_URI, PROJECTION, selection, qStringsArr, null);
127            if (c != null) return c.getCount() > 0;
128        } catch(Throwable t) {
129            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
130        } finally {
131            if (c != null) {
132                c.close();
133            }
134        }
135        return false;
136    }
137
138    private final static int clamp(int x, int low, int high) {
139        return (x < low) ? low : ((x > high) ? high : x);
140    }
141
142    private static int priorityBumpMap(int incomingScore) {
143        //assumption is that scale runs from [-2*pm, 2*pm]
144        int pm = NOTIFICATION_PRIORITY_MULTIPLIER;
145        int theScore = incomingScore;
146        // enforce input in range
147        theScore = clamp(theScore, -2 * pm, 2 * pm);
148        if (theScore != incomingScore) return incomingScore;
149        // map -20 -> -20 and -10 -> 5 (when pm = 10)
150        if (theScore <= -pm) {
151            theScore += 1.5 * (theScore + 2 * pm);
152        } else {
153            // map 0 -> 10, 10 -> 15, 20 -> 20;
154            theScore += 0.5 * (2 * pm - theScore);
155        }
156        if (DBG) Slog.v(TAG, "priorityBumpMap: score before: " + incomingScore
157                + ", score after " + theScore + ".");
158        return theScore;
159    }
160
161    @Override
162    public void initialize(Context context) {
163        if (DBG) Slog.v(TAG, "Initializing  " + getClass().getSimpleName() + ".");
164        mContext = context;
165        mEnabled = ENABLE_CONTACT_SCORER && 1 == Settings.Global.getInt(
166                mContext.getContentResolver(), SETTING_ENABLE_SCORER, 0);
167    }
168
169    @Override
170    public int getScore(Notification notification, int score) {
171        if (notification == null || !mEnabled) {
172            if (DBG) Slog.w(TAG, "empty notification? scorer disabled?");
173            return score;
174        }
175        boolean hasStarredPriority = hasStarredContact(notification.extras);
176
177        if (DBG) {
178            if (hasStarredPriority) {
179                Slog.v(TAG, "Notification references starred contact. Promoted!");
180            } else {
181                Slog.v(TAG, "Notification lacks any starred contact reference. Not promoted!");
182            }
183        }
184        if (hasStarredPriority) score = priorityBumpMap(score);
185        return score;
186    }
187}
188
189