• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 Esmertec AG.
3  * Copyright (C) 2008 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mms.util;
19 
20 import com.android.mms.ui.MessageUtils;
21 import com.google.android.mms.util.SqliteWrapper;
22 
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.database.Cursor;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.drawable.BitmapDrawable;
29 import android.net.Uri;
30 import android.provider.ContactsContract.Contacts;
31 import android.provider.ContactsContract.Data;
32 import android.provider.ContactsContract.Presence;
33 import android.provider.ContactsContract.CommonDataKinds.Email;
34 import android.provider.ContactsContract.CommonDataKinds.Phone;
35 import android.provider.Telephony.Mms;
36 import android.telephony.PhoneNumberUtils;
37 import android.text.TextUtils;
38 import android.util.Log;
39 
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.util.HashMap;
43 import java.util.Map;
44 import java.util.regex.Matcher;
45 
46 /**
47  * This class caches query results of contact database and provides convenient
48  * methods to return contact display name, etc.
49  *
50  * TODO: To improve performance, we should make contacts query by ourselves instead of
51  *       doing it one by one calling the CallerInfo API. In the long term, the contacts
52  *       database could have a caching layer to ease the work for all apps.
53  */
54 public class ContactInfoCache {
55     private static final String TAG = "Mms/cache";
56 
57     private static final boolean LOCAL_DEBUG = false;
58 
59     private static final String SEPARATOR = ";";
60 
61     // query params for caller id lookup
62     private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
63             + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'";
64 
65     // Utilizing private API
66     private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI;
67 
68     private static final String[] CALLER_ID_PROJECTION = new String[] {
69             Phone.NUMBER,                   // 0
70             Phone.LABEL,                    // 1
71             Phone.DISPLAY_NAME,             // 2
72             Phone.CONTACT_ID,               // 3
73             Phone.CONTACT_PRESENCE,         // 4
74             Phone.CONTACT_STATUS,           // 5
75     };
76     private static final int PHONE_NUMBER_COLUMN = 0;
77     private static final int PHONE_LABEL_COLUMN = 1;
78     private static final int CONTACT_NAME_COLUMN = 2;
79     private static final int CONTACT_ID_COLUMN = 3;
80     private static final int CONTACT_PRESENCE_COLUMN = 4;
81     private static final int CONTACT_STATUS_COLUMN = 5;
82 
83     // query params for contact lookup by email
84     private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI;
85 
86     private static final String EMAIL_SELECTION = Email.DATA + "=? AND " + Data.MIMETYPE + "='"
87             + Email.CONTENT_ITEM_TYPE + "'";
88 
89     private static final String[] EMAIL_PROJECTION = new String[] {
90             Email.DISPLAY_NAME,           // 0
91             Email.CONTACT_PRESENCE,       // 1
92             Email.CONTACT_ID,             // 2
93             Phone.DISPLAY_NAME,           //
94     };
95     private static final int EMAIL_NAME_COLUMN = 0;
96     private static final int EMAIL_STATUS_COLUMN = 1;
97     private static final int EMAIL_ID_COLUMN = 2;
98     private static final int EMAIL_CONTACT_NAME_COLUMN = 3;
99 
100     private static ContactInfoCache sInstance;
101 
102     private final Context mContext;
103 
104     private String[] mContactInfoSelectionArgs = new String[1];
105 
106     // cached contact info
107     private final HashMap<String, CacheEntry> mCache = new HashMap<String, CacheEntry>();
108 
109     // for background cache rebuilding
110     private Thread mCacheRebuilder = null;
111     private Object mCacheRebuildLock = new Object();
112     private boolean mPhoneCacheInvalidated = false;
113     private boolean mEmailCacheInvalidated = false;
114 
115     /**
116      * CacheEntry stores the caller id or email lookup info.
117      */
118     public class CacheEntry {
119         /**
120          * phone number
121          */
122         public String phoneNumber;
123         /**
124          * phone label
125          */
126         public String phoneLabel;
127         /**
128          * name of the contact
129          */
130         public String name;
131         /**
132          * the contact id in the contacts people table
133          */
134         public long person_id;
135         /**
136          * the presence icon resource id
137          */
138         public int presenceResId;
139         /*
140          * custom presence
141          */
142         public String presenceText;
143         /**
144          * Avatar image for this contact.
145          */
146         public BitmapDrawable mAvatar;
147 
148         /**
149          * If true, it indicates the CacheEntry has old info. We want to give the user of this
150          * class a chance to use the old info, as it can still be useful for displaying something
151          * rather than nothing in the UI. But this flag indicates that the CacheEntry needs to be
152          * updated.
153          */
154         private boolean isStale;
155 
156         /**
157          * Returns true if this CacheEntry needs to be updated. However, cache may still contain
158          * the old information.
159          *
160          */
isStale()161         public boolean isStale() {
162             return isStale;
163         }
164 
165         @Override
toString()166         public String toString() {
167             StringBuilder buf = new StringBuilder("name=" + name);
168             buf.append(", phone=" + phoneNumber);
169             buf.append(", pid=" + person_id);
170             buf.append(", presence=" + presenceResId);
171             buf.append(", stale=" + isStale);
172             return buf.toString();
173         }
174     };
175 
ContactInfoCache(Context context)176     private ContactInfoCache(Context context) {
177         mContext = context;
178     }
179 
180     /**
181      * invalidates the cache entries by marking CacheEntry.isStale to true.
182      */
invalidateCache()183     public void invalidateCache() {
184         synchronized (mCache) {
185             for (Map.Entry<String, CacheEntry> e: mCache.entrySet()) {
186                 CacheEntry entry = e.getValue();
187                 entry.isStale = true;
188             }
189         }
190     }
191 
192     /**
193      * invalidates a single cache entry. Can pass in an email or number.
194      */
invalidateContact(String emailOrNumber)195     public void invalidateContact(String emailOrNumber) {
196         synchronized (mCache) {
197             CacheEntry entry = mCache.get(emailOrNumber);
198             if (entry != null) {
199                 entry.isStale = true;
200             }
201         }
202     }
203 
204     /**
205      * Initialize the global instance. Should call only once.
206      */
init(Context context)207     public static void init(Context context) {
208         sInstance = new ContactInfoCache(context);
209     }
210 
211     /**
212      * Get the global instance.
213      */
getInstance()214     public static ContactInfoCache getInstance() {
215         return sInstance;
216     }
217 
dump()218     public void dump() {
219         synchronized (mCache) {
220             Log.i(TAG, "ContactInfoCache.dump");
221 
222             for (String name : mCache.keySet()) {
223                 CacheEntry entry = mCache.get(name);
224                 if (entry != null) {
225                     Log.i(TAG, "key=" + name + ", cacheEntry={" + entry.toString() + '}');
226                 } else {
227                     Log.i(TAG, "key=" + name + ", cacheEntry={null}");
228                 }
229             }
230         }
231     }
232 
233     /**
234      * Returns the caller info in CacheEntry.
235      */
getContactInfo(String numberOrEmail, boolean allowQuery)236     public CacheEntry getContactInfo(String numberOrEmail, boolean allowQuery) {
237         if (Mms.isEmailAddress(numberOrEmail)) {
238             return getContactInfoForEmailAddress(numberOrEmail, allowQuery);
239         } else {
240             return getContactInfoForPhoneNumber(numberOrEmail, allowQuery);
241         }
242     }
243 
getContactInfo(String numberOrEmail)244     public CacheEntry getContactInfo(String numberOrEmail) {
245         return getContactInfo(numberOrEmail, true);
246     }
247 
248     /**
249      * Returns the caller info in a CacheEntry. If 'noQuery' is set to true, then this
250      * method only checks in the cache and makes no content provider query.
251      *
252      * @param number the phone number for the contact.
253      * @param allowQuery allow (potentially blocking) query the content provider if true.
254      * @return the CacheEntry containing the contact info.
255      */
getContactInfoForPhoneNumber(String number, boolean allowQuery)256     public CacheEntry getContactInfoForPhoneNumber(String number, boolean allowQuery) {
257         // TODO: numbers like "6501234567" and "+16501234567" are equivalent.
258         // we should convert them into a uniform format so that we don't cache
259         // them twice.
260         number = PhoneNumberUtils.stripSeparators(number);
261         synchronized (mCache) {
262             if (mCache.containsKey(number)) {
263                 CacheEntry entry = mCache.get(number);
264                 if (LOCAL_DEBUG) {
265                     log("getContactInfo: number=" + number + ", name=" + entry.name +
266                             ", presence=" + entry.presenceResId);
267                 }
268                 if (!allowQuery || !entry.isStale()) {
269                     return entry;
270                 }
271             } else if (!allowQuery) {
272                 return null;
273             }
274         }
275         CacheEntry entry = queryContactInfoByNumber(number);
276         synchronized (mCache) {
277             mCache.put(number, entry);
278         }
279         return entry;
280     }
281 
282     /**
283      * Queries the caller id info with the phone number.
284      * @return a CacheEntry containing the caller id info corresponding to the number.
285      */
queryContactInfoByNumber(String number)286     private CacheEntry queryContactInfoByNumber(String number) {
287         CacheEntry entry = new CacheEntry();
288         entry.phoneNumber = number;
289 
290         //if (LOCAL_DEBUG) log("queryContactInfoByNumber: number=" + number);
291 
292         mContactInfoSelectionArgs[0] = number;
293 
294         Cursor cursor = mContext.getContentResolver().query(
295                 PHONES_WITH_PRESENCE_URI,
296                 CALLER_ID_PROJECTION,
297                 CALLER_ID_SELECTION,
298                 mContactInfoSelectionArgs,
299                 null);
300 
301         if (cursor == null) {
302             Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" +
303                     " contact uri used " + PHONES_WITH_PRESENCE_URI);
304             return entry;
305         }
306 
307         try {
308             if (cursor.moveToFirst()) {
309                 entry.phoneLabel = cursor.getString(PHONE_LABEL_COLUMN);
310                 entry.name = cursor.getString(CONTACT_NAME_COLUMN);
311                 entry.person_id = cursor.getLong(CONTACT_ID_COLUMN);
312                 entry.presenceResId = getPresenceIconResourceId(
313                         cursor.getInt(CONTACT_PRESENCE_COLUMN));
314                 entry.presenceText = cursor.getString(CONTACT_STATUS_COLUMN);
315                 if (LOCAL_DEBUG) {
316                     log("queryContactInfoByNumber: name=" + entry.name + ", number=" + number +
317                             ", presence=" + entry.presenceResId);
318                 }
319 
320                 loadAvatar(entry, cursor);
321             }
322         } finally {
323             cursor.close();
324         }
325 
326         return entry;
327     }
328 
loadAvatar(CacheEntry entry, Cursor cursor)329     private void loadAvatar(CacheEntry entry, Cursor cursor) {
330         if (entry.person_id == 0 || entry.mAvatar != null) {
331             return;
332         }
333 
334         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.person_id);
335 
336         InputStream avatarDataStream =
337             Contacts.openContactPhotoInputStream(
338                     mContext.getContentResolver(),
339                     contactUri);
340         if (avatarDataStream != null) {
341             Bitmap b = BitmapFactory.decodeStream(avatarDataStream);
342 
343             BitmapDrawable bd =
344                 new BitmapDrawable(mContext.getResources(), b);
345 
346             entry.mAvatar = bd;
347             try {
348                 avatarDataStream.close();
349             } catch (IOException e) {
350                 entry.mAvatar = null;
351             }
352         }
353     }
354 
355     /**
356      * Get the display names of contacts. Contacts can be either email address or
357      * phone number.
358      *
359      * @param address the addresses to lookup, separated by ";"
360      * @return a nicely formatted version of the contact names to display
361      */
getContactName(String address)362     public String getContactName(String address) {
363         if (TextUtils.isEmpty(address)) {
364             return "";
365         }
366 
367         StringBuilder result = new StringBuilder();
368         for (String value : address.split(SEPARATOR)) {
369             if (value.length() > 0) {
370                 result.append(SEPARATOR);
371                 if (MessageUtils.isLocalNumber(value)) {
372                     result.append(mContext.getString(com.android.internal.R.string.me));
373                 } else if (Mms.isEmailAddress(value)) {
374                     result.append(getDisplayName(value));
375                 } else {
376                     result.append(getCallerId(value));
377                 }
378             }
379         }
380 
381         if (result.length() > 0) {
382             // Skip the first ";"
383             return result.substring(1);
384         }
385 
386         return "";
387     }
388 
389     /**
390      * Get the display name of an email address. If the address already contains
391      * the name, parse and return it. Otherwise, query the contact database. Cache
392      * query results for repeated queries.
393      */
getDisplayName(String email)394     public String getDisplayName(String email) {
395         Matcher match = Mms.NAME_ADDR_EMAIL_PATTERN.matcher(email);
396         if (match.matches()) {
397             // email has display name
398             return getEmailDisplayName(match.group(1));
399         }
400 
401         CacheEntry entry = getContactInfoForEmailAddress(email, true /* allow query */);
402         if (entry != null && entry.name != null) {
403             return entry.name;
404         }
405 
406         return email;
407     }
408 
409     /**
410      * Returns the contact info for a given email address
411      *
412      * @param email the email address.
413      * @param allowQuery allow making (potentially blocking) content provider queries if true.
414      * @return a CacheEntry if the contact is found.
415      */
getContactInfoForEmailAddress(String email, boolean allowQuery)416     public CacheEntry getContactInfoForEmailAddress(String email, boolean allowQuery) {
417         synchronized (mCache) {
418             if (mCache.containsKey(email)) {
419                 CacheEntry entry = mCache.get(email);
420                 if (!allowQuery || !entry.isStale()) {
421                     return entry;
422                 }
423             } else if (!allowQuery) {
424                 return null;
425             }
426         }
427         CacheEntry entry = queryEmailDisplayName(email);
428         synchronized (mCache) {
429             mCache.put(email, entry);
430 
431             return entry;
432         }
433     }
434 
435     /**
436      * A cached version of CallerInfo.getCallerId().
437      */
getCallerId(String number)438     private String getCallerId(String number) {
439         ContactInfoCache.CacheEntry entry = getContactInfo(number);
440         if (entry != null && !TextUtils.isEmpty(entry.name)) {
441             return entry.name;
442         }
443         return number;
444     }
445 
getEmailDisplayName(String displayString)446     private static String getEmailDisplayName(String displayString) {
447         Matcher match = Mms.QUOTED_STRING_PATTERN.matcher(displayString);
448         if (match.matches()) {
449             return match.group(1);
450         }
451 
452         return displayString;
453     }
454 
getPresenceIconResourceId(int presence)455     private int getPresenceIconResourceId(int presence) {
456         if (presence != Presence.OFFLINE) {
457             return Presence.getPresenceIconResourceId(presence);
458         }
459 
460         return 0;
461     }
462 
463     /**
464      * Query the contact email table to get the name of an email address.
465      */
queryEmailDisplayName(String email)466     private CacheEntry queryEmailDisplayName(String email) {
467         CacheEntry entry = new CacheEntry();
468 
469         mContactInfoSelectionArgs[0] = email;
470 
471         Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(),
472                 EMAIL_WITH_PRESENCE_URI,
473                 EMAIL_PROJECTION,
474                 EMAIL_SELECTION,
475                 mContactInfoSelectionArgs,
476                 null);
477 
478         if (cursor != null) {
479             try {
480                 while (cursor.moveToNext()) {
481                     entry.presenceResId = getPresenceIconResourceId(
482                             cursor.getInt(EMAIL_STATUS_COLUMN));
483                     entry.person_id = cursor.getLong(EMAIL_ID_COLUMN);
484 
485                     String name = cursor.getString(EMAIL_NAME_COLUMN);
486                     if (TextUtils.isEmpty(name)) {
487                         name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN);
488                     }
489                     if (!TextUtils.isEmpty(name)) {
490                         entry.name = name;
491                         loadAvatar(entry, cursor);
492                         if (LOCAL_DEBUG) {
493                             log("queryEmailDisplayName: name=" + entry.name + ", email=" + email +
494                                     ", presence=" + entry.presenceResId);
495                         }
496                         break;
497                     }
498 
499                 }
500             } finally {
501                 cursor.close();
502             }
503         }
504         return entry;
505     }
506 
log(String msg)507     private void log(String msg) {
508         Log.d(TAG, "[ContactInfoCache] " + msg);
509     }
510 }
511