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