ValidateNotificationPeople.java revision fb69da323b97ae4712054d37fb2f7d54cddb7dbd
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        Object people = extras.get(Notification.EXTRA_PEOPLE);
143        if (people instanceof String[]) {
144            return (String[]) people;
145        }
146
147        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;
170        }
171
172        if (people instanceof String) {
173            String[] array = new String[1];
174            array[0] = (String) people;
175            return array;
176        }
177
178        if (people instanceof char[]) {
179            String[] array = new String[1];
180            array[0] = new String((char[]) people);
181            return array;
182        }
183
184        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;
192            final int N = charSeqArray.length;
193            String[] array = new String[N];
194            for (int i = 0; i < N; i++) {
195                array[i] = charSeqArray[i].toString();
196            }
197            return array;
198        }
199
200        return null;
201    }
202
203    private LookupResult resolvePhoneContact(final String number) {
204        Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
205                Uri.encode(number));
206        return searchContacts(phoneUri);
207    }
208
209    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();
218        Cursor c = null;
219        try {
220            c = mContext.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
221            if (c != null && c.getCount() > 0) {
222                c.moveToFirst();
223                lookupResult.readContact(c);
224            }
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        }
232        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
243    public RankingReconsideration process(NotificationRecord record) {
244        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;
261        private boolean mStarred;
262
263        public LookupResult() {
264            mId = INVALID_ID;
265            mStarred = false;
266            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
267        }
268
269        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
286        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;
297            } else if (mStarred) {
298                return STARRED_CONTACT;
299            } else {
300                return VALID_CONTACT;
301            }
302        }
303
304        public LookupResult setStarred(boolean starred) {
305            mStarred = starred;
306            return this;
307        }
308
309        public LookupResult setId(int id) {
310            mId = id;
311            return this;
312        }
313    }
314}
315
316