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