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