blob: a629a5f7aae3ffa24bc12fc295bee03c4d1c5e99 [file] [log] [blame]
Chris Wrenf9536642014-04-17 10:01:54 -04001/*
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";
Chris Wren44d81a42014-05-14 17:38:05 -040048 private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED };
Chris Wrenf9536642014-04-17 10:01:54 -040049 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;
Chris Wren44d81a42014-05-14 17:38:05 -040054 private static final float STARRED_CONTACT = 1f;
Chris Wrenf9536642014-04-17 10:01:54 -040055
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
Chris Wren470c1ac2014-05-21 15:28:10 -040062 private RankingReconsideration validatePeople(final NotificationRecord record) {
Chris Wrenf9536642014-04-17 10:01:54 -040063 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());
Chris Wren470c1ac2014-05-21 15:28:10 -0400102 return new RankingReconsideration(record.getKey()) {
103 float mContactAffinity = NONE;
Chris Wrenf9536642014-04-17 10:01:54 -0400104 @Override
105 public void work() {
Chris Wren470c1ac2014-05-21 15:28:10 -0400106 if (INFO) Slog.i(TAG, "Executing: validation for: " + record.getKey());
Chris Wrenf9536642014-04-17 10:01:54 -0400107 for (final String handle: pendingLookups) {
Chris Wren44d81a42014-05-14 17:38:05 -0400108 LookupResult lookupResult = null;
Chris Wrenf9536642014-04-17 10:01:54 -0400109 final Uri uri = Uri.parse(handle);
110 if ("tel".equals(uri.getScheme())) {
111 if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
Chris Wren44d81a42014-05-14 17:38:05 -0400112 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());
Chris Wrenf9536642014-04-17 10:01:54 -0400116 } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
117 if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
Chris Wren44d81a42014-05-14 17:38:05 -0400118 lookupResult = searchContacts(uri);
Chris Wrenf9536642014-04-17 10:01:54 -0400119 } else {
Chris Wren44d81a42014-05-14 17:38:05 -0400120 lookupResult = new LookupResult(); // invalid person for the cache
Chris Wrenf9536642014-04-17 10:01:54 -0400121 Slog.w(TAG, "unsupported URI " + handle);
122 }
Chris Wren44d81a42014-05-14 17:38:05 -0400123 if (lookupResult != null) {
124 synchronized (mPeopleCache) {
125 mPeopleCache.put(handle, lookupResult);
126 }
Chris Wren470c1ac2014-05-21 15:28:10 -0400127 mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity());
Chris Wren44d81a42014-05-14 17:38:05 -0400128 }
Chris Wrenf9536642014-04-17 10:01:54 -0400129 }
Chris Wren470c1ac2014-05-21 15:28:10 -0400130 }
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());
Chris Wrenf9536642014-04-17 10:01:54 -0400137 }
138 };
139 }
140
141 private String[] getExtraPeople(Bundle extras) {
Chris Wrenfb69da32014-05-15 18:03:11 -0400142 Object people = extras.get(Notification.EXTRA_PEOPLE);
143 if (people instanceof String[]) {
144 return (String[]) people;
Chris Wrenf9536642014-04-17 10:01:54 -0400145 }
146
Chris Wrenfb69da32014-05-15 18:03:11 -0400147 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;
Chris Wrenf9536642014-04-17 10:01:54 -0400170 }
171
Chris Wrenfb69da32014-05-15 18:03:11 -0400172 if (people instanceof String) {
173 String[] array = new String[1];
174 array[0] = (String) people;
175 return array;
Chris Wrenf9536642014-04-17 10:01:54 -0400176 }
177
Chris Wrenfb69da32014-05-15 18:03:11 -0400178 if (people instanceof char[]) {
179 String[] array = new String[1];
180 array[0] = new String((char[]) people);
181 return array;
Chris Wrenf9536642014-04-17 10:01:54 -0400182 }
183
Chris Wrenfb69da32014-05-15 18:03:11 -0400184 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;
Chris Wrenf9536642014-04-17 10:01:54 -0400192 final int N = charSeqArray.length;
Chris Wrenfb69da32014-05-15 18:03:11 -0400193 String[] array = new String[N];
Chris Wrenf9536642014-04-17 10:01:54 -0400194 for (int i = 0; i < N; i++) {
Chris Wrenfb69da32014-05-15 18:03:11 -0400195 array[i] = charSeqArray[i].toString();
Chris Wrenf9536642014-04-17 10:01:54 -0400196 }
Chris Wrenfb69da32014-05-15 18:03:11 -0400197 return array;
Chris Wrenf9536642014-04-17 10:01:54 -0400198 }
199
Chris Wrenf9536642014-04-17 10:01:54 -0400200 return null;
201 }
202
Chris Wren44d81a42014-05-14 17:38:05 -0400203 private LookupResult resolvePhoneContact(final String number) {
204 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
205 Uri.encode(number));
206 return searchContacts(phoneUri);
Chris Wrenf9536642014-04-17 10:01:54 -0400207 }
208
Chris Wren44d81a42014-05-14 17:38:05 -0400209 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();
Chris Wrenf9536642014-04-17 10:01:54 -0400218 Cursor c = null;
219 try {
Chris Wren44d81a42014-05-14 17:38:05 -0400220 c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
Chris Wrenf9536642014-04-17 10:01:54 -0400221 if (c != null && c.getCount() > 0) {
222 c.moveToFirst();
Chris Wren44d81a42014-05-14 17:38:05 -0400223 lookupResult.readContact(c);
Chris Wrenf9536642014-04-17 10:01:54 -0400224 }
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 }
Chris Wrenf9536642014-04-17 10:01:54 -0400232 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
Chris Wren470c1ac2014-05-21 15:28:10 -0400243 public RankingReconsideration process(NotificationRecord record) {
Chris Wrenf9536642014-04-17 10:01:54 -0400244 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;
Chris Wren44d81a42014-05-14 17:38:05 -0400261 private boolean mStarred;
Chris Wrenf9536642014-04-17 10:01:54 -0400262
Chris Wren44d81a42014-05-14 17:38:05 -0400263 public LookupResult() {
264 mId = INVALID_ID;
265 mStarred = false;
Chris Wrenf9536642014-04-17 10:01:54 -0400266 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
267 }
268
Chris Wren44d81a42014-05-14 17:38:05 -0400269 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
Chris Wrenf9536642014-04-17 10:01:54 -0400286 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;
Chris Wren44d81a42014-05-14 17:38:05 -0400297 } else if (mStarred) {
298 return STARRED_CONTACT;
Chris Wrenf9536642014-04-17 10:01:54 -0400299 } else {
Chris Wren44d81a42014-05-14 17:38:05 -0400300 return VALID_CONTACT;
Chris Wrenf9536642014-04-17 10:01:54 -0400301 }
302 }
303
Chris Wren44d81a42014-05-14 17:38:05 -0400304 public LookupResult setStarred(boolean starred) {
305 mStarred = starred;
306 return this;
307 }
308
Chris Wrenf9536642014-04-17 10:01:54 -0400309 public LookupResult setId(int id) {
310 mId = id;
311 return this;
312 }
313 }
314}
315