ValidateNotificationPeople.java revision 99f4c7d0c9c9c29ced22da0a8af4d1a04b0ef186
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.content.pm.PackageManager; 22import android.database.ContentObserver; 23import android.database.Cursor; 24import android.net.Uri; 25import android.os.Bundle; 26import android.os.Handler; 27import android.os.UserHandle; 28import android.provider.ContactsContract; 29import android.provider.ContactsContract.Contacts; 30import android.provider.Settings; 31import android.text.TextUtils; 32import android.util.ArrayMap; 33import android.util.Log; 34import android.util.LruCache; 35import android.util.Slog; 36 37import java.util.ArrayList; 38import java.util.LinkedList; 39import java.util.Map; 40 41/** 42 * This {@link NotificationSignalExtractor} attempts to validate 43 * people references. Also elevates the priority of real people. 44 * 45 * {@hide} 46 */ 47public class ValidateNotificationPeople implements NotificationSignalExtractor { 48 private static final String TAG = "ValidateNotificationPeople"; 49 private static final boolean INFO = true; 50 private static final boolean DEBUG = false; 51 52 private static final boolean ENABLE_PEOPLE_VALIDATOR = true; 53 private static final String SETTING_ENABLE_PEOPLE_VALIDATOR = 54 "validate_notification_people_enabled"; 55 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED }; 56 private static final int MAX_PEOPLE = 10; 57 private static final int PEOPLE_CACHE_SIZE = 200; 58 59 /** Indicates that the notification does not reference any valid contacts. */ 60 static final float NONE = 0f; 61 62 /** 63 * Affinity will be equal to or greater than this value on notifications 64 * that reference a valid contact. 65 */ 66 static final float VALID_CONTACT = 0.5f; 67 68 /** 69 * Affinity will be equal to or greater than this value on notifications 70 * that reference a starred contact. 71 */ 72 static final float STARRED_CONTACT = 1f; 73 74 protected boolean mEnabled; 75 private Context mBaseContext; 76 77 // maps raw person handle to resolved person object 78 private LruCache<String, LookupResult> mPeopleCache; 79 private Map<Integer, Context> mUserToContextMap; 80 private Handler mHandler; 81 private ContentObserver mObserver; 82 private int mEvictionCount; 83 84 public void initialize(Context context) { 85 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + "."); 86 mUserToContextMap = new ArrayMap<>(); 87 mBaseContext = context; 88 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE); 89 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt( 90 mBaseContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1); 91 if (mEnabled) { 92 mHandler = new Handler(); 93 mObserver = new ContentObserver(mHandler) { 94 @Override 95 public void onChange(boolean selfChange, Uri uri, int userId) { 96 super.onChange(selfChange, uri, userId); 97 if (DEBUG || mEvictionCount % 100 == 0) { 98 if (INFO) Slog.i(TAG, "mEvictionCount: " + mEvictionCount); 99 } 100 mPeopleCache.evictAll(); 101 mEvictionCount++; 102 } 103 }; 104 mBaseContext.getContentResolver().registerContentObserver(Contacts.CONTENT_URI, true, 105 mObserver, UserHandle.USER_ALL); 106 } 107 } 108 109 public RankingReconsideration process(NotificationRecord record) { 110 if (!mEnabled) { 111 if (INFO) Slog.i(TAG, "disabled"); 112 return null; 113 } 114 if (record == null || record.getNotification() == null) { 115 if (INFO) Slog.i(TAG, "skipping empty notification"); 116 return null; 117 } 118 if (record.getUserId() == UserHandle.USER_ALL) { 119 if (INFO) Slog.i(TAG, "skipping global notification"); 120 return null; 121 } 122 Context context = getContextAsUser(record.getUser()); 123 if (context == null) { 124 if (INFO) Slog.i(TAG, "skipping notification that lacks a context"); 125 return null; 126 } 127 return validatePeople(context, record); 128 } 129 130 @Override 131 public void setConfig(RankingConfig config) { 132 // ignore: config has no relevant information yet. 133 } 134 135 public float getContactAffinity(UserHandle userHandle, Bundle extras) { 136 if (DEBUG) Slog.d(TAG, "checking affinity for " + userHandle); 137 if (extras == null) return NONE; 138 final String key = Long.toString(System.nanoTime()); 139 final float[] affinityOut = new float[1]; 140 Context context = getContextAsUser(userHandle); 141 if (context == null) { 142 return NONE; 143 } 144 final PeopleRankingReconsideration prr = validatePeople(context, key, extras, affinityOut); 145 float affinity = affinityOut[0]; 146 if (prr != null) { 147 prr.work(); 148 affinity = Math.max(prr.getContactAffinity(), affinity); 149 } 150 return affinity; 151 } 152 153 private Context getContextAsUser(UserHandle userHandle) { 154 Context context = mUserToContextMap.get(userHandle.getIdentifier()); 155 if (context == null) { 156 try { 157 context = mBaseContext.createPackageContextAsUser("android", 0, userHandle); 158 mUserToContextMap.put(userHandle.getIdentifier(), context); 159 } catch (PackageManager.NameNotFoundException e) { 160 Log.e(TAG, "failed to create package context for lookups", e); 161 } 162 } 163 return context; 164 } 165 166 private RankingReconsideration validatePeople(Context context, 167 final NotificationRecord record) { 168 final String key = record.getKey(); 169 final Bundle extras = record.getNotification().extras; 170 final float[] affinityOut = new float[1]; 171 final RankingReconsideration rr = validatePeople(context, key, extras, affinityOut); 172 record.setContactAffinity(affinityOut[0]); 173 return rr; 174 } 175 176 private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras, 177 float[] affinityOut) { 178 float affinity = NONE; 179 if (extras == null) { 180 return null; 181 } 182 183 final String[] people = getExtraPeople(extras); 184 if (people == null || people.length == 0) { 185 return null; 186 } 187 188 if (INFO) Slog.i(TAG, "Validating: " + key); 189 final LinkedList<String> pendingLookups = new LinkedList<String>(); 190 for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) { 191 final String handle = people[personIdx]; 192 if (TextUtils.isEmpty(handle)) continue; 193 194 synchronized (mPeopleCache) { 195 final String cacheKey = getCacheKey(context.getUserId(), handle); 196 LookupResult lookupResult = mPeopleCache.get(cacheKey); 197 if (lookupResult == null || lookupResult.isExpired()) { 198 pendingLookups.add(handle); 199 } else { 200 if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId); 201 } 202 if (lookupResult != null) { 203 affinity = Math.max(affinity, lookupResult.getAffinity()); 204 } 205 } 206 } 207 208 // record the best available data, so far: 209 affinityOut[0] = affinity; 210 211 if (pendingLookups.isEmpty()) { 212 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 213 return null; 214 } 215 216 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key); 217 return new PeopleRankingReconsideration(context, key, pendingLookups); 218 } 219 220 private String getCacheKey(int userId, String handle) { 221 return Integer.toString(userId) + ":" + handle; 222 } 223 224 // VisibleForTesting 225 public static String[] getExtraPeople(Bundle extras) { 226 Object people = extras.get(Notification.EXTRA_PEOPLE); 227 if (people instanceof String[]) { 228 return (String[]) people; 229 } 230 231 if (people instanceof ArrayList) { 232 ArrayList arrayList = (ArrayList) people; 233 234 if (arrayList.isEmpty()) { 235 return null; 236 } 237 238 if (arrayList.get(0) instanceof String) { 239 ArrayList<String> stringArray = (ArrayList<String>) arrayList; 240 return stringArray.toArray(new String[stringArray.size()]); 241 } 242 243 if (arrayList.get(0) instanceof CharSequence) { 244 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList; 245 final int N = charSeqList.size(); 246 String[] array = new String[N]; 247 for (int i = 0; i < N; i++) { 248 array[i] = charSeqList.get(i).toString(); 249 } 250 return array; 251 } 252 253 return null; 254 } 255 256 if (people instanceof String) { 257 String[] array = new String[1]; 258 array[0] = (String) people; 259 return array; 260 } 261 262 if (people instanceof char[]) { 263 String[] array = new String[1]; 264 array[0] = new String((char[]) people); 265 return array; 266 } 267 268 if (people instanceof CharSequence) { 269 String[] array = new String[1]; 270 array[0] = ((CharSequence) people).toString(); 271 return array; 272 } 273 274 if (people instanceof CharSequence[]) { 275 CharSequence[] charSeqArray = (CharSequence[]) people; 276 final int N = charSeqArray.length; 277 String[] array = new String[N]; 278 for (int i = 0; i < N; i++) { 279 array[i] = charSeqArray[i].toString(); 280 } 281 return array; 282 } 283 284 return null; 285 } 286 287 private LookupResult resolvePhoneContact(Context context, final String number) { 288 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 289 Uri.encode(number)); 290 return searchContacts(context, phoneUri); 291 } 292 293 private LookupResult resolveEmailContact(Context context, final String email) { 294 Uri numberUri = Uri.withAppendedPath( 295 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 296 Uri.encode(email)); 297 return searchContacts(context, numberUri); 298 } 299 300 private LookupResult searchContacts(Context context, Uri lookupUri) { 301 LookupResult lookupResult = new LookupResult(); 302 Cursor c = null; 303 try { 304 c = context.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null); 305 if (c != null && c.getCount() > 0) { 306 c.moveToFirst(); 307 lookupResult.readContact(c); 308 } 309 } catch(Throwable t) { 310 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 311 } finally { 312 if (c != null) { 313 c.close(); 314 } 315 } 316 return lookupResult; 317 } 318 319 private static class LookupResult { 320 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr 321 public static final int INVALID_ID = -1; 322 323 private final long mExpireMillis; 324 private int mId; 325 private boolean mStarred; 326 327 public LookupResult() { 328 mId = INVALID_ID; 329 mStarred = false; 330 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 331 } 332 333 public void readContact(Cursor cursor) { 334 final int idIdx = cursor.getColumnIndex(Contacts._ID); 335 if (idIdx >= 0) { 336 mId = cursor.getInt(idIdx); 337 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId); 338 } else { 339 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID"); 340 } 341 final int starIdx = cursor.getColumnIndex(Contacts.STARRED); 342 if (starIdx >= 0) { 343 mStarred = cursor.getInt(starIdx) != 0; 344 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred); 345 } else { 346 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); 347 } 348 } 349 350 public boolean isExpired() { 351 return mExpireMillis < System.currentTimeMillis(); 352 } 353 354 public boolean isInvalid() { 355 return mId == INVALID_ID || isExpired(); 356 } 357 358 public float getAffinity() { 359 if (isInvalid()) { 360 return NONE; 361 } else if (mStarred) { 362 return STARRED_CONTACT; 363 } else { 364 return VALID_CONTACT; 365 } 366 } 367 368 public LookupResult setStarred(boolean starred) { 369 mStarred = starred; 370 return this; 371 } 372 373 public LookupResult setId(int id) { 374 mId = id; 375 return this; 376 } 377 } 378 379 private class PeopleRankingReconsideration extends RankingReconsideration { 380 private final LinkedList<String> mPendingLookups; 381 private final Context mContext; 382 383 private float mContactAffinity = NONE; 384 385 private PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups) { 386 super(key); 387 mContext = context; 388 mPendingLookups = pendingLookups; 389 } 390 391 @Override 392 public void work() { 393 if (INFO) Slog.i(TAG, "Executing: validation for: " + mKey); 394 for (final String handle: mPendingLookups) { 395 LookupResult lookupResult = null; 396 final Uri uri = Uri.parse(handle); 397 if ("tel".equals(uri.getScheme())) { 398 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle); 399 lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart()); 400 } else if ("mailto".equals(uri.getScheme())) { 401 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 402 lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart()); 403 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 404 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 405 lookupResult = searchContacts(mContext, uri); 406 } else { 407 lookupResult = new LookupResult(); // invalid person for the cache 408 Slog.w(TAG, "unsupported URI " + handle); 409 } 410 if (lookupResult != null) { 411 synchronized (mPeopleCache) { 412 final String cacheKey = getCacheKey(mContext.getUserId(), handle); 413 mPeopleCache.put(cacheKey, lookupResult); 414 } 415 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); 416 } 417 } 418 } 419 420 @Override 421 public void applyChangesLocked(NotificationRecord operand) { 422 float affinityBound = operand.getContactAffinity(); 423 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); 424 if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); 425 } 426 427 public float getContactAffinity() { 428 return mContactAffinity; 429 } 430 } 431} 432 433