diff --git a/src/com/fsck/k9/helper/Contacts.java b/src/com/fsck/k9/helper/Contacts.java index 6e8f9fbb4..56866c21c 100644 --- a/src/com/fsck/k9/helper/Contacts.java +++ b/src/com/fsck/k9/helper/Contacts.java @@ -1,55 +1,76 @@ package com.fsck.k9.helper; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.content.Intent; +import android.provider.ContactsContract; +import android.util.Log; + +import com.fsck.k9.K9; import com.fsck.k9.mail.Address; +import java.util.ArrayList; + /** - * Helper class to access the contacts stored on the device. This is needed - * because the original contacts API introduced with SDK 1 was deprecated with - * SDK 5 and will eventually be removed in newer SDK versions. - * A class that uses the latest contacts API available on the device will be - * loaded at runtime. - * - * @see ContactsSdk5 - * @see ContactsSdk5p + * Helper class to access the contacts stored on the device. */ -public abstract class Contacts { +public class Contacts { /** - * Instance of the SDK specific class that interfaces with the contacts - * API. + * The order in which the search results are returned by + * {@link #searchContacts(CharSequence)}. */ - private static Contacts sInstance = null; + protected static final String SORT_ORDER = + ContactsContract.CommonDataKinds.Email.TIMES_CONTACTED + " DESC, " + + ContactsContract.Contacts.DISPLAY_NAME + ", " + + ContactsContract.CommonDataKinds.Email._ID; /** - * Get SDK specific instance of the Contacts class. + * Array of columns to load from the database. + * + * Important: The _ID field is needed by + * {@link com.fsck.k9.EmailAddressAdapter} or more specificly by + * {@link android.widget.ResourceCursorAdapter}. + */ + protected static final String PROJECTION[] = { + ContactsContract.CommonDataKinds.Email._ID, + ContactsContract.Contacts.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Email.DATA, + ContactsContract.CommonDataKinds.Email.CONTACT_ID + }; + + /** + * Index of the name field in the projection. This must match the order in + * {@link #PROJECTION}. + */ + protected static final int NAME_INDEX = 1; + + /** + * Index of the email address field in the projection. This must match the + * order in {@link #PROJECTION}. + */ + protected static final int EMAIL_INDEX = 2; + + /** + * Index of the contact id field in the projection. This must match the order in + * {@link #PROJECTION}. + */ + protected static final int CONTACT_ID_INDEX = 3; + + + /** + * Get instance of the Contacts class. + * + *

Note: This is left over from the days when we needed to have SDK-specific code to access + * the contacts.

* * @param context A {@link Context} instance. * @return Appropriate {@link Contacts} instance for this device. */ public static Contacts getInstance(Context context) { - Context appContext = context.getApplicationContext(); - if (sInstance == null) { - /* - * Check the version of the SDK we are running on. Choose an - * implementation class designed for that version of the SDK. - */ - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ECLAIR_MR1) { - /* - * The new API was introduced with SDK 5. But Android versions < 2.2 - * need some additional code to be able to search for phonetic names. - */ - sInstance = new ContactsSdk5p(appContext); - } else { - sInstance = new ContactsSdk5(appContext); - } - } - - return sInstance; + return new Contacts(context); } @@ -76,7 +97,25 @@ public abstract class Contacts { * the instance also contains the (display) name of that * entity. */ - public abstract void createContact(Address email); + public void createContact(final Address email) { + final Uri contactUri = Uri.fromParts("mailto", email.getAddress(), null); + + final Intent contactIntent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT); + contactIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + contactIntent.setData(contactUri); + + // Pass along full E-mail string for possible create dialog + contactIntent.putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION, + email.toString()); + + // Only provide personal name hint if we have one + final String senderPersonal = email.getPersonal(); + if (senderPersonal != null) { + contactIntent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal); + } + + mContext.startActivity(contactIntent); + } /** * Start the activity to add a phone number to an existing contact or add a new one. @@ -84,7 +123,13 @@ public abstract class Contacts { * @param phoneNumber * The phone number to add to a contact, or to use when creating a new contact. */ - public abstract void addPhoneContact(String phoneNumber); + public void addPhoneContact(final String phoneNumber) { + Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + addIntent.putExtra(ContactsContract.Intents.Insert.PHONE, Uri.decode(phoneNumber)); + addIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(addIntent); + } /** * Check whether the provided email address belongs to one of the contacts. @@ -93,16 +138,52 @@ public abstract class Contacts { * @return true, if the email address belongs to a contact. * false, otherwise. */ - public abstract boolean isInContacts(String emailAddress); + public boolean isInContacts(final String emailAddress) { + boolean result = false; + + final Cursor c = getContactByAddress(emailAddress); + + if (c != null) { + if (c.getCount() > 0) { + result = true; + } + c.close(); + } + + return result; + } /** * Filter the contacts matching the given search term. * - * @param filter The search term to filter the contacts. + * @param constraint The search term to filter the contacts. * @return A {@link Cursor} instance that can be used to get the * matching contacts. */ - public abstract Cursor searchContacts(CharSequence filter); + public Cursor searchContacts(final CharSequence constraint) { + final String filter = (constraint == null) ? "" : constraint.toString(); + final Uri uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Email.CONTENT_FILTER_URI, Uri.encode(filter)); + final Cursor c = mContentResolver.query( + uri, + PROJECTION, + null, + null, + SORT_ORDER); + + if (c != null) { + /* + * To prevent expensive execution in the UI thread: + * Cursors get lazily executed, so if you don't call anything on + * the cursor before returning it from the background thread you'll + * have a complied program for the cursor, but it won't have been + * executed to generate the data yet. Often the execution is more + * expensive than the compilation... + */ + c.getCount(); + } + + return c; + } /** * Get the name of the contact an email address belongs to. @@ -111,7 +192,24 @@ public abstract class Contacts { * @return The name of the contact the email address belongs to. Or * null if there's no matching contact. */ - public abstract String getNameForAddress(String address); + public String getNameForAddress(String address) { + if (address == null) { + return null; + } + + final Cursor c = getContactByAddress(address); + + String name = null; + if (c != null) { + if (c.getCount() > 0) { + c.moveToFirst(); + name = getName(c); + } + c.close(); + } + + return name; + } /** * Extract the name from a {@link Cursor} instance returned by @@ -120,7 +218,9 @@ public abstract class Contacts { * @param cursor The {@link Cursor} instance. * @return The name of the contact in the {@link Cursor}'s current row. */ - public abstract String getName(Cursor cursor); + public String getName(Cursor cursor) { + return cursor.getString(NAME_INDEX); + } /** * Extract the email address from a {@link Cursor} instance returned by @@ -130,7 +230,9 @@ public abstract class Contacts { * @return The email address of the contact in the {@link Cursor}'s current * row. */ - public abstract String getEmail(Cursor cursor); + public String getEmail(Cursor cursor) { + return cursor.getString(EMAIL_INDEX); + } /** * Mark contacts with the provided email addresses as contacted. @@ -138,14 +240,30 @@ public abstract class Contacts { * @param addresses Array of {@link Address} objects describing the * contacts to be marked as contacted. */ - public abstract void markAsContacted(final Address[] addresses); + public void markAsContacted(final Address[] addresses) { + //TODO: Optimize! Potentially a lot of database queries + for (final Address address : addresses) { + final Cursor c = getContactByAddress(address.getAddress()); + + if (c != null) { + if (c.getCount() > 0) { + c.moveToFirst(); + final long personId = c.getLong(CONTACT_ID_INDEX); + ContactsContract.Contacts.markAsContacted(mContentResolver, personId); + } + c.close(); + } + } + } /** * Creates the intent necessary to open a contact picker. * * @return The intent necessary to open a contact picker. */ - public abstract Intent contactPickerIntent(); + public Intent contactPickerIntent() { + return new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); + } /** * Given a contact picker intent, returns a {@code ContactItem} instance for that contact. @@ -156,7 +274,52 @@ public abstract class Contacts { * @return A {@link ContactItem} instance describing the picked contact. Or {@code null} if the * contact doesn't have any email addresses. */ - public abstract ContactItem extractInfoFromContactPickerIntent(final Intent intent); + public ContactItem extractInfoFromContactPickerIntent(final Intent intent) { + Cursor cursor = null; + ArrayList email = new ArrayList(); + + try { + Uri result = intent.getData(); + String displayName = null; + + // Get the contact id from the Uri + String id = result.getLastPathSegment(); + + cursor = mContentResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, PROJECTION, + ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?", new String[] { id }, null); + + if (cursor != null) { + while (cursor.moveToNext()) { + String address = cursor.getString(EMAIL_INDEX); + if (address != null) { + email.add(address); + } + + if (displayName == null) { + displayName = cursor.getString(NAME_INDEX); + } + } + + // Return 'null' if no email addresses have been found + if (email.size() == 0) { + return null; + } + + // Use the first email address found as display name + if (displayName == null) { + displayName = email.get(0); + } + + return new ContactItem(displayName, email); + } + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Failed to get email data", e); + } finally { + Utility.closeQuietly(cursor); + } + + return null; + } /** * Get URI to the picture of the contact with the supplied email address. @@ -168,7 +331,47 @@ public abstract class Contacts { * @return URI to the picture of the contact with the supplied email address. {@code null} if * no such contact could be found or the contact doesn't have a picture. */ - public abstract Uri getPhotoUri(String address); + public Uri getPhotoUri(String address) { + Long contactId; + try { + final Cursor c = getContactByAddress(address); + if (c == null) { + return null; + } + + try { + if (!c.moveToFirst()) { + return null; + } + + contactId = c.getLong(CONTACT_ID_INDEX); + } finally { + c.close(); + } + + Cursor cur = mContentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, + ContactsContract.Data.CONTACT_ID + "=" + contactId + " AND " + + ContactsContract.Data.MIMETYPE + "='" + + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "'", null, + null); + if (cur == null) { + return null; + } + if (!cur.moveToFirst()) { + cur.close(); + return null; // no photo + } + // Ok, they have a photo + cur.close(); + Uri person = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); + return Uri.withAppendedPath(person, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Couldn't fetch photo for contact with email " + address, e); + return null; + } + } /** * Does the device actually have a Contacts application suitable for @@ -184,4 +387,24 @@ public abstract class Contacts { } return mHasContactPicker; } + + /** + * Return a {@link Cursor} instance that can be used to fetch information + * about the contact with the given email address. + * + * @param address The email address to search for. + * @return A {@link Cursor} instance that can be used to fetch information + * about the contact with the given email address + */ + private Cursor getContactByAddress(final String address) { + final Uri uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(address)); + final Cursor c = mContentResolver.query( + uri, + PROJECTION, + null, + null, + SORT_ORDER); + return c; + } + } diff --git a/src/com/fsck/k9/helper/ContactsSdk5.java b/src/com/fsck/k9/helper/ContactsSdk5.java deleted file mode 100644 index e9efef10e..000000000 --- a/src/com/fsck/k9/helper/ContactsSdk5.java +++ /dev/null @@ -1,305 +0,0 @@ -package com.fsck.k9.helper; - -import java.util.ArrayList; - -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Intents; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.Intents.Insert; - -import com.fsck.k9.mail.Address; -import com.fsck.k9.K9; - -/** - * Access the contacts on the device using the API introduced with SDK 5. - * - * @see android.provider.ContactsContract - */ -public class ContactsSdk5 extends com.fsck.k9.helper.Contacts { - /** - * The order in which the search results are returned by - * {@link #searchContacts(CharSequence)}. - */ - protected static final String SORT_ORDER = - Email.TIMES_CONTACTED + " DESC, " + - Contacts.DISPLAY_NAME + ", " + - Email._ID; - - /** - * Array of columns to load from the database. - * - * Important: The _ID field is needed by - * {@link com.fsck.k9.EmailAddressAdapter} or more specificly by - * {@link android.widget.ResourceCursorAdapter}. - */ - protected static final String PROJECTION[] = { - Email._ID, - Contacts.DISPLAY_NAME, - Email.DATA, - Email.CONTACT_ID - }; - - /** - * Index of the name field in the projection. This must match the order in - * {@link #PROJECTION}. - */ - protected static final int NAME_INDEX = 1; - - /** - * Index of the email address field in the projection. This must match the - * order in {@link #PROJECTION}. - */ - protected static final int EMAIL_INDEX = 2; - - /** - * Index of the contact id field in the projection. This must match the order in - * {@link #PROJECTION}. - */ - protected static final int CONTACT_ID_INDEX = 3; - - - public ContactsSdk5(final Context context) { - super(context); - } - - @Override - public void createContact(final Address email) { - final Uri contactUri = Uri.fromParts("mailto", email.getAddress(), null); - - final Intent contactIntent = new Intent(Intents.SHOW_OR_CREATE_CONTACT); - contactIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - contactIntent.setData(contactUri); - - // Pass along full E-mail string for possible create dialog - contactIntent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, - email.toString()); - - // Only provide personal name hint if we have one - final String senderPersonal = email.getPersonal(); - if (senderPersonal != null) { - contactIntent.putExtra(Intents.Insert.NAME, senderPersonal); - } - - mContext.startActivity(contactIntent); - } - - @Override - public void addPhoneContact(final String phoneNumber) { - Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); - addIntent.putExtra(Insert.PHONE, Uri.decode(phoneNumber)); - addIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(addIntent); - } - - @Override - public boolean isInContacts(final String emailAddress) { - boolean result = false; - - final Cursor c = getContactByAddress(emailAddress); - - if (c != null) { - if (c.getCount() > 0) { - result = true; - } - c.close(); - } - - return result; - } - - @Override - public Cursor searchContacts(final CharSequence constraint) { - final String filter = (constraint == null) ? "" : constraint.toString(); - final Uri uri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(filter)); - final Cursor c = mContentResolver.query( - uri, - PROJECTION, - null, - null, - SORT_ORDER); - - if (c != null) { - /* - * To prevent expensive execution in the UI thread: - * Cursors get lazily executed, so if you don't call anything on - * the cursor before returning it from the background thread you'll - * have a complied program for the cursor, but it won't have been - * executed to generate the data yet. Often the execution is more - * expensive than the compilation... - */ - c.getCount(); - } - - return c; - } - - @Override - public String getNameForAddress(String address) { - if (address == null) { - return null; - } - - final Cursor c = getContactByAddress(address); - - String name = null; - if (c != null) { - if (c.getCount() > 0) { - c.moveToFirst(); - name = getName(c); - } - c.close(); - } - - return name; - } - - @Override - public String getName(Cursor c) { - return c.getString(NAME_INDEX); - } - - @Override - public String getEmail(Cursor c) { - return c.getString(EMAIL_INDEX); - } - - @Override - public void markAsContacted(final Address[] addresses) { - //TODO: Optimize! Potentially a lot of database queries - for (final Address address : addresses) { - final Cursor c = getContactByAddress(address.getAddress()); - - if (c != null) { - if (c.getCount() > 0) { - c.moveToFirst(); - final long personId = c.getLong(CONTACT_ID_INDEX); - ContactsContract.Contacts.markAsContacted(mContentResolver, personId); - } - c.close(); - } - } - } - - @Override - public Intent contactPickerIntent() { - return new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); - } - - @Override - public ContactItem extractInfoFromContactPickerIntent(final Intent data) { - Cursor cursor = null; - ArrayList email = new ArrayList(); - - try { - Uri result = data.getData(); - String displayName = null; - - // Get the contact id from the Uri - String id = result.getLastPathSegment(); - - cursor = mContentResolver.query(Email.CONTENT_URI, PROJECTION, - Email.CONTACT_ID + "=?", new String[] { id }, null); - - if (cursor != null) { - while (cursor.moveToNext()) { - String address = cursor.getString(EMAIL_INDEX); - if (address != null) { - email.add(address); - } - - if (displayName == null) { - displayName = cursor.getString(NAME_INDEX); - } - } - - // Return 'null' if no email addresses have been found - if (email.size() == 0) { - return null; - } - - // Use the first email address found as display name - if (displayName == null) { - displayName = email.get(0); - } - - return new ContactItem(displayName, email); - } - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Failed to get email data", e); - } finally { - Utility.closeQuietly(cursor); - } - - return null; - } - - /** - * Return a {@link Cursor} instance that can be used to fetch information - * about the contact with the given email address. - * - * @param address The email address to search for. - * @return A {@link Cursor} instance that can be used to fetch information - * about the contact with the given email address - */ - private Cursor getContactByAddress(final String address) { - final Uri uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(address)); - final Cursor c = mContentResolver.query( - uri, - PROJECTION, - null, - null, - SORT_ORDER); - return c; - } - - @Override - public Uri getPhotoUri(String address) { - Long contactId; - try { - final Cursor c = getContactByAddress(address); - if (c == null) { - return null; - } - - try { - if (!c.moveToFirst()) { - return null; - } - - contactId = c.getLong(CONTACT_ID_INDEX); - } finally { - c.close(); - } - - Cursor cur = mContentResolver.query( - ContactsContract.Data.CONTENT_URI, - null, - ContactsContract.Data.CONTACT_ID + "=" + contactId + " AND " - + ContactsContract.Data.MIMETYPE + "='" - + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "'", null, - null); - if (cur == null) { - return null; - } - if (!cur.moveToFirst()) { - cur.close(); - return null; // no photo - } - // Ok, they have a photo - cur.close(); - Uri person = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); - return Uri.withAppendedPath(person, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY); - } catch (Exception e) { - Log.e(K9.LOG_TAG, "Couldn't fetch photo for contact with email " + address, e); - return null; - } - } -} diff --git a/src/com/fsck/k9/helper/ContactsSdk5p.java b/src/com/fsck/k9/helper/ContactsSdk5p.java deleted file mode 100644 index bb5db650a..000000000 --- a/src/com/fsck/k9/helper/ContactsSdk5p.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.fsck.k9.helper; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.StructuredName; - -/** - * Access the contacts on the device using the API introduced with SDK 5. - * Use some additional code to make search work with phonetic names. - * - * Android versions >= 2.2 (Froyo) support searching for phonetic names - * out of the box (see {@link ContactsSdk5}). - * - * @see android.provider.ContactsContract - */ -public class ContactsSdk5p extends ContactsSdk5 { - public ContactsSdk5p(final Context context) { - super(context); - } - - @Override - public Cursor searchContacts(final CharSequence constraint) { - if (constraint == null) { - return null; - } - - // Lookup using Email.CONTENT_FILTER_URI to get matching contact ids. - // This does all sorts of magic we don't want to replicate. - final String filter = constraint.toString(); - final Uri uri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(filter)); - final Cursor cursor = mContentResolver.query( - uri, - new String[] {Email.CONTACT_ID}, - null, - null, - null); - - final StringBuilder matches = new StringBuilder(); - if ((cursor != null) && (cursor.getCount() > 0)) { - boolean first = true; - while (cursor.moveToNext()) { - if (first) { - first = false; - } else { - matches.append(","); - } - matches.append(cursor.getLong(0)); - } - cursor.close(); - } - - // Find contacts with email addresses that have been found using - // Email.CONTENT_FILTER_URI above or ones that have a matching phonetic name. - final String where = Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" + - " AND " + - "(" + - // Match if found by Email.CONTENT_FILTER_URI - Email.CONTACT_ID + " IN (" + matches.toString() + ")" + - " OR " + - // Match if phonetic given name starts with "constraint" - StructuredName.PHONETIC_GIVEN_NAME + " LIKE ?" + - " OR " + - // Match if phonetic given name contains a word that starts with "constraint" - StructuredName.PHONETIC_GIVEN_NAME + " LIKE ?" + - " OR " + - // Match if phonetic middle name starts with "constraint" - StructuredName.PHONETIC_MIDDLE_NAME + " LIKE ?" + - " OR " + - // Match if phonetic middle name contains a word that starts with "constraint" - StructuredName.PHONETIC_MIDDLE_NAME + " LIKE ?" + - " OR " + - // Match if phonetic family name starts with "constraint" - StructuredName.PHONETIC_FAMILY_NAME + " LIKE ?" + - " OR " + - // Match if phonetic family name contains a word that starts with "constraint" - StructuredName.PHONETIC_FAMILY_NAME + " LIKE ?" + - ")"; - final String filter1 = constraint.toString() + "%"; - final String filter2 = "% " + filter1; - final String[] args = new String[] {filter1, filter2, filter1, filter2, filter1, filter2}; - final Cursor c = mContentResolver.query( - Email.CONTENT_URI, - PROJECTION, - where, - args, - SORT_ORDER); - - if (c != null) { - /* - * To prevent expensive execution in the UI thread: - * Cursors get lazily executed, so if you don't call anything on - * the cursor before returning it from the background thread you'll - * have a complied program for the cursor, but it won't have been - * executed to generate the data yet. Often the execution is more - * expensive than the compilation... - */ - c.getCount(); - } - - return c; - } -}