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