From 0b3e939ac87c0b8fc4e45b08ea01f62e701f38c5 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 22 Oct 2012 19:17:08 -0700 Subject: [PATCH] Improve support for "me" contact. 1) Add >= ICS profile support (the system-supported "me" contact). 2) Improve <= Gingerbread support for me contact by auto-detecting contacts that have the same number as the SIM card. 3) Tie in identity key import/export support to the "me" contact. 4) Don't display a "me" selection option in preference if it can be auto-detected. 5) Refactor out the ContactAccessorNewApi back into the base class. --- AndroidManifest.xml | 38 +- res/values/strings.xml | 1 + res/xml/preferences.xml | 2 +- .../ApplicationPreferencesActivity.java | 75 +++- .../ContactSelectionListFragment.java | 6 +- .../ContactSelectionRecentFragment.java | 8 +- .../securesms/ConversationItem.java | 37 +- .../securesms/ConversationListActivity.java | 4 +- .../securesms/contacts/ContactAccessor.java | 363 +++++++++++++-- .../contacts/ContactAccessorNewApi.java | 424 ------------------ .../contacts/ContactIdentityManager.java | 28 ++ .../ContactIdentityManagerGingerbread.java | 137 ++++++ .../contacts/ContactIdentityManagerICS.java | 78 ++++ 13 files changed, 662 insertions(+), 539 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java create mode 100644 src/org/thoughtcrime/securesms/contacts/ContactIdentityManager.java create mode 100644 src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerGingerbread.java create mode 100644 src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4ceacc9a7a..c9ae93b500 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -7,26 +7,28 @@ + android:label="Access to TextSecure Secrets" + android:protectionLevel="signature" /> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + Don\'t copy + Currently: %s Not found! No valid identity key was found in the specified contact. You don\'t have an identity key! diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index fd58dd9607..df6695a8cc 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -38,7 +38,7 @@ android:title="@string/preferences__pref_enter_sends_title" /> - + rawContactIds = ContactIdentityManager + .getInstance(ApplicationPreferencesActivity.this) + .getSelfIdentityRawContactIds(); + + if (rawContactIds== null) { Toast.makeText(ApplicationPreferencesActivity.this, R.string.ApplicationPreferenceActivity_you_have_not_yet_defined_a_contact_for_yourself, Toast.LENGTH_LONG).show(); return true; } - ContactAccessor.getInstance().insertIdentityKey(ApplicationPreferencesActivity.this, Uri.parse(contactUri), + ContactAccessor.getInstance().insertIdentityKey(ApplicationPreferencesActivity.this, rawContactIds, IdentityKeyUtil.getIdentityKey(ApplicationPreferencesActivity.this)); Toast.makeText(ApplicationPreferencesActivity.this, @@ -205,7 +247,8 @@ public class ApplicationPreferencesActivity extends SherlockPreferenceActivity { MasterSecret masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret"); if (masterSecret != null) { - Intent importIntent = ContactAccessor.getInstance().getIntentForContactSelection(); + Intent importIntent = new Intent(Intent.ACTION_PICK); + importIntent.setType(ContactsContract.Contacts.CONTENT_TYPE); startActivityForResult(importIntent, IMPORT_IDENTITY_ID); } else { Toast.makeText(ApplicationPreferencesActivity.this, diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 5a0bacae00..2c27a1652b 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -292,11 +292,7 @@ public class ContactSelectionListFragment extends SherlockListFragment if (!isChecked) throw new AssertionError("We shouldn't be unchecking data that doesn't exist."); - existing = new ContactData(); - existing.id = contactData.id; - existing.name = contactData.name; - existing.numbers = new LinkedList(); - + existing = new ContactData(contactData.id, contactData.name); selectedContacts.put(existing.id, existing); } diff --git a/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java index 81b1fefa64..b6df83200e 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java @@ -182,13 +182,7 @@ public class ContactSelectionRecentFragment extends SherlockListFragment else if (type == Calls.OUTGOING_TYPE || type == RedPhoneCallTypes.OUTGOING) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_outgoing_call)); else if (type == Calls.MISSED_TYPE || type == RedPhoneCallTypes.MISSED) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_missed_call)); - this.contactData = new ContactData(); - - if (name != null) - this.contactData.name = name; - - this.contactData.id = id; - this.contactData.numbers = new LinkedList(); + this.contactData = new ContactData(id, name); this.contactData.numbers.add(new NumberData(null, number)); if (selectedContacts.containsKey(id)) diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index c85b9097af..c52b64663c 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -26,10 +26,8 @@ import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.os.Message; -import android.preference.PreferenceManager; import android.provider.Contacts.Intents; import android.provider.ContactsContract.QuickContact; -import android.telephony.TelephonyManager; import android.text.Spannable; import android.text.format.DateUtils; import android.text.style.ForegroundColorSpan; @@ -43,6 +41,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import org.thoughtcrime.securesms.contacts.ContactIdentityManager; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.MessageRecord; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -52,7 +51,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.protocol.Tag; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; -import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.service.SendReceiveService; import java.io.File; @@ -231,24 +229,16 @@ public class ConversationItem extends LinearLayout { } private void setContactPhotoForUserIdentity() { - String configuredContact = PreferenceManager.getDefaultSharedPreferences(context).getString(ApplicationPreferencesActivity.IDENTITY_PREF, null); - - try { - if (configuredContact != null) { - Recipient recipient = RecipientFactory.getRecipientForUri(context, Uri.parse(configuredContact)); - if (recipient != null) { - contactPhoto.setImageBitmap(recipient.getContactPhoto()); - return; - } - } + Uri selfIdentityContact = ContactIdentityManager.getInstance(context).getSelfIdentityUri(); - if (hasLocalNumber()) { - contactPhoto.setImageBitmap(RecipientFactory.getRecipientsFromString(context, getLocalNumber()).getPrimaryRecipient().getContactPhoto()); - } else { - contactPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_contact_picture)); + if (selfIdentityContact!= null) { + Recipient recipient = RecipientFactory.getRecipientForUri(context, selfIdentityContact); + if (recipient != null) { + contactPhoto.setImageBitmap(recipient.getContactPhoto()); + return; } - } catch (RecipientFormattingException rfe) { - Log.w("ConversationItem", rfe); + } else { + contactPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_contact_picture)); } } @@ -280,15 +270,6 @@ public class ConversationItem extends LinearLayout { setBodyImage(messageRecord); } - private String getLocalNumber() { - return ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number(); - } - - private boolean hasLocalNumber() { - String number = getLocalNumber(); - return (number != null) && (number.trim().length() > 0); - } - private void setStatusIcons(MessageRecord messageRecord) { failedImage.setVisibility(messageRecord.isFailed() ? View.VISIBLE : View.GONE); secureImage.setVisibility(messageRecord.isSecure() ? View.VISIBLE : View.GONE); diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 87e80a9a70..507e1d8085 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -12,6 +12,7 @@ import android.database.ContentObserver; import android.os.Bundle; import android.os.IBinder; import android.os.Parcelable; +import android.provider.ContactsContract; import android.util.Log; import com.actionbarsherlock.app.SherlockFragmentActivity; @@ -20,7 +21,6 @@ import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import org.thoughtcrime.securesms.ApplicationExportManager.ApplicationExportListener; -import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -249,7 +249,7 @@ public class ConversationListActivity extends SherlockFragmentActivity } }; - getContentResolver().registerContentObserver(ContactAccessor.getInstance().getContactsUri(), + getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, observer); } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index 3a80393efa..b439efab53 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -17,56 +17,257 @@ package org.thoughtcrime.securesms.contacts; import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; -import android.content.Intent; import android.database.Cursor; +import android.database.MergeCursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.PhoneLookup; import android.support.v4.content.CursorLoader; +import android.telephony.PhoneNumberUtils; +import android.util.Log; import org.thoughtcrime.securesms.crypto.IdentityKey; +import org.thoughtcrime.securesms.crypto.InvalidKeyException; +import org.thoughtcrime.securesms.util.Base64; +import java.io.IOException; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; /** - * Android changed their contacts API pretty heavily between - * 1.x and 2.x. This class provides a common interface to both - * API operations, using a singleton pattern that will Class.forName - * the correct one so we don't trigger NoClassDefFound exceptions on - * old platforms. + * This class was originally a layer of indirection between + * ContactAccessorNewApi and ContactAccesorOldApi, which corresponded + * to the API changes between 1.x and 2.x. + * + * Now that we no longer support 1.x, this class mostly serves as a place + * to encapsulate Contact-related logic. It's still a singleton, mostly + * just because that's how it's currently called from everywhere. * * @author Moxie Marlinspike */ -public abstract class ContactAccessor { - public static final int UNIQUE_ID = 0; - public static final int DISPLAY_NAME = 1; +public class ContactAccessor { - private static final ContactAccessor sInstance = new ContactAccessorNewApi(); + private static final ContactAccessor instance = new ContactAccessor(); public static synchronized ContactAccessor getInstance() { - return sInstance; - } - - public abstract NameAndNumber getNameAndNumberFromContact(Context context, Uri uri); - public abstract String getNameFromContact(Context context, Uri uri); - public abstract IdentityKey importIdentityKey(Context context, Uri uri); - public abstract void insertIdentityKey(Context context, Uri uri, IdentityKey identityKey); - public abstract Intent getIntentForContactSelection(); - public abstract List getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver); - public abstract List getGroupMembership(Context context, long groupId); - public abstract Cursor getCursorForContactGroups(Context context); - public abstract CursorLoader getCursorLoaderForContactGroups(Context context); - public abstract CursorLoader getCursorLoaderForContactsWithNumbers(Context context); - public abstract Cursor getCursorForContactsWithNumbers(Context context); - public abstract GroupData getGroupData(Context context, Cursor cursor); - public abstract ContactData getContactData(Context context, Cursor cursor); - public abstract Cursor getCursorForRecipientFilter(CharSequence constraint, ContentResolver mContentResolver); - public abstract CharSequence phoneTypeToString(Context mContext, int type, CharSequence label); - public abstract String getNameForNumber(Context context, String number); - public abstract Uri getContactsUri(); + return instance; + } + + public CursorLoader getCursorLoaderForContactsWithNumbers(Context context) { + Uri uri = ContactsContract.Contacts.CONTENT_URI; + String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1"; + + return new CursorLoader(context, uri, null, selection, null, + ContactsContract.Contacts.DISPLAY_NAME + " ASC"); + } + + public CursorLoader getCursorLoaderForContactGroups(Context context) { + return new CursorLoader(context, ContactsContract.Groups.CONTENT_URI, + null, null, null, ContactsContract.Groups.TITLE + " ASC"); + } + + public Cursor getCursorForContactsWithNumbers(Context context) { + Uri uri = ContactsContract.Contacts.CONTENT_URI; + String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1"; + + return context.getContentResolver().query(uri, null, selection, null, + ContactsContract.Contacts.DISPLAY_NAME + " ASC"); + } + + public String getNameFromContact(Context context, Uri uri) { + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(uri, new String[] {Contacts.DISPLAY_NAME}, + null, null, null); + + if (cursor != null && cursor.moveToFirst()) + return cursor.getString(0); + + } finally { + if (cursor != null) + cursor.close(); + } + + return null; + } + + public String getNameForNumber(Context context, String number) { + Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + + try { + if (cursor != null && cursor.moveToFirst()) + return cursor.getString(cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); + } finally { + if (cursor != null) + cursor.close(); + } + + return null; + } + + public GroupData getGroupData(Context context, Cursor cursor) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID)); + String title = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE)); + + return new GroupData(id, title); + } + + public ContactData getContactData(Context context, Cursor cursor) { + return getContactData(context, + cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)), + cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID))); +} + + private ContactData getContactData(Context context, String displayName, long id) { + ContactData contactData = new ContactData(id, displayName); + Cursor numberCursor = null; + + try { + numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, + Phone.CONTACT_ID + " = ?", + new String[] {contactData.id + ""}, null); + + while (numberCursor != null && numberCursor.moveToNext()) { + int type = numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)); + String label = numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL)); + String number = numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.NUMBER)); + String typeLabel = Phone.getTypeLabel(context.getResources(), type, label).toString(); + + contactData.numbers.add(new NumberData(typeLabel, number)); + } + } finally { + if (numberCursor != null) + numberCursor.close(); + } + + return contactData; + } + + public List getGroupMembership(Context context, long groupId) { + LinkedList contacts = new LinkedList(); + Cursor groupMembership = null; + + try { + String selection = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? AND " + + ContactsContract.CommonDataKinds.GroupMembership.MIMETYPE + " = ?"; + String[] args = new String[] {groupId+"", + ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE}; + + groupMembership = context.getContentResolver().query(Data.CONTENT_URI, null, selection, args, null); + + while (groupMembership != null && groupMembership.moveToNext()) { + String displayName = groupMembership.getString(groupMembership.getColumnIndexOrThrow(Data.DISPLAY_NAME)); + long contactId = groupMembership.getLong(groupMembership.getColumnIndexOrThrow(Data.CONTACT_ID)); + + contacts.add(getContactData(context, displayName, contactId)); + } + } finally { + if (groupMembership != null) + groupMembership.close(); + } + + return contacts; + } + + public List getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver) { + LinkedList numberList = new LinkedList(); + Cursor cursor = null; + + try { + cursor = contentResolver.query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, + Uri.encode(constraint)), + null, null, null, null); + + while (cursor != null && cursor.moveToNext()) { + numberList.add(cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER))); + } + + } finally { + if (cursor != null) + cursor.close(); + } + + return numberList; + } + + public CharSequence phoneTypeToString(Context mContext, int type, CharSequence label) { + return Phone.getTypeLabel(mContext.getResources(), type, label); + } + + public void insertIdentityKey(Context context, List rawContactIds, IdentityKey identityKey) { + for (long rawContactId : rawContactIds) { + Log.w("ContactAccessorNewApi", "Inserting data for raw contact id: " + rawContactId); + ContentValues contentValues = new ContentValues(); + contentValues.put(Data.RAW_CONTACT_ID, rawContactId); + contentValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); + contentValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM); + contentValues.put(Im.CUSTOM_PROTOCOL, "TextSecure-IdentityKey"); + contentValues.put(Im.DATA, Base64.encodeBytes(identityKey.serialize())); + + context.getContentResolver().insert(Data.CONTENT_URI, contentValues); + } + } + + public IdentityKey importIdentityKey(Context context, Uri uri) { + long contactId = getContactIdFromLookupUri(context, uri); + String selection = Im.CONTACT_ID + " = ? AND " + Im.PROTOCOL + " = ? AND " + Im.CUSTOM_PROTOCOL + " = ?"; + String[] selectionArgs = new String[] {contactId+"", Im.PROTOCOL_CUSTOM+"", "TextSecure-IdentityKey"}; + + Cursor cursor = context.getContentResolver().query(Data.CONTENT_URI, null, selection, selectionArgs, null); + + try { + if (cursor != null && cursor.moveToFirst()) { + String data = cursor.getString(cursor.getColumnIndexOrThrow(Im.DATA)); + + if (data != null) + return new IdentityKey(Base64.decode(data), 0); + + } + } catch (InvalidKeyException e) { + Log.w("ContactAccessorNewApi", e); + return null; + } catch (IOException e) { + Log.w("ContactAccessorNewApi", e); + return null; + } finally { + if (cursor != null) + cursor.close(); + } + + return null; + } + + private long getContactIdFromLookupUri(Context context, Uri uri) { + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(uri, + new String[] {ContactsContract.Contacts._ID}, + null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } else { + return -1; + } + + } finally { + if (cursor != null) + cursor.close(); + } + } public static class NumberData implements Parcelable { @@ -80,8 +281,8 @@ public abstract class ContactAccessor { } }; - public String number; - public String type; + public final String number; + public final String type; public NumberData(String type, String number) { this.type = type; @@ -104,8 +305,13 @@ public abstract class ContactAccessor { } public static class GroupData { - public long id; - public String name; + public final long id; + public final String name; + + public GroupData(long id, String name) { + this.id = id; + this.name = name; + } } public static class ContactData implements Parcelable { @@ -120,11 +326,15 @@ public abstract class ContactAccessor { } }; - public long id; - public String name; - public List numbers; + public final long id; + public final String name; + public final List numbers; - public ContactData() {} + public ContactData(long id, String name) { + this.id = id; + this.name = name; + this.numbers = new LinkedList(); + } public ContactData(Parcel in) { id = in.readLong(); @@ -144,4 +354,81 @@ public abstract class ContactAccessor { } } + /*** + * If the code below looks shitty to you, that's because it was taken + * directly from the Android source, where shitty code is all you get. + */ + + public Cursor getCursorForRecipientFilter(CharSequence constraint, + ContentResolver mContentResolver) + { + final String SORT_ORDER = Contacts.TIMES_CONTACTED + " DESC," + + Contacts.DISPLAY_NAME + "," + Phone.TYPE; + + final String[] PROJECTION_PHONE = { + Phone._ID, // 0 + Phone.CONTACT_ID, // 1 + Phone.TYPE, // 2 + Phone.NUMBER, // 3 + Phone.LABEL, // 4 + Phone.DISPLAY_NAME, // 5 + }; + + String phone = ""; + String cons = null; + + if (constraint != null) { + cons = constraint.toString(); + + if (RecipientsAdapter.usefulAsDigits(cons)) { + phone = PhoneNumberUtils.convertKeypadLettersToDigits(cons); + if (phone.equals(cons)) { + phone = ""; + } else { + phone = phone.trim(); + } + } + } + + Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(cons)); + String selection = String.format("%s=%s OR %s=%s OR %s=%s", + Phone.TYPE, + Phone.TYPE_MOBILE, + Phone.TYPE, + Phone.TYPE_WORK_MOBILE, + Phone.TYPE, + Phone.TYPE_MMS); + + Cursor phoneCursor = mContentResolver.query(uri, + PROJECTION_PHONE, + null, + null, + SORT_ORDER); + + if (phone.length() > 0) { + ArrayList result = new ArrayList(); + result.add(Integer.valueOf(-1)); // ID + result.add(Long.valueOf(-1)); // CONTACT_ID + result.add(Integer.valueOf(Phone.TYPE_CUSTOM)); // TYPE + result.add(phone); // NUMBER + + /* + * The "\u00A0" keeps Phone.getDisplayLabel() from deciding + * to display the default label ("Home") next to the transformation + * of the letters into numbers. + */ + result.add("\u00A0"); // LABEL + result.add(cons); // NAME + + ArrayList wrap = new ArrayList(); + wrap.add(result); + + ArrayListCursor translated = new ArrayListCursor(PROJECTION_PHONE, wrap); + + return new MergeCursor(new Cursor[] { translated, phoneCursor }); + } else { + return phoneCursor; + } + } + } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java deleted file mode 100644 index 7aa331c07f..0000000000 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java +++ /dev/null @@ -1,424 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.contacts; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.MergeCursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Im; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.PhoneLookup; -import android.provider.ContactsContract.RawContacts; -import android.support.v4.content.CursorLoader; -import android.telephony.PhoneNumberUtils; -import android.util.Log; - -import org.thoughtcrime.securesms.crypto.IdentityKey; -import org.thoughtcrime.securesms.crypto.InvalidKeyException; -import org.thoughtcrime.securesms.util.Base64; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -/** - * Interface into the Android 2.x+ contacts operations. - * - * @author Stuart Anderson - */ - -public class ContactAccessorNewApi extends ContactAccessor { - - private static final String SORT_ORDER = Contacts.TIMES_CONTACTED + " DESC," + Contacts.DISPLAY_NAME + "," + Phone.TYPE; - - private static final String[] PROJECTION_PHONE = { - Phone._ID, // 0 - Phone.CONTACT_ID, // 1 - Phone.TYPE, // 2 - Phone.NUMBER, // 3 - Phone.LABEL, // 4 - Phone.DISPLAY_NAME, // 5 - }; - - @Override - public List getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver) { - LinkedList numberList = new LinkedList(); - Cursor cursor = null; - - try { - cursor = contentResolver.query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(constraint)), - null, null, null, null); - - while (cursor != null && cursor.moveToNext()) - numberList.add(cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER))); - - } finally { - if (cursor != null) - cursor.close(); - } - - return numberList; - } - - @Override - public Cursor getCursorForRecipientFilter(CharSequence constraint, ContentResolver mContentResolver) { - String phone = ""; - String cons = null; - if (constraint != null) { - cons = constraint.toString(); - - if (RecipientsAdapter.usefulAsDigits(cons)) { - phone = PhoneNumberUtils.convertKeypadLettersToDigits(cons); - if (phone.equals(cons)) { - phone = ""; - } else { - phone = phone.trim(); - } - } - } - - Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(cons)); - String selection = String.format("%s=%s OR %s=%s OR %s=%s", - Phone.TYPE, - Phone.TYPE_MOBILE, - Phone.TYPE, - Phone.TYPE_WORK_MOBILE, - Phone.TYPE, - Phone.TYPE_MMS); - - Cursor phoneCursor = mContentResolver.query(uri, - PROJECTION_PHONE, - null, - null, - SORT_ORDER); - - - - if (phone.length() > 0) { - ArrayList result = new ArrayList(); - result.add(Integer.valueOf(-1)); // ID - result.add(Long.valueOf(-1)); // CONTACT_ID - result.add(Integer.valueOf(Phone.TYPE_CUSTOM)); // TYPE - result.add(phone); // NUMBER - - /* - * The "\u00A0" keeps Phone.getDisplayLabel() from deciding - * to display the default label ("Home") next to the transformation - * of the letters into numbers. - */ - result.add("\u00A0"); // LABEL - result.add(cons); // NAME - - ArrayList wrap = new ArrayList(); - wrap.add(result); - - ArrayListCursor translated = new ArrayListCursor(PROJECTION_PHONE, wrap); - - return new MergeCursor(new Cursor[] { translated, phoneCursor }); - } else { - return phoneCursor; - } - - } - - @Override - public CharSequence phoneTypeToString( Context mContext, int type, CharSequence label ) { - return Phone.getTypeLabel(mContext.getResources(), type, label); - } - - @Override - public Intent getIntentForContactSelection() { - Intent intent = new Intent(Intent.ACTION_PICK); - intent.setType(ContactsContract.Contacts.CONTENT_TYPE); - return intent; - } - - private long getContactIdFromLookupUri(Context context, Uri uri) { - Cursor cursor = null; - - try { - cursor = context.getContentResolver().query(uri, new String[] {ContactsContract.Contacts._ID}, null, null, null); - - if (cursor != null && cursor.moveToFirst()) - return cursor.getLong(0); - else - return -1; - - } finally { - if (cursor != null) - cursor.close(); - } - } - - private ArrayList getRawContactIds(Context context, long contactId) { - Cursor cursor = null; - ArrayList rawContactIds = new ArrayList(); - - try { - cursor = context.getContentResolver().query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID}, - RawContacts.CONTACT_ID + " = ?", new String[] {contactId+""}, - null); - - if (cursor == null) - return rawContactIds; - - while (cursor.moveToNext()) { - rawContactIds.add(Long.valueOf(cursor.getLong(0))); - } - } finally { - if (cursor != null) - cursor.close(); - } - - return rawContactIds; - } - - @Override - public void insertIdentityKey(Context context, Uri uri, IdentityKey identityKey) { - long contactId = getContactIdFromLookupUri(context, uri); - Log.w("ContactAccessorNewApi", "Got contact ID: " + contactId + " from uri: " + uri.toString()); - ArrayList rawContactIds = getRawContactIds(context, contactId); - - for (long rawContactId : rawContactIds) { - Log.w("ContactAccessorNewApi", "Inserting data for raw contact id: " + rawContactId); - ContentValues contentValues = new ContentValues(); - contentValues.put(Data.RAW_CONTACT_ID, rawContactId); - contentValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); - contentValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM); - contentValues.put(Im.CUSTOM_PROTOCOL, "TextSecure-IdentityKey"); - contentValues.put(Im.DATA, Base64.encodeBytes(identityKey.serialize())); - - context.getContentResolver().insert(Data.CONTENT_URI, contentValues); - } - } - - @Override - public IdentityKey importIdentityKey(Context context, Uri uri) { - long contactId = getContactIdFromLookupUri(context, uri); - String selection = Im.CONTACT_ID + " = ? AND " + Im.PROTOCOL + " = ? AND " + Im.CUSTOM_PROTOCOL + " = ?"; - String[] selectionArgs = new String[] {contactId+"", Im.PROTOCOL_CUSTOM+"", "TextSecure-IdentityKey"}; - - Cursor cursor = context.getContentResolver().query(Data.CONTENT_URI, null, selection, selectionArgs, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - String data = cursor.getString(cursor.getColumnIndexOrThrow(Im.DATA)); - - if (data != null) - return new IdentityKey(Base64.decode(data), 0); - - } - } catch (InvalidKeyException e) { - Log.w("ContactAccessorNewApi", e); - return null; - } catch (IOException e) { - Log.w("ContactAccessorNewApi", e); - return null; - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - @Override - public String getNameFromContact(Context context, Uri uri) { - Cursor cursor = null; - - try { - cursor = context.getContentResolver().query(uri, new String[] {Contacts.DISPLAY_NAME}, null, null, null); - - if (cursor != null && cursor.moveToFirst()) - return cursor.getString(0); - - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - private String getMobileNumberForId(Context context, long id) { - Cursor cursor = null; - - try { - cursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ? AND " + Phone.TYPE + " = ?", - new String[] {id+"", Phone.TYPE_MOBILE+""}, null); - - if (cursor != null && cursor.moveToFirst()) - return cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER)); - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - @Override - public NameAndNumber getNameAndNumberFromContact(Context context, Uri uri) { - Log.w("ContactAccessorNewApi", "Get name and number from: " + uri.toString()); - Cursor cursor = null; - - try { - NameAndNumber results = new NameAndNumber(); - cursor = context.getContentResolver().query(uri, new String[] {Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - results.name = cursor.getString(1); - results.number = getMobileNumberForId(context, cursor.getLong(0)); - return results; - } - - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - @Override - public CursorLoader getCursorLoaderForContactsWithNumbers(Context context) { - Uri uri = ContactsContract.Contacts.CONTENT_URI; - String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1"; - - return new CursorLoader(context, uri, null, selection, null, - ContactsContract.Contacts.DISPLAY_NAME + " ASC"); - } - - @Override - public Cursor getCursorForContactsWithNumbers(Context context) { - Uri uri = ContactsContract.Contacts.CONTENT_URI; - String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1"; - - return context.getContentResolver().query(uri, null, selection, null, - ContactsContract.Contacts.DISPLAY_NAME + " ASC"); - } - - private ContactData getContactData(Context context, String displayName, long id) { - ContactData contactData = new ContactData(); - contactData.id = id; - contactData.name = displayName; - contactData.numbers = new LinkedList(); - - Cursor numberCursor = null; - - try { - numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ?", - new String[] {contactData.id + ""}, null); - - while (numberCursor != null && numberCursor.moveToNext()) - contactData.numbers.add(new NumberData(Phone.getTypeLabel(context.getResources(), - numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)), - numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL))).toString(), - numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.NUMBER)))); - } finally { - if (numberCursor != null) - numberCursor.close(); - } - - return contactData; - - } - - @Override - public ContactData getContactData(Context context, Cursor cursor) { - return getContactData(context, - cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)), - cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID))); - } - - @Override - public Cursor getCursorForContactGroups(Context context) { - return context.getContentResolver().query(ContactsContract.Groups.CONTENT_URI, null, null, null, ContactsContract.Groups.TITLE + " ASC"); - } - - @Override - public CursorLoader getCursorLoaderForContactGroups(Context context) { - return new CursorLoader(context, ContactsContract.Groups.CONTENT_URI, - null, null, null, ContactsContract.Groups.TITLE + " ASC"); - } - - @Override - public List getGroupMembership(Context context, long groupId) { - LinkedList contacts = new LinkedList(); - Cursor groupMembership = null; - - try { - String selection = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? AND " + - ContactsContract.CommonDataKinds.GroupMembership.MIMETYPE + " = ?"; - String[] args = new String[] {groupId+"", - ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE}; - - groupMembership = context.getContentResolver().query(Data.CONTENT_URI, null, selection, args, null); - - while (groupMembership != null && groupMembership.moveToNext()) { - String displayName = groupMembership.getString(groupMembership.getColumnIndexOrThrow(Data.DISPLAY_NAME)); - long contactId = groupMembership.getLong(groupMembership.getColumnIndexOrThrow(Data.CONTACT_ID)); - - contacts.add(getContactData(context, displayName, contactId)); - } - } finally { - if (groupMembership != null) - groupMembership.close(); - } - - return contacts; - } - - @Override - public GroupData getGroupData(Context context, Cursor cursor) { - GroupData groupData = new GroupData(); - groupData.id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID)); - groupData.name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE)); - - return groupData; - } - - @Override - public String getNameForNumber(Context context, String number) { - Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); - Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) - return cursor.getString(cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - @Override - public Uri getContactsUri() { - return ContactsContract.Contacts.CONTENT_URI; - } - -} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/contacts/ContactIdentityManager.java b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManager.java new file mode 100644 index 0000000000..d829d3be6b --- /dev/null +++ b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManager.java @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.contacts; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; + +import java.util.List; + +public abstract class ContactIdentityManager { + + public static ContactIdentityManager getInstance(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return new ContactIdentityManagerICS(context); + else + return new ContactIdentityManagerGingerbread(context); + } + + protected final Context context; + + public ContactIdentityManager(Context context) { + this.context = context.getApplicationContext(); + } + + public abstract Uri getSelfIdentityUri(); + public abstract boolean isSelfIdentityAutoDetected(); + public abstract List getSelfIdentityRawContactIds(); + +} diff --git a/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerGingerbread.java b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerGingerbread.java new file mode 100644 index 0000000000..5f4793ecb0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerGingerbread.java @@ -0,0 +1,137 @@ +package org.thoughtcrime.securesms.contacts; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.PhoneLookup; +import android.provider.ContactsContract.RawContacts; +import android.telephony.TelephonyManager; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; + +import java.util.ArrayList; +import java.util.List; + +class ContactIdentityManagerGingerbread extends ContactIdentityManager { + + public ContactIdentityManagerGingerbread(Context context) { + super(context); + } + + @Override + public Uri getSelfIdentityUri() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String contactUriString = preferences.getString(ApplicationPreferencesActivity.IDENTITY_PREF, null); + + if (hasLocalNumber()) { + return getContactUriForNumber(getLocalNumber()); + } else if (contactUriString != null) { + return Uri.parse(contactUriString); + } + + return null; + } + + @Override + public boolean isSelfIdentityAutoDetected() { + return hasLocalNumber() && getContactUriForNumber(getLocalNumber()) != null; + } + + @Override + public List getSelfIdentityRawContactIds() { + long selfIdentityContactId = getSelfIdentityContactId(); + + if (selfIdentityContactId == -1) + return null; + + Cursor cursor = null; + ArrayList rawContactIds = new ArrayList(); + + try { + cursor = context.getContentResolver().query(RawContacts.CONTENT_URI, + new String[] {RawContacts._ID}, + RawContacts.CONTACT_ID + " = ?", + new String[] {selfIdentityContactId+""}, + null); + + if (cursor == null || cursor.getCount() == 0) + return null; + + while (cursor.moveToNext()) { + rawContactIds.add(Long.valueOf(cursor.getLong(0))); + } + + return rawContactIds; + + } finally { + if (cursor != null) + cursor.close(); + } + } + + private Uri getContactUriForNumber(String number) { + String[] PROJECTION = new String[] { + PhoneLookup.DISPLAY_NAME, + PhoneLookup.LOOKUP_KEY, + PhoneLookup._ID, + }; + + Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(uri, PROJECTION, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + return Contacts.getLookupUri(cursor.getLong(2), cursor.getString(1)); + } + } finally { + if (cursor != null) + cursor.close(); + } + + return null; + } + + private long getSelfIdentityContactId() { + Uri contactUri = getSelfIdentityUri(); + + if (contactUri == null) + return -1; + + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(contactUri, + new String[] {ContactsContract.Contacts._ID}, + null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } else { + return -1; + } + + } finally { + if (cursor != null) + cursor.close(); + } + } + + private String getLocalNumber() { + return ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)) + .getLine1Number(); + } + + private boolean hasLocalNumber() { + String number = getLocalNumber(); + return (number != null) && (number.trim().length() > 0); + } + + + +} diff --git a/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java new file mode 100644 index 0000000000..c8652ef6b5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java @@ -0,0 +1,78 @@ +package org.thoughtcrime.securesms.contacts; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.PhoneLookup; + +import java.util.LinkedList; +import java.util.List; + +class ContactIdentityManagerICS extends ContactIdentityManager { + + public ContactIdentityManagerICS(Context context) { + super(context); + } + + @SuppressLint("NewApi") + @Override + public Uri getSelfIdentityUri() { + String[] PROJECTION = new String[] { + PhoneLookup.DISPLAY_NAME, + PhoneLookup.LOOKUP_KEY, + PhoneLookup._ID, + }; + + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(ContactsContract.Profile.CONTENT_URI, + PROJECTION, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + return Contacts.getLookupUri(cursor.getLong(2), cursor.getString(1)); + } + } finally { + if (cursor != null) + cursor.close(); + } + + return null; + } + + @Override + public boolean isSelfIdentityAutoDetected() { + return true; + } + + @Override + public List getSelfIdentityRawContactIds() { + List results = new LinkedList(); + + String[] PROJECTION = new String[] { + ContactsContract.Profile._ID + }; + + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI, + PROJECTION, null, null, null); + + if (cursor == null || cursor.getCount() == 0) + return null; + + while (cursor.moveToNext()) { + results.add(cursor.getLong(0)); + } + + return results; + } finally { + if (cursor != null) + cursor.close(); + } + } +}