Signal-Android/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java

227 wiersze
8.4 KiB
Java

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.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.Util;
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;
private final Context context;
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 String ABOUT_COLUMN = "about";
static final int NORMAL_TYPE = 0;
static final int PUSH_TYPE = 1 << 0;
static final int NEW_PHONE_TYPE = 1 << 2;
static final int NEW_USERNAME_TYPE = 1 << 3;
static final int RECENT_TYPE = 1 << 4;
static final int DIVIDER_TYPE = 1 << 5;
/** 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 -> CursorUtil.requireLong(cursor, RecipientDatabase.ID)));
add(new Pair<>(NAME_COLUMN, cursor -> {
String system = CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_JOINED_NAME);
String profile = CursorUtil.requireString(cursor, RecipientDatabase.SEARCH_PROFILE_NAME);
return Util.getFirstNonEmpty(system, profile);
}));
add(new Pair<>(NUMBER_COLUMN, cursor -> {
String phone = CursorUtil.requireString(cursor, RecipientDatabase.PHONE);
String email = CursorUtil.requireString(cursor, RecipientDatabase.EMAIL);
if (phone != null) {
phone = PhoneNumberFormatter.prettyPrint(phone);
}
return Util.getFirstNonEmpty(phone, email);
}));
add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> CursorUtil.requireInt(cursor, RecipientDatabase.SYSTEM_PHONE_TYPE)));
add(new Pair<>(LABEL_COLUMN, cursor -> CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_PHONE_LABEL)));
add(new Pair<>(CONTACT_TYPE_COLUMN, cursor -> {
int registered = CursorUtil.requireInt(cursor, RecipientDatabase.REGISTERED);
return registered == RecipientDatabase.RegisteredState.REGISTERED.getId() ? PUSH_TYPE : NORMAL_TYPE;
}));
add(new Pair<>(ABOUT_COLUMN, cursor -> {
String aboutEmoji = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT_EMOJI);
String about = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT);
if (!Util.isEmpty(aboutEmoji)) {
if (!Util.isEmpty(about)) {
return aboutEmoji + " " + about;
} else {
return aboutEmoji;
}
} else if (!Util.isEmpty(about)) {
return about;
} else {
return "";
}
}));
}};
public ContactRepository(@NonNull Context context, @NonNull String noteToSelfTitle) {
this.recipientDatabase = SignalDatabase.recipients();
this.noteToSelfTitle = noteToSelfTitle;
this.context = context.getApplicationContext();
}
@WorkerThread
public @NonNull Cursor querySignalContacts(@NonNull String query) {
return querySignalContacts(query, true);
}
@WorkerThread
public @NonNull Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts(includeSelf)
: recipientDatabase.querySignalContacts(query, includeSelf);
cursor = handleNoteToSelfQuery(query, includeSelf, cursor);
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
}
@WorkerThread
public @NonNull Cursor queryNonGroupContacts(@NonNull String query, boolean includeSelf) {
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getNonGroupContacts(includeSelf)
: recipientDatabase.queryNonGroupContacts(query, includeSelf);
cursor = handleNoteToSelfQuery(query, includeSelf, cursor);
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
}
private @NonNull Cursor handleNoteToSelfQuery(@NonNull String query, boolean includeSelf, Cursor cursor) {
if (includeSelf && noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
Recipient self = Recipient.self();
boolean nameMatch = self.getDisplayName(context).toLowerCase().contains(query.toLowerCase());
boolean numberMatch = self.getE164().isPresent() && self.requireE164().contains(query);
boolean shouldAdd = !nameMatch && !numberMatch;
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION_NAMES);
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, self.getE164().orElse(""), self.getEmail().orElse(null), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), self.getAbout(), self.getAboutEmoji(), null, true, noteToSelfTitle, noteToSelfTitle });
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}
}
return cursor;
}
@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);
}
}