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