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