Home | History | Annotate | Download | only in notification
      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 
     17 package com.android.internal.notification;
     18 
     19 import android.app.Notification;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.os.Bundle;
     24 import android.provider.ContactsContract;
     25 import android.provider.Settings;
     26 import android.text.SpannableString;
     27 import android.util.Slog;
     28 
     29 import java.util.ArrayList;
     30 import java.util.Arrays;
     31 import java.util.Collections;
     32 import 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 
     43 public 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