ValidateNotificationPeople.java revision da4bd209cffad7e47a4bc6e9f02c4bfc333d3d8d
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 (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) { 146 final String key = record.getKey(); 147 final Bundle extras = record.getNotification().extras; 148 final float[] affinityOut = new float[1]; 149 final RankingReconsideration rr = validatePeople(context, key, extras, affinityOut); 150 record.setContactAffinity(affinityOut[0]); 151 return rr; 152 } 153 154 private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras, 155 float[] affinityOut) { 156 float affinity = NONE; 157 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 166 if (INFO) Slog.i(TAG, "Validating: " + key); 167 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) { 173 final String cacheKey = getCacheKey(context.getUserId(), handle); 174 LookupResult lookupResult = mPeopleCache.get(cacheKey); 175 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: 187 affinityOut[0] = affinity; 188 189 if (pendingLookups.isEmpty()) { 190 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 191 return null; 192 } 193 194 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key); 195 return new PeopleRankingReconsideration(context, key, pendingLookups); 196 } 197 198 private String getCacheKey(int userId, String handle) { 199 return Integer.toString(userId) + ":" + handle; 200 } 201 202 // VisibleForTesting 203 public static String[] getExtraPeople(Bundle extras) { 204 Object people = extras.get(Notification.EXTRA_PEOPLE); 205 if (people instanceof String[]) { 206 return (String[]) people; 207 } 208 209 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; 232 } 233 234 if (people instanceof String) { 235 String[] array = new String[1]; 236 array[0] = (String) people; 237 return array; 238 } 239 240 if (people instanceof char[]) { 241 String[] array = new String[1]; 242 array[0] = new String((char[]) people); 243 return array; 244 } 245 246 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; 254 final int N = charSeqArray.length; 255 String[] array = new String[N]; 256 for (int i = 0; i < N; i++) { 257 array[i] = charSeqArray[i].toString(); 258 } 259 return array; 260 } 261 262 return null; 263 } 264 265 private LookupResult resolvePhoneContact(Context context, final String number) { 266 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 267 Uri.encode(number)); 268 return searchContacts(context, phoneUri); 269 } 270 271 private LookupResult resolveEmailContact(Context context, final String email) { 272 Uri numberUri = Uri.withAppendedPath( 273 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 274 Uri.encode(email)); 275 return searchContacts(context, numberUri); 276 } 277 278 private LookupResult searchContacts(Context context, Uri lookupUri) { 279 LookupResult lookupResult = new LookupResult(); 280 Cursor c = null; 281 try { 282 c = context.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null); 283 if (c != null && c.getCount() > 0) { 284 c.moveToFirst(); 285 lookupResult.readContact(c); 286 } 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 } 294 return lookupResult; 295 } 296 297 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; 303 private boolean mStarred; 304 305 public LookupResult() { 306 mId = INVALID_ID; 307 mStarred = false; 308 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 309 } 310 311 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 328 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; 339 } else if (mStarred) { 340 return STARRED_CONTACT; 341 } else { 342 return VALID_CONTACT; 343 } 344 } 345 346 public LookupResult setStarred(boolean starred) { 347 mStarred = starred; 348 return this; 349 } 350 351 public LookupResult setId(int id) { 352 mId = id; 353 return this; 354 } 355 } 356 357 private class PeopleRankingReconsideration extends RankingReconsideration { 358 private final LinkedList<String> mPendingLookups; 359 private final Context mContext; 360 361 private float mContactAffinity = NONE; 362 363 private PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups) { 364 super(key); 365 mContext = context; 366 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); 377 lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart()); 378 } else if ("mailto".equals(uri.getScheme())) { 379 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 380 lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart()); 381 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 382 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 383 lookupResult = searchContacts(mContext, uri); 384 } 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) { 390 final String cacheKey = getCacheKey(mContext.getUserId(), handle); 391 mPeopleCache.put(cacheKey, lookupResult); 392 } 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 } 409} 410 411