ValidateNotificationPeople.java revision 4ad99682dd726c6b5de9e9b34c0e8daaa52139ad
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        String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
143        if (people != null) {
144            return people;
145        }
146
147        ArrayList<String> stringArray = extras.getStringArrayList(Notification.EXTRA_PEOPLE);
148        if (stringArray != null) {
149            return (String[]) stringArray.toArray();
150        }
151
152        String string = extras.getString(Notification.EXTRA_PEOPLE);
153        if (string != null) {
154            people = new String[1];
155            people[0] = string;
156            return people;
157        }
158        char[] charArray = extras.getCharArray(Notification.EXTRA_PEOPLE);
159        if (charArray != null) {
160            people = new String[1];
161            people[0] = new String(charArray);
162            return people;
163        }
164
165        CharSequence charSeq = extras.getCharSequence(Notification.EXTRA_PEOPLE);
166        if (charSeq != null) {
167            people = new String[1];
168            people[0] = charSeq.toString();
169            return people;
170        }
171
172        CharSequence[] charSeqArray = extras.getCharSequenceArray(Notification.EXTRA_PEOPLE);
173        if (charSeqArray != null) {
174            final int N = charSeqArray.length;
175            people = new String[N];
176            for (int i = 0; i < N; i++) {
177                people[i] = charSeqArray[i].toString();
178            }
179            return people;
180        }
181
182        ArrayList<CharSequence> charSeqList =
183                extras.getCharSequenceArrayList(Notification.EXTRA_PEOPLE);
184        if (charSeqList != null) {
185            final int N = charSeqList.size();
186            people = new String[N];
187            for (int i = 0; i < N; i++) {
188                people[i] = charSeqList.get(i).toString();
189            }
190            return people;
191        }
192        return null;
193    }
194
195    private LookupResult resolvePhoneContact(final String number) {
196        Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
197                Uri.encode(number));
198        return searchContacts(phoneUri);
199    }
200
201    private LookupResult resolveEmailContact(final String email) {
202        Uri numberUri = Uri.withAppendedPath(
203                ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
204                Uri.encode(email));
205        return searchContacts(numberUri);
206    }
207
208    private LookupResult searchContacts(Uri lookupUri) {
209        LookupResult lookupResult = new LookupResult();
210        Cursor c = null;
211        try {
212            c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
213            if (c != null && c.getCount() > 0) {
214                c.moveToFirst();
215                lookupResult.readContact(c);
216            }
217        } catch(Throwable t) {
218            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
219        } finally {
220            if (c != null) {
221                c.close();
222            }
223        }
224        return lookupResult;
225    }
226
227    public void initialize(Context context) {
228        if (DEBUG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
229        mContext = context;
230        mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
231        mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
232                mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
233    }
234
235    public RankingReconsideration process(NotificationRecord record) {
236        if (!mEnabled) {
237            if (INFO) Slog.i(TAG, "disabled");
238            return null;
239        }
240        if (record == null || record.getNotification() == null) {
241            if (INFO) Slog.i(TAG, "skipping empty notification");
242            return null;
243        }
244        return validatePeople(record);
245    }
246
247    private static class LookupResult {
248        private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000;  // 1hr
249        public static final int INVALID_ID = -1;
250
251        private final long mExpireMillis;
252        private int mId;
253        private boolean mStarred;
254
255        public LookupResult() {
256            mId = INVALID_ID;
257            mStarred = false;
258            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
259        }
260
261        public void readContact(Cursor cursor) {
262            final int idIdx = cursor.getColumnIndex(Contacts._ID);
263            if (idIdx >= 0) {
264                mId = cursor.getInt(idIdx);
265                if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId);
266            } else {
267                if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID");
268            }
269            final int starIdx = cursor.getColumnIndex(Contacts.STARRED);
270            if (starIdx >= 0) {
271                mStarred = cursor.getInt(starIdx) != 0;
272                if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred);
273            } else {
274                if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED");
275            }
276        }
277
278        public boolean isExpired() {
279            return mExpireMillis < System.currentTimeMillis();
280        }
281
282        public boolean isInvalid() {
283            return mId == INVALID_ID || isExpired();
284        }
285
286        public float getAffinity() {
287            if (isInvalid()) {
288                return NONE;
289            } else if (mStarred) {
290                return STARRED_CONTACT;
291            } else {
292                return VALID_CONTACT;
293            }
294        }
295
296        public LookupResult setStarred(boolean starred) {
297            mStarred = starred;
298            return this;
299        }
300
301        public LookupResult setId(int id) {
302            mId = id;
303            return this;
304        }
305    }
306}
307
308