blob: f266916d3646b974a3d56da874ab6e56d640afea [file] [log] [blame]
Chris Wrenf9536642014-04-17 10:01:54 -04001/*
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
17package com.android.server.notification;
18
19import android.app.Notification;
20import android.content.Context;
Chris Wrenda4bd202014-09-04 15:53:52 -040021import android.content.pm.PackageManager;
Chris Wrenf9536642014-04-17 10:01:54 -040022import android.database.Cursor;
23import android.net.Uri;
24import android.os.Bundle;
Chris Wrenda4bd202014-09-04 15:53:52 -040025import android.os.UserHandle;
Chris Wrenf9536642014-04-17 10:01:54 -040026import android.provider.ContactsContract;
27import android.provider.ContactsContract.Contacts;
28import android.provider.Settings;
29import android.text.TextUtils;
Chris Wrenda4bd202014-09-04 15:53:52 -040030import android.util.ArrayMap;
31import android.util.Log;
Chris Wrenf9536642014-04-17 10:01:54 -040032import android.util.LruCache;
33import android.util.Slog;
34
Chris Wrenf9536642014-04-17 10:01:54 -040035import java.util.ArrayList;
36import java.util.LinkedList;
Chris Wrenda4bd202014-09-04 15:53:52 -040037import java.util.Map;
Chris Wrenf9536642014-04-17 10:01:54 -040038
39/**
40 * This {@link NotificationSignalExtractor} attempts to validate
41 * people references. Also elevates the priority of real people.
Chris Wren92af3722014-05-27 16:37:02 -040042 *
43 * {@hide}
Chris Wrenf9536642014-04-17 10:01:54 -040044 */
45public class ValidateNotificationPeople implements NotificationSignalExtractor {
46 private static final String TAG = "ValidateNotificationPeople";
47 private static final boolean INFO = true;
48 private static final boolean DEBUG = false;
49
50 private static final boolean ENABLE_PEOPLE_VALIDATOR = true;
51 private static final String SETTING_ENABLE_PEOPLE_VALIDATOR =
52 "validate_notification_people_enabled";
Chris Wren44d81a42014-05-14 17:38:05 -040053 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED };
Chris Wrenf9536642014-04-17 10:01:54 -040054 private static final int MAX_PEOPLE = 10;
55 private static final int PEOPLE_CACHE_SIZE = 200;
56
Chris Wren99f963e2014-05-28 16:52:42 -040057 /** Indicates that the notification does not reference any valid contacts. */
58 static final float NONE = 0f;
59
60 /**
61 * Affinity will be equal to or greater than this value on notifications
62 * that reference a valid contact.
63 */
64 static final float VALID_CONTACT = 0.5f;
65
66 /**
67 * Affinity will be equal to or greater than this value on notifications
68 * that reference a starred contact.
69 */
70 static final float STARRED_CONTACT = 1f;
Chris Wrenf9536642014-04-17 10:01:54 -040071
72 protected boolean mEnabled;
Chris Wrenda4bd202014-09-04 15:53:52 -040073 private Context mBaseContext;
Chris Wrenf9536642014-04-17 10:01:54 -040074
75 // maps raw person handle to resolved person object
76 private LruCache<String, LookupResult> mPeopleCache;
Chris Wrenda4bd202014-09-04 15:53:52 -040077 private Map<Integer, Context> mUserToContextMap;
Chris Wrenf9536642014-04-17 10:01:54 -040078
Chris Wrenda4bd202014-09-04 15:53:52 -040079 public void initialize(Context context) {
80 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + ".");
81 mUserToContextMap = new ArrayMap<>();
82 mBaseContext = context;
83 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
84 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
85 mBaseContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
86 }
87
88 public RankingReconsideration process(NotificationRecord record) {
89 if (!mEnabled) {
90 if (INFO) Slog.i(TAG, "disabled");
91 return null;
92 }
93 if (record == null || record.getNotification() == null) {
94 if (INFO) Slog.i(TAG, "skipping empty notification");
95 return null;
96 }
97 if (record.getUserId() == UserHandle.USER_ALL) {
98 if (INFO) Slog.i(TAG, "skipping global notification");
99 return null;
100 }
101 Context context = getContextAsUser(record.getUser());
102 if (context == null) {
103 if (INFO) Slog.i(TAG, "skipping notification that lacks a context");
104 return null;
105 }
106 return validatePeople(context, record);
107 }
108
109 @Override
110 public void setConfig(RankingConfig config) {
111 // ignore: config has no relevant information yet.
112 }
113
114 public float getContactAffinity(UserHandle userHandle, Bundle extras) {
115 if (extras == null) return NONE;
116 final String key = Long.toString(System.nanoTime());
117 final float[] affinityOut = new float[1];
118 Context context = getContextAsUser(userHandle);
119 if (context == null) {
120 return NONE;
121 }
122 final PeopleRankingReconsideration prr = validatePeople(context, key, extras, affinityOut);
123 float affinity = affinityOut[0];
124 if (prr != null) {
125 prr.work();
126 affinity = Math.max(prr.getContactAffinity(), affinity);
127 }
128 return affinity;
129 }
130
131 private Context getContextAsUser(UserHandle userHandle) {
132 Context context = mUserToContextMap.get(userHandle.getIdentifier());
133 if (context == null) {
134 try {
135 context = mBaseContext.createPackageContextAsUser("android", 0, userHandle);
136 mUserToContextMap.put(userHandle.getIdentifier(), context);
137 } catch (PackageManager.NameNotFoundException e) {
138 Log.e(TAG, "failed to create package context for lookups", e);
139 }
140 }
141 return context;
142 }
143
144 private RankingReconsideration validatePeople(Context context,
145 final NotificationRecord record) {
John Spurlock2b122f42014-08-27 16:29:47 -0400146 final String key = record.getKey();
147 final Bundle extras = record.getNotification().extras;
148 final float[] affinityOut = new float[1];
Chris Wrenda4bd202014-09-04 15:53:52 -0400149 final RankingReconsideration rr = validatePeople(context, key, extras, affinityOut);
John Spurlock2b122f42014-08-27 16:29:47 -0400150 record.setContactAffinity(affinityOut[0]);
151 return rr;
152 }
153
Chris Wrenda4bd202014-09-04 15:53:52 -0400154 private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras,
John Spurlock2b122f42014-08-27 16:29:47 -0400155 float[] affinityOut) {
Chris Wrenf9536642014-04-17 10:01:54 -0400156 float affinity = NONE;
Chris Wrenf9536642014-04-17 10:01:54 -0400157 if (extras == null) {
158 return null;
159 }
160
161 final String[] people = getExtraPeople(extras);
162 if (people == null || people.length == 0) {
163 return null;
164 }
165
John Spurlock2b122f42014-08-27 16:29:47 -0400166 if (INFO) Slog.i(TAG, "Validating: " + key);
Chris Wrenf9536642014-04-17 10:01:54 -0400167 final LinkedList<String> pendingLookups = new LinkedList<String>();
168 for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
169 final String handle = people[personIdx];
170 if (TextUtils.isEmpty(handle)) continue;
171
172 synchronized (mPeopleCache) {
Chris Wrenda4bd202014-09-04 15:53:52 -0400173 final String cacheKey = getCacheKey(context.getUserId(), handle);
174 LookupResult lookupResult = mPeopleCache.get(cacheKey);
Chris Wrenf9536642014-04-17 10:01:54 -0400175 if (lookupResult == null || lookupResult.isExpired()) {
176 pendingLookups.add(handle);
177 } else {
178 if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId);
179 }
180 if (lookupResult != null) {
181 affinity = Math.max(affinity, lookupResult.getAffinity());
182 }
183 }
184 }
185
186 // record the best available data, so far:
John Spurlock2b122f42014-08-27 16:29:47 -0400187 affinityOut[0] = affinity;
Chris Wrenf9536642014-04-17 10:01:54 -0400188
189 if (pendingLookups.isEmpty()) {
190 if (INFO) Slog.i(TAG, "final affinity: " + affinity);
191 return null;
192 }
193
John Spurlock2b122f42014-08-27 16:29:47 -0400194 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key);
Chris Wrenda4bd202014-09-04 15:53:52 -0400195 return new PeopleRankingReconsideration(context, key, pendingLookups);
196 }
197
198 private String getCacheKey(int userId, String handle) {
199 return Integer.toString(userId) + ":" + handle;
Chris Wrenf9536642014-04-17 10:01:54 -0400200 }
201
Chris Wren92af3722014-05-27 16:37:02 -0400202 // VisibleForTesting
203 public static String[] getExtraPeople(Bundle extras) {
Chris Wrenfb69da32014-05-15 18:03:11 -0400204 Object people = extras.get(Notification.EXTRA_PEOPLE);
205 if (people instanceof String[]) {
206 return (String[]) people;
Chris Wrenf9536642014-04-17 10:01:54 -0400207 }
208
Chris Wrenfb69da32014-05-15 18:03:11 -0400209 if (people instanceof ArrayList) {
210 ArrayList arrayList = (ArrayList) people;
211
212 if (arrayList.isEmpty()) {
213 return null;
214 }
215
216 if (arrayList.get(0) instanceof String) {
217 ArrayList<String> stringArray = (ArrayList<String>) arrayList;
218 return stringArray.toArray(new String[stringArray.size()]);
219 }
220
221 if (arrayList.get(0) instanceof CharSequence) {
222 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList;
223 final int N = charSeqList.size();
224 String[] array = new String[N];
225 for (int i = 0; i < N; i++) {
226 array[i] = charSeqList.get(i).toString();
227 }
228 return array;
229 }
230
231 return null;
Chris Wrenf9536642014-04-17 10:01:54 -0400232 }
233
Chris Wrenfb69da32014-05-15 18:03:11 -0400234 if (people instanceof String) {
235 String[] array = new String[1];
236 array[0] = (String) people;
237 return array;
Chris Wrenf9536642014-04-17 10:01:54 -0400238 }
239
Chris Wrenfb69da32014-05-15 18:03:11 -0400240 if (people instanceof char[]) {
241 String[] array = new String[1];
242 array[0] = new String((char[]) people);
243 return array;
Chris Wrenf9536642014-04-17 10:01:54 -0400244 }
245
Chris Wrenfb69da32014-05-15 18:03:11 -0400246 if (people instanceof CharSequence) {
247 String[] array = new String[1];
248 array[0] = ((CharSequence) people).toString();
249 return array;
250 }
251
252 if (people instanceof CharSequence[]) {
253 CharSequence[] charSeqArray = (CharSequence[]) people;
Chris Wrenf9536642014-04-17 10:01:54 -0400254 final int N = charSeqArray.length;
Chris Wrenfb69da32014-05-15 18:03:11 -0400255 String[] array = new String[N];
Chris Wrenf9536642014-04-17 10:01:54 -0400256 for (int i = 0; i < N; i++) {
Chris Wrenfb69da32014-05-15 18:03:11 -0400257 array[i] = charSeqArray[i].toString();
Chris Wrenf9536642014-04-17 10:01:54 -0400258 }
Chris Wrenfb69da32014-05-15 18:03:11 -0400259 return array;
Chris Wrenf9536642014-04-17 10:01:54 -0400260 }
261
Chris Wrenf9536642014-04-17 10:01:54 -0400262 return null;
263 }
264
Chris Wrenda4bd202014-09-04 15:53:52 -0400265 private LookupResult resolvePhoneContact(Context context, final String number) {
Chris Wren44d81a42014-05-14 17:38:05 -0400266 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
267 Uri.encode(number));
Chris Wrenda4bd202014-09-04 15:53:52 -0400268 return searchContacts(context, phoneUri);
Chris Wrenf9536642014-04-17 10:01:54 -0400269 }
270
Chris Wrenda4bd202014-09-04 15:53:52 -0400271 private LookupResult resolveEmailContact(Context context, final String email) {
Chris Wren44d81a42014-05-14 17:38:05 -0400272 Uri numberUri = Uri.withAppendedPath(
273 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
274 Uri.encode(email));
Chris Wrenda4bd202014-09-04 15:53:52 -0400275 return searchContacts(context, numberUri);
Chris Wren44d81a42014-05-14 17:38:05 -0400276 }
277
Chris Wrenda4bd202014-09-04 15:53:52 -0400278 private LookupResult searchContacts(Context context, Uri lookupUri) {
Chris Wren44d81a42014-05-14 17:38:05 -0400279 LookupResult lookupResult = new LookupResult();
Chris Wrenf9536642014-04-17 10:01:54 -0400280 Cursor c = null;
281 try {
Chris Wrenda4bd202014-09-04 15:53:52 -0400282 c = context.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
Chris Wrenf9536642014-04-17 10:01:54 -0400283 if (c != null && c.getCount() > 0) {
284 c.moveToFirst();
Chris Wren44d81a42014-05-14 17:38:05 -0400285 lookupResult.readContact(c);
Chris Wrenf9536642014-04-17 10:01:54 -0400286 }
287 } catch(Throwable t) {
288 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
289 } finally {
290 if (c != null) {
291 c.close();
292 }
293 }
Chris Wrenf9536642014-04-17 10:01:54 -0400294 return lookupResult;
295 }
296
Chris Wrenf9536642014-04-17 10:01:54 -0400297 private static class LookupResult {
298 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr
299 public static final int INVALID_ID = -1;
300
301 private final long mExpireMillis;
302 private int mId;
Chris Wren44d81a42014-05-14 17:38:05 -0400303 private boolean mStarred;
Chris Wrenf9536642014-04-17 10:01:54 -0400304
Chris Wren44d81a42014-05-14 17:38:05 -0400305 public LookupResult() {
306 mId = INVALID_ID;
307 mStarred = false;
Chris Wrenf9536642014-04-17 10:01:54 -0400308 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
309 }
310
Chris Wren44d81a42014-05-14 17:38:05 -0400311 public void readContact(Cursor cursor) {
312 final int idIdx = cursor.getColumnIndex(Contacts._ID);
313 if (idIdx >= 0) {
314 mId = cursor.getInt(idIdx);
315 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId);
316 } else {
317 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID");
318 }
319 final int starIdx = cursor.getColumnIndex(Contacts.STARRED);
320 if (starIdx >= 0) {
321 mStarred = cursor.getInt(starIdx) != 0;
322 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred);
323 } else {
324 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED");
325 }
326 }
327
Chris Wrenf9536642014-04-17 10:01:54 -0400328 public boolean isExpired() {
329 return mExpireMillis < System.currentTimeMillis();
330 }
331
332 public boolean isInvalid() {
333 return mId == INVALID_ID || isExpired();
334 }
335
336 public float getAffinity() {
337 if (isInvalid()) {
338 return NONE;
Chris Wren44d81a42014-05-14 17:38:05 -0400339 } else if (mStarred) {
340 return STARRED_CONTACT;
Chris Wrenf9536642014-04-17 10:01:54 -0400341 } else {
Chris Wren44d81a42014-05-14 17:38:05 -0400342 return VALID_CONTACT;
Chris Wrenf9536642014-04-17 10:01:54 -0400343 }
344 }
345
Chris Wren44d81a42014-05-14 17:38:05 -0400346 public LookupResult setStarred(boolean starred) {
347 mStarred = starred;
348 return this;
349 }
350
Chris Wrenf9536642014-04-17 10:01:54 -0400351 public LookupResult setId(int id) {
352 mId = id;
353 return this;
354 }
355 }
John Spurlock2b122f42014-08-27 16:29:47 -0400356
357 private class PeopleRankingReconsideration extends RankingReconsideration {
358 private final LinkedList<String> mPendingLookups;
Chris Wrenda4bd202014-09-04 15:53:52 -0400359 private final Context mContext;
John Spurlock2b122f42014-08-27 16:29:47 -0400360
361 private float mContactAffinity = NONE;
362
Chris Wrenda4bd202014-09-04 15:53:52 -0400363 private PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups) {
John Spurlock2b122f42014-08-27 16:29:47 -0400364 super(key);
Chris Wrenda4bd202014-09-04 15:53:52 -0400365 mContext = context;
John Spurlock2b122f42014-08-27 16:29:47 -0400366 mPendingLookups = pendingLookups;
367 }
368
369 @Override
370 public void work() {
371 if (INFO) Slog.i(TAG, "Executing: validation for: " + mKey);
372 for (final String handle: mPendingLookups) {
373 LookupResult lookupResult = null;
374 final Uri uri = Uri.parse(handle);
375 if ("tel".equals(uri.getScheme())) {
376 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
Chris Wrenda4bd202014-09-04 15:53:52 -0400377 lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart());
John Spurlock2b122f42014-08-27 16:29:47 -0400378 } else if ("mailto".equals(uri.getScheme())) {
379 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle);
Chris Wrenda4bd202014-09-04 15:53:52 -0400380 lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart());
John Spurlock2b122f42014-08-27 16:29:47 -0400381 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
382 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
Chris Wrenda4bd202014-09-04 15:53:52 -0400383 lookupResult = searchContacts(mContext, uri);
John Spurlock2b122f42014-08-27 16:29:47 -0400384 } else {
385 lookupResult = new LookupResult(); // invalid person for the cache
386 Slog.w(TAG, "unsupported URI " + handle);
387 }
388 if (lookupResult != null) {
389 synchronized (mPeopleCache) {
Chris Wrenda4bd202014-09-04 15:53:52 -0400390 final String cacheKey = getCacheKey(mContext.getUserId(), handle);
391 mPeopleCache.put(cacheKey, lookupResult);
John Spurlock2b122f42014-08-27 16:29:47 -0400392 }
393 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity());
394 }
395 }
396 }
397
398 @Override
399 public void applyChangesLocked(NotificationRecord operand) {
400 float affinityBound = operand.getContactAffinity();
401 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound));
402 if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity());
403 }
404
405 public float getContactAffinity() {
406 return mContactAffinity;
407 }
408 }
Chris Wrenf9536642014-04-17 10:01:54 -0400409}
410