ValidateNotificationPeople.java revision 7381daa0b99ef5beb224ffd2544a156af40e78d1
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.content.pm.PackageManager;
22import android.database.Cursor;
23import android.net.Uri;
24import android.os.Bundle;
25import android.os.UserHandle;
26import android.provider.ContactsContract;
27import android.provider.ContactsContract.Contacts;
28import android.provider.Settings;
29import android.text.TextUtils;
30import android.util.ArrayMap;
31import android.util.Log;
32import android.util.LruCache;
33import android.util.Slog;
34
35import java.util.ArrayList;
36import java.util.LinkedList;
37import java.util.Map;
38
39/**
40 * This {@link NotificationSignalExtractor} attempts to validate
41 * people references. Also elevates the priority of real people.
42 *
43 * {@hide}
44 */
45public class ValidateNotificationPeople implements NotificationSignalExtractor {
46    private static final String TAG = "ValidateNotificationPeople";
47    private static final boolean INFO = true;
48    private static final boolean DEBUG = false;
49
50    private static final boolean ENABLE_PEOPLE_VALIDATOR = true;
51    private static final String SETTING_ENABLE_PEOPLE_VALIDATOR =
52            "validate_notification_people_enabled";
53    private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED };
54    private static final int MAX_PEOPLE = 10;
55    private static final int PEOPLE_CACHE_SIZE = 200;
56
57    /** Indicates that the notification does not reference any valid contacts. */
58    static final float NONE = 0f;
59
60    /**
61     * Affinity will be equal to or greater than this value on notifications
62     * that reference a valid contact.
63     */
64    static final float VALID_CONTACT = 0.5f;
65
66    /**
67     * Affinity will be equal to or greater than this value on notifications
68     * that reference a starred contact.
69     */
70    static final float STARRED_CONTACT = 1f;
71
72    protected boolean mEnabled;
73    private Context mBaseContext;
74
75    // maps raw person handle to resolved person object
76    private LruCache<String, LookupResult> mPeopleCache;
77    private Map<Integer, Context> mUserToContextMap;
78
79    public void initialize(Context context) {
80        if (DEBUG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
81        mUserToContextMap = new ArrayMap<>();
82        mBaseContext = context;
83        mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
84        mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
85                mBaseContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
86    }
87
88    public RankingReconsideration process(NotificationRecord record) {
89        if (!mEnabled) {
90            if (INFO) Slog.i(TAG, "disabled");
91            return null;
92        }
93        if (record == null || record.getNotification() == null) {
94            if (INFO) Slog.i(TAG, "skipping empty notification");
95            return null;
96        }
97        if (record.getUserId() == UserHandle.USER_ALL) {
98            if (INFO) Slog.i(TAG, "skipping global notification");
99            return null;
100        }
101        Context context = getContextAsUser(record.getUser());
102        if (context == null) {
103            if (INFO) Slog.i(TAG, "skipping notification that lacks a context");
104            return null;
105        }
106        return validatePeople(context, record);
107    }
108
109    @Override
110    public void setConfig(RankingConfig config) {
111        // ignore: config has no relevant information yet.
112    }
113
114    public float getContactAffinity(UserHandle userHandle, Bundle extras) {
115        if (DEBUG) Slog.d(TAG, "checking affinity for " + userHandle);
116        if (extras == null) return NONE;
117        final String key = Long.toString(System.nanoTime());
118        final float[] affinityOut = new float[1];
119        Context context = getContextAsUser(userHandle);
120        if (context == null) {
121            return NONE;
122        }
123        final PeopleRankingReconsideration prr = validatePeople(context, key, extras, affinityOut);
124        float affinity = affinityOut[0];
125        if (prr != null) {
126            prr.work();
127            affinity = Math.max(prr.getContactAffinity(), affinity);
128        }
129        return affinity;
130    }
131
132    private Context getContextAsUser(UserHandle userHandle) {
133        Context context = mUserToContextMap.get(userHandle.getIdentifier());
134        if (context == null) {
135            try {
136                context = mBaseContext.createPackageContextAsUser("android", 0, userHandle);
137                mUserToContextMap.put(userHandle.getIdentifier(), context);
138            } catch (PackageManager.NameNotFoundException e) {
139                Log.e(TAG, "failed to create package context for lookups", e);
140            }
141        }
142        return context;
143    }
144
145    private RankingReconsideration validatePeople(Context context,
146            final NotificationRecord record) {
147        final String key = record.getKey();
148        final Bundle extras = record.getNotification().extras;
149        final float[] affinityOut = new float[1];
150        final RankingReconsideration rr = validatePeople(context, key, extras, affinityOut);
151        record.setContactAffinity(affinityOut[0]);
152        return rr;
153    }
154
155    private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras,
156            float[] affinityOut) {
157        float affinity = NONE;
158        if (extras == null) {
159            return null;
160        }
161
162        final String[] people = getExtraPeople(extras);
163        if (people == null || people.length == 0) {
164            return null;
165        }
166
167        if (INFO) Slog.i(TAG, "Validating: " + key);
168        final LinkedList<String> pendingLookups = new LinkedList<String>();
169        for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
170            final String handle = people[personIdx];
171            if (TextUtils.isEmpty(handle)) continue;
172
173            synchronized (mPeopleCache) {
174                final String cacheKey = getCacheKey(context.getUserId(), handle);
175                LookupResult lookupResult = mPeopleCache.get(cacheKey);
176                if (lookupResult == null || lookupResult.isExpired()) {
177                    pendingLookups.add(handle);
178                } else {
179                    if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId);
180                }
181                if (lookupResult != null) {
182                    affinity = Math.max(affinity, lookupResult.getAffinity());
183                }
184            }
185        }
186
187        // record the best available data, so far:
188        affinityOut[0] = affinity;
189
190        if (pendingLookups.isEmpty()) {
191            if (INFO) Slog.i(TAG, "final affinity: " + affinity);
192            return null;
193        }
194
195        if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + key);
196        return new PeopleRankingReconsideration(context, key, pendingLookups);
197    }
198
199    private String getCacheKey(int userId, String handle) {
200        return Integer.toString(userId) + ":" + handle;
201    }
202
203    // VisibleForTesting
204    public static String[] getExtraPeople(Bundle extras) {
205        Object people = extras.get(Notification.EXTRA_PEOPLE);
206        if (people instanceof String[]) {
207            return (String[]) people;
208        }
209
210        if (people instanceof ArrayList) {
211            ArrayList arrayList = (ArrayList) people;
212
213            if (arrayList.isEmpty()) {
214                return null;
215            }
216
217            if (arrayList.get(0) instanceof String) {
218                ArrayList<String> stringArray = (ArrayList<String>) arrayList;
219                return stringArray.toArray(new String[stringArray.size()]);
220            }
221
222            if (arrayList.get(0) instanceof CharSequence) {
223                ArrayList<CharSequence> charSeqList = (ArrayList<CharSequence>) arrayList;
224                final int N = charSeqList.size();
225                String[] array = new String[N];
226                for (int i = 0; i < N; i++) {
227                    array[i] = charSeqList.get(i).toString();
228                }
229                return array;
230            }
231
232            return null;
233        }
234
235        if (people instanceof String) {
236            String[] array = new String[1];
237            array[0] = (String) people;
238            return array;
239        }
240
241        if (people instanceof char[]) {
242            String[] array = new String[1];
243            array[0] = new String((char[]) people);
244            return array;
245        }
246
247        if (people instanceof CharSequence) {
248            String[] array = new String[1];
249            array[0] = ((CharSequence) people).toString();
250            return array;
251        }
252
253        if (people instanceof CharSequence[]) {
254            CharSequence[] charSeqArray = (CharSequence[]) people;
255            final int N = charSeqArray.length;
256            String[] array = new String[N];
257            for (int i = 0; i < N; i++) {
258                array[i] = charSeqArray[i].toString();
259            }
260            return array;
261        }
262
263        return null;
264    }
265
266    private LookupResult resolvePhoneContact(Context context, final String number) {
267        Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
268                Uri.encode(number));
269        return searchContacts(context, phoneUri);
270    }
271
272    private LookupResult resolveEmailContact(Context context, final String email) {
273        Uri numberUri = Uri.withAppendedPath(
274                ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
275                Uri.encode(email));
276        return searchContacts(context, numberUri);
277    }
278
279    private LookupResult searchContacts(Context context, Uri lookupUri) {
280        LookupResult lookupResult = new LookupResult();
281        Cursor c = null;
282        try {
283            c = context.getContentResolver().query(lookupUri, LOOKUP_PROJECTION, null, null, null);
284            if (c != null && c.getCount() > 0) {
285                c.moveToFirst();
286                lookupResult.readContact(c);
287            }
288        } catch(Throwable t) {
289            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
290        } finally {
291            if (c != null) {
292                c.close();
293            }
294        }
295        return lookupResult;
296    }
297
298    private static class LookupResult {
299        private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000;  // 1hr
300        public static final int INVALID_ID = -1;
301
302        private final long mExpireMillis;
303        private int mId;
304        private boolean mStarred;
305
306        public LookupResult() {
307            mId = INVALID_ID;
308            mStarred = false;
309            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
310        }
311
312        public void readContact(Cursor cursor) {
313            final int idIdx = cursor.getColumnIndex(Contacts._ID);
314            if (idIdx >= 0) {
315                mId = cursor.getInt(idIdx);
316                if (DEBUG) Slog.d(TAG, "contact _ID is: " + mId);
317            } else {
318                if (DEBUG) Slog.d(TAG, "invalid cursor: no _ID");
319            }
320            final int starIdx = cursor.getColumnIndex(Contacts.STARRED);
321            if (starIdx >= 0) {
322                mStarred = cursor.getInt(starIdx) != 0;
323                if (DEBUG) Slog.d(TAG, "contact STARRED is: " + mStarred);
324            } else {
325                if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED");
326            }
327        }
328
329        public boolean isExpired() {
330            return mExpireMillis < System.currentTimeMillis();
331        }
332
333        public boolean isInvalid() {
334            return mId == INVALID_ID || isExpired();
335        }
336
337        public float getAffinity() {
338            if (isInvalid()) {
339                return NONE;
340            } else if (mStarred) {
341                return STARRED_CONTACT;
342            } else {
343                return VALID_CONTACT;
344            }
345        }
346
347        public LookupResult setStarred(boolean starred) {
348            mStarred = starred;
349            return this;
350        }
351
352        public LookupResult setId(int id) {
353            mId = id;
354            return this;
355        }
356    }
357
358    private class PeopleRankingReconsideration extends RankingReconsideration {
359        private final LinkedList<String> mPendingLookups;
360        private final Context mContext;
361
362        private float mContactAffinity = NONE;
363
364        private PeopleRankingReconsideration(Context context, String key, LinkedList<String> pendingLookups) {
365            super(key);
366            mContext = context;
367            mPendingLookups = pendingLookups;
368        }
369
370        @Override
371        public void work() {
372            if (INFO) Slog.i(TAG, "Executing: validation for: " + mKey);
373            for (final String handle: mPendingLookups) {
374                LookupResult lookupResult = null;
375                final Uri uri = Uri.parse(handle);
376                if ("tel".equals(uri.getScheme())) {
377                    if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
378                    lookupResult = resolvePhoneContact(mContext, uri.getSchemeSpecificPart());
379                } else if ("mailto".equals(uri.getScheme())) {
380                    if (DEBUG) Slog.d(TAG, "checking mailto URI: " + handle);
381                    lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart());
382                } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
383                    if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
384                    lookupResult = searchContacts(mContext, uri);
385                } else {
386                    lookupResult = new LookupResult();  // invalid person for the cache
387                    Slog.w(TAG, "unsupported URI " + handle);
388                }
389                if (lookupResult != null) {
390                    synchronized (mPeopleCache) {
391                        final String cacheKey = getCacheKey(mContext.getUserId(), handle);
392                        mPeopleCache.put(cacheKey, lookupResult);
393                    }
394                    mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity());
395                }
396            }
397        }
398
399        @Override
400        public void applyChangesLocked(NotificationRecord operand) {
401            float affinityBound = operand.getContactAffinity();
402            operand.setContactAffinity(Math.max(mContactAffinity, affinityBound));
403            if (INFO) Slog.i(TAG, "final affinity: " + operand.getContactAffinity());
404        }
405
406        public float getContactAffinity() {
407            return mContactAffinity;
408        }
409    }
410}
411
412