/* * Copyright 2011 David Brazdil * * 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 uk.ac.cam.db538.cryptosms.data; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import uk.ac.cam.db538.cryptosms.R; import uk.ac.cam.db538.cryptosms.ui.UtilsSimIssues; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.telephony.PhoneNumberUtils; /* * Class handling a single contact in the database */ public class Contact implements Comparable<Contact> { 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('+*'))"; 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 }; @SuppressWarnings("unused") 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; @SuppressWarnings("unused") private static final int CONTACT_PRESENCE_COLUMN = 4; @SuppressWarnings("unused") private static final int CONTACT_STATUS_COLUMN = 5; private static ArrayList<Contact> mCacheContact = new ArrayList<Contact>(); private String mPhoneNumber; private String mLabel; private String mName; private long mPersonId; private BitmapDrawable mAvatar; byte[] mAvatarData; private Contact(String phoneNumber) { setPhoneNumber(phoneNumber); setName(""); setLabel(""); mPersonId = 0; } private Contact(long contactId) { setPhoneNumber(""); setName(""); setLabel(""); mPersonId = contactId; } /** * Gets avatar picture of the user * * @param context the context * @param defaultValue the default value * @return the avatar */ public synchronized Drawable getAvatar(Context context, Drawable defaultValue) { if (mAvatar == null) { if (mAvatarData != null) { Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length); mAvatar = new BitmapDrawable(context.getResources(), b); } } return mAvatar != null ? mAvatar : defaultValue; } /** * Returns whether this contact is stored in the database * * @return true, if successful */ public synchronized boolean existsInDatabase() { return (mPersonId > 0); } public synchronized Uri getUri() { return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId); } // GETTERS / SETTERS public String getPhoneNumber() { return mPhoneNumber; } public void setPhoneNumber(String phoneNumber) { this.mPhoneNumber = phoneNumber; } public String getLabel() { return mLabel; } public void setLabel(String label) { this.mLabel = label; } public String getName() { return mName; } public void setName(String name) { this.mName = name; } public long getId() { return this.mPersonId; } // STATIC STUFF /** * Queries the caller id info with the phone number. * * @param context the context * @param phoneNumber the phone number * @return a Contact containing the caller id info corresponding to the number. */ public static Contact getContact(Context context, String phoneNumber) { phoneNumber = UtilsSimIssues.formatPhoneNumber(phoneNumber); // check whether it is already in the cache for (Contact contact : mCacheContact) if (PhoneNumberUtils.compare(contact.getPhoneNumber(), phoneNumber)) return contact; // if it's not, create a new one Contact entry = new Contact(phoneNumber); mCacheContact.add(entry); // 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(phoneNumber)); Cursor cursor = context.getContentResolver().query( PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, new String[] { phoneNumber }, null); if (cursor == null) { return entry; } try { if (cursor.moveToFirst()) { synchronized (entry) { entry.setLabel(cursor.getString(PHONE_LABEL_COLUMN)); entry.setName(cursor.getString(CONTACT_NAME_COLUMN)); entry.mPersonId = cursor.getLong(CONTACT_ID_COLUMN); } byte[] data = loadAvatarData(context, entry); synchronized (entry) { entry.mAvatarData = data; } } } finally { cursor.close(); } return entry; } /** * Queries the caller id info with the phone number. * * @param context the context * @param phoneNumber the phone number * @param contactId the contact id * @return a Contact containing the caller id info corresponding to the number. */ public static Contact getContact(Context context, String phoneNumber, long contactId) { phoneNumber = UtilsSimIssues.formatPhoneNumber(phoneNumber); // check whether it is already in the cache for (Contact contact : mCacheContact) if (PhoneNumberUtils.compare(contact.getPhoneNumber(), phoneNumber)) return contact; // if it's not, create a new one Contact entry = new Contact(phoneNumber); // 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(phoneNumber)); Cursor cursor = context.getContentResolver().query( PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, new String[] { phoneNumber }, null); if (cursor == null) { return entry; } boolean found = false; try { if (cursor.moveToFirst()) { do { synchronized (entry) { entry.setLabel(cursor.getString(PHONE_LABEL_COLUMN)); entry.setName(cursor.getString(CONTACT_NAME_COLUMN)); entry.mPersonId = cursor.getLong(CONTACT_ID_COLUMN); } if (entry.mPersonId == contactId) { found = true; byte[] data = loadAvatarData(context, entry); synchronized (entry) { entry.mAvatarData = data; } } } while (cursor.moveToNext() && !found); } } finally { cursor.close(); } if (!found) return null; mCacheContact.add(entry); return entry; } /* * Load the avatar data from the cursor into memory. Don't decode the data * until someone calls for it (see getAvatar). Hang onto the raw data so that * we can compare it when the data is reloaded. */ private static byte[] loadAvatarData(Context context, Contact entry) { byte [] data = null; if (entry.mPersonId == 0 || entry.mAvatar != null) { return null; } Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId); InputStream avatarDataStream = Contacts.openContactPhotoInputStream( context.getContentResolver(), contactUri); try { if (avatarDataStream != null) { data = new byte[avatarDataStream.available()]; avatarDataStream.read(data, 0, data.length); } } catch (IOException ex) { // } finally { try { if (avatarDataStream != null) { avatarDataStream.close(); } } catch (IOException e) { } } return data; } public static class PhoneNumber { private int mType; private String mPhoneNumber; private Context mContext; /** * Instantiates a new phone number. * * @param context the context * @param type the type * @param phoneNumber the phone number */ public PhoneNumber(Context context, int type, String phoneNumber) { setType(type); setPhoneNumber(phoneNumber); mContext = context; } public void setType(int type) { this.mType = type; } public int getType() { return mType; } public void setPhoneNumber(String phoneNumber) { this.mPhoneNumber = phoneNumber; } public String getPhoneNumber() { return mPhoneNumber; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { Resources res = mContext.getResources(); String type = ""; switch (mType) { case Phone.TYPE_HOME: type += res.getString(R.string.contacts_type_home); break; case Phone.TYPE_WORK: type += res.getString(R.string.contacts_type_work); break; case Phone.TYPE_MOBILE: type += res.getString(R.string.contacts_type_mobile); break; } return mPhoneNumber + " (" + type + ")"; } } /** * Gets all phone numbers of a given contact. * * @param context the context * @param contactId the contact id * @return the phone numbers */ public static ArrayList<PhoneNumber> getPhoneNumbers(Context context, long contactId) { ContentResolver cr = context.getContentResolver(); ArrayList<PhoneNumber> list = new ArrayList<PhoneNumber>(); Cursor phones = cr.query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = " + contactId, null, null); while (phones.moveToNext()) { list.add(new PhoneNumber( context, phones.getInt(phones.getColumnIndex(Phone.TYPE)), phones.getString(phones.getColumnIndex(Phone.NUMBER)) )); } phones.close(); return list; } /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override public int compareTo(Contact another) { String nameThis = (this.existsInDatabase() ? this.getName() : this.getPhoneNumber()); String nameAnother = (another.existsInDatabase() ? another.getName() : another.getPhoneNumber()); return nameThis.compareToIgnoreCase(nameAnother); } }