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 17 package com.android.server.notification; 18 19 import android.annotation.Nullable; 20 import android.app.Notification; 21 import android.app.Person; 22 import android.content.ContentProvider; 23 import android.content.Context; 24 import android.content.pm.PackageManager; 25 import android.database.ContentObserver; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.AsyncTask; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.UserHandle; 32 import android.os.UserManager; 33 import android.provider.ContactsContract; 34 import android.provider.ContactsContract.Contacts; 35 import android.provider.Settings; 36 import android.text.TextUtils; 37 import android.util.ArrayMap; 38 import android.util.ArraySet; 39 import android.util.Log; 40 import android.util.LruCache; 41 import android.util.Slog; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import libcore.util.EmptyArray; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.LinkedList; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 import java.util.concurrent.Semaphore; 54 import java.util.concurrent.TimeUnit; 55 56 /** 57 * This {@link NotificationSignalExtractor} attempts to validate 58 * people references. Also elevates the priority of real people. 59 * 60 * {@hide} 61 */ 62 public class ValidateNotificationPeople implements NotificationSignalExtractor { 63 // Using a shorter log tag since setprop has a limit of 32chars on variable name. 64 private static final String TAG = "ValidateNoPeople"; 65 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);; 66 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 67 68 private static final boolean ENABLE_PEOPLE_VALIDATOR = true; 69 private static final String SETTING_ENABLE_PEOPLE_VALIDATOR = 70 "validate_notification_people_enabled"; 71 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.LOOKUP_KEY, 72 Contacts.STARRED, Contacts.HAS_PHONE_NUMBER }; 73 private static final int MAX_PEOPLE = 10; 74 private static final int PEOPLE_CACHE_SIZE = 200; 75 76 /** Columns used to look up phone numbers for contacts. */ 77 @VisibleForTesting 78 static final String[] PHONE_LOOKUP_PROJECTION = 79 { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER, 80 ContactsContract.CommonDataKinds.Phone.NUMBER }; 81 82 /** Indicates that the notification does not reference any valid contacts. */ 83 static final float NONE = 0f; 84 85 /** 86 * Affinity will be equal to or greater than this value on notifications 87 * that reference a valid contact. 88 */ 89 static final float VALID_CONTACT = 0.5f; 90 91 /** 92 * Affinity will be equal to or greater than this value on notifications 93 * that reference a starred contact. 94 */ 95 static final float STARRED_CONTACT = 1f; 96 97 protected boolean mEnabled; 98 private Context mBaseContext; 99 100 // maps raw person handle to resolved person object 101 private LruCache<String, LookupResult> mPeopleCache; 102 private Map<Integer, Context> mUserToContextMap; 103 private Handler mHandler; 104 private ContentObserver mObserver; 105 private int mEvictionCount; 106 private NotificationUsageStats mUsageStats; 107 initialize(Context context, NotificationUsageStats usageStats)108 public void initialize(Context context, NotificationUsageStats usageStats) { 109 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + "."); 110 mUserToContextMap = new ArrayMap<>(); 111 mBaseContext = context; 112 mUsageStats = usageStats; 113 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE); 114 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt( 115 mBaseContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1); 116 if (mEnabled) { 117 mHandler = new Handler(); 118 mObserver = new ContentObserver(mHandler) { 119 @Override 120 public void onChange(boolean selfChange, Uri uri, int userId) { 121 super.onChange(selfChange, uri, userId); 122 if (DEBUG || mEvictionCount % 100 == 0) { 123 if (VERBOSE) Slog.i(TAG, "mEvictionCount: " + mEvictionCount); 124 } 125 mPeopleCache.evictAll(); 126 mEvictionCount++; 127 } 128 }; 129 mBaseContext.getContentResolver().registerContentObserver(Contacts.CONTENT_URI, true, 130 mObserver, UserHandle.USER_ALL); 131 } 132 } 133 134 // For tests: just do the setting of various local variables without actually doing work 135 @VisibleForTesting initForTests(Context context, NotificationUsageStats usageStats, LruCache peopleCache)136 protected void initForTests(Context context, NotificationUsageStats usageStats, 137 LruCache peopleCache) { 138 mUserToContextMap = new ArrayMap<>(); 139 mBaseContext = context; 140 mUsageStats = usageStats; 141 mPeopleCache = peopleCache; 142 mEnabled = true; 143 } 144 process(NotificationRecord record)145 public RankingReconsideration process(NotificationRecord record) { 146 if (!mEnabled) { 147 if (VERBOSE) Slog.i(TAG, "disabled"); 148 return null; 149 } 150 if (record == null || record.getNotification() == null) { 151 if (VERBOSE) Slog.i(TAG, "skipping empty notification"); 152 return null; 153 } 154 if (record.getUserId() == UserHandle.USER_ALL) { 155 if (VERBOSE) Slog.i(TAG, "skipping global notification"); 156 return null; 157 } 158 Context context = getContextAsUser(record.getUser()); 159 if (context == null) { 160 if (VERBOSE) Slog.i(TAG, "skipping notification that lacks a context"); 161 return null; 162 } 163 return validatePeople(context, record); 164 } 165 166 @Override setConfig(RankingConfig config)167 public void setConfig(RankingConfig config) { 168 // ignore: config has no relevant information yet. 169 } 170 171 @Override setZenHelper(ZenModeHelper helper)172 public void setZenHelper(ZenModeHelper helper) { 173 174 } 175 176 /** 177 * @param extras extras of the notification with EXTRA_PEOPLE populated 178 * @param timeoutMs timeout in milliseconds to wait for contacts response 179 * @param timeoutAffinity affinity to return when the timeout specified via 180 * <code>timeoutMs</code> is hit 181 */ getContactAffinity(UserHandle userHandle, Bundle extras, int timeoutMs, float timeoutAffinity)182 public float getContactAffinity(UserHandle userHandle, Bundle extras, int timeoutMs, 183 float timeoutAffinity) { 184 if (DEBUG) Slog.d(TAG, "checking affinity for " + userHandle); 185 if (extras == null) return NONE; 186 final String key = Long.toString(System.nanoTime()); 187 final float[] affinityOut = new float[1]; 188 Context context = getContextAsUser(userHandle); 189 if (context == null) { 190 return NONE; 191 } 192 final PeopleRankingReconsideration prr = 193 validatePeople(context, key, extras, null, affinityOut, null); 194 float affinity = affinityOut[0]; 195 196 if (prr != null) { 197 // Perform the heavy work on a background thread so we can abort when we hit the 198 // timeout. 199 final Semaphore s = new Semaphore(0); 200 AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { 201 @Override 202 public void run() { 203 prr.work(); 204 s.release(); 205 } 206 }); 207 208 try { 209 if (!s.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) { 210 Slog.w(TAG, "Timeout while waiting for affinity: " + key + ". " 211 + "Returning timeoutAffinity=" + timeoutAffinity); 212 return timeoutAffinity; 213 } 214 } catch (InterruptedException e) { 215 Slog.w(TAG, "InterruptedException while waiting for affinity: " + key + ". " 216 + "Returning affinity=" + affinity, e); 217 return affinity; 218 } 219 220 affinity = Math.max(prr.getContactAffinity(), affinity); 221 } 222 return affinity; 223 } 224 getContextAsUser(UserHandle userHandle)225 private Context getContextAsUser(UserHandle userHandle) { 226 Context context = mUserToContextMap.get(userHandle.getIdentifier()); 227 if (context == null) { 228 try { 229 context = mBaseContext.createPackageContextAsUser("android", 0, userHandle); 230 mUserToContextMap.put(userHandle.getIdentifier(), context); 231 } catch (PackageManager.NameNotFoundException e) { 232 Log.e(TAG, "failed to create package context for lookups", e); 233 } 234 } 235 return context; 236 } 237 238 @VisibleForTesting validatePeople(Context context, final NotificationRecord record)239 protected RankingReconsideration validatePeople(Context context, 240 final NotificationRecord record) { 241 final String key = record.getKey(); 242 final Bundle extras = record.getNotification().extras; 243 final float[] affinityOut = new float[1]; 244 ArraySet<String> phoneNumbersOut = new ArraySet<>(); 245 final PeopleRankingReconsideration rr = 246 validatePeople(context, key, extras, record.getPeopleOverride(), affinityOut, 247 phoneNumbersOut); 248 final float affinity = affinityOut[0]; 249 record.setContactAffinity(affinity); 250 if (phoneNumbersOut.size() > 0) { 251 record.mergePhoneNumbers(phoneNumbersOut); 252 } 253 if (rr == null) { 254 mUsageStats.registerPeopleAffinity(record, affinity > NONE, affinity == STARRED_CONTACT, 255 true /* cached */); 256 } else { 257 rr.setRecord(record); 258 } 259 return rr; 260 } 261 validatePeople(Context context, String key, Bundle extras, List<String> peopleOverride, float[] affinityOut, ArraySet<String> phoneNumbersOut)262 private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras, 263 List<String> peopleOverride, float[] affinityOut, ArraySet<String> phoneNumbersOut) { 264 float affinity = NONE; 265 if (extras == null) { 266 return null; 267 } 268 final Set<String> people = new ArraySet<>(peopleOverride); 269 final String[] notificationPeople = getExtraPeople(extras); 270 if (notificationPeople != null ) { 271 people.addAll(Arrays.asList(notificationPeople)); 272 } 273 274 if (VERBOSE) Slog.i(TAG, "Validating: " + key + " for " + context.getUserId()); 275 final LinkedList<String> pendingLookups = new LinkedList<String>(); 276 int personIdx = 0; 277 for (String handle : people) { 278 if (TextUtils.isEmpty(handle)) continue; 279 280 synchronized (mPeopleCache) { 281 final String cacheKey = getCacheKey(context.getUserId(), handle); 282 LookupResult lookupResult = mPeopleCache.get(cacheKey); 283 if (lookupResult == null || lookupResult.isExpired()) { 284 pendingLookups.add(handle); 285 } else { 286 if (DEBUG) Slog.d(TAG, "using cached lookupResult"); 287 } 288 if (lookupResult != null) { 289 affinity = Math.max(affinity, lookupResult.getAffinity()); 290 291 // add all phone numbers associated with this lookup result, if they exist 292 // and if requested 293 if (phoneNumbersOut != null) { 294 ArraySet<String> phoneNumbers = lookupResult.getPhoneNumbers(); 295 if (phoneNumbers != null && phoneNumbers.size() > 0) { 296 phoneNumbersOut.addAll(phoneNumbers); 297 } 298 } 299 } 300 } 301 if (++personIdx == MAX_PEOPLE) { 302 break; 303 } 304 } 305 306 // record the best available data, so far: 307 affinityOut[0] = affinity; 308 309 if (pendingLookups.isEmpty()) { 310 if (VERBOSE) Slog.i(TAG, "final affinity: " + affinity); 311 return null; 312 } 313 314 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key); 315 return new PeopleRankingReconsideration(context, key, pendingLookups); 316 } 317 318 @VisibleForTesting getCacheKey(int userId, String handle)319 protected static String getCacheKey(int userId, String handle) { 320 return Integer.toString(userId) + ":" + handle; 321 } 322 323 // VisibleForTesting getExtraPeople(Bundle extras)324 public static String[] getExtraPeople(Bundle extras) { 325 String[] peopleList = getExtraPeopleForKey(extras, Notification.EXTRA_PEOPLE_LIST); 326 String[] legacyPeople = getExtraPeopleForKey(extras, Notification.EXTRA_PEOPLE); 327 return combineLists(legacyPeople, peopleList); 328 } 329 combineLists(String[] first, String[] second)330 private static String[] combineLists(String[] first, String[] second) { 331 if (first == null) { 332 return second; 333 } 334 if (second == null) { 335 return first; 336 } 337 ArraySet<String> people = new ArraySet<>(first.length + second.length); 338 for (String person: first) { 339 people.add(person); 340 } 341 for (String person: second) { 342 people.add(person); 343 } 344 return people.toArray(EmptyArray.STRING); 345 } 346 347 @Nullable getExtraPeopleForKey(Bundle extras, String key)348 private static String[] getExtraPeopleForKey(Bundle extras, String key) { 349 Object people = extras.get(key); 350 if (people instanceof String[]) { 351 return (String[]) people; 352 } 353 354 if (people instanceof ArrayList) { 355 ArrayList arrayList = (ArrayList) people; 356 357 if (arrayList.isEmpty()) { 358 return null; 359 } 360 361 if (arrayList.get(0) instanceof String) { 362 ArrayList<String> stringArray = (ArrayList<String>) arrayList; 363 return stringArray.toArray(new String[stringArray.size()]); 364 } 365 366 if (arrayList.get(0) instanceof CharSequence) { 367 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList; 368 final int N = charSeqList.size(); 369 String[] array = new String[N]; 370 for (int i = 0; i < N; i++) { 371 array[i] = charSeqList.get(i).toString(); 372 } 373 return array; 374 } 375 376 if (arrayList.get(0) instanceof Person) { 377 ArrayList<Person> list = (ArrayList<Person>) arrayList; 378 final int N = list.size(); 379 String[] array = new String[N]; 380 for (int i = 0; i < N; i++) { 381 array[i] = list.get(i).resolveToLegacyUri(); 382 } 383 return array; 384 } 385 386 return null; 387 } 388 389 if (people instanceof String) { 390 String[] array = new String[1]; 391 array[0] = (String) people; 392 return array; 393 } 394 395 if (people instanceof char[]) { 396 String[] array = new String[1]; 397 array[0] = new String((char[]) people); 398 return array; 399 } 400 401 if (people instanceof CharSequence) { 402 String[] array = new String[1]; 403 array[0] = ((CharSequence) people).toString(); 404 return array; 405 } 406 407 if (people instanceof CharSequence[]) { 408 CharSequence[] charSeqArray = (CharSequence[]) people; 409 final int N = charSeqArray.length; 410 String[] array = new String[N]; 411 for (int i = 0; i < N; i++) { 412 array[i] = charSeqArray[i].toString(); 413 } 414 return array; 415 } 416 417 return null; 418 } 419 resolvePhoneContact(Context context, final String number)420 private LookupResult resolvePhoneContact(Context context, final String number) { 421 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 422 Uri.encode(number)); 423 return searchContacts(context, phoneUri); 424 } 425 resolveEmailContact(Context context, final String email)426 private LookupResult resolveEmailContact(Context context, final String email) { 427 Uri numberUri = Uri.withAppendedPath( 428 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 429 Uri.encode(email)); 430 return searchContacts(context, numberUri); 431 } 432 433 @VisibleForTesting searchContacts(Context context, Uri lookupUri)434 LookupResult searchContacts(Context context, Uri lookupUri) { 435 LookupResult lookupResult = new LookupResult(); 436 final Uri corpLookupUri = 437 ContactsContract.Contacts.createCorpLookupUriFromEnterpriseLookupUri(lookupUri); 438 if (corpLookupUri == null) { 439 addContacts(lookupResult, context, lookupUri); 440 } else { 441 addWorkContacts(lookupResult, context, corpLookupUri); 442 } 443 return lookupResult; 444 } 445 446 @VisibleForTesting 447 // Performs a contacts search using searchContacts, and then follows up by looking up 448 // any phone numbers associated with the resulting contact information and merge those 449 // into the lookup result as well. Will have no additional effect if the contact does 450 // not have any phone numbers. searchContactsAndLookupNumbers(Context context, Uri lookupUri)451 LookupResult searchContactsAndLookupNumbers(Context context, Uri lookupUri) { 452 LookupResult lookupResult = searchContacts(context, lookupUri); 453 String phoneLookupKey = lookupResult.getPhoneLookupKey(); 454 if (phoneLookupKey != null) { 455 String selection = Contacts.LOOKUP_KEY + " = ?"; 456 String[] selectionArgs = new String[] { phoneLookupKey }; 457 try (Cursor cursor = context.getContentResolver().query( 458 ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PHONE_LOOKUP_PROJECTION, 459 selection, selectionArgs, /* sortOrder= */ null)) { 460 if (cursor == null) { 461 Slog.w(TAG, "Cursor is null when querying contact phone number."); 462 return lookupResult; 463 } 464 465 while (cursor.moveToNext()) { 466 lookupResult.mergePhoneNumber(cursor); 467 } 468 } catch (Throwable t) { 469 Slog.w(TAG, "Problem getting content resolver or querying phone numbers.", t); 470 } 471 } 472 return lookupResult; 473 } 474 addWorkContacts(LookupResult lookupResult, Context context, Uri corpLookupUri)475 private void addWorkContacts(LookupResult lookupResult, Context context, Uri corpLookupUri) { 476 final int workUserId = findWorkUserId(context); 477 if (workUserId == -1) { 478 Slog.w(TAG, "Work profile user ID not found for work contact: " + corpLookupUri); 479 return; 480 } 481 final Uri corpLookupUriWithUserId = 482 ContentProvider.maybeAddUserId(corpLookupUri, workUserId); 483 addContacts(lookupResult, context, corpLookupUriWithUserId); 484 } 485 486 /** Returns the user ID of the managed profile or -1 if none is found. */ findWorkUserId(Context context)487 private int findWorkUserId(Context context) { 488 final UserManager userManager = context.getSystemService(UserManager.class); 489 final int[] profileIds = 490 userManager.getProfileIds(context.getUserId(), /* enabledOnly= */ true); 491 for (int profileId : profileIds) { 492 if (userManager.isManagedProfile(profileId)) { 493 return profileId; 494 } 495 } 496 return -1; 497 } 498 499 /** Modifies the given lookup result to add contacts found at the given URI. */ addContacts(LookupResult lookupResult, Context context, Uri uri)500 private void addContacts(LookupResult lookupResult, Context context, Uri uri) { 501 try (Cursor c = context.getContentResolver().query( 502 uri, LOOKUP_PROJECTION, null, null, null)) { 503 if (c == null) { 504 Slog.w(TAG, "Null cursor from contacts query."); 505 return; 506 } 507 while (c.moveToNext()) { 508 lookupResult.mergeContact(c); 509 } 510 } catch (Throwable t) { 511 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 512 } 513 } 514 515 @VisibleForTesting 516 protected static class LookupResult { 517 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr 518 519 private final long mExpireMillis; 520 private float mAffinity = NONE; 521 private boolean mHasPhone = false; 522 private String mPhoneLookupKey = null; 523 private ArraySet<String> mPhoneNumbers = new ArraySet<>(); 524 LookupResult()525 public LookupResult() { 526 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 527 } 528 mergeContact(Cursor cursor)529 public void mergeContact(Cursor cursor) { 530 mAffinity = Math.max(mAffinity, VALID_CONTACT); 531 532 // Contact ID 533 int id; 534 final int idIdx = cursor.getColumnIndex(Contacts._ID); 535 if (idIdx >= 0) { 536 id = cursor.getInt(idIdx); 537 if (DEBUG) Slog.d(TAG, "contact _ID is: " + id); 538 } else { 539 id = -1; 540 Slog.i(TAG, "invalid cursor: no _ID"); 541 } 542 543 // Lookup key for potentially looking up contact phone number later 544 final int lookupKeyIdx = cursor.getColumnIndex(Contacts.LOOKUP_KEY); 545 if (lookupKeyIdx >= 0) { 546 mPhoneLookupKey = cursor.getString(lookupKeyIdx); 547 if (DEBUG) Slog.d(TAG, "contact LOOKUP_KEY is: " + mPhoneLookupKey); 548 } else { 549 if (DEBUG) Slog.d(TAG, "invalid cursor: no LOOKUP_KEY"); 550 } 551 552 // Starred 553 final int starIdx = cursor.getColumnIndex(Contacts.STARRED); 554 if (starIdx >= 0) { 555 boolean isStarred = cursor.getInt(starIdx) != 0; 556 if (isStarred) { 557 mAffinity = Math.max(mAffinity, STARRED_CONTACT); 558 } 559 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + isStarred); 560 } else { 561 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); 562 } 563 564 // whether a phone number is present 565 final int hasPhoneIdx = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER); 566 if (hasPhoneIdx >= 0) { 567 mHasPhone = cursor.getInt(hasPhoneIdx) != 0; 568 if (DEBUG) Slog.d(TAG, "contact HAS_PHONE_NUMBER is: " + mHasPhone); 569 } else { 570 if (DEBUG) Slog.d(TAG, "invalid cursor: no HAS_PHONE_NUMBER"); 571 } 572 } 573 574 // Returns the phone lookup key that is cached in this result, or null 575 // if the contact has no known phone info. getPhoneLookupKey()576 public String getPhoneLookupKey() { 577 if (!mHasPhone) { 578 return null; 579 } 580 return mPhoneLookupKey; 581 } 582 583 // Merge phone numbers found in this lookup and store them in mPhoneNumbers. mergePhoneNumber(Cursor cursor)584 public void mergePhoneNumber(Cursor cursor) { 585 final int normalizedNumIdx = cursor.getColumnIndex( 586 ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); 587 if (normalizedNumIdx >= 0) { 588 mPhoneNumbers.add(cursor.getString(normalizedNumIdx)); 589 } else { 590 if (DEBUG) Slog.d(TAG, "cursor data not found: no NORMALIZED_NUMBER"); 591 } 592 593 final int numIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); 594 if (numIdx >= 0) { 595 mPhoneNumbers.add(cursor.getString(numIdx)); 596 } else { 597 if (DEBUG) Slog.d(TAG, "cursor data not found: no NUMBER"); 598 } 599 } 600 getPhoneNumbers()601 public ArraySet<String> getPhoneNumbers() { 602 return mPhoneNumbers; 603 } 604 605 @VisibleForTesting isExpired()606 protected boolean isExpired() { 607 return mExpireMillis < System.currentTimeMillis(); 608 } 609 isInvalid()610 private boolean isInvalid() { 611 return mAffinity == NONE || isExpired(); 612 } 613 getAffinity()614 public float getAffinity() { 615 if (isInvalid()) { 616 return NONE; 617 } 618 return mAffinity; 619 } 620 } 621 622 private class PeopleRankingReconsideration extends RankingReconsideration { 623 private final LinkedList<String> mPendingLookups; 624 private final Context mContext; 625 626 // Amount of time to wait for a result from the contacts db before rechecking affinity. 627 private static final long LOOKUP_TIME = 1000; 628 private float mContactAffinity = NONE; 629 private ArraySet<String> mPhoneNumbers = null; 630 private NotificationRecord mRecord; 631 PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups)632 private PeopleRankingReconsideration(Context context, String key, 633 LinkedList<String> pendingLookups) { 634 super(key, LOOKUP_TIME); 635 mContext = context; 636 mPendingLookups = pendingLookups; 637 } 638 639 @Override work()640 public void work() { 641 if (VERBOSE) Slog.i(TAG, "Executing: validation for: " + mKey); 642 long timeStartMs = System.currentTimeMillis(); 643 for (final String handle: mPendingLookups) { 644 final String cacheKey = getCacheKey(mContext.getUserId(), handle); 645 LookupResult lookupResult = null; 646 boolean cacheHit = false; 647 synchronized (mPeopleCache) { 648 lookupResult = mPeopleCache.get(cacheKey); 649 if (lookupResult != null && !lookupResult.isExpired()) { 650 // The name wasn't already added to the cache, no need to retry 651 cacheHit = true; 652 } 653 } 654 if (!cacheHit) { 655 final Uri uri = Uri.parse(handle); 656 if ("tel".equals(uri.getScheme())) { 657 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle); 658 lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart()); 659 } else if ("mailto".equals(uri.getScheme())) { 660 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 661 lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart()); 662 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 663 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 664 // only look up phone number if this is a contact lookup uri and thus isn't 665 // already directly a phone number. 666 lookupResult = searchContactsAndLookupNumbers(mContext, uri); 667 } else { 668 lookupResult = new LookupResult(); // invalid person for the cache 669 if (!"name".equals(uri.getScheme())) { 670 Slog.w(TAG, "unsupported URI " + handle); 671 } 672 } 673 } 674 if (lookupResult != null) { 675 if (!cacheHit) { 676 synchronized (mPeopleCache) { 677 mPeopleCache.put(cacheKey, lookupResult); 678 } 679 } 680 if (DEBUG) { 681 Slog.d(TAG, "lookup contactAffinity is " + lookupResult.getAffinity()); 682 } 683 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); 684 // merge any phone numbers found in this lookup result 685 if (lookupResult.getPhoneNumbers() != null) { 686 if (mPhoneNumbers == null) { 687 mPhoneNumbers = new ArraySet<>(); 688 } 689 mPhoneNumbers.addAll(lookupResult.getPhoneNumbers()); 690 } 691 } else { 692 if (DEBUG) Slog.d(TAG, "lookupResult is null"); 693 } 694 } 695 if (DEBUG) { 696 Slog.d(TAG, "Validation finished in " + (System.currentTimeMillis() - timeStartMs) + 697 "ms"); 698 } 699 700 if (mRecord != null) { 701 mUsageStats.registerPeopleAffinity(mRecord, mContactAffinity > NONE, 702 mContactAffinity == STARRED_CONTACT, false /* cached */); 703 } 704 } 705 706 @Override applyChangesLocked(NotificationRecord operand)707 public void applyChangesLocked(NotificationRecord operand) { 708 float affinityBound = operand.getContactAffinity(); 709 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); 710 if (VERBOSE) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); 711 operand.mergePhoneNumbers(mPhoneNumbers); 712 } 713 getContactAffinity()714 public float getContactAffinity() { 715 return mContactAffinity; 716 } 717 setRecord(NotificationRecord record)718 public void setRecord(NotificationRecord record) { 719 mRecord = record; 720 } 721 } 722 } 723