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