ValidateNotificationPeople.java revision 54bbef435ed857fc68941672799fc8001c101119
1/* 2* Copyright (C) 2014 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.server.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.ContactsContract.Contacts; 26import android.provider.Settings; 27import android.text.TextUtils; 28import android.util.LruCache; 29import android.util.Slog; 30 31import java.util.ArrayList; 32import java.util.LinkedList; 33 34/** 35 * This {@link NotificationSignalExtractor} attempts to validate 36 * people references. Also elevates the priority of real people. 37 * 38 * {@hide} 39 */ 40public class ValidateNotificationPeople implements NotificationSignalExtractor { 41 private static final String TAG = "ValidateNotificationPeople"; 42 private static final boolean INFO = true; 43 private static final boolean DEBUG = false; 44 45 private static final boolean ENABLE_PEOPLE_VALIDATOR = true; 46 private static final String SETTING_ENABLE_PEOPLE_VALIDATOR = 47 "validate_notification_people_enabled"; 48 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED }; 49 private static final int MAX_PEOPLE = 10; 50 private static final int PEOPLE_CACHE_SIZE = 200; 51 52 /** Indicates that the notification does not reference any valid contacts. */ 53 static final float NONE = 0f; 54 55 /** 56 * Affinity will be equal to or greater than this value on notifications 57 * that reference a valid contact. 58 */ 59 static final float VALID_CONTACT = 0.5f; 60 61 /** 62 * Affinity will be equal to or greater than this value on notifications 63 * that reference a starred contact. 64 */ 65 static final float STARRED_CONTACT = 1f; 66 67 protected boolean mEnabled; 68 private Context mContext; 69 70 // maps raw person handle to resolved person object 71 private LruCache<String, LookupResult> mPeopleCache; 72 73 private RankingReconsideration validatePeople(final NotificationRecord record) { 74 float affinity = NONE; 75 Bundle extras = record.getNotification().extras; 76 if (extras == null) { 77 return null; 78 } 79 80 final String[] people = getExtraPeople(extras); 81 if (people == null || people.length == 0) { 82 return null; 83 } 84 85 if (INFO) Slog.i(TAG, "Validating: " + record.sbn.getKey()); 86 final LinkedList<String> pendingLookups = new LinkedList<String>(); 87 for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) { 88 final String handle = people[personIdx]; 89 if (TextUtils.isEmpty(handle)) continue; 90 91 synchronized (mPeopleCache) { 92 LookupResult lookupResult = mPeopleCache.get(handle); 93 if (lookupResult == null || lookupResult.isExpired()) { 94 pendingLookups.add(handle); 95 } else { 96 if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId); 97 } 98 if (lookupResult != null) { 99 affinity = Math.max(affinity, lookupResult.getAffinity()); 100 } 101 } 102 } 103 104 // record the best available data, so far: 105 record.setContactAffinity(affinity); 106 107 if (pendingLookups.isEmpty()) { 108 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 109 return null; 110 } 111 112 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey()); 113 return new RankingReconsideration(record.getKey()) { 114 float mContactAffinity = NONE; 115 @Override 116 public void work() { 117 if (INFO) Slog.i(TAG, "Executing: validation for: " + record.getKey()); 118 for (final String handle: pendingLookups) { 119 LookupResult lookupResult = null; 120 final Uri uri = Uri.parse(handle); 121 if ("tel".equals(uri.getScheme())) { 122 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle); 123 lookupResult = resolvePhoneContact(uri.getSchemeSpecificPart()); 124 } else if ("mailto".equals(uri.getScheme())) { 125 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 126 lookupResult = resolveEmailContact(uri.getSchemeSpecificPart()); 127 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 128 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 129 lookupResult = searchContacts(uri); 130 } else { 131 lookupResult = new LookupResult(); // invalid person for the cache 132 Slog.w(TAG, "unsupported URI " + handle); 133 } 134 if (lookupResult != null) { 135 synchronized (mPeopleCache) { 136 mPeopleCache.put(handle, lookupResult); 137 } 138 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); 139 } 140 } 141 } 142 143 @Override 144 public void applyChangesLocked(NotificationRecord operand) { 145 float affinityBound = operand.getContactAffinity(); 146 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); 147 if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); 148 } 149 }; 150 } 151 152 // VisibleForTesting 153 public static String[] getExtraPeople(Bundle extras) { 154 Object people = extras.get(Notification.EXTRA_PEOPLE); 155 if (people instanceof String[]) { 156 return (String[]) people; 157 } 158 159 if (people instanceof ArrayList) { 160 ArrayList arrayList = (ArrayList) people; 161 162 if (arrayList.isEmpty()) { 163 return null; 164 } 165 166 if (arrayList.get(0) instanceof String) { 167 ArrayList<String> stringArray = (ArrayList<String>) arrayList; 168 return stringArray.toArray(new String[stringArray.size()]); 169 } 170 171 if (arrayList.get(0) instanceof CharSequence) { 172 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList; 173 final int N = charSeqList.size(); 174 String[] array = new String[N]; 175 for (int i = 0; i < N; i++) { 176 array[i] = charSeqList.get(i).toString(); 177 } 178 return array; 179 } 180 181 return null; 182 } 183 184 if (people instanceof String) { 185 String[] array = new String[1]; 186 array[0] = (String) people; 187 return array; 188 } 189 190 if (people instanceof char[]) { 191 String[] array = new String[1]; 192 array[0] = new String((char[]) people); 193 return array; 194 } 195 196 if (people instanceof CharSequence) { 197 String[] array = new String[1]; 198 array[0] = ((CharSequence) people).toString(); 199 return array; 200 } 201 202 if (people instanceof CharSequence[]) { 203 CharSequence[] charSeqArray = (CharSequence[]) people; 204 final int N = charSeqArray.length; 205 String[] array = new String[N]; 206 for (int i = 0; i < N; i++) { 207 array[i] = charSeqArray[i].toString(); 208 } 209 return array; 210 } 211 212 return null; 213 } 214 215 private LookupResult resolvePhoneContact(final String number) { 216 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 217 Uri.encode(number)); 218 return searchContacts(phoneUri); 219 } 220 221 private LookupResult resolveEmailContact(final String email) { 222 Uri numberUri = Uri.withAppendedPath( 223 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 224 Uri.encode(email)); 225 return searchContacts(numberUri); 226 } 227 228 private LookupResult searchContacts(Uri lookupUri) { 229 LookupResult lookupResult = new LookupResult(); 230 Cursor c = null; 231 try { 232 c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null); 233 if (c != null && c.getCount() > 0) { 234 c.moveToFirst(); 235 lookupResult.readContact(c); 236 } 237 } catch(Throwable t) { 238 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 239 } finally { 240 if (c != null) { 241 c.close(); 242 } 243 } 244 return lookupResult; 245 } 246 247 public void initialize(Context context) { 248 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + "."); 249 mContext = context; 250 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE); 251 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt( 252 mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1); 253 } 254 255 public RankingReconsideration process(NotificationRecord record) { 256 if (!mEnabled) { 257 if (INFO) Slog.i(TAG, "disabled"); 258 return null; 259 } 260 if (record == null || record.getNotification() == null) { 261 if (INFO) Slog.i(TAG, "skipping empty notification"); 262 return null; 263 } 264 return validatePeople(record); 265 } 266 267 @Override 268 public void setConfig(RankingConfig config) { 269 // ignore: config has no relevant information yet. 270 } 271 272 private static class LookupResult { 273 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr 274 public static final int INVALID_ID = -1; 275 276 private final long mExpireMillis; 277 private int mId; 278 private boolean mStarred; 279 280 public LookupResult() { 281 mId = INVALID_ID; 282 mStarred = false; 283 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 284 } 285 286 public void readContact(Cursor cursor) { 287 final int idIdx = cursor.getColumnIndex(Contacts._ID); 288 if (idIdx >= 0) { 289 mId = cursor.getInt(idIdx); 290 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId); 291 } else { 292 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID"); 293 } 294 final int starIdx = cursor.getColumnIndex(Contacts.STARRED); 295 if (starIdx >= 0) { 296 mStarred = cursor.getInt(starIdx) != 0; 297 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred); 298 } else { 299 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); 300 } 301 } 302 303 public boolean isExpired() { 304 return mExpireMillis < System.currentTimeMillis(); 305 } 306 307 public boolean isInvalid() { 308 return mId == INVALID_ID || isExpired(); 309 } 310 311 public float getAffinity() { 312 if (isInvalid()) { 313 return NONE; 314 } else if (mStarred) { 315 return STARRED_CONTACT; 316 } else { 317 return VALID_CONTACT; 318 } 319 } 320 321 public LookupResult setStarred(boolean starred) { 322 mStarred = starred; 323 return this; 324 } 325 326 public LookupResult setId(int id) { 327 mId = id; 328 return this; 329 } 330 } 331} 332 333