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