• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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