Search contacts via the RecipientDatabase.

master
Greyson Parrelli 2019-08-26 18:09:01 -04:00
parent 0e2d52026e
commit 582028f2c2
29 changed files with 519 additions and 351 deletions

View File

@ -29,7 +29,7 @@
android:layout_marginEnd="16dp"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
<org.thoughtcrime.securesms.components.FromTextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -38,7 +38,7 @@
android:ellipsize="marquee"
style="@style/Signal.Text.Body"
android:fontFamily="sans-serif-medium"
tools:text="Frieeeeeeedrich Nieeeeeeeeeetzsche" />
tools:text="Ottttooooooooo Ocataaaaaaaavius" />
<LinearLayout android:id="@+id/number_container"
android:orientation="horizontal"

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterToolbar;

View File

@ -37,6 +37,8 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
@ -45,6 +47,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
@ -362,6 +365,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
try {
accountManager.setProfileName(profileKey, name);
TextSecurePreferences.setProfileName(context, name);
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), name);
} catch (IOException e) {
Log.w(TAG, e);
return false;

View File

@ -27,7 +27,6 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;

View File

@ -24,7 +24,6 @@ import android.view.MenuItem;
import android.view.View;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;

View File

@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.gcm.FcmUtil;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
@ -734,19 +735,21 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
TextSecurePreferences.setFcmToken(RegistrationActivity.this, registrationState.gcmToken.orNull());
TextSecurePreferences.setFcmDisabled(RegistrationActivity.this, !registrationState.gcmToken.isPresent());
TextSecurePreferences.setWebsocketRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setLocalNumber(RegistrationActivity.this, registrationState.e164number);
DatabaseFactory.getIdentityDatabase(RegistrationActivity.this)
.saveIdentity(Recipient.external(RegistrationActivity.this, registrationState.e164number).getId(),
.saveIdentity(Recipient.self().getId(),
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setVerifying(RegistrationActivity.this, false);
TextSecurePreferences.setPushRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setLocalNumber(RegistrationActivity.this, registrationState.e164number);
TextSecurePreferences.setPushServerPassword(RegistrationActivity.this, registrationState.password);
TextSecurePreferences.setSignedPreKeyRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
TextSecurePreferences.setUnauthorizedReceived(RegistrationActivity.this, false);
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(Recipient.self().getId(), true);
DatabaseFactory.getRecipientDatabase(this).setRegistered(Recipient.self().getId(), RecipientDatabase.RegisteredState.REGISTERED);
}
private void handleSuccessfulRegistration() {

View File

@ -24,7 +24,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Process;
import android.provider.OpenableColumns;
import androidx.annotation.NonNull;
@ -39,7 +38,6 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.logging.Log;

View File

@ -85,7 +85,7 @@ public class ContactAccessor {
}
public Cursor getAllSystemContacts(Context context) {
return context.getContentResolver().query(Phone.CONTENT_URI, new String[] {Phone.NUMBER, Phone.DISPLAY_NAME, Phone.LABEL, Phone.PHOTO_URI, Phone._ID, Phone.LOOKUP_KEY}, null, null, null);
return context.getContentResolver().query(Phone.CONTENT_URI, new String[] {Phone.NUMBER, Phone.DISPLAY_NAME, Phone.LABEL, Phone.PHOTO_URI, Phone._ID, Phone.LOOKUP_KEY, Phone.TYPE}, null, null, null);
}
public boolean isSystemContact(Context context, String number) {

View File

@ -0,0 +1,180 @@
package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.util.Pair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Repository for all contacts. Allows you to filter them via queries.
*
* Currently this is implemented to return cursors. This is to ease the migration between this class
* and the previous way we'd query contacts: {@link ContactsDatabase}. It's much easier in the
* short-term to mock the cursor interface rather than try to switch everything over to models.
*/
public class ContactRepository {
private final RecipientDatabase recipientDatabase;
private final String noteToSelfTitle;
public static final String ID_COLUMN = "id";
static final String NAME_COLUMN = "name";
static final String NUMBER_COLUMN = "number";
static final String NUMBER_TYPE_COLUMN = "number_type";
static final String LABEL_COLUMN = "label";
static final String CONTACT_TYPE_COLUMN = "contact_type";
static final int NORMAL_TYPE = 0;
static final int PUSH_TYPE = 1;
static final int NEW_TYPE = 2;
static final int RECENT_TYPE = 3;
static final int DIVIDER_TYPE = 4;
/** Maps the recipient results to the legacy contact column names */
private static final List<Pair<String, ValueMapper>> SEARCH_CURSOR_MAPPERS = new ArrayList<Pair<String, ValueMapper>>() {{
add(new Pair<>(ID_COLUMN, cursor -> cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID))));
add(new Pair<>(NAME_COLUMN, cursor -> {
String system = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_DISPLAY_NAME));
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SIGNAL_PROFILE_NAME));
return !TextUtils.isEmpty(system) ? system : profile;
}));
add(new Pair<>(NUMBER_COLUMN, cursor -> {
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
return !TextUtils.isEmpty(phone) ? phone : email;
}));
add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_TYPE))));
add(new Pair<>(LABEL_COLUMN, cursor -> cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_LABEL))));
add(new Pair<>(CONTACT_TYPE_COLUMN, cursor -> {
int registered = cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.REGISTERED));
return registered == RecipientDatabase.RegisteredState.REGISTERED.getId() ? PUSH_TYPE : NORMAL_TYPE;
}));
}};
public ContactRepository(@NonNull Context context) {
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
this.noteToSelfTitle = context.getString(R.string.note_to_self);
}
@WorkerThread
public Cursor querySignalContacts(@NonNull String query) {
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts()
: recipientDatabase.querySignalContacts(query);
if (noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
Recipient self = Recipient.self();
boolean nameMatch = self.getDisplayName().toLowerCase().contains(query.toLowerCase());
boolean numberMatch = self.requireAddress().serialize() != null && self.requireAddress().serialize().contains(query);
boolean shouldAdd = !nameMatch && !numberMatch;
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION);
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, null, self.requireAddress().serialize(), null, null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), noteToSelfTitle });
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}
}
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
}
@WorkerThread
public Cursor queryNonSignalContacts(@NonNull String query) {
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getNonSignalContacts()
: recipientDatabase.queryNonSignalContacts(query);
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
}
/**
* This lets us mock the legacy cursor interface while using the new cursor, even though the data
* doesn't quite match up exactly.
*/
private static class SearchCursorWrapper extends CursorWrapper {
private final Cursor wrapped;
private final String[] columnNames;
private final List<Pair<String, ValueMapper>> mappers;
private final Map<String, Integer> positions;
SearchCursorWrapper(Cursor cursor, @NonNull List<Pair<String, ValueMapper>> mappers) {
super(cursor);
this.wrapped = cursor;
this.mappers = mappers;
this.positions = new HashMap<>();
this.columnNames = new String[mappers.size()];
for (int i = 0; i < mappers.size(); i++) {
Pair<String, ValueMapper> pair = mappers.get(i);
positions.put(pair.first(), i);
columnNames[i] = pair.first();
}
}
@Override
public int getColumnCount() {
return mappers.size();
}
@Override
public String[] getColumnNames() {
return columnNames;
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
Integer index = positions.get(columnName);
if (index != null) {
return index;
} else {
throw new IllegalArgumentException();
}
}
@Override
public String getString(int columnIndex) {
return String.valueOf(mappers.get(columnIndex).second().get(wrapped));
}
@Override
public int getInt(int columnIndex) {
return (int) mappers.get(columnIndex).second().get(wrapped);
}
@Override
public long getLong(int columnIndex) {
return (long) mappers.get(columnIndex).second().get(wrapped);
}
}
private interface ValueMapper<T> {
T get(@NonNull Cursor cursor);
}
}

View File

@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolde
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
import org.thoughtcrime.securesms.util.Util;
@ -77,7 +78,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
super(itemView);
}
public abstract void bind(@NonNull GlideRequests glideRequests, int type, String name, String number, String label, int color, boolean multiSelect);
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect);
public abstract void unbind(@NonNull GlideRequests glideRequests);
public abstract void setChecked(boolean checked);
}
@ -96,8 +97,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return (ContactSelectionListItem) itemView;
}
public void bind(@NonNull GlideRequests glideRequests, int type, String name, String number, String label, int color, boolean multiSelect) {
getView().set(glideRequests, type, name, number, label, color, multiSelect);
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect) {
getView().set(glideRequests, recipientId, type, name, number, label, color, multiSelect);
}
@Override
@ -121,7 +122,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
@Override
public void bind(@NonNull GlideRequests glideRequests, int type, String name, String number, String label, int color, boolean multiSelect) {
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect) {
this.label.setText(name);
}
@ -158,7 +159,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
int contactType = getContactType(i);
if (contactType == ContactsDatabase.DIVIDER_TYPE) return -1;
if (contactType == ContactRepository.DIVIDER_TYPE) return -1;
return Util.hashCode(getHeaderString(i), getContactType(i));
}
@ -173,25 +174,27 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN));
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
String rawId = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN));
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN ));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN ));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN ));
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
int color = (contactType == ContactsDatabase.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
int color = (contactType == ContactRepository.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
drawables.getColor(1, 0xff000000);
viewHolder.unbind(glideRequests);
viewHolder.bind(glideRequests, contactType, name, number, labelText, color, multiSelect);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect);
viewHolder.setChecked(selectedContacts.contains(number));
}
@Override
public int getItemViewType(@NonNull Cursor cursor) {
if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.DIVIDER_TYPE) {
if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN)) == ContactRepository.DIVIDER_TYPE) {
return VIEW_TYPE_DIVIDER;
} else {
return VIEW_TYPE_CONTACT;
@ -237,12 +240,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
private @NonNull String getHeaderString(int position) {
int contactType = getContactType(position);
if (contactType == ContactsDatabase.RECENT_TYPE || contactType == ContactsDatabase.DIVIDER_TYPE) {
if (contactType == ContactRepository.RECENT_TYPE || contactType == ContactRepository.DIVIDER_TYPE) {
return " ";
}
Cursor cursor = getCursorAtPositionOrThrow(position);
String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN));
String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN));
if (letter != null) {
letter = letter.trim();
@ -259,11 +262,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
private int getContactType(int position) {
final Cursor cursor = getCursorAtPositionOrThrow(position);
return cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
return cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN));
}
private boolean isPush(int position) {
return getContactType(position) == ContactsDatabase.PUSH_TYPE;
return getContactType(position) == ContactRepository.PUSH_TYPE;
}
public interface ItemClickListener {

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@ -11,11 +13,13 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@ -27,7 +31,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
private AvatarImageView contactPhotoImage;
private TextView numberView;
private TextView nameView;
private FromTextView nameView;
private TextView labelView;
private CheckBox checkBox;
@ -55,16 +59,23 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
ViewUtil.setTextViewGravityStart(this.nameView, getContext());
}
public void set(@NonNull GlideRequests glideRequests, int type, String name, String number, String label, int color, boolean multiSelect) {
public void set(@NonNull GlideRequests glideRequests,
@Nullable RecipientId recipientId,
int type,
String name,
String number,
String label,
int color,
boolean multiSelect)
{
this.glideRequests = glideRequests;
this.number = number;
if (type == ContactsDatabase.NEW_TYPE) {
if (type == ContactRepository.NEW_TYPE) {
this.recipient = null;
this.contactPhotoImage.setAvatar(glideRequests, Recipient.UNKNOWN, false);
} else if (!TextUtils.isEmpty(number)) {
// TODO [greyson] We really don't want to have to do a read like this here
this.recipient = Recipient.external(getContext(), number).live();
} else if (recipientId != null) {
this.recipient = Recipient.live(recipientId);
this.recipient.observeForever(this);
if (this.recipient.get().getName() != null) {
@ -72,15 +83,13 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
}
}
Recipient recipientSnapshot = recipient != null ? recipient.get() : null;
this.nameView.setTextColor(color);
this.numberView.setTextColor(color);
this.contactPhotoImage.setAvatar(glideRequests, recipient != null ? recipient.get() : null, false);
this.contactPhotoImage.setAvatar(glideRequests, recipientSnapshot, false);
if (!multiSelect && recipient != null && recipient.get().isLocalNumber()) {
name = getContext().getString(R.string.note_to_self);
}
setText(type, name, number, label);
setText(recipientSnapshot, type, name, number, label);
if (multiSelect) this.checkBox.setVisibility(View.VISIBLE);
else this.checkBox.setVisibility(View.GONE);
@ -97,12 +106,12 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
}
}
private void setText(int type, String name, String number, String label) {
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label) {
if (number == null || number.isEmpty() || GroupUtil.isEncodedGroup(number)) {
this.nameView.setEnabled(false);
this.numberView.setText("");
this.labelView.setVisibility(View.GONE);
} else if (type == ContactsDatabase.PUSH_TYPE) {
} else if (type == ContactRepository.PUSH_TYPE) {
this.numberView.setText(number);
this.nameView.setEnabled(true);
this.labelView.setVisibility(View.GONE);
@ -113,7 +122,11 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.labelView.setVisibility(View.VISIBLE);
}
this.nameView.setText(name);
if (recipient != null) {
this.nameView.setText(recipient);
} else {
this.nameView.setText(name);
}
}
public String getNumber() {
@ -123,6 +136,6 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
contactPhotoImage.setAvatar(glideRequests, recipient, false);
nameView.setText(recipient.toShortString());
nameView.setText(recipient);
}
}

View File

@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.ArrayList;
import java.util.List;
@ -56,11 +57,12 @@ public class ContactsCursorLoader extends CursorLoader {
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_GROUPS;
}
private static final String[] CONTACT_PROJECTION = new String[]{ContactsDatabase.NAME_COLUMN,
ContactsDatabase.NUMBER_COLUMN,
ContactsDatabase.NUMBER_TYPE_COLUMN,
ContactsDatabase.LABEL_COLUMN,
ContactsDatabase.CONTACT_TYPE_COLUMN};
private static final String[] CONTACT_PROJECTION = new String[]{ContactRepository.ID_COLUMN,
ContactRepository.NAME_COLUMN,
ContactRepository.NUMBER_COLUMN,
ContactRepository.NUMBER_TYPE_COLUMN,
ContactRepository.LABEL_COLUMN,
ContactRepository.CONTACT_TYPE_COLUMN};
private static final int RECENT_CONVERSATION_MAX = 25;
@ -68,13 +70,16 @@ public class ContactsCursorLoader extends CursorLoader {
private final int mode;
private final boolean recents;
private final ContactRepository contactRepository;
public ContactsCursorLoader(@NonNull Context context, int mode, String filter, boolean recents)
{
super(context);
this.filter = filter;
this.mode = mode;
this.recents = recents;
this.filter = filter == null ? "" : filter;
this.mode = mode;
this.recents = recents;
this.contactRepository = new ContactRepository(context);
}
@Override
@ -131,31 +136,34 @@ public class ContactsCursorLoader extends CursorLoader {
private Cursor getRecentsHeaderCursor() {
MatrixCursor recentsHeader = new MatrixCursor(CONTACT_PROJECTION);
recentsHeader.addRow(new Object[]{ getContext().getString(R.string.ContactsCursorLoader_recent_chats),
recentsHeader.addRow(new Object[]{ null,
getContext().getString(R.string.ContactsCursorLoader_recent_chats),
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactsDatabase.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE });
return recentsHeader;
}
private Cursor getContactsHeaderCursor() {
MatrixCursor contactsHeader = new MatrixCursor(CONTACT_PROJECTION, 1);
contactsHeader.addRow(new Object[] { getContext().getString(R.string.ContactsCursorLoader_contacts),
contactsHeader.addRow(new Object[] { null,
getContext().getString(R.string.ContactsCursorLoader_contacts),
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactsDatabase.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE });
return contactsHeader;
}
private Cursor getGroupsHeaderCursor() {
MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1);
groupHeader.addRow(new Object[]{ getContext().getString(R.string.ContactsCursorLoader_groups),
groupHeader.addRow(new Object[]{ null,
getContext().getString(R.string.ContactsCursorLoader_groups),
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactsDatabase.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE });
return groupHeader;
}
@ -168,32 +176,32 @@ public class ContactsCursorLoader extends CursorLoader {
ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations);
ThreadRecord threadRecord;
while ((threadRecord = reader.getNext()) != null) {
recentConversations.addRow(new Object[] { threadRecord.getRecipient().toShortString(),
recentConversations.addRow(new Object[] { threadRecord.getRecipient().getId().serialize(),
threadRecord.getRecipient().toShortString(),
threadRecord.getRecipient().requireAddress().serialize(),
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactsDatabase.RECENT_TYPE });
ContactRepository.RECENT_TYPE });
}
}
return recentConversations;
}
private List<Cursor> getContactsCursors() {
ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(getContext());
List<Cursor> cursorList = new ArrayList<>(2);
List<Cursor> cursorList = new ArrayList<>(2);
if (!Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
return cursorList;
}
if (pushEnabled(mode)) {
cursorList.add(contactsDatabase.queryTextSecureContacts(filter));
cursorList.add(contactRepository.querySignalContacts(filter));
}
if (pushEnabled(mode) && smsEnabled(mode)) {
cursorList.add(contactsDatabase.querySystemContacts(filter));
cursorList.add(contactRepository.queryNonSignalContacts(filter));
} else if (smsEnabled(mode)) {
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
cursorList.add(filterNonPushContacts(contactRepository.queryNonSignalContacts(filter)));
}
return cursorList;
}
@ -203,11 +211,12 @@ public class ContactsCursorLoader extends CursorLoader {
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(filter)) {
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
groupContacts.addRow(new Object[] { groupRecord.getTitle(),
groupContacts.addRow(new Object[] { groupRecord.getRecipientId().serialize(),
groupRecord.getTitle(),
groupRecord.getEncodedId(),
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"",
ContactsDatabase.NORMAL_TYPE });
ContactRepository.NORMAL_TYPE });
}
}
return groupContacts;
@ -215,11 +224,12 @@ public class ContactsCursorLoader extends CursorLoader {
private Cursor getNewNumberCursor() {
MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1);
newNumberCursor.addRow(new Object[] { getContext().getString(R.string.contact_selection_list__unknown_contact),
newNumberCursor.addRow(new Object[] { null,
getContext().getString(R.string.contact_selection_list__unknown_contact),
filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2",
ContactsDatabase.NEW_TYPE });
ContactRepository.NEW_TYPE });
return newNumberCursor;
}
@ -228,15 +238,16 @@ public class ContactsCursorLoader extends CursorLoader {
final long startMillis = System.currentTimeMillis();
final MatrixCursor matrix = new MatrixCursor(CONTACT_PROJECTION);
while (cursor.moveToNext()) {
final String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN));
final Recipient recipient = Recipient.external(getContext(), number);
final RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN)));
final Recipient recipient = Recipient.resolved(id);
if (recipient.resolve().getRegistered() != RecipientDatabase.RegisteredState.REGISTERED) {
matrix.addRow(new Object[]{cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN)),
number,
cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN)),
ContactsDatabase.NORMAL_TYPE});
matrix.addRow(new Object[]{cursor.getLong(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN)),
ContactRepository.NORMAL_TYPE});
}
}
Log.i(TAG, "filterNonPushContacts() -> " + (System.currentTimeMillis() - startMillis) + "ms");

View File

@ -17,31 +17,23 @@
package org.thoughtcrime.securesms.contacts;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@ -64,18 +56,6 @@ public class ContactsDatabase {
private static final String CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call";
private static final String SYNC = "__TS";
static final String NAME_COLUMN = "name";
static final String NUMBER_COLUMN = "number";
static final String NUMBER_TYPE_COLUMN = "number_type";
static final String LABEL_COLUMN = "label";
static final String CONTACT_TYPE_COLUMN = "contact_type";
static final int NORMAL_TYPE = 0;
static final int PUSH_TYPE = 1;
static final int NEW_TYPE = 2;
static final int RECENT_TYPE = 3;
static final int DIVIDER_TYPE = 4;
private final Context context;
public ContactsDatabase(Context context) {
@ -151,109 +131,6 @@ public class ContactsDatabase {
}
}
@SuppressLint("Recycle")
public @NonNull Cursor querySystemContacts(@Nullable String filter) {
Uri uri;
if (!TextUtils.isEmpty(filter)) {
uri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(filter));
} else {
uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
}
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
uri = uri.buildUpon().appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").build();
}
String[] projection = new String[]{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 LOCALIZED ASC";
Map<String, String> projectionMap = new HashMap<String, String>() {{
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);
}};
String formattedNumber = "REPLACE(REPLACE(REPLACE(REPLACE(data1,' ',''),'-',''),'(',''),')','')";
String excludeSelection = "(" + formattedNumber +" NOT IN " +
"(SELECT data1 FROM view_data WHERE "+formattedNumber+" = data1) " +
"OR "+formattedNumber+" = data1)" +
"AND " + formattedNumber + "NOT IN (SELECT "+formattedNumber+" FROM view_data where mimetype = '"+CONTACT_MIMETYPE+"')" ;
String fallbackSelection = ContactsContract.Data.SYNC2 + " IS NULL OR " + ContactsContract.Data.SYNC2 + " != '" + SYNC + "'";
Cursor cursor;
try {
cursor = context.getContentResolver().query(uri, projection, excludeSelection, null, sort);
} catch (Exception e) {
Log.w(TAG, e);
cursor = context.getContentResolver().query(uri, projection, fallbackSelection, null, sort);
}
return new ProjectionMappingCursor(cursor, projectionMap, new Pair<>(CONTACT_TYPE_COLUMN, NORMAL_TYPE));
}
@SuppressLint("Recycle")
public @NonNull Cursor queryTextSecureContacts(String filter) {
String[] projection = new String[] {ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Data.DATA1};
String sort = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
Map<String, String> projectionMap = new HashMap<String, String>(){{
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[] {CONTACT_MIMETYPE},
sort);
} else {
cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.MIMETYPE + " = ? AND (" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? OR " + ContactsContract.Data.DATA1 + " LIKE ?)",
new String[] {CONTACT_MIMETYPE,
"%" + filter + "%", "%" + filter + "%"},
sort);
if (context.getString(R.string.note_to_self).toLowerCase().contains(filter.toLowerCase())) {
Optional<SystemContactInfo> self = getSystemContactInfo(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
boolean shouldAdd = true;
if (self.isPresent()) {
boolean nameMatch = self.get().name != null && self.get().name.toLowerCase().contains(filter.toLowerCase());
boolean numberMatch = self.get().number != null && self.get().number.contains(filter);
shouldAdd = !nameMatch && !numberMatch;
}
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(projection);
selfCursor.addRow(new Object[]{ context.getString(R.string.note_to_self), TextSecurePreferences.getLocalNumber(context)});
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}
}
}
return new ProjectionMappingCursor(cursor, projectionMap,
new Pair<>(LABEL_COLUMN, "TextSecure"),
new Pair<>(NUMBER_TYPE_COLUMN, 0),
new Pair<>(CONTACT_TYPE_COLUMN, PUSH_TYPE));
}
public @Nullable Cursor getNameDetails(long contactId) {
String[] projection = new String[] { ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
@ -573,105 +450,6 @@ public class ContactsDatabase {
}
}
private static class ProjectionMappingCursor extends CursorWrapper {
private final Map<String, String> projectionMap;
private final Pair<String, Object>[] extras;
@SafeVarargs
ProjectionMappingCursor(Cursor cursor,
Map<String, String> projectionMap,
Pair<String, Object>... extras)
{
super(cursor);
this.projectionMap = projectionMap;
this.extras = extras;
}
@Override
public int getColumnCount() {
return super.getColumnCount() + extras.length;
}
@Override
public int getColumnIndex(String columnName) {
for (int i=0;i<extras.length;i++) {
if (extras[i].first.equals(columnName)) {
return super.getColumnCount() + i;
}
}
return super.getColumnIndex(projectionMap.get(columnName));
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
int index = getColumnIndex(columnName);
if (index == -1) throw new IllegalArgumentException("Bad column name!");
else return index;
}
@Override
public String getColumnName(int columnIndex) {
int baseColumnCount = super.getColumnCount();
if (columnIndex >= baseColumnCount) {
int offset = columnIndex - baseColumnCount;
return extras[offset].first;
}
return getReverseProjection(super.getColumnName(columnIndex));
}
@Override
public String[] getColumnNames() {
String[] names = super.getColumnNames();
String[] allNames = new String[names.length + extras.length];
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
public int getInt(int columnIndex) {
if (columnIndex >= super.getColumnCount()) {
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;
}
}
private static class SystemContactInfo {
private final String name;
private final String number;

View File

@ -75,7 +75,7 @@ public class ContactShareEditActivity extends PassphraseRequiredActionBarActivit
ContactShareEditAdapter contactAdapter = new ContactShareEditAdapter(GlideApp.with(this), dynamicLanguage.getCurrentLocale(), this);
contactList.setAdapter(contactAdapter);
ContactRepository contactRepository = new ContactRepository(this,
SharedContactRepository contactRepository = new SharedContactRepository(this,
AsyncTask.THREAD_POOL_EXECUTOR,
DatabaseFactory.getContactsDatabase(this));

View File

@ -19,10 +19,10 @@ class ContactShareEditViewModel extends ViewModel {
private final MutableLiveData<List<Contact>> contacts;
private final SingleLiveEvent<Event> events;
private final ContactRepository repo;
private final SharedContactRepository repo;
ContactShareEditViewModel(@NonNull List<Uri> contactUris,
@NonNull ContactRepository contactRepository)
@NonNull SharedContactRepository contactRepository)
{
contacts = new MutableLiveData<>();
events = new SingleLiveEvent<>();
@ -98,9 +98,9 @@ class ContactShareEditViewModel extends ViewModel {
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final List<Uri> contactUris;
private final ContactRepository contactRepository;
private final SharedContactRepository contactRepository;
Factory(@NonNull List<Uri> contactUris, @NonNull ContactRepository contactRepository) {
Factory(@NonNull List<Uri> contactUris, @NonNull SharedContactRepository contactRepository) {
this.contactUris = contactUris;
this.contactRepository = contactRepository;
}

View File

@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.contactshare.Contact.Email;
import org.thoughtcrime.securesms.contactshare.Contact.Name;
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
@ -36,17 +35,17 @@ import ezvcard.VCard;
import static org.thoughtcrime.securesms.contactshare.Contact.*;
public class ContactRepository {
public class SharedContactRepository {
private static final String TAG = ContactRepository.class.getSimpleName();
private static final String TAG = SharedContactRepository.class.getSimpleName();
private final Context context;
private final Executor executor;
private final ContactsDatabase contactsDatabase;
ContactRepository(@NonNull Context context,
@NonNull Executor executor,
@NonNull ContactsDatabase contactsDatabase)
SharedContactRepository(@NonNull Context context,
@NonNull Executor executor,
@NonNull ContactsDatabase contactsDatabase)
{
this.context = context.getApplicationContext();
this.executor = executor;

View File

@ -7,6 +7,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.search.SearchRepository;
@ -37,8 +38,8 @@ public class ConversationSearchViewModel extends AndroidViewModel {
debouncer = new Debouncer(500);
searchRepository = new SearchRepository(context,
DatabaseFactory.getSearchDatabase(context),
DatabaseFactory.getContactsDatabase(context),
DatabaseFactory.getThreadDatabase(context),
new ContactRepository(application),
ContactAccessor.getInstance(),
SignalExecutors.SERIAL);
}

View File

@ -4,6 +4,8 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -36,8 +38,8 @@ public class RecipientDatabase extends Database {
static final String TABLE_NAME = "recipient";
public static final String ID = "_id";
private static final String UUID = "uuid";
static final String PHONE = "phone";
static final String EMAIL = "email";
public static final String PHONE = "phone";
public static final String EMAIL = "email";
static final String GROUP_ID = "group_id";
private static final String BLOCKED = "blocked";
private static final String MESSAGE_RINGTONE = "message_ringtone";
@ -50,22 +52,25 @@ public class RecipientDatabase extends Database {
private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder";
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
private static final String MESSAGE_EXPIRATION_TIME = "message_expiration_time";
static final String REGISTERED = "registered";
private static final String SYSTEM_DISPLAY_NAME = "system_display_name";
public static final String REGISTERED = "registered";
public static final String SYSTEM_DISPLAY_NAME = "system_display_name";
private static final String SYSTEM_PHOTO_URI = "system_photo_uri";
private static final String SYSTEM_PHONE_LABEL = "system_phone_label";
public static final String SYSTEM_PHONE_TYPE = "system_phone_type";
public static final String SYSTEM_PHONE_LABEL = "system_phone_label";
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
private static final String PROFILE_KEY = "profile_key";
private static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
public static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String SORT_NAME = "sort_name";
private static final String[] RECIPIENT_PROJECTION = new String[] {
UUID, PHONE, EMAIL, GROUP_ID,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
@ -73,6 +78,8 @@ public class RecipientDatabase extends Database {
private static final String[] ID_PROJECTION = new String[] { ID };
public static final String[] SEARCH_PROJECTION = new String[] { ID, SYSTEM_DISPLAY_NAME, SIGNAL_PROFILE_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "IFNULL(" + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ") AS " + SORT_NAME };
private static Address addressFromCursor(Cursor cursor) {
String phone = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
@ -159,6 +166,7 @@ public class RecipientDatabase extends Database {
SYSTEM_DISPLAY_NAME + " TEXT DEFAULT NULL, " +
SYSTEM_PHOTO_URI + " TEXT DEFAULT NULL, " +
SYSTEM_PHONE_LABEL + " TEXT DEFAULT NULL, " +
SYSTEM_PHONE_TYPE + " INTEGER DEFAULT -1, " +
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " +
@ -262,8 +270,7 @@ public class RecipientDatabase extends Database {
if (cursor != null && cursor.moveToNext()) {
return getRecipientSettings(cursor);
} else {
// TODO: Maybe not make this an error?
throw new AssertionError("Couldn't find recipient!");
throw new AssertionError("Couldn't find recipient! id: " + id.serialize());
}
}
}
@ -551,6 +558,66 @@ public class RecipientDatabase extends Database {
Stream.of(updates.entrySet()).forEach(entry -> Recipient.live(entry.getKey()).refresh());
}
}
public @Nullable Cursor getSignalContacts() {
String selection = BLOCKED + " = ? AND " +
REGISTERED + " = ? AND " +
GROUP_ID + " IS NULL AND " +
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + SIGNAL_PROFILE_NAME + " NOT NULL)";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1" };
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
public @Nullable Cursor querySignalContacts(@NonNull String query) {
query = TextUtils.isEmpty(query) ? "*" : query;
query = "%" + query + "%";
String selection = BLOCKED + " = ? AND " +
REGISTERED + " = ? AND " +
GROUP_ID + " IS NULL AND " +
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
"(" +
PHONE + " LIKE ? OR " +
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
SIGNAL_PROFILE_NAME + " LIKE ?" +
")";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query };
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
public @Nullable Cursor getNonSignalContacts() {
String selection = BLOCKED + " = ? AND " +
REGISTERED + " != ? AND " +
GROUP_ID + " IS NULL AND " +
SYSTEM_DISPLAY_NAME + " NOT NULL AND " +
"(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL)";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
String orderBy = SYSTEM_DISPLAY_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
public @Nullable Cursor queryNonSignalContacts(@NonNull String query) {
query = TextUtils.isEmpty(query) ? "*" : query;
query = "%" + query + "%";
String selection = BLOCKED + " = ? AND " +
REGISTERED + " != ? AND " +
GROUP_ID + " IS NULL AND " +
"(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL) AND " +
"(" +
PHONE + " LIKE ? OR " +
SYSTEM_DISPLAY_NAME + " LIKE ?" +
")";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query };
String orderBy = SYSTEM_DISPLAY_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
private void update(@NonNull RecipientId id, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -567,11 +634,18 @@ public class RecipientDatabase extends Database {
this.database = database;
}
public void setSystemContactInfo(@NonNull RecipientId id, @Nullable String displayName, @Nullable String photoUri, @Nullable String systemPhoneLabel, @Nullable String systemContactUri) {
public void setSystemContactInfo(@NonNull RecipientId id,
@Nullable String displayName,
@Nullable String photoUri,
@Nullable String systemPhoneLabel,
int systemPhoneType,
@Nullable String systemContactUri)
{
ContentValues contentValues = new ContentValues(1);
contentValues.put(SYSTEM_DISPLAY_NAME, displayName);
contentValues.put(SYSTEM_PHOTO_URI, photoUri);
contentValues.put(SYSTEM_PHONE_LABEL, systemPhoneLabel);
contentValues.put(SYSTEM_PHONE_TYPE, systemPhoneType);
contentValues.put(SYSTEM_CONTACT_URI, systemContactUri);
update(id, contentValues);

View File

@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DelimiterUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
@ -76,6 +77,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int REVEALABLE_MESSAGES = 22;
private static final int VIEW_ONCE_ONLY = 23;
private static final int RECIPIENT_IDS = 24;
private static final int RECIPIENT_SEARCH = 25;
private static final int DATABASE_VERSION = 25;
private static final String DATABASE_NAME = "signal.db";
@ -490,6 +492,27 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
RecipientIdMigrationHelper.execute(db);
}
if (oldVersion < RECIPIENT_SEARCH) {
db.execSQL("ALTER TABLE recipient ADD COLUMN system_phone_type INTEGER DEFAULT -1");
String localNumber = TextSecurePreferences.getLocalNumber(context);
if (!TextUtils.isEmpty(localNumber)) {
try (Cursor cursor = db.query("recipient", null, "phone = ?", new String[] { localNumber }, null, null, null)) {
if (cursor == null || !cursor.moveToFirst()) {
ContentValues values = new ContentValues();
values.put("phone", localNumber);
values.put("registered", 1);
values.put("profile_sharing", 1);
values.put("signal_profile_name", TextSecurePreferences.getProfileName(context));
db.insert("recipient", null, values);
} else {
db.execSQL("UPDATE recipient SET registered = ?, profile_sharing = ?, signal_profile_name = ? WHERE phone = ?",
new String[] { "1", "1", TextSecurePreferences.getProfileName(context), localNumber });
}
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
import java.util.Arrays;
import java.util.HashMap;
@ -82,6 +83,7 @@ public final class JobManagerFactories {
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory());
// Dead jobs
put("PushContentReceiveJob", new FailingJob.Factory());

View File

@ -7,8 +7,7 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@ -32,15 +31,15 @@ class CameraContactsRepository {
private final Context context;
private final ThreadDatabase threadDatabase;
private final GroupDatabase groupDatabase;
private final ContactsDatabase contactsDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactRepository contactRepository;
CameraContactsRepository(@NonNull Context context) {
this.context = context.getApplicationContext();
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
this.contactsDatabase = DatabaseFactory.getContactsDatabase(context);
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
this.contactRepository = new ContactRepository(context);
}
void getCameraContacts(@NonNull Callback<CameraContacts> callback) {
@ -80,9 +79,10 @@ class CameraContactsRepository {
private @NonNull List<Recipient> getContacts(@NonNull String query) {
List<Recipient> recipients = new ArrayList<>();
try (Cursor cursor = contactsDatabase.queryTextSecureContacts(query)) {
try (Cursor cursor = contactRepository.querySignalContacts(query)) {
while (cursor.moveToNext()) {
Recipient recipient = Recipient.external(context, cursor.getString(1));
RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN)));
Recipient recipient = Recipient.resolved(id);
recipients.add(recipient);
}
}

View File

@ -11,6 +11,7 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
@ -38,8 +39,9 @@ public class ApplicationMigrations {
private static final MutableLiveData<Boolean> UI_BLOCKING_MIGRATION_RUNNING = new MutableLiveData<>();
private static final class Version {
static final int LEGACY = 455;
static final int RECIPIENT_ID = 525; // TODO [greyson] USE PROPER APPLICATION VERSION
static final int LEGACY = 455;
static final int RECIPIENT_ID = 525; // TODO [greyson] USE PROPER APPLICATION VERSION
static final int RECIPIENT_SEARCH = 525; // TODO [greyson] USE PROPER APPLICATION VERSION
}
/**
@ -133,6 +135,10 @@ public class ApplicationMigrations {
jobs.add(new DatabaseMigrationJob());
}
if (lastSeenVersion < Version.RECIPIENT_SEARCH) {
jobs.add(new RecipientSearchMigrationJob());
}
return jobs;
}
}

View File

@ -0,0 +1,54 @@
package org.thoughtcrime.securesms.migrations;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import java.io.IOException;
/**
* We added a column for keeping track of the phone number type ("mobile", "home", etc) to the
* recipient database, and therefore we need to do a directory sync to fill in that column.
*/
public class RecipientSearchMigrationJob extends MigrationJob {
public static final String KEY = "RecipientSearchMigrationJob";
RecipientSearchMigrationJob() {
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY).build());
}
private RecipientSearchMigrationJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
boolean isUiBlocking() {
return false;
}
@Override
void performMigration() throws Exception {
DirectoryHelper.refreshDirectory(context, false);
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return e instanceof IOException;
}
public static class Factory implements Job.Factory<RecipientSearchMigrationJob> {
@Override
public @NonNull RecipientSearchMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RecipientSearchMigrationJob(parameters);
}
}
}

View File

@ -152,7 +152,7 @@ public final class LiveRecipient {
private @NonNull RecipientDetails getIndividualRecipientDetails(RecipientSettings settings) {
boolean systemContact = !TextUtils.isEmpty(settings.getSystemDisplayName());
boolean isLocalNumber = settings.getAddress().serialize().equals(TextSecurePreferences.getLocalNumber(context));
return new RecipientDetails(null, Optional.absent(), systemContact, isLocalNumber, settings, null);
return new RecipientDetails(context, null, Optional.absent(), systemContact, isLocalNumber, settings, null);
}
@WorkerThread
@ -172,10 +172,10 @@ public final class LiveRecipient {
avatarId = Optional.of(groupRecord.get().getAvatarId());
}
return new RecipientDetails(title, avatarId, false, false, settings, members);
return new RecipientDetails(context, title, avatarId, false, false, settings, members);
}
return new RecipientDetails(unnamedGroupName, Optional.absent(), false, false, settings, null);
return new RecipientDetails(context, unnamedGroupName, Optional.absent(), false, false, settings, null);
}
@Override

View File

@ -207,6 +207,20 @@ public class Recipient {
return this.name;
}
public @NonNull String getDisplayName() {
String name = getName();
if (!TextUtils.isEmpty(name)) {
return name;
}
String profileName = getProfileName();
if (!TextUtils.isEmpty(profileName)) {
return profileName;
}
return requireAddress().serialize();
}
public @NonNull MaterialColor getColor() {
if (isGroup()) return MaterialColor.GROUP;
else if (color != null) return color;

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@ -33,7 +35,7 @@ public class RecipientDetails {
final VibrateState callVibrateState;
final boolean blocked;
final int expireMessages;
final List<Recipient> participants;
final List<Recipient> participants;
final String profileName;
final boolean seenInviteReminder;
final Optional<Integer> defaultSubscriptionId;
@ -47,7 +49,8 @@ public class RecipientDetails {
final UnidentifiedAccessMode unidentifiedAccessMode;
final boolean forceSmsSelection;
RecipientDetails(@Nullable String name,
RecipientDetails(@NonNull Context context,
@Nullable String name,
@NonNull Optional<Long> groupAvatarId,
boolean systemContact,
boolean isLocalNumber,
@ -68,7 +71,7 @@ public class RecipientDetails {
this.blocked = settings.isBlocked();
this.expireMessages = settings.getExpireMessages();
this.participants = participants == null ? new LinkedList<>() : participants;
this.profileName = settings.getProfileName();
this.profileName = isLocalNumber ? TextSecurePreferences.getProfileName(context) : settings.getProfileName();
this.seenInviteReminder = settings.hasSeenInviteReminder();
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
this.registered = settings.getRegistered();

View File

@ -16,6 +16,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
@ -67,8 +68,8 @@ public class SearchFragment extends Fragment implements SearchListAdapter.EventL
SearchRepository searchRepository = new SearchRepository(getContext(),
DatabaseFactory.getSearchDatabase(getContext()),
DatabaseFactory.getContactsDatabase(getContext()),
DatabaseFactory.getThreadDatabase(getContext()),
new ContactRepository(requireContext()),
ContactAccessor.getInstance(),
SignalExecutors.SERIAL);
viewModel = ViewModelProviders.of(this, new SearchViewModel.Factory(searchRepository)).get(SearchViewModel.class);

View File

@ -12,6 +12,7 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.CursorList;
@ -56,26 +57,26 @@ public class SearchRepository {
}
}
private final Context context;
private final SearchDatabase searchDatabase;
private final ContactsDatabase contactsDatabase;
private final ThreadDatabase threadDatabase;
private final ContactAccessor contactAccessor;
private final Executor executor;
private final Context context;
private final SearchDatabase searchDatabase;
private final ContactRepository contactRepository;
private final ThreadDatabase threadDatabase;
private final ContactAccessor contactAccessor;
private final Executor executor;
public SearchRepository(@NonNull Context context,
@NonNull SearchDatabase searchDatabase,
@NonNull ContactsDatabase contactsDatabase,
@NonNull ThreadDatabase threadDatabase,
@NonNull ContactRepository contactRepository,
@NonNull ContactAccessor contactAccessor,
@NonNull Executor executor)
{
this.context = context.getApplicationContext();
this.searchDatabase = searchDatabase;
this.contactsDatabase = contactsDatabase;
this.threadDatabase = threadDatabase;
this.contactAccessor = contactAccessor;
this.executor = executor;
this.context = context.getApplicationContext();
this.searchDatabase = searchDatabase;
this.threadDatabase = threadDatabase;
this.contactRepository = contactRepository;
this.contactAccessor = contactAccessor;
this.executor = executor;
}
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
@ -125,8 +126,8 @@ public class SearchRepository {
return CursorList.emptyList();
}
Cursor textSecureContacts = contactsDatabase.queryTextSecureContacts(query);
Cursor systemContacts = contactsDatabase.querySystemContacts(query);
Cursor textSecureContacts = contactRepository.querySignalContacts(query);
Cursor systemContacts = contactRepository.queryNonSignalContacts(query);
MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
return new CursorList<>(contacts, new RecipientModelBuilder(context));

View File

@ -205,11 +205,12 @@ public class DirectoryHelper {
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, contactUri.toString());
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {