Clean up contact queries.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-07-14 14:31:03 -07:00
parent d1940fe0f9
commit 704f2b91e2
7 changed files with 206 additions and 358 deletions

View File

@ -32,9 +32,10 @@ import android.widget.AdapterView;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -140,11 +141,10 @@ public class PushContactSelectionListFragment extends Fragment
@Override @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (getActivity().getIntent().getBooleanExtra(PushContactSelectionActivity.PUSH_ONLY_EXTRA, false)) { boolean pushOnly = getActivity().getIntent().getBooleanExtra(PushContactSelectionActivity.PUSH_ONLY_EXTRA, false);
return ContactAccessor.getInstance().getCursorLoaderForPushContacts(getActivity(), cursorFilter); boolean supportsSms = TextSecurePreferences.isSmsEnabled(getActivity());
} else {
return ContactAccessor.getInstance().getCursorLoaderForContacts(getActivity(), cursorFilter); return new ContactsCursorLoader(getActivity(), !pushOnly && supportsSms, cursorFilter);
}
} }
@Override @Override

View File

@ -23,13 +23,9 @@ import android.database.MergeCursor;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.PhoneLookup;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.telephony.PhoneNumberUtils; import android.telephony.PhoneNumberUtils;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -65,35 +61,6 @@ public class ContactAccessor {
return instance; 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 Loader<Cursor> getCursorLoaderForContacts(Context context, String filter) {
return new ContactsCursorLoader(context, filter, false);
}
public Loader<Cursor> getCursorLoaderForPushContacts(Context context, String filter) {
return new ContactsCursorLoader(context, filter, true);
}
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 Collection<ContactData> getContactsWithPush(Context context) { public Collection<ContactData> getContactsWithPush(Context context) {
final ContentResolver resolver = context.getContentResolver(); final ContentResolver resolver = context.getContentResolver();
final String[] inProjection = new String[]{PhoneLookup._ID, PhoneLookup.DISPLAY_NAME}; final String[] inProjection = new String[]{PhoneLookup._ID, PhoneLookup.DISPLAY_NAME};
@ -136,34 +103,6 @@ public class ContactAccessor {
return null; 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)));
}
public ContactData getContactData(Context context, Uri uri) { public ContactData getContactData(Context context, Uri uri) {
return getContactData(context, getNameFromContact(context, uri), Long.parseLong(uri.getLastPathSegment())); return getContactData(context, getNameFromContact(context, uri), Long.parseLong(uri.getLastPathSegment()));
} }
@ -193,32 +132,6 @@ public class ContactAccessor {
return contactData; return contactData;
} }
public List<ContactData> getGroupMembership(Context context, long groupId) {
LinkedList<ContactData> contacts = new LinkedList<ContactData>();
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<String> getNumbersForThreadSearchFilter(Context context, String constraint) { public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
LinkedList<String> numberList = new LinkedList<>(); LinkedList<String> numberList = new LinkedList<>();
Cursor cursor = null; Cursor cursor = null;
@ -258,26 +171,6 @@ public class ContactAccessor {
return Phone.getTypeLabel(mContext.getResources(), type, label); return Phone.getTypeLabel(mContext.getResources(), type, label);
} }
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 { public static class NumberData implements Parcelable {
public static final Parcelable.Creator<NumberData> CREATOR = new Parcelable.Creator<NumberData>() { public static final Parcelable.Creator<NumberData> CREATOR = new Parcelable.Creator<NumberData>() {
@ -313,16 +206,6 @@ public class ContactAccessor {
} }
} }
public static class GroupData {
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 { public static class ContactData implements Parcelable {
public static final Parcelable.Creator<ContactData> CREATOR = new Parcelable.Creator<ContactData>() { public static final Parcelable.Creator<ContactData> CREATOR = new Parcelable.Creator<ContactData>() {
@ -345,13 +228,6 @@ public class ContactAccessor {
this.numbers = new LinkedList<NumberData>(); this.numbers = new LinkedList<NumberData>();
} }
public ContactData(long id, String name, List<NumberData> numbers) {
this.id = id;
this.name = name;
this.numbers = numbers;
}
public ContactData(Parcel in) { public ContactData(Parcel in) {
id = in.readLong(); id = in.readLong();
name = in.readString(); name = in.readString();

View File

@ -70,21 +70,21 @@ public class ContactSelectionListAdapter extends CursorAdapter
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN)); long id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN));
int type = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN)); int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN)); String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN)); String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN)); int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN)); String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN));
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(), String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(),
numberType, label).toString(); numberType, label).toString();
int color = (type == ContactsDatabase.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) : int color = (contactType == ContactsDatabase.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
drawables.getColor(1, 0xff000000); drawables.getColor(1, 0xff000000);
((ContactSelectionListItem)view).unbind(); ((ContactSelectionListItem)view).unbind();
((ContactSelectionListItem)view).set(id, type, name, number, labelText, color, multiSelect); ((ContactSelectionListItem)view).set(id, contactType, name, number, labelText, color, multiSelect);
((ContactSelectionListItem)view).setChecked(selectedContacts.containsKey(id)); ((ContactSelectionListItem)view).setChecked(selectedContacts.containsKey(id));
} }
@ -105,10 +105,10 @@ public class ContactSelectionListAdapter extends CursorAdapter
cursor.moveToPosition(i); cursor.moveToPosition(i);
int type = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN)); int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
if (type == ContactsDatabase.PUSH_TYPE) holder.text.setText(R.string.contact_selection_list__header_textsecure_users); if (contactType == ContactsDatabase.PUSH_TYPE) holder.text.setText(R.string.contact_selection_list__header_textsecure_users);
else holder.text.setText(R.string.contact_selection_list__header_other); else holder.text.setText(R.string.contact_selection_list__header_other);
return convertView; return convertView;
} }
@ -118,7 +118,7 @@ public class ContactSelectionListAdapter extends CursorAdapter
Cursor cursor = getCursor(); Cursor cursor = getCursor();
cursor.moveToPosition(i); cursor.moveToPosition(i);
return cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN)); return cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
} }
public Map<Long, String> getSelectedContacts() { public Map<Long, String> getSelectedContacts() {

View File

@ -18,12 +18,15 @@ package org.thoughtcrime.securesms.contacts;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.MergeCursor;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import junit.framework.Assert; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.NumberUtil;
import java.util.concurrent.Semaphore; import java.util.ArrayList;
/** /**
* CursorLoader that initializes a ContactsDatabase instance * CursorLoader that initializes a ContactsDatabase instance
@ -31,47 +34,34 @@ import java.util.concurrent.Semaphore;
* @author Jake McGinty * @author Jake McGinty
*/ */
public class ContactsCursorLoader extends CursorLoader { public class ContactsCursorLoader extends CursorLoader {
private static final String TAG = ContactsCursorLoader.class.getSimpleName();
private static final int DB_PERMITS = 100;
private final Context context; private static final String TAG = ContactsCursorLoader.class.getSimpleName();
private final String filter;
private final boolean pushOnly;
private final Semaphore dbSemaphore = new Semaphore(DB_PERMITS);
private ContactsDatabase db;
public ContactsCursorLoader(Context context, String filter, boolean pushOnly) { private final String filter;
private boolean includeSmsContacts;
public ContactsCursorLoader(Context context, boolean includeSmsContacts, String filter) {
super(context); super(context);
this.context = context;
this.filter = filter; this.filter = filter;
this.pushOnly = pushOnly; this.includeSmsContacts = includeSmsContacts;
this.db = new ContactsDatabase(context);
} }
@Override @Override
public Cursor loadInBackground() { public Cursor loadInBackground() {
try { ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(getContext());
dbSemaphore.acquire(); ArrayList<Cursor> cursorList = new ArrayList<>(3);
return db.query(filter, pushOnly);
} catch (InterruptedException ie) {
throw new AssertionError(ie);
} finally {
dbSemaphore.release();
}
}
@Override cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
public void onReset() {
Log.w(TAG, "onReset()"); if (includeSmsContacts) {
try { cursorList.add(contactsDatabase.querySystemContacts(filter));
dbSemaphore.acquire(DB_PERMITS);
db.close();
db = new ContactsDatabase(context);
} catch (InterruptedException ie) {
throw new AssertionError(ie);
} finally {
dbSemaphore.release(DB_PERMITS);
} }
super.onReset();
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
cursorList.add(contactsDatabase.getNewNumberCursor(filter));
}
return new MergeCursor(cursorList.toArray(new Cursor[0]));
} }
} }

View File

@ -18,33 +18,26 @@ package org.thoughtcrime.securesms.contacts;
import android.accounts.Account; import android.accounts.Account;
import android.content.ContentProviderOperation; import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.OperationApplicationException; import android.content.OperationApplicationException;
import android.database.Cursor; import android.database.Cursor;
import android.database.CursorWrapper; import android.database.CursorWrapper;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri; import android.net.Uri;
import android.os.RemoteException; import android.os.RemoteException;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.RawContacts;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Pair;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -55,46 +48,25 @@ import java.util.Set;
* @author Jake McGinty * @author Jake McGinty
*/ */
public class ContactsDatabase { public class ContactsDatabase {
private static final String TAG = ContactsDatabase.class.getSimpleName(); private static final String TAG = ContactsDatabase.class.getSimpleName();
private final DatabaseOpenHelper dbHelper;
private final Context context;
public static final String TABLE_NAME = "CONTACTS"; public static final String ID_COLUMN = "_id";
public static final String ID_COLUMN = ContactsContract.CommonDataKinds.Phone._ID; public static final String NAME_COLUMN = "name";
public static final String NAME_COLUMN = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME; public static final String NUMBER_COLUMN = "number";
public static final String NUMBER_TYPE_COLUMN = ContactsContract.CommonDataKinds.Phone.TYPE; public static final String NUMBER_TYPE_COLUMN = "number_type";
public static final String NUMBER_COLUMN = ContactsContract.CommonDataKinds.Phone.NUMBER; public static final String LABEL_COLUMN = "label";
public static final String LABEL_COLUMN = ContactsContract.CommonDataKinds.Phone.LABEL; public static final String CONTACT_TYPE_COLUMN = "contact_type";
public static final String TYPE_COLUMN = "type";
private static final String FILTER_SELECTION = NAME_COLUMN + " LIKE ? OR " + NUMBER_COLUMN + " LIKE ?";
private static final String CONTACT_LIST_SORT = NAME_COLUMN + " COLLATE NOCASE ASC";
private static final String[] ANDROID_PROJECTION = new String[]{ID_COLUMN,
NAME_COLUMN,
NUMBER_TYPE_COLUMN,
LABEL_COLUMN,
NUMBER_COLUMN};
private static final String[] CONTACTS_PROJECTION = new String[]{ID_COLUMN,
NAME_COLUMN,
NUMBER_TYPE_COLUMN,
LABEL_COLUMN,
NUMBER_COLUMN,
TYPE_COLUMN};
public static final int NORMAL_TYPE = 0; public static final int NORMAL_TYPE = 0;
public static final int PUSH_TYPE = 1; public static final int PUSH_TYPE = 1;
public static final int GROUP_TYPE = 2;
private final Context context;
public ContactsDatabase(Context context) { public ContactsDatabase(Context context) {
this.dbHelper = new DatabaseOpenHelper(context);
this.context = context; this.context = context;
} }
public void close() {
dbHelper.close();
}
public synchronized void setRegisteredUsers(Account account, List<String> e164numbers) public synchronized void setRegisteredUsers(Account account, List<String> e164numbers)
throws RemoteException, OperationApplicationException throws RemoteException, OperationApplicationException
{ {
@ -153,6 +125,7 @@ public class ContactsDatabase {
.withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index) .withValueBackReference(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, index)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, e164number) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, e164number)
.withValue(ContactsContract.Data.SYNC2, "__TS")
.build()); .build());
operations.add(ContentProviderOperation.newInsert(dataUri) operations.add(ContentProviderOperation.newInsert(dataUri)
@ -176,183 +149,184 @@ public class ContactsDatabase {
.withSelection(BaseColumns._ID + " = ?", new String[] {String.valueOf(rowId)}) .withSelection(BaseColumns._ID + " = ?", new String[] {String.valueOf(rowId)})
.build()); .build());
} }
public @NonNull Cursor querySystemContacts(String filter) {
Uri uri;
public Cursor query(String filter, boolean pushOnly) {
// FIXME: This doesn't make sense to me. You pass in pushOnly, but then
// conditionally check to see whether other contacts should be included
// in the query method itself? I don't think this method should have any
// understanding of that stuff.
final boolean includeAndroidContacts = !pushOnly && TextSecurePreferences.isSmsEnabled(context);
final Cursor localCursor = queryLocalDb(filter);
final Cursor androidCursor;
final MatrixCursor newNumberCursor;
if (includeAndroidContacts) {
androidCursor = queryAndroidDb(filter);
} else {
androidCursor = null;
}
if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) {
newNumberCursor = new MatrixCursor(CONTACTS_PROJECTION, 1);
newNumberCursor.addRow(new Object[]{-1L, context.getString(R.string.contact_selection_list__unknown_contact),
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, "\u21e2", filter, NORMAL_TYPE});
} else {
newNumberCursor = null;
}
List<Cursor> cursors = new ArrayList<Cursor>();
if (localCursor != null) cursors.add(localCursor);
if (androidCursor != null) cursors.add(androidCursor);
if (newNumberCursor != null) cursors.add(newNumberCursor);
switch (cursors.size()) {
case 0: return null;
case 1: return cursors.get(0);
default: return new MergeCursor(cursors.toArray(new Cursor[]{}));
}
}
private Cursor queryAndroidDb(String filter) {
final Uri baseUri;
if (!TextUtils.isEmpty(filter)) { if (!TextUtils.isEmpty(filter)) {
baseUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(filter));
Uri.encode(filter));
} else { } else {
baseUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
} }
Cursor cursor = context.getContentResolver().query(baseUri, ANDROID_PROJECTION, null, null, CONTACT_LIST_SORT);
return cursor == null ? null : new TypedCursorWrapper(cursor); String[] projection = new String[]{ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.LABEL};
String sort = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " COLLATE NOCASE ASC";
Map<String, String> projectionMap = new HashMap<String, String>() {{
put(ID_COLUMN, ContactsContract.CommonDataKinds.Phone._ID);
put(NAME_COLUMN, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
put(NUMBER_COLUMN, ContactsContract.CommonDataKinds.Phone.NUMBER);
put(NUMBER_TYPE_COLUMN, ContactsContract.CommonDataKinds.Phone.TYPE);
put(LABEL_COLUMN, ContactsContract.CommonDataKinds.Phone.LABEL);
}};
Cursor cursor = context.getContentResolver().query(uri, projection,
ContactsContract.Data.SYNC2 + " IS NULL OR " +
ContactsContract.Data.SYNC2 + " != ?",
new String[] {"__TS"},
sort);
return new ProjectionMappingCursor(cursor, projectionMap,
new Pair<String, Object>(CONTACT_TYPE_COLUMN, NORMAL_TYPE));
} }
private Cursor queryLocalDb(String filter) { public @NonNull Cursor queryTextSecureContacts(String filter) {
final String selection; String[] projection = new String[] {ContactsContract.Data._ID,
final String[] selectionArgs; ContactsContract.Contacts.DISPLAY_NAME,
final String fuzzyFilter = "%" + filter + "%"; ContactsContract.Data.DATA1};
if (!TextUtils.isEmpty(filter)) {
selection = FILTER_SELECTION; String sort = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE NOCASE ASC";
selectionArgs = new String[]{fuzzyFilter, fuzzyFilter};
Map<String, String> projectionMap = new HashMap<String, String>(){{
put(ID_COLUMN, ContactsContract.Data._ID);
put(NAME_COLUMN, ContactsContract.Contacts.DISPLAY_NAME);
put(NUMBER_COLUMN, ContactsContract.Data.DATA1);
}};
Cursor cursor;
if (TextUtils.isEmpty(filter)) {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.MIMETYPE + " = ?",
new String[] {"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact"},
sort);
} else { } else {
selection = null; cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
selectionArgs = null; projection,
ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
new String[] {"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact",
"%" + filter + "%"},
sort);
} }
return queryLocalDb(selection, selectionArgs, null);
return new ProjectionMappingCursor(cursor, projectionMap,
new Pair<String, Object>(LABEL_COLUMN, "TextSecure"),
new Pair<String, Object>(NUMBER_TYPE_COLUMN, 0),
new Pair<String, Object>(CONTACT_TYPE_COLUMN, PUSH_TYPE));
} }
private Cursor queryLocalDb(String selection, String[] selectionArgs, String[] columns) { public Cursor getNewNumberCursor(String filter) {
SQLiteDatabase localDb = dbHelper.getReadableDatabase(); MatrixCursor newNumberCursor = new MatrixCursor(new String[] {ID_COLUMN, NAME_COLUMN, NUMBER_COLUMN, NUMBER_TYPE_COLUMN, LABEL_COLUMN, CONTACT_TYPE_COLUMN}, 1);
final Cursor localCursor; newNumberCursor.addRow(new Object[]{-1L, context.getString(R.string.contact_selection_list__unknown_contact),
if (localDb != null) localCursor = localDb.query(TABLE_NAME, columns, selection, selectionArgs, null, null, CONTACT_LIST_SORT); filter, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
else localCursor = null; "\u21e2", NORMAL_TYPE});
if (localCursor != null && !localCursor.moveToFirst()) {
localCursor.close(); return newNumberCursor;
return null;
}
return localCursor;
} }
private static class DatabaseOpenHelper extends SQLiteOpenHelper { private static class ProjectionMappingCursor extends CursorWrapper {
private final Context context; private final Map<String, String> projectionMap;
private SQLiteDatabase mDatabase; private final Pair<String, Object>[] extras;
private static final String TABLE_CREATE = @SafeVarargs
"CREATE TABLE " + TABLE_NAME + " (" + public ProjectionMappingCursor(Cursor cursor,
ID_COLUMN + " INTEGER PRIMARY KEY, " + Map<String, String> projectionMap,
NAME_COLUMN + " TEXT, " + Pair<String, Object>... extras)
NUMBER_TYPE_COLUMN + " INTEGER, " + {
LABEL_COLUMN + " TEXT, " +
NUMBER_COLUMN + " TEXT, " +
TYPE_COLUMN + " INTEGER);";
DatabaseOpenHelper(Context context) {
super(context, null, null, 1);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(TAG, "onCreate called for contacts database.");
mDatabase = db;
mDatabase.execSQL(TABLE_CREATE);
if (TextSecurePreferences.isPushRegistered(context)) {
try {
loadPushUsers();
} catch (IOException ioe) {
Log.e(TAG, "Issue when trying to load push users into memory db.", ioe);
}
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
private void loadPushUsers() throws IOException {
Log.d(TAG, "populating push users into virtual db.");
Collection<ContactAccessor.ContactData> pushUsers = ContactAccessor.getInstance().getContactsWithPush(context);
for (ContactAccessor.ContactData user : pushUsers) {
ContentValues values = new ContentValues();
values.put(ID_COLUMN, user.id);
values.put(NAME_COLUMN, user.name);
values.put(NUMBER_TYPE_COLUMN, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM);
values.put(LABEL_COLUMN, (String)null);
values.put(NUMBER_COLUMN, user.numbers.get(0).number);
values.put(TYPE_COLUMN, PUSH_TYPE);
mDatabase.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
}
Log.d(TAG, "finished populating push users.");
}
}
private static class TypedCursorWrapper extends CursorWrapper {
private final int pushColumnIndex;
public TypedCursorWrapper(Cursor cursor) {
super(cursor); super(cursor);
pushColumnIndex = cursor.getColumnCount(); this.projectionMap = projectionMap;
this.extras = extras;
} }
@Override @Override
public int getColumnCount() { public int getColumnCount() {
return super.getColumnCount() + 1; return super.getColumnCount() + extras.length;
} }
@Override @Override
public int getColumnIndex(String columnName) { public int getColumnIndex(String columnName) {
if (TYPE_COLUMN.equals(columnName)) return super.getColumnCount(); for (int i=0;i<extras.length;i++) {
else return super.getColumnIndex(columnName); if (extras[i].first.equals(columnName)) {
return super.getColumnCount() + i;
}
}
return super.getColumnIndex(projectionMap.get(columnName));
} }
@Override @Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
if (TYPE_COLUMN.equals(columnName)) return super.getColumnCount(); int index = getColumnIndex(columnName);
else return super.getColumnIndexOrThrow(columnName);
if (index == -1) throw new IllegalArgumentException("Bad column name!");
else return index;
} }
@Override @Override
public String getColumnName(int columnIndex) { public String getColumnName(int columnIndex) {
if (columnIndex == pushColumnIndex) return TYPE_COLUMN; int baseColumnCount = super.getColumnCount();
else return super.getColumnName(columnIndex);
if (columnIndex >= baseColumnCount) {
int offset = columnIndex - baseColumnCount;
return extras[offset].first;
}
return getReverseProjection(super.getColumnName(columnIndex));
} }
@Override @Override
public String[] getColumnNames() { public String[] getColumnNames() {
final String[] columns = new String[super.getColumnCount() + 1]; String[] names = super.getColumnNames();
System.arraycopy(super.getColumnNames(), 0, columns, 0, super.getColumnCount()); String[] allNames = new String[names.length + extras.length];
columns[pushColumnIndex] = TYPE_COLUMN;
return columns; for (int i=0;i<names.length;i++) {
allNames[i] = getReverseProjection(names[i]);
}
for (int i=0;i<extras.length;i++) {
allNames[names.length + i] = extras[i].first;
}
return allNames;
} }
@Override @Override
public int getInt(int columnIndex) { public int getInt(int columnIndex) {
if (columnIndex == pushColumnIndex) return NORMAL_TYPE; if (columnIndex >= super.getColumnCount()) {
else return super.getInt(columnIndex); int offset = columnIndex - super.getColumnCount();
return (Integer)extras[offset].second;
}
return super.getInt(columnIndex);
}
@Override
public String getString(int columnIndex) {
if (columnIndex >= super.getColumnCount()) {
int offset = columnIndex - super.getColumnCount();
return (String)extras[offset].second;
}
return super.getString(columnIndex);
}
private @Nullable String getReverseProjection(String columnName) {
for (Map.Entry<String, String> entry : projectionMap.entrySet()) {
if (entry.getValue().equals(columnName)) {
return entry.getKey();
}
}
return null;
} }
} }
} }

View File

@ -26,6 +26,7 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -86,6 +87,7 @@ public class DatabaseFactory {
private final PushDatabase pushDatabase; private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase; private final GroupDatabase groupDatabase;
private final RecipientPreferenceDatabase recipientPreferenceDatabase; private final RecipientPreferenceDatabase recipientPreferenceDatabase;
private final ContactsDatabase contactsDatabase;
public static DatabaseFactory getInstance(Context context) { public static DatabaseFactory getInstance(Context context) {
synchronized (lock) { synchronized (lock) {
@ -148,6 +150,10 @@ public class DatabaseFactory {
return getInstance(context).recipientPreferenceDatabase; return getInstance(context).recipientPreferenceDatabase;
} }
public static ContactsDatabase getContactsDatabase(Context context) {
return getInstance(context).contactsDatabase;
}
private DatabaseFactory(Context context) { private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION); this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper); this.sms = new SmsDatabase(context, databaseHelper);
@ -163,6 +169,7 @@ public class DatabaseFactory {
this.pushDatabase = new PushDatabase(context, databaseHelper); this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper); this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientPreferenceDatabase = new RecipientPreferenceDatabase(context, databaseHelper); this.recipientPreferenceDatabase = new RecipientPreferenceDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
} }
public void reset(Context context) { public void reset(Context context) {

View File

@ -12,6 +12,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsDatabase; import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NotInDirectoryException; import org.thoughtcrime.securesms.database.NotInDirectoryException;
import org.thoughtcrime.securesms.database.TextSecureDirectory; import org.thoughtcrime.securesms.database.TextSecureDirectory;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory; import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
@ -94,7 +95,7 @@ public class DirectoryHelper {
} }
try { try {
new ContactsDatabase(context).setRegisteredUsers(account.get(), e164numbers); DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get(), e164numbers);
} catch (RemoteException | OperationApplicationException e) { } catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }