ValidateNotificationPeople.java revision 2b122f4c2e691f0319e4f9ea5873989792bb56a6
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 final String key = record.getKey(); 75 final Bundle extras = record.getNotification().extras; 76 final float[] affinityOut = new float[1]; 77 final RankingReconsideration rr = validatePeople(key, extras, affinityOut); 78 record.setContactAffinity(affinityOut[0]); 79 return rr; 80 } 81 82 private PeopleRankingReconsideration validatePeople(String key, Bundle extras, 83 float[] affinityOut) { 84 float affinity = NONE; 85 if (extras == null) { 86 return null; 87 } 88 89 final String[] people = getExtraPeople(extras); 90 if (people == null || people.length == 0) { 91 return null; 92 } 93 94 if (INFO) Slog.i(TAG, "Validating: " + key); 95 final LinkedList<String> pendingLookups = new LinkedList<String>(); 96 for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) { 97 final String handle = people[personIdx]; 98 if (TextUtils.isEmpty(handle)) continue; 99 100 synchronized (mPeopleCache) { 101 LookupResult lookupResult = mPeopleCache.get(handle); 102 if (lookupResult == null || lookupResult.isExpired()) { 103 pendingLookups.add(handle); 104 } else { 105 if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId); 106 } 107 if (lookupResult != null) { 108 affinity = Math.max(affinity, lookupResult.getAffinity()); 109 } 110 } 111 } 112 113 // record the best available data, so far: 114 affinityOut[0] = affinity; 115 116 if (pendingLookups.isEmpty()) { 117 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 118 return null; 119 } 120 121 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key); 122 return new PeopleRankingReconsideration(key, pendingLookups); 123 } 124 125 // VisibleForTesting 126 public static String[] getExtraPeople(Bundle extras) { 127 Object people = extras.get(Notification.EXTRA_PEOPLE); 128 if (people instanceof String[]) { 129 return (String[]) people; 130 } 131 132 if (people instanceof ArrayList) { 133 ArrayList arrayList = (ArrayList) people; 134 135 if (arrayList.isEmpty()) { 136 return null; 137 } 138 139 if (arrayList.get(0) instanceof String) { 140 ArrayList<String> stringArray = (ArrayList<String>) arrayList; 141 return stringArray.toArray(new String[stringArray.size()]); 142 } 143 144 if (arrayList.get(0) instanceof CharSequence) { 145 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList; 146 final int N = charSeqList.size(); 147 String[] array = new String[N]; 148 for (int i = 0; i < N; i++) { 149 array[i] = charSeqList.get(i).toString(); 150 } 151 return array; 152 } 153 154 return null; 155 } 156 157 if (people instanceof String) { 158 String[] array = new String[1]; 159 array[0] = (String) people; 160 return array; 161 } 162 163 if (people instanceof char[]) { 164 String[] array = new String[1]; 165 array[0] = new String((char[]) people); 166 return array; 167 } 168 169 if (people instanceof CharSequence) { 170 String[] array = new String[1]; 171 array[0] = ((CharSequence) people).toString(); 172 return array; 173 } 174 175 if (people instanceof CharSequence[]) { 176 CharSequence[] charSeqArray = (CharSequence[]) people; 177 final int N = charSeqArray.length; 178 String[] array = new String[N]; 179 for (int i = 0; i < N; i++) { 180 array[i] = charSeqArray[i].toString(); 181 } 182 return array; 183 } 184 185 return null; 186 } 187 188 private LookupResult resolvePhoneContact(final String number) { 189 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 190 Uri.encode(number)); 191 return searchContacts(phoneUri); 192 } 193 194 private LookupResult resolveEmailContact(final String email) { 195 Uri numberUri = Uri.withAppendedPath( 196 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 197 Uri.encode(email)); 198 return searchContacts(numberUri); 199 } 200 201 private LookupResult searchContacts(Uri lookupUri) { 202 LookupResult lookupResult = new LookupResult(); 203 Cursor c = null; 204 try { 205 c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null); 206 if (c != null && c.getCount() > 0) { 207 c.moveToFirst(); 208 lookupResult.readContact(c); 209 } 210 } catch(Throwable t) { 211 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 212 } finally { 213 if (c != null) { 214 c.close(); 215 } 216 } 217 return lookupResult; 218 } 219 220 public void initialize(Context context) { 221 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + "."); 222 mContext = context; 223 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE); 224 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt( 225 mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1); 226 } 227 228 public RankingReconsideration process(NotificationRecord record) { 229 if (!mEnabled) { 230 if (INFO) Slog.i(TAG, "disabled"); 231 return null; 232 } 233 if (record == null || record.getNotification() == null) { 234 if (INFO) Slog.i(TAG, "skipping empty notification"); 235 return null; 236 } 237 return validatePeople(record); 238 } 239 240 @Override 241 public void setConfig(RankingConfig config) { 242 // ignore: config has no relevant information yet. 243 } 244 245 public float getContactAffinity(Bundle extras) { 246 if (extras == null) return NONE; 247 final String key = Long.toString(System.nanoTime()); 248 final float[] affinityOut = new float[1]; 249 final PeopleRankingReconsideration prr = validatePeople(key, extras, affinityOut); 250 float affinity = affinityOut[0]; 251 if (prr != null) { 252 prr.work(); 253 affinity = Math.max(prr.getContactAffinity(), affinity); 254 } 255 return affinity; 256 } 257 258 private static class LookupResult { 259 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr 260 public static final int INVALID_ID = -1; 261 262 private final long mExpireMillis; 263 private int mId; 264 private boolean mStarred; 265 266 public LookupResult() { 267 mId = INVALID_ID; 268 mStarred = false; 269 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 270 } 271 272 public void readContact(Cursor cursor) { 273 final int idIdx = cursor.getColumnIndex(Contacts._ID); 274 if (idIdx >= 0) { 275 mId = cursor.getInt(idIdx); 276 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId); 277 } else { 278 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID"); 279 } 280 final int starIdx = cursor.getColumnIndex(Contacts.STARRED); 281 if (starIdx >= 0) { 282 mStarred = cursor.getInt(starIdx) != 0; 283 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred); 284 } else { 285 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); 286 } 287 } 288 289 public boolean isExpired() { 290 return mExpireMillis < System.currentTimeMillis(); 291 } 292 293 public boolean isInvalid() { 294 return mId == INVALID_ID || isExpired(); 295 } 296 297 public float getAffinity() { 298 if (isInvalid()) { 299 return NONE; 300 } else if (mStarred) { 301 return STARRED_CONTACT; 302 } else { 303 return VALID_CONTACT; 304 } 305 } 306 307 public LookupResult setStarred(boolean starred) { 308 mStarred = starred; 309 return this; 310 } 311 312 public LookupResult setId(int id) { 313 mId = id; 314 return this; 315 } 316 } 317 318 private class PeopleRankingReconsideration extends RankingReconsideration { 319 private final LinkedList<String> mPendingLookups; 320 321 private float mContactAffinity = NONE; 322 323 private PeopleRankingReconsideration(String key, LinkedList<String> pendingLookups) { 324 super(key); 325 mPendingLookups = pendingLookups; 326 } 327 328 @Override 329 public void work() { 330 if (INFO) Slog.i(TAG, "Executing: validation for: " + mKey); 331 for (final String handle: mPendingLookups) { 332 LookupResult lookupResult = null; 333 final Uri uri = Uri.parse(handle); 334 if ("tel".equals(uri.getScheme())) { 335 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle); 336 lookupResult = resolvePhoneContact(uri.getSchemeSpecificPart()); 337 } else if ("mailto".equals(uri.getScheme())) { 338 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 339 lookupResult = resolveEmailContact(uri.getSchemeSpecificPart()); 340 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 341 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 342 lookupResult = searchContacts(uri); 343 } else { 344 lookupResult = new LookupResult(); // invalid person for the cache 345 Slog.w(TAG, "unsupported URI " + handle); 346 } 347 if (lookupResult != null) { 348 synchronized (mPeopleCache) { 349 mPeopleCache.put(handle, lookupResult); 350 } 351 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); 352 } 353 } 354 } 355 356 @Override 357 public void applyChangesLocked(NotificationRecord operand) { 358 float affinityBound = operand.getContactAffinity(); 359 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); 360 if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); 361 } 362 363 public float getContactAffinity() { 364 return mContactAffinity; 365 } 366 } 367} 368 369