ValidateNotificationPeople.java revision 44d81a4b86f73ecf79861bc576c90b8f8e4ba761
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 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                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                        affinity = Math.max(affinity, lookupResult.getAffinity());
128                    }
129                }
130                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
192    private LookupResult resolvePhoneContact(final String number) {
193        Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
194                Uri.encode(number));
195        return searchContacts(phoneUri);
196    }
197
198    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();
207        Cursor c = null;
208        try {
209            c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
210            if (c != null && c.getCount() > 0) {
211                c.moveToFirst();
212                lookupResult.readContact(c);
213            }
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        }
221        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;
250        private boolean mStarred;
251
252        public LookupResult() {
253            mId = INVALID_ID;
254            mStarred = false;
255            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
256        }
257
258        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
275        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;
286            } else if (mStarred) {
287                return STARRED_CONTACT;
288            } else {
289                return VALID_CONTACT;
290            }
291        }
292
293        public LookupResult setStarred(boolean starred) {
294            mStarred = starred;
295            return this;
296        }
297
298        public LookupResult setId(int id) {
299            mId = id;
300            return this;
301        }
302    }
303}
304
305