blob: b5c2730a341cd978a32e2445be31dbeab4521370 [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
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;
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 }
127 affinity = Math.max(affinity, lookupResult.getAffinity());
128 }
Chris Wrenf9536642014-04-17 10:01:54 -0400129 }
Chris Wrenf9536642014-04-17 10:01:54 -0400130 float affinityBound = mRecord.getContactAffinity();
131 affinity = Math.max(affinity, affinityBound);
132 mRecord.setContactAffinity(affinity);
133 if (INFO) Slog.i(TAG, "final affinity: " + affinity);
134 }
135 };
136 }
137
138 private String[] getExtraPeople(Bundle extras) {
139 String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
140 if (people != null) {
141 return people;
142 }
143
144 ArrayList<String> stringArray = extras.getStringArrayList(Notification.EXTRA_PEOPLE);
145 if (stringArray != null) {
146 return (String[]) stringArray.toArray();
147 }
148
149 String string = extras.getString(Notification.EXTRA_PEOPLE);
150 if (string != null) {
151 people = new String[1];
152 people[0] = string;
153 return people;
154 }
155 char[] charArray = extras.getCharArray(Notification.EXTRA_PEOPLE);
156 if (charArray != null) {
157 people = new String[1];
158 people[0] = new String(charArray);
159 return people;
160 }
161
162 CharSequence charSeq = extras.getCharSequence(Notification.EXTRA_PEOPLE);
163 if (charSeq != null) {
164 people = new String[1];
165 people[0] = charSeq.toString();
166 return people;
167 }
168
169 CharSequence[] charSeqArray = extras.getCharSequenceArray(Notification.EXTRA_PEOPLE);
170 if (charSeqArray != null) {
171 final int N = charSeqArray.length;
172 people = new String[N];
173 for (int i = 0; i < N; i++) {
174 people[i] = charSeqArray[i].toString();
175 }
176 return people;
177 }
178
179 ArrayList<CharSequence> charSeqList =
180 extras.getCharSequenceArrayList(Notification.EXTRA_PEOPLE);
181 if (charSeqList != null) {
182 final int N = charSeqList.size();
183 people = new String[N];
184 for (int i = 0; i < N; i++) {
185 people[i] = charSeqList.get(i).toString();
186 }
187 return people;
188 }
189 return null;
190 }
191
Chris Wren44d81a42014-05-14 17:38:05 -0400192 private LookupResult resolvePhoneContact(final String number) {
193 Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
194 Uri.encode(number));
195 return searchContacts(phoneUri);
Chris Wrenf9536642014-04-17 10:01:54 -0400196 }
197
Chris Wren44d81a42014-05-14 17:38:05 -0400198 private LookupResult resolveEmailContact(final String email) {
199 Uri numberUri = Uri.withAppendedPath(
200 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
201 Uri.encode(email));
202 return searchContacts(numberUri);
203 }
204
205 private LookupResult searchContacts(Uri lookupUri) {
206 LookupResult lookupResult = new LookupResult();
Chris Wrenf9536642014-04-17 10:01:54 -0400207 Cursor c = null;
208 try {
Chris Wren44d81a42014-05-14 17:38:05 -0400209 c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
Chris Wrenf9536642014-04-17 10:01:54 -0400210 if (c != null && c.getCount() > 0) {
211 c.moveToFirst();
Chris Wren44d81a42014-05-14 17:38:05 -0400212 lookupResult.readContact(c);
Chris Wrenf9536642014-04-17 10:01:54 -0400213 }
214 } catch(Throwable t) {
215 Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
216 } finally {
217 if (c != null) {
218 c.close();
219 }
220 }
Chris Wrenf9536642014-04-17 10:01:54 -0400221 return lookupResult;
222 }
223
224 public void initialize(Context context) {
225 if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + ".");
226 mContext = context;
227 mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
228 mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
229 mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
230 }
231
232 public RankingFuture process(NotificationManagerService.NotificationRecord record) {
233 if (!mEnabled) {
234 if (INFO) Slog.i(TAG, "disabled");
235 return null;
236 }
237 if (record == null || record.getNotification() == null) {
238 if (INFO) Slog.i(TAG, "skipping empty notification");
239 return null;
240 }
241 return validatePeople(record);
242 }
243
244 private static class LookupResult {
245 private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr
246 public static final int INVALID_ID = -1;
247
248 private final long mExpireMillis;
249 private int mId;
Chris Wren44d81a42014-05-14 17:38:05 -0400250 private boolean mStarred;
Chris Wrenf9536642014-04-17 10:01:54 -0400251
Chris Wren44d81a42014-05-14 17:38:05 -0400252 public LookupResult() {
253 mId = INVALID_ID;
254 mStarred = false;
Chris Wrenf9536642014-04-17 10:01:54 -0400255 mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
256 }
257
Chris Wren44d81a42014-05-14 17:38:05 -0400258 public void readContact(Cursor cursor) {
259 final int idIdx = cursor.getColumnIndex(Contacts._ID);
260 if (idIdx >= 0) {
261 mId = cursor.getInt(idIdx);
262 if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId);
263 } else {
264 if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID");
265 }
266 final int starIdx = cursor.getColumnIndex(Contacts.STARRED);
267 if (starIdx >= 0) {
268 mStarred = cursor.getInt(starIdx) != 0;
269 if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred);
270 } else {
271 if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED");
272 }
273 }
274
Chris Wrenf9536642014-04-17 10:01:54 -0400275 public boolean isExpired() {
276 return mExpireMillis < System.currentTimeMillis();
277 }
278
279 public boolean isInvalid() {
280 return mId == INVALID_ID || isExpired();
281 }
282
283 public float getAffinity() {
284 if (isInvalid()) {
285 return NONE;
Chris Wren44d81a42014-05-14 17:38:05 -0400286 } else if (mStarred) {
287 return STARRED_CONTACT;
Chris Wrenf9536642014-04-17 10:01:54 -0400288 } else {
Chris Wren44d81a42014-05-14 17:38:05 -0400289 return VALID_CONTACT;
Chris Wrenf9536642014-04-17 10:01:54 -0400290 }
291 }
292
Chris Wren44d81a42014-05-14 17:38:05 -0400293 public LookupResult setStarred(boolean starred) {
294 mStarred = starred;
295 return this;
296 }
297
Chris Wrenf9536642014-04-17 10:01:54 -0400298 public LookupResult setId(int id) {
299 mId = id;
300 return this;
301 }
302 }
303}
304