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