ValidateNotificationPeople.java revision f953664dc17dca23bd724bd64f89189c16c83263
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 */
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 };
49    private static final int MAX_PEOPLE = 10;
50    private static final int PEOPLE_CACHE_SIZE = 200;
51
52    private static final float NONE = 0f;
53    private static final float VALID_CONTACT = 0.5f;
54    // TODO private static final float STARRED_CONTACT = 1f;
55
56    protected boolean mEnabled;
57    private Context mContext;
58
59    // maps raw person handle to resolved person object
60    private LruCache<String, LookupResult> mPeopleCache;
61
62    private RankingFuture validatePeople(NotificationRecord record) {
63        float affinity = NONE;
64        Bundle extras = record.getNotification().extras;
65        if (extras == null) {
66            return null;
67        }
68
69        final String[] people = getExtraPeople(extras);
70        if (people == null || people.length == 0) {
71            return null;
72        }
73
74        if (INFO) Slog.i(TAG, "Validating: " + record.sbn.getKey());
75        final LinkedList<String> pendingLookups = new LinkedList<String>();
76        for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
77            final String handle = people[personIdx];
78            if (TextUtils.isEmpty(handle)) continue;
79
80            synchronized (mPeopleCache) {
81                LookupResult lookupResult = mPeopleCache.get(handle);
82                if (lookupResult == null || lookupResult.isExpired()) {
83                    pendingLookups.add(handle);
84                } else {
85                    if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId);
86                }
87                if (lookupResult != null) {
88                    affinity = Math.max(affinity, lookupResult.getAffinity());
89                }
90            }
91        }
92
93        // record the best available data, so far:
94        record.setContactAffinity(affinity);
95
96        if (pendingLookups.isEmpty()) {
97            if (INFO) Slog.i(TAG, "final affinity: " + affinity);
98            return null;
99        }
100
101        if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey());
102        return new RankingFuture(record) {
103            @Override
104            public void work() {
105                if (INFO) Slog.i(TAG, "Executing: validation for: " + mRecord.sbn.getKey());
106                float affinity = NONE;
107                LookupResult lookupResult = null;
108                for (final String handle: pendingLookups) {
109                    final Uri uri = Uri.parse(handle);
110                    if ("tel".equals(uri.getScheme())) {
111                        if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
112                        lookupResult = resolvePhoneContact(handle, uri.getSchemeSpecificPart());
113                    } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
114                        if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
115                        lookupResult = resolveContactsUri(handle, uri);
116                    } else {
117                        Slog.w(TAG, "unsupported URI " + handle);
118                    }
119                }
120                if (lookupResult != null) {
121                    affinity = Math.max(affinity, lookupResult.getAffinity());
122                }
123
124                float affinityBound = mRecord.getContactAffinity();
125                affinity = Math.max(affinity, affinityBound);
126                mRecord.setContactAffinity(affinity);
127                if (INFO) Slog.i(TAG, "final affinity: " + affinity);
128            }
129        };
130    }
131
132    private String[] getExtraPeople(Bundle extras) {
133        String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
134        if (people != null) {
135            return people;
136        }
137
138        ArrayList<String> stringArray = extras.getStringArrayList(Notification.EXTRA_PEOPLE);
139        if (stringArray != null) {
140            return (String[]) stringArray.toArray();
141        }
142
143        String string = extras.getString(Notification.EXTRA_PEOPLE);
144        if (string != null) {
145            people = new String[1];
146            people[0] = string;
147            return people;
148        }
149        char[] charArray = extras.getCharArray(Notification.EXTRA_PEOPLE);
150        if (charArray != null) {
151            people = new String[1];
152            people[0] = new String(charArray);
153            return people;
154        }
155
156        CharSequence charSeq = extras.getCharSequence(Notification.EXTRA_PEOPLE);
157        if (charSeq != null) {
158            people = new String[1];
159            people[0] = charSeq.toString();
160            return people;
161        }
162
163        CharSequence[] charSeqArray = extras.getCharSequenceArray(Notification.EXTRA_PEOPLE);
164        if (charSeqArray != null) {
165            final int N = charSeqArray.length;
166            people = new String[N];
167            for (int i = 0; i < N; i++) {
168                people[i] = charSeqArray[i].toString();
169            }
170            return people;
171        }
172
173        ArrayList<CharSequence> charSeqList =
174                extras.getCharSequenceArrayList(Notification.EXTRA_PEOPLE);
175        if (charSeqList != null) {
176            final int N = charSeqList.size();
177            people = new String[N];
178            for (int i = 0; i < N; i++) {
179                people[i] = charSeqList.get(i).toString();
180            }
181            return people;
182        }
183        return null;
184    }
185
186    private LookupResult resolvePhoneContact(final String handle, final String number) {
187        LookupResult lookupResult = null;
188        Cursor c = null;
189        try {
190            Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
191                    Uri.encode(number));
192            c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null);
193            if (c != null && c.getCount() > 0) {
194                c.moveToFirst();
195                final int idIdx = c.getColumnIndex(Contacts._ID);
196                final int id = c.getInt(idIdx);
197                if (DEBUG) Slog.d(TAG, "is valid: " + id);
198                lookupResult = new LookupResult(id);
199            }
200        } catch(Throwable t) {
201            Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
202        } finally {
203            if (c != null) {
204                c.close();
205            }
206        }
207        if (lookupResult == null) {
208            lookupResult = new LookupResult(LookupResult.INVALID_ID);
209        }
210        synchronized (mPeopleCache) {
211            mPeopleCache.put(handle, lookupResult);
212        }
213        return lookupResult;
214    }
215
216    private LookupResult resolveContactsUri(String handle, final Uri personUri) {
217        LookupResult lookupResult = null;
218        Cursor c = null;
219        try {
220            c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null);
221            if (c != null && c.getCount() > 0) {
222                c.moveToFirst();
223                final int idIdx = c.getColumnIndex(Contacts._ID);
224                final int id = c.getInt(idIdx);
225                if (DEBUG) Slog.d(TAG, "is valid: " + id);
226                lookupResult = new LookupResult(id);
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        if (lookupResult == null) {
236            lookupResult = new LookupResult(LookupResult.INVALID_ID);
237        }
238        synchronized (mPeopleCache) {
239            mPeopleCache.put(handle, lookupResult);
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 RankingFuture process(NotificationManagerService.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
271        public LookupResult(int id) {
272            mId = id;
273            mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
274        }
275
276        public boolean isExpired() {
277            return mExpireMillis < System.currentTimeMillis();
278        }
279
280        public boolean isInvalid() {
281            return mId == INVALID_ID || isExpired();
282        }
283
284        public float getAffinity() {
285            if (isInvalid()) {
286                return NONE;
287            } else {
288                return VALID_CONTACT;  // TODO: finer grained result: stars
289            }
290        }
291
292        public LookupResult setId(int id) {
293            mId = id;
294            return this;
295        }
296    }
297}
298
299