ValidateNotificationPeople.java revision f953664dc17dca23bd724bd64f89189c16c83263
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 }; 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 // TODO 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 RankingFuture validatePeople(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 RankingFuture(record) { 103 @Override 104 public void work() { 105 if (INFO) Slog.i(TAG, "Executing: validation for: " + mRecord.sbn.getKey()); 106 float affinity = NONE; 107 LookupResult lookupResult = null; 108 for (final String handle: pendingLookups) { 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(handle, uri.getSchemeSpecificPart()); 113 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 114 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); 115 lookupResult = resolveContactsUri(handle, uri); 116 } else { 117 Slog.w(TAG, "unsupported URI " + handle); 118 } 119 } 120 if (lookupResult != null) { 121 affinity = Math.max(affinity, lookupResult.getAffinity()); 122 } 123 124 float affinityBound = mRecord.getContactAffinity(); 125 affinity = Math.max(affinity, affinityBound); 126 mRecord.setContactAffinity(affinity); 127 if (INFO) Slog.i(TAG, "final affinity: " + affinity); 128 } 129 }; 130 } 131 132 private String[] getExtraPeople(Bundle extras) { 133 String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE); 134 if (people != null) { 135 return people; 136 } 137 138 ArrayList<String> stringArray = extras.getStringArrayList(Notification.EXTRA_PEOPLE); 139 if (stringArray != null) { 140 return (String[]) stringArray.toArray(); 141 } 142 143 String string = extras.getString(Notification.EXTRA_PEOPLE); 144 if (string != null) { 145 people = new String[1]; 146 people[0] = string; 147 return people; 148 } 149 char[] charArray = extras.getCharArray(Notification.EXTRA_PEOPLE); 150 if (charArray != null) { 151 people = new String[1]; 152 people[0] = new String(charArray); 153 return people; 154 } 155 156 CharSequence charSeq = extras.getCharSequence(Notification.EXTRA_PEOPLE); 157 if (charSeq != null) { 158 people = new String[1]; 159 people[0] = charSeq.toString(); 160 return people; 161 } 162 163 CharSequence[] charSeqArray = extras.getCharSequenceArray(Notification.EXTRA_PEOPLE); 164 if (charSeqArray != null) { 165 final int N = charSeqArray.length; 166 people = new String[N]; 167 for (int i = 0; i < N; i++) { 168 people[i] = charSeqArray[i].toString(); 169 } 170 return people; 171 } 172 173 ArrayList<CharSequence> charSeqList = 174 extras.getCharSequenceArrayList(Notification.EXTRA_PEOPLE); 175 if (charSeqList != null) { 176 final int N = charSeqList.size(); 177 people = new String[N]; 178 for (int i = 0; i < N; i++) { 179 people[i] = charSeqList.get(i).toString(); 180 } 181 return people; 182 } 183 return null; 184 } 185 186 private LookupResult resolvePhoneContact(final String handle, final String number) { 187 LookupResult lookupResult = null; 188 Cursor c = null; 189 try { 190 Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, 191 Uri.encode(number)); 192 c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null); 193 if (c != null && c.getCount() > 0) { 194 c.moveToFirst(); 195 final int idIdx = c.getColumnIndex(Contacts._ID); 196 final int id = c.getInt(idIdx); 197 if (DEBUG) Slog.d(TAG, "is valid: " + id); 198 lookupResult = new LookupResult(id); 199 } 200 } catch(Throwable t) { 201 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); 202 } finally { 203 if (c != null) { 204 c.close(); 205 } 206 } 207 if (lookupResult == null) { 208 lookupResult = new LookupResult(LookupResult.INVALID_ID); 209 } 210 synchronized (mPeopleCache) { 211 mPeopleCache.put(handle, lookupResult); 212 } 213 return lookupResult; 214 } 215 216 private LookupResult resolveContactsUri(String handle, final Uri personUri) { 217 LookupResult lookupResult = null; 218 Cursor c = null; 219 try { 220 c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null); 221 if (c != null && c.getCount() > 0) { 222 c.moveToFirst(); 223 final int idIdx = c.getColumnIndex(Contacts._ID); 224 final int id = c.getInt(idIdx); 225 if (DEBUG) Slog.d(TAG, "is valid: " + id); 226 lookupResult = new LookupResult(id); 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 if (lookupResult == null) { 236 lookupResult = new LookupResult(LookupResult.INVALID_ID); 237 } 238 synchronized (mPeopleCache) { 239 mPeopleCache.put(handle, lookupResult); 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 RankingFuture process(NotificationManagerService.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 271 public LookupResult(int id) { 272 mId = id; 273 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; 274 } 275 276 public boolean isExpired() { 277 return mExpireMillis < System.currentTimeMillis(); 278 } 279 280 public boolean isInvalid() { 281 return mId == INVALID_ID || isExpired(); 282 } 283 284 public float getAffinity() { 285 if (isInvalid()) { 286 return NONE; 287 } else { 288 return VALID_CONTACT; // TODO: finer grained result: stars 289 } 290 } 291 292 public LookupResult setId(int id) { 293 mId = id; 294 return this; 295 } 296 } 297} 298 299