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