blob: fa7d495d5eb8a9d5dccaf0d1d1b2339e19a80cc1 [file] [log] [blame]
/*
* Copyright (C) 2008 Esmertec AG.
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mms.util;
import com.android.mms.ui.MessageUtils;
import com.google.android.mms.util.SqliteWrapper;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Presence;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.Telephony.Mms;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
/**
* This class caches query results of contact database and provides convenient
* methods to return contact display name, etc.
*
* TODO: To improve performance, we should make contacts query by ourselves instead of
* doing it one by one calling the CallerInfo API. In the long term, the contacts
* database could have a caching layer to ease the work for all apps.
*/
public class ContactInfoCache {
private static final String TAG = "Mms/cache";
private static final boolean LOCAL_DEBUG = false;
private static final String SEPARATOR = ";";
// query params for caller id lookup
// TODO this query uses non-public API. Figure out a way to expose this functionality
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE normalized_number GLOB('+*'))";
// Utilizing private API
private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI;
private static final String[] CALLER_ID_PROJECTION = new String[] {
Phone.NUMBER, // 0
Phone.LABEL, // 1
Phone.DISPLAY_NAME, // 2
Phone.CONTACT_ID, // 3
Phone.CONTACT_PRESENCE, // 4
Phone.CONTACT_STATUS, // 5
};
private static final int PHONE_NUMBER_COLUMN = 0;
private static final int PHONE_LABEL_COLUMN = 1;
private static final int CONTACT_NAME_COLUMN = 2;
private static final int CONTACT_ID_COLUMN = 3;
private static final int CONTACT_PRESENCE_COLUMN = 4;
private static final int CONTACT_STATUS_COLUMN = 5;
// query params for contact lookup by email
private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI;
private static final String EMAIL_SELECTION = Email.DATA + "=? AND " + Data.MIMETYPE + "='"
+ Email.CONTENT_ITEM_TYPE + "'";
private static final String[] EMAIL_PROJECTION = new String[] {
Email.DISPLAY_NAME, // 0
Email.CONTACT_PRESENCE, // 1
Email.CONTACT_ID, // 2
Phone.DISPLAY_NAME, //
};
private static final int EMAIL_NAME_COLUMN = 0;
private static final int EMAIL_STATUS_COLUMN = 1;
private static final int EMAIL_ID_COLUMN = 2;
private static final int EMAIL_CONTACT_NAME_COLUMN = 3;
private static ContactInfoCache sInstance;
private final Context mContext;
// cached contact info
private final HashMap<String, CacheEntry> mCache = new HashMap<String, CacheEntry>();
/**
* CacheEntry stores the caller id or email lookup info.
*/
public class CacheEntry {
/**
* phone number
*/
public String phoneNumber;
/**
* phone label
*/
public String phoneLabel;
/**
* name of the contact
*/
public String name;
/**
* the contact id in the contacts people table
*/
public long person_id;
/**
* the presence icon resource id
*/
public int presenceResId;
/*
* custom presence
*/
public String presenceText;
/**
* Avatar image for this contact.
*/
public BitmapDrawable mAvatar;
/**
* If true, it indicates the CacheEntry has old info. We want to give the user of this
* class a chance to use the old info, as it can still be useful for displaying something
* rather than nothing in the UI. But this flag indicates that the CacheEntry needs to be
* updated.
*/
private boolean isStale;
/**
* Returns true if this CacheEntry needs to be updated. However, cache may still contain
* the old information.
*
*/
public boolean isStale() {
return isStale;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder("name=" + name);
buf.append(", phone=" + phoneNumber);
buf.append(", pid=" + person_id);
buf.append(", presence=" + presenceResId);
buf.append(", stale=" + isStale);
return buf.toString();
}
};
private ContactInfoCache(Context context) {
mContext = context;
}
/**
* invalidates the cache entries by marking CacheEntry.isStale to true.
*/
public void invalidateCache() {
synchronized (mCache) {
for (Map.Entry<String, CacheEntry> e: mCache.entrySet()) {
CacheEntry entry = e.getValue();
entry.isStale = true;
}
}
}
/**
* invalidates a single cache entry. Can pass in an email or number.
*/
public void invalidateContact(String emailOrNumber) {
synchronized (mCache) {
CacheEntry entry = mCache.get(emailOrNumber);
if (entry != null) {
entry.isStale = true;
}
}
}
/**
* Initialize the global instance. Should call only once.
*/
public static void init(Context context) {
sInstance = new ContactInfoCache(context);
}
/**
* Get the global instance.
*/
public static ContactInfoCache getInstance() {
return sInstance;
}
public void dump() {
synchronized (mCache) {
Log.i(TAG, "ContactInfoCache.dump");
for (String name : mCache.keySet()) {
CacheEntry entry = mCache.get(name);
if (entry != null) {
Log.i(TAG, "key=" + name + ", cacheEntry={" + entry.toString() + '}');
} else {
Log.i(TAG, "key=" + name + ", cacheEntry={null}");
}
}
}
}
/**
* Returns the caller info in CacheEntry.
*/
public CacheEntry getContactInfo(String numberOrEmail, boolean allowQuery) {
if (Mms.isEmailAddress(numberOrEmail)) {
return getContactInfoForEmailAddress(numberOrEmail, allowQuery);
} else {
return getContactInfoForPhoneNumber(numberOrEmail, allowQuery);
}
}
public CacheEntry getContactInfo(String numberOrEmail) {
return getContactInfo(numberOrEmail, true);
}
/**
* Returns the caller info in a CacheEntry. If 'noQuery' is set to true, then this
* method only checks in the cache and makes no content provider query.
*
* @param number the phone number for the contact.
* @param allowQuery allow (potentially blocking) query the content provider if true.
* @return the CacheEntry containing the contact info.
*/
public CacheEntry getContactInfoForPhoneNumber(String number, boolean allowQuery) {
// TODO: numbers like "6501234567" and "+16501234567" are equivalent.
// we should convert them into a uniform format so that we don't cache
// them twice.
number = PhoneNumberUtils.stripSeparators(number);
synchronized (mCache) {
if (mCache.containsKey(number)) {
CacheEntry entry = mCache.get(number);
if (LOCAL_DEBUG) {
log("getContactInfo: number=" + number + ", name=" + entry.name +
", presence=" + entry.presenceResId);
}
if (!allowQuery || !entry.isStale()) {
return entry;
}
} else if (!allowQuery) {
return null;
}
}
CacheEntry entry = queryContactInfoByNumber(number);
synchronized (mCache) {
mCache.put(number, entry);
}
return entry;
}
/**
* Queries the caller id info with the phone number.
* @return a CacheEntry containing the caller id info corresponding to the number.
*/
private CacheEntry queryContactInfoByNumber(String number) {
CacheEntry entry = new CacheEntry();
entry.phoneNumber = number;
//if (LOCAL_DEBUG) log("queryContactInfoByNumber: number=" + number);
String contactInfoSelectionArgs[] = new String[1];
contactInfoSelectionArgs[0] = number;
// We need to include the phone number in the selection string itself rather then
// selection arguments, because SQLite needs to see the exact pattern of GLOB
// to generate the correct query plan
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(number));
Cursor cursor = mContext.getContentResolver().query(
PHONES_WITH_PRESENCE_URI,
CALLER_ID_PROJECTION,
selection,
contactInfoSelectionArgs,
null);
if (cursor == null) {
Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" +
" contact uri used " + PHONES_WITH_PRESENCE_URI);
return entry;
}
try {
if (cursor.moveToFirst()) {
entry.phoneLabel = cursor.getString(PHONE_LABEL_COLUMN);
entry.name = cursor.getString(CONTACT_NAME_COLUMN);
entry.person_id = cursor.getLong(CONTACT_ID_COLUMN);
entry.presenceResId = getPresenceIconResourceId(
cursor.getInt(CONTACT_PRESENCE_COLUMN));
entry.presenceText = cursor.getString(CONTACT_STATUS_COLUMN);
if (LOCAL_DEBUG) {
log("queryContactInfoByNumber: name=" + entry.name + ", number=" + number +
", presence=" + entry.presenceResId);
}
loadAvatar(entry, cursor);
}
} finally {
cursor.close();
}
return entry;
}
private void loadAvatar(CacheEntry entry, Cursor cursor) {
if (entry.person_id == 0 || entry.mAvatar != null) {
return;
}
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.person_id);
InputStream avatarDataStream =
Contacts.openContactPhotoInputStream(
mContext.getContentResolver(),
contactUri);
if (avatarDataStream != null) {
Bitmap b = BitmapFactory.decodeStream(avatarDataStream);
BitmapDrawable bd =
new BitmapDrawable(mContext.getResources(), b);
entry.mAvatar = bd;
try {
avatarDataStream.close();
} catch (IOException e) {
entry.mAvatar = null;
}
}
}
/**
* Get the display names of contacts. Contacts can be either email address or
* phone number.
*
* @param address the addresses to lookup, separated by ";"
* @return a nicely formatted version of the contact names to display
*/
public String getContactName(String address) {
if (TextUtils.isEmpty(address)) {
return "";
}
StringBuilder result = new StringBuilder();
for (String value : address.split(SEPARATOR)) {
if (value.length() > 0) {
result.append(SEPARATOR);
if (MessageUtils.isLocalNumber(value)) {
result.append(mContext.getString(com.android.internal.R.string.me));
} else if (Mms.isEmailAddress(value)) {
result.append(getDisplayName(value));
} else {
result.append(getCallerId(value));
}
}
}
if (result.length() > 0) {
// Skip the first ";"
return result.substring(1);
}
return "";
}
/**
* Get the display name of an email address. If the address already contains
* the name, parse and return it. Otherwise, query the contact database. Cache
* query results for repeated queries.
*/
public String getDisplayName(String email) {
Matcher match = Mms.NAME_ADDR_EMAIL_PATTERN.matcher(email);
if (match.matches()) {
// email has display name
return getEmailDisplayName(match.group(1));
}
CacheEntry entry = getContactInfoForEmailAddress(email, true /* allow query */);
if (entry != null && entry.name != null) {
return entry.name;
}
return email;
}
/**
* Returns the contact info for a given email address
*
* @param email the email address.
* @param allowQuery allow making (potentially blocking) content provider queries if true.
* @return a CacheEntry if the contact is found.
*/
public CacheEntry getContactInfoForEmailAddress(String email, boolean allowQuery) {
synchronized (mCache) {
if (mCache.containsKey(email)) {
CacheEntry entry = mCache.get(email);
if (!allowQuery || !entry.isStale()) {
return entry;
}
} else if (!allowQuery) {
return null;
}
}
CacheEntry entry = queryEmailDisplayName(email);
synchronized (mCache) {
mCache.put(email, entry);
return entry;
}
}
/**
* A cached version of CallerInfo.getCallerId().
*/
private String getCallerId(String number) {
ContactInfoCache.CacheEntry entry = getContactInfo(number);
if (entry != null && !TextUtils.isEmpty(entry.name)) {
return entry.name;
}
return number;
}
private static String getEmailDisplayName(String displayString) {
Matcher match = Mms.QUOTED_STRING_PATTERN.matcher(displayString);
if (match.matches()) {
return match.group(1);
}
return displayString;
}
private int getPresenceIconResourceId(int presence) {
if (presence != Presence.OFFLINE) {
return Presence.getPresenceIconResourceId(presence);
}
return 0;
}
/**
* Query the contact email table to get the name of an email address.
*/
private CacheEntry queryEmailDisplayName(String email) {
CacheEntry entry = new CacheEntry();
String contactInfoSelectionArgs[] = new String[1];
contactInfoSelectionArgs[0] = email;
Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(),
EMAIL_WITH_PRESENCE_URI,
EMAIL_PROJECTION,
EMAIL_SELECTION,
contactInfoSelectionArgs,
null);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
entry.presenceResId = getPresenceIconResourceId(
cursor.getInt(EMAIL_STATUS_COLUMN));
entry.person_id = cursor.getLong(EMAIL_ID_COLUMN);
String name = cursor.getString(EMAIL_NAME_COLUMN);
if (TextUtils.isEmpty(name)) {
name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN);
}
if (!TextUtils.isEmpty(name)) {
entry.name = name;
loadAvatar(entry, cursor);
if (LOCAL_DEBUG) {
log("queryEmailDisplayName: name=" + entry.name + ", email=" + email +
", presence=" + entry.presenceResId);
}
break;
}
}
} finally {
cursor.close();
}
}
return entry;
}
private void log(String msg) {
Log.d(TAG, "[ContactInfoCache] " + msg);
}
}