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