ValidateNotificationPeople.java revision 7381daa0b99ef5beb224ffd2544a156af40e78d1
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 17package com.android.server.notification; 18 19import android.app.Notification; 20import android.content.Context; 21import android.content.pm.PackageManager; 22import android.database.Cursor; 23import android.net.Uri; 24import android.os.Bundle; 25import android.os.UserHandle; 26import android.provider.ContactsContract; 27import android.provider.ContactsContract.Contacts; 28import android.provider.Settings; 29import android.text.TextUtils; 30import android.util.ArrayMap; 31import android.util.Log; 32import android.util.LruCache; 33import android.util.Slog; 34 35import java.util.ArrayList; 36import java.util.LinkedList; 37import java.util.Map; 38 39/** 40 * This {@link NotificationSignalExtractor} attempts to validate 41 * people references. Also elevates the priority of real people. 42 * 43 * {@hide} 44 */ 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"; 53 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED }; 54 private static final int MAX_PEOPLE = 10; 55 private static final int PEOPLE_CACHE_SIZE = 200; 56 57 /** 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; 71 72 protected boolean mEnabled; 73 private Context mBaseContext; 74 75 // maps raw person handle to resolved person object 76 private LruCache<String, LookupResult> mPeopleCache; 77 private Map<Integer, Context> mUserToContextMap; 78 79 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 (DEBUG) Slog.d(TAG, "checking affinity for " + userHandle); 116 if (extras == null) return NONE; 117 final String key = Long.toString(System.nanoTime()); 118 final float[] affinityOut = new float[1]; 119 Context context = getContextAsUser(userHandle); 120 if (context == null) { 121 return NONE; 122 } 123 final PeopleRankingReconsideration prr = validatePeople(context, key, extras, affinityOut); 124 float affinity = affinityOut[0]; 125 if (prr != null) { 126 prr.work(); 127 affinity = Math.max(prr.getContactAffinity(), affinity); 128 } 129 return affinity; 130 } 131 132 private Context getContextAsUser(UserHandle userHandle) { 133 Context context = mUserToContextMap.get(userHandle.getIdentifier()); 134 if (context == null) { 135 try { 136 context = mBaseContext.createPackageContextAsUser("android", 0, userHandle); 137 mUserToContextMap.put(userHandle.getIdentifier(), context); 138 } catch (PackageManager.NameNotFoundException e) { 139 Log.e(TAG, "failed to create package context for lookups", e); 140 } 141 } 142 return context; 143 } 144 145 private RankingReconsideration validatePeople(Context context, 146 final NotificationRecord record) { 147 final String key = record.getKey(); 148 final Bundle extras = record.getNotification().extras; 149 final float[] affinityOut = new float[1]; 150 final RankingReconsideration rr = validatePeople(context, key, extras, affinityOut); 151 record.setContactAffinity(affinityOut[0]); 152 return rr; 153 } 154 155 private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras, 156 float[] affinityOut) { 157 float affinity = NONE; 158 if (extras == null) { 159 return null; 160 } 161 162 final String[] people = getExtraPeople(extras); 163 if (people == null || people.length == 0) { 164 return null; 165 } 166 167 if (INFO) Slog.i(TAG, "Validating: " + key); 168 final LinkedList<String> pendingLookups = new LinkedList<String>(); 169 for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) { 170 final String handle = people[personIdx]; 171 if (TextUtils.isEmpty(handle)) continue; 172 173 synchronized (mPeopleCache) { 174 final String cacheKey = getCacheKey(context.getUserId(), handle); 175 LookupResult lookupResult = mPeopleCache.get(cacheKey); 176 if (lookupResult == null || lookupResult.isExpired()) { 177 pendingLookups.add(handle); 178 } else { 179 if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId); 180 } 181 if (lookupResult != null) { 182 affinity = Math.max(affinity, lookupResult.getAffinity()); 183 } 184 } 185 } 186 187 // record the best available data, so far: 188 affinityOut[0] = affinity; 189 190 if (pendingLookups.isEmpty()) { 191 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 192 return null; 193 } 194 195 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key); 196 return new PeopleRankingReconsideration(context, key, pendingLookups); 197 } 198 199 private String getCacheKey(int userId, String handle) { 200 return Integer.toString(userId) + ":" + handle; 201 } 202 203 // VisibleForTesting 204 public static String[] getExtraPeople(Bundle extras) { 205 Object people = extras.get(Notification.EXTRA_PEOPLE); 206 if (people instanceof String[]) { 207 return (String[]) people; 208 } 209 210 if (people instanceof ArrayList) { 211 ArrayList arrayList = (ArrayList) people; 212 213 if (arrayList.isEmpty()) { 214 return null; 215 } 216 217 if (arrayList.get(0) instanceof String) { 218 ArrayList<String> stringArray = (ArrayList<String>) arrayList; 219 return stringArray.toArray(new String[stringArray.size()]); 220 } 221 222 if (arrayList.get(0) instanceof CharSequence) { 223 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList; 224 final int N = charSeqList.size(); 225 String[] array = new String[N]; 226 for (int i = 0; i < N; i++) { 227 array[i] = charSeqList.get(i).toString(); 228 } 229 return array; 230 } 231 232 return null; 233 } 234 235 if (people instanceof String) { 236 String[] array = new String[1]; 237 array[0] = (String) people; 238 return array; 239 } 240 241 if (people instanceof char[]) { 242 String[] array = new String[1]; 243 array[0] = new String((char[]) people); 244 return array; 245 } 246 247 if (people instanceof CharSequence) { 248 String[] array = new String[1]; 249 array[0] = ((CharSequence) people).toString(); 250 return array; 251 } 252 253 if (people instanceof CharSequence[]) { 254 CharSequence[] charSeqArray = (CharSequence[]) people; 255 final int N = charSeqArray.length; 256 String[] array = new String[N]; 257 for (int i = 0; i < N; i++) { 258 array[i] = charSeqArray[i].toString(); 259 } 260 return array; 261 } 262 263 return null; 264 } 265 266 private LookupResult resolvePhoneContact(Context context, final String number) { 267 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 268 Uri.encode(number)); 269 return searchContacts(context, phoneUri); 270 } 271 272 private LookupResult resolveEmailContact(Context context, final String email) { 273 Uri numberUri = Uri.withAppendedPath( 274 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 275 Uri.encode(email)); 276 return searchContacts(context, numberUri); 277 } 278 279 private LookupResult searchContacts(Context context, Uri lookupUri) { 280 LookupResult lookupResult = new LookupResult(); 281 Cursor c = null; 282 try { 283 c = context.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null); 284 if (c != null && c.getCount() > 0) { 285 c.moveToFirst(); 286 lookupResult.readContact(c); 287 } 288 } catch(Throwable t) { 289 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 290 } finally { 291 if (c != null) { 292 c.close(); 293 } 294 } 295 return lookupResult; 296 } 297 298 private static class LookupResult { 299 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr 300 public static final int INVALID_ID = -1; 301 302 private final long mExpireMillis; 303 private int mId; 304 private boolean mStarred; 305 306 public LookupResult() { 307 mId = INVALID_ID; 308 mStarred = false; 309 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 310 } 311 312 public void readContact(Cursor cursor) { 313 final int idIdx = cursor.getColumnIndex(Contacts._ID); 314 if (idIdx >= 0) { 315 mId = cursor.getInt(idIdx); 316 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId); 317 } else { 318 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID"); 319 } 320 final int starIdx = cursor.getColumnIndex(Contacts.STARRED); 321 if (starIdx >= 0) { 322 mStarred = cursor.getInt(starIdx) != 0; 323 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred); 324 } else { 325 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); 326 } 327 } 328 329 public boolean isExpired() { 330 return mExpireMillis < System.currentTimeMillis(); 331 } 332 333 public boolean isInvalid() { 334 return mId == INVALID_ID || isExpired(); 335 } 336 337 public float getAffinity() { 338 if (isInvalid()) { 339 return NONE; 340 } else if (mStarred) { 341 return STARRED_CONTACT; 342 } else { 343 return VALID_CONTACT; 344 } 345 } 346 347 public LookupResult setStarred(boolean starred) { 348 mStarred = starred; 349 return this; 350 } 351 352 public LookupResult setId(int id) { 353 mId = id; 354 return this; 355 } 356 } 357 358 private class PeopleRankingReconsideration extends RankingReconsideration { 359 private final LinkedList<String> mPendingLookups; 360 private final Context mContext; 361 362 private float mContactAffinity = NONE; 363 364 private PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups) { 365 super(key); 366 mContext = context; 367 mPendingLookups = pendingLookups; 368 } 369 370 @Override 371 public void work() { 372 if (INFO) Slog.i(TAG, "Executing: validation for: " + mKey); 373 for (final String handle: mPendingLookups) { 374 LookupResult lookupResult = null; 375 final Uri uri = Uri.parse(handle); 376 if ("tel".equals(uri.getScheme())) { 377 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle); 378 lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart()); 379 } else if ("mailto".equals(uri.getScheme())) { 380 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 381 lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart()); 382 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 383 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 384 lookupResult = searchContacts(mContext, uri); 385 } else { 386 lookupResult = new LookupResult(); // invalid person for the cache 387 Slog.w(TAG, "unsupported URI " + handle); 388 } 389 if (lookupResult != null) { 390 synchronized (mPeopleCache) { 391 final String cacheKey = getCacheKey(mContext.getUserId(), handle); 392 mPeopleCache.put(cacheKey, lookupResult); 393 } 394 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); 395 } 396 } 397 } 398 399 @Override 400 public void applyChangesLocked(NotificationRecord operand) { 401 float affinityBound = operand.getContactAffinity(); 402 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); 403 if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); 404 } 405 406 public float getContactAffinity() { 407 return mContactAffinity; 408 } 409 } 410} 411 412