ValidateNotificationPeople.java revision 99f963ea04d7a86219ece00c356f3f6bce33b6d6
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.database.Cursor; 22import android.net.Uri; 23import android.os.Bundle; 24import android.provider.ContactsContract; 25import android.provider.ContactsContract.Contacts; 26import android.provider.Settings; 27import android.text.TextUtils; 28import android.util.LruCache; 29import android.util.Slog; 30 31import java.util.ArrayList; 32import java.util.LinkedList; 33 34/** 35 * This {@link NotificationSignalExtractor} attempts to validate 36 * people references. Also elevates the priority of real people. 37 */ 38public class ValidateNotificationPeople implements NotificationSignalExtractor { 39 private static final String TAG = "ValidateNotificationPeople"; 40 private static final boolean INFO = true; 41 private static final boolean DEBUG = false; 42 43 private static final boolean ENABLE_PEOPLE_VALIDATOR = true; 44 private static final String SETTING_ENABLE_PEOPLE_VALIDATOR = 45 "validate_notification_people_enabled"; 46 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED }; 47 private static final int MAX_PEOPLE = 10; 48 private static final int PEOPLE_CACHE_SIZE = 200; 49 50 /** Indicates that the notification does not reference any valid contacts. */ 51 static final float NONE = 0f; 52 53 /** 54 * Affinity will be equal to or greater than this value on notifications 55 * that reference a valid contact. 56 */ 57 static final float VALID_CONTACT = 0.5f; 58 59 /** 60 * Affinity will be equal to or greater than this value on notifications 61 * that reference a starred contact. 62 */ 63 static final float STARRED_CONTACT = 1f; 64 65 protected boolean mEnabled; 66 private Context mContext; 67 68 // maps raw person handle to resolved person object 69 private LruCache<String, LookupResult> mPeopleCache; 70 71 private RankingReconsideration validatePeople(final NotificationRecord record) { 72 float affinity = NONE; 73 Bundle extras = record.getNotification().extras; 74 if (extras == null) { 75 return null; 76 } 77 78 final String[] people = getExtraPeople(extras); 79 if (people == null || people.length == 0) { 80 return null; 81 } 82 83 if (INFO) Slog.i(TAG, "Validating: " + record.sbn.getKey()); 84 final LinkedList<String> pendingLookups = new LinkedList<String>(); 85 for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) { 86 final String handle = people[personIdx]; 87 if (TextUtils.isEmpty(handle)) continue; 88 89 synchronized (mPeopleCache) { 90 LookupResult lookupResult = mPeopleCache.get(handle); 91 if (lookupResult == null || lookupResult.isExpired()) { 92 pendingLookups.add(handle); 93 } else { 94 if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId); 95 } 96 if (lookupResult != null) { 97 affinity = Math.max(affinity, lookupResult.getAffinity()); 98 } 99 } 100 } 101 102 // record the best available data, so far: 103 record.setContactAffinity(affinity); 104 105 if (pendingLookups.isEmpty()) { 106 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 107 return null; 108 } 109 110 if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey()); 111 return new RankingReconsideration(record.getKey()) { 112 float mContactAffinity = NONE; 113 @Override 114 public void work() { 115 if (INFO) Slog.i(TAG, "Executing: validation for: " + record.getKey()); 116 for (final String handle: pendingLookups) { 117 LookupResult lookupResult = null; 118 final Uri uri = Uri.parse(handle); 119 if ("tel".equals(uri.getScheme())) { 120 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle); 121 lookupResult = resolvePhoneContact(uri.getSchemeSpecificPart()); 122 } else if ("mailto".equals(uri.getScheme())) { 123 if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle); 124 lookupResult = resolveEmailContact(uri.getSchemeSpecificPart()); 125 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 126 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 127 lookupResult = searchContacts(uri); 128 } else { 129 lookupResult = new LookupResult(); // invalid person for the cache 130 Slog.w(TAG, "unsupported URI " + handle); 131 } 132 if (lookupResult != null) { 133 synchronized (mPeopleCache) { 134 mPeopleCache.put(handle, lookupResult); 135 } 136 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); 137 } 138 } 139 } 140 141 @Override 142 public void applyChangesLocked(NotificationRecord operand) { 143 float affinityBound = operand.getContactAffinity(); 144 operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); 145 if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); 146 } 147 }; 148 } 149 150 private String[] getExtraPeople(Bundle extras) { 151 Object people = extras.get(Notification.EXTRA_PEOPLE); 152 if (people instanceof String[]) { 153 return (String[]) people; 154 } 155 156 if (people instanceof ArrayList) { 157 ArrayList arrayList = (ArrayList) people; 158 159 if (arrayList.isEmpty()) { 160 return null; 161 } 162 163 if (arrayList.get(0) instanceof String) { 164 ArrayList<String> stringArray = (ArrayList<String>) arrayList; 165 return stringArray.toArray(new String[stringArray.size()]); 166 } 167 168 if (arrayList.get(0) instanceof CharSequence) { 169 ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList; 170 final int N = charSeqList.size(); 171 String[] array = new String[N]; 172 for (int i = 0; i < N; i++) { 173 array[i] = charSeqList.get(i).toString(); 174 } 175 return array; 176 } 177 178 return null; 179 } 180 181 if (people instanceof String) { 182 String[] array = new String[1]; 183 array[0] = (String) people; 184 return array; 185 } 186 187 if (people instanceof char[]) { 188 String[] array = new String[1]; 189 array[0] = new String((char[]) people); 190 return array; 191 } 192 193 if (people instanceof CharSequence) { 194 String[] array = new String[1]; 195 array[0] = ((CharSequence) people).toString(); 196 return array; 197 } 198 199 if (people instanceof CharSequence[]) { 200 CharSequence[] charSeqArray = (CharSequence[]) people; 201 final int N = charSeqArray.length; 202 String[] array = new String[N]; 203 for (int i = 0; i < N; i++) { 204 array[i] = charSeqArray[i].toString(); 205 } 206 return array; 207 } 208 209 return null; 210 } 211 212 private LookupResult resolvePhoneContact(final String number) { 213 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 214 Uri.encode(number)); 215 return searchContacts(phoneUri); 216 } 217 218 private LookupResult resolveEmailContact(final String email) { 219 Uri numberUri = Uri.withAppendedPath( 220 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, 221 Uri.encode(email)); 222 return searchContacts(numberUri); 223 } 224 225 private LookupResult searchContacts(Uri lookupUri) { 226 LookupResult lookupResult = new LookupResult(); 227 Cursor c = null; 228 try { 229 c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null); 230 if (c != null && c.getCount() > 0) { 231 c.moveToFirst(); 232 lookupResult.readContact(c); 233 } 234 } catch(Throwable t) { 235 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 236 } finally { 237 if (c != null) { 238 c.close(); 239 } 240 } 241 return lookupResult; 242 } 243 244 public void initialize(Context context) { 245 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + "."); 246 mContext = context; 247 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE); 248 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt( 249 mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1); 250 } 251 252 public RankingReconsideration process(NotificationRecord record) { 253 if (!mEnabled) { 254 if (INFO) Slog.i(TAG, "disabled"); 255 return null; 256 } 257 if (record == null || record.getNotification() == null) { 258 if (INFO) Slog.i(TAG, "skipping empty notification"); 259 return null; 260 } 261 return validatePeople(record); 262 } 263 264 private static class LookupResult { 265 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr 266 public static final int INVALID_ID = -1; 267 268 private final long mExpireMillis; 269 private int mId; 270 private boolean mStarred; 271 272 public LookupResult() { 273 mId = INVALID_ID; 274 mStarred = false; 275 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 276 } 277 278 public void readContact(Cursor cursor) { 279 final int idIdx = cursor.getColumnIndex(Contacts._ID); 280 if (idIdx >= 0) { 281 mId = cursor.getInt(idIdx); 282 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId); 283 } else { 284 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID"); 285 } 286 final int starIdx = cursor.getColumnIndex(Contacts.STARRED); 287 if (starIdx >= 0) { 288 mStarred = cursor.getInt(starIdx) != 0; 289 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred); 290 } else { 291 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); 292 } 293 } 294 295 public boolean isExpired() { 296 return mExpireMillis < System.currentTimeMillis(); 297 } 298 299 public boolean isInvalid() { 300 return mId == INVALID_ID || isExpired(); 301 } 302 303 public float getAffinity() { 304 if (isInvalid()) { 305 return NONE; 306 } else if (mStarred) { 307 return STARRED_CONTACT; 308 } else { 309 return VALID_CONTACT; 310 } 311 } 312 313 public LookupResult setStarred(boolean starred) { 314 mStarred = starred; 315 return this; 316 } 317 318 public LookupResult setId(int id) { 319 mId = id; 320 return this; 321 } 322 } 323} 324 325