ValidateNotificationPeople.java revision 54bbef435ed857fc68941672799fc8001c101119
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 java.util.ArrayList;
32import java.util.LinkedList;
33
34/**
35 * This {@link NotificationSignalExtractor} attempts to validate
36 * people references. Also elevates the priority of real people.
37 *
38 * {@hide}
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    /** Indicates that the notification does not reference any valid contacts. */
53    static final float NONE = 0f;
54
55    /**
56     * Affinity will be equal to or greater than this value on notifications
57     * that reference a valid contact.
58     */
59    static final float VALID_CONTACT = 0.5f;
60
61    /**
62     * Affinity will be equal to or greater than this value on notifications
63     * that reference a starred contact.
64     */
65    static final float STARRED_CONTACT = 1f;
66
67    protected boolean mEnabled;
68    private Context mContext;
69
70    // maps raw person handle to resolved person object
71    private LruCache<String, LookupResult> mPeopleCache;
72
73    private RankingReconsideration validatePeople(final NotificationRecord record) {
74        float affinity = NONE;
75        Bundle extras = record.getNotification().extras;
76        if (extras == null) {
77            return null;
78        }
79
80        final String[] people = getExtraPeople(extras);
81        if (people == null || people.length == 0) {
82            return null;
83        }
84
85        if (INFO) Slog.i(TAG, "Validating: " + record.sbn.getKey());
86        final LinkedList<String> pendingLookups = new LinkedList<String>();
87        for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
88            final String handle = people[personIdx];
89            if (TextUtils.isEmpty(handle)) continue;
90
91            synchronized (mPeopleCache) {
92                LookupResult lookupResult = mPeopleCache.get(handle);
93                if (lookupResult == null || lookupResult.isExpired()) {
94                    pendingLookups.add(handle);
95                } else {
96                    if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId);
97                }
98                if (lookupResult != null) {
99                    affinity = Math.max(affinity, lookupResult.getAffinity());
100                }
101            }
102        }
103
104        // record the best available data, so far:
105        record.setContactAffinity(affinity);
106
107        if (pendingLookups.isEmpty()) {
108            if (INFO) Slog.i(TAG, "final affinity: " + affinity);
109            return null;
110        }
111
112        if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey());
113        return new RankingReconsideration(record.getKey()) {
114            float mContactAffinity = NONE;
115            @Override
116            public void work() {
117                if (INFO) Slog.i(TAG, "Executing: validation for: " + record.getKey());
118                for (final String handle: pendingLookups) {
119                    LookupResult lookupResult = null;
120                    final Uri uri = Uri.parse(handle);
121                    if ("tel".equals(uri.getScheme())) {
122                        if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
123                        lookupResult = resolvePhoneContact(uri.getSchemeSpecificPart());
124                    } else if ("mailto".equals(uri.getScheme())) {
125                        if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle);
126                        lookupResult = resolveEmailContact(uri.getSchemeSpecificPart());
127                    } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
128                        if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
129                        lookupResult = searchContacts(uri);
130                    } else {
131                        lookupResult = new LookupResult();  // invalid person for the cache
132                        Slog.w(TAG, "unsupported URI " + handle);
133                    }
134                    if (lookupResult != null) {
135                        synchronized (mPeopleCache) {
136                            mPeopleCache.put(handle, lookupResult);
137                        }
138                        mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity());
139                    }
140                }
141            }
142
143            @Override
144            public void applyChangesLocked(NotificationRecord operand) {
145                float affinityBound = operand.getContactAffinity();
146                operand.setContactAffinity(Math.max(mContactAffinity, affinityBound));
147                if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity());
148            }
149        };
150    }
151
152    // VisibleForTesting
153    public static String[] getExtraPeople(Bundle extras) {
154        Object people = extras.get(Notification.EXTRA_PEOPLE);
155        if (people instanceof String[]) {
156            return (String[]) people;
157        }
158
159        if (people instanceof ArrayList) {
160            ArrayList arrayList = (ArrayList) people;
161
162            if (arrayList.isEmpty()) {
163                return null;
164            }
165
166            if (arrayList.get(0) instanceof String) {
167                ArrayList<String> stringArray = (ArrayList<String>) arrayList;
168                return stringArray.toArray(new String[stringArray.size()]);
169            }
170
171            if (arrayList.get(0) instanceof CharSequence) {
172                ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList;
173                final int N = charSeqList.size();
174                String[] array = new String[N];
175                for (int i = 0; i < N; i++) {
176                    array[i] = charSeqList.get(i).toString();
177                }
178                return array;
179            }
180
181            return null;
182        }
183
184        if (people instanceof String) {
185            String[] array = new String[1];
186            array[0] = (String) people;
187            return array;
188        }
189
190        if (people instanceof char[]) {
191            String[] array = new String[1];
192            array[0] = new String((char[]) people);
193            return array;
194        }
195
196        if (people instanceof CharSequence) {
197            String[] array = new String[1];
198            array[0] = ((CharSequence) people).toString();
199            return array;
200        }
201
202        if (people instanceof CharSequence[]) {
203            CharSequence[] charSeqArray = (CharSequence[]) people;
204            final int N = charSeqArray.length;
205            String[] array = new String[N];
206            for (int i = 0; i < N; i++) {
207                array[i] = charSeqArray[i].toString();
208            }
209            return array;
210        }
211
212        return null;
213    }
214
215    private LookupResult resolvePhoneContact(final String number) {
216        Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
217                Uri.encode(number));
218        return searchContacts(phoneUri);
219    }
220
221    private LookupResult resolveEmailContact(final String email) {
222        Uri numberUri = Uri.withAppendedPath(
223                ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
224                Uri.encode(email));
225        return searchContacts(numberUri);
226    }
227
228    private LookupResult searchContacts(Uri lookupUri) {
229        LookupResult lookupResult = new LookupResult();
230        Cursor c = null;
231        try {
232            c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
233            if (c != null && c.getCount() > 0) {
234                c.moveToFirst();
235                lookupResult.readContact(c);
236            }
237        } catch(Throwable t) {
238            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
239        } finally {
240            if (c != null) {
241                c.close();
242            }
243        }
244        return lookupResult;
245    }
246
247    public void initialize(Context context) {
248        if (DEBUG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
249        mContext = context;
250        mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
251        mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
252                mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
253    }
254
255    public RankingReconsideration process(NotificationRecord record) {
256        if (!mEnabled) {
257            if (INFO) Slog.i(TAG, "disabled");
258            return null;
259        }
260        if (record == null || record.getNotification() == null) {
261            if (INFO) Slog.i(TAG, "skipping empty notification");
262            return null;
263        }
264        return validatePeople(record);
265    }
266
267    @Override
268    public void setConfig(RankingConfig config) {
269        // ignore: config has no relevant information yet.
270    }
271
272    private static class LookupResult {
273        private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000;  // 1hr
274        public static final int INVALID_ID = -1;
275
276        private final long mExpireMillis;
277        private int mId;
278        private boolean mStarred;
279
280        public LookupResult() {
281            mId = INVALID_ID;
282            mStarred = false;
283            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
284        }
285
286        public void readContact(Cursor cursor) {
287            final int idIdx = cursor.getColumnIndex(Contacts._ID);
288            if (idIdx >= 0) {
289                mId = cursor.getInt(idIdx);
290                if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId);
291            } else {
292                if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID");
293            }
294            final int starIdx = cursor.getColumnIndex(Contacts.STARRED);
295            if (starIdx >= 0) {
296                mStarred = cursor.getInt(starIdx) != 0;
297                if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred);
298            } else {
299                if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED");
300            }
301        }
302
303        public boolean isExpired() {
304            return mExpireMillis < System.currentTimeMillis();
305        }
306
307        public boolean isInvalid() {
308            return mId == INVALID_ID || isExpired();
309        }
310
311        public float getAffinity() {
312            if (isInvalid()) {
313                return NONE;
314            } else if (mStarred) {
315                return STARRED_CONTACT;
316            } else {
317                return VALID_CONTACT;
318            }
319        }
320
321        public LookupResult setStarred(boolean starred) {
322            mStarred = starred;
323            return this;
324        }
325
326        public LookupResult setId(int id) {
327            mId = id;
328            return this;
329        }
330    }
331}
332
333