From 243b4b9414f0fc661ad81865da7776c3d02f084c Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 26 Mar 2021 09:24:40 -0300 Subject: [PATCH] Refactor ContactsCursorLoader to implement factory pattern. Utilization of the factory pattern will enable us to more easily change what contacts we present to the user for a specific screen in the future instead of continuing to modify and potentially introduce bugs to this screen. --- .../ContactSelectionListFragment.java | 59 +++-- .../AbstractContactsCursorLoader.java | 55 +++++ .../contacts/ContactsCursorLoader.java | 214 ++++-------------- .../contacts/ContactsCursorRows.java | 153 +++++++++++++ 4 files changed, 297 insertions(+), 184 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/AbstractContactsCursorLoader.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorRows.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 8e394aef6f..974efa9759 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -58,6 +58,7 @@ import com.pnikosis.materialishprogress.ProgressWheel; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.components.emoji.WarningTextView; +import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactChip; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; @@ -114,22 +115,23 @@ public final class ContactSelectionListFragment extends LoggingFragment public static final String CAN_SELECT_SELF = "can_select_self"; public static final String DISPLAY_CHIPS = "display_chips"; - private ConstraintLayout constraintLayout; - private TextView emptyText; - private OnContactSelectedListener onContactSelectedListener; - private SwipeRefreshLayout swipeRefresh; - private View showContactsLayout; - private Button showContactsButton; - private TextView showContactsDescription; - private ProgressWheel showContactsProgress; - private String cursorFilter; - private RecyclerView recyclerView; - private RecyclerViewFastScroller fastScroller; - private ContactSelectionListAdapter cursorRecyclerViewAdapter; - private ChipGroup chipGroup; - private HorizontalScrollView chipGroupScrollContainer; - private WarningTextView groupLimit; - private OnSelectionLimitReachedListener onSelectionLimitReachedListener; + private ConstraintLayout constraintLayout; + private TextView emptyText; + private OnContactSelectedListener onContactSelectedListener; + private SwipeRefreshLayout swipeRefresh; + private View showContactsLayout; + private Button showContactsButton; + private TextView showContactsDescription; + private ProgressWheel showContactsProgress; + private String cursorFilter; + private RecyclerView recyclerView; + private RecyclerViewFastScroller fastScroller; + private ContactSelectionListAdapter cursorRecyclerViewAdapter; + private ChipGroup chipGroup; + private HorizontalScrollView chipGroupScrollContainer; + private WarningTextView groupLimit; + private OnSelectionLimitReachedListener onSelectionLimitReachedListener; + private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider; @Nullable private FixedViewsAdapter headerAdapter; @@ -162,6 +164,14 @@ public final class ContactSelectionListFragment extends LoggingFragment if (context instanceof OnSelectionLimitReachedListener) { onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context; } + + if (context instanceof AbstractContactsCursorLoaderFactoryProvider) { + cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context; + } + + if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) { + cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context; + } } @Override @@ -387,10 +397,15 @@ public final class ContactSelectionListFragment extends LoggingFragment @Override public @NonNull Loader onCreateLoader(int id, Bundle args) { - FragmentActivity activity = requireActivity(); - return new ContactsCursorLoader(activity, - activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL), - cursorFilter, activity.getIntent().getBooleanExtra(RECENTS, false)); + FragmentActivity activity = requireActivity(); + int displayMode = activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL); + boolean displayRecents = activity.getIntent().getBooleanExtra(RECENTS, false); + + if (cursorFactoryProvider != null) { + return cursorFactoryProvider.get().create(); + } else { + return new ContactsCursorLoader.Factory(activity, displayMode, cursorFilter, displayRecents).create(); + } } @Override @@ -696,4 +711,8 @@ public final class ContactSelectionListFragment extends LoggingFragment public interface ScrollCallback { void onBeginScroll(); } + + public interface AbstractContactsCursorLoaderFactoryProvider { + @NonNull AbstractContactsCursorLoader.Factory get(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/AbstractContactsCursorLoader.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/AbstractContactsCursorLoader.java new file mode 100644 index 0000000000..729e068988 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/AbstractContactsCursorLoader.java @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.contacts; + +import android.content.Context; +import android.database.Cursor; +import android.database.MergeCursor; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.loader.content.CursorLoader; + +import java.util.List; + +public abstract class AbstractContactsCursorLoader extends CursorLoader { + + private final String filter; + + protected AbstractContactsCursorLoader(@NonNull Context context, @Nullable String filter) { + super(context); + + this.filter = sanitizeFilter(filter); + } + + @Override + public final Cursor loadInBackground() { + List cursorList = TextUtils.isEmpty(filter) ? getUnfilteredResults() + : getFilteredResults(); + if (cursorList.size() > 0) { + return new MergeCursor(cursorList.toArray(new Cursor[0])); + } + return null; + } + + protected final String getFilter() { + return filter; + } + + protected abstract List getUnfilteredResults(); + + protected abstract List getFilteredResults(); + + private static @NonNull String sanitizeFilter(@Nullable String filter) { + if (filter == null) { + return ""; + } else if (filter.startsWith("@")) { + return filter.substring(1); + } else { + return filter; + } + } + + public interface Factory { + @NonNull AbstractContactsCursorLoader create(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java index 1cac105539..e6f86802b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java @@ -20,13 +20,8 @@ import android.Manifest; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; -import android.database.MergeCursor; -import android.provider.ContactsContract; -import android.text.TextUtils; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.loader.content.CursorLoader; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; @@ -37,7 +32,6 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.phonenumbers.NumberUtil; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.FeatureFlags; @@ -51,7 +45,7 @@ import java.util.List; * * @author Jake McGinty */ -public class ContactsCursorLoader extends CursorLoader { +public class ContactsCursorLoader extends AbstractContactsCursorLoader { private static final String TAG = ContactsCursorLoader.class.getSimpleName(); @@ -64,60 +58,30 @@ public class ContactsCursorLoader extends CursorLoader { public static final int FLAG_BLOCK = 1 << 5; public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5; public static final int FLAG_HIDE_NEW = 1 << 6; - public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF; + public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF; } - 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, - ContactRepository.ABOUT_COLUMN}; - private static final int RECENT_CONVERSATION_MAX = 25; - private final String filter; private final int mode; private final boolean recents; private final ContactRepository contactRepository; - public ContactsCursorLoader(@NonNull Context context, int mode, String filter, boolean recents) + private ContactsCursorLoader(@NonNull Context context, int mode, String filter, boolean recents) { - super(context); + super(context, filter); if (flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS) && !flagSet(mode, DisplayMode.FLAG_ACTIVE_GROUPS)) { throw new AssertionError("Inactive group flag set, but the active group flag isn't!"); } - this.filter = sanitizeFilter(filter); this.mode = mode; this.recents = recents; this.contactRepository = new ContactRepository(context); } - @Override - public Cursor loadInBackground() { - List cursorList = TextUtils.isEmpty(filter) ? getUnfilteredResults() - : getFilteredResults(); - if (cursorList.size() > 0) { - return new MergeCursor(cursorList.toArray(new Cursor[0])); - } - return null; - } - - private static @NonNull String sanitizeFilter(@Nullable String filter) { - if (filter == null) { - return ""; - } else if (filter.startsWith("@")) { - return filter.substring(1); - } else { - return filter; - } - } - - private List getUnfilteredResults() { + protected final List getUnfilteredResults() { ArrayList cursorList = new ArrayList<>(); if (groupsOnly(mode)) { @@ -131,7 +95,7 @@ public class ContactsCursorLoader extends CursorLoader { return cursorList; } - private List getFilteredResults() { + protected final List getFilteredResults() { ArrayList cursorList = new ArrayList<>(); addContactsSection(cursorList); @@ -153,7 +117,7 @@ public class ContactsCursorLoader extends CursorLoader { Cursor recentConversations = getRecentConversationsCursor(); if (recentConversations.getCount() > 0) { - cursorList.add(getRecentsHeaderCursor()); + cursorList.add(ContactsCursorRows.forRecentsHeader(getContext())); cursorList.add(recentConversations); } } @@ -162,7 +126,7 @@ public class ContactsCursorLoader extends CursorLoader { List contacts = getContactsCursors(); if (!isCursorListEmpty(contacts)) { - cursorList.add(getContactsHeaderCursor()); + cursorList.add(ContactsCursorRows.forContactsHeader(getContext())); cursorList.addAll(contacts); } } @@ -175,7 +139,7 @@ public class ContactsCursorLoader extends CursorLoader { Cursor groups = getRecentConversationsCursor(true); if (groups.getCount() > 0) { - cursorList.add(getRecentsHeaderCursor()); + cursorList.add(ContactsCursorRows.forRecentsHeader(getContext())); cursorList.add(groups); } } @@ -188,89 +152,28 @@ public class ContactsCursorLoader extends CursorLoader { Cursor groups = getGroupsCursor(); if (groups.getCount() > 0) { - cursorList.add(getGroupsHeaderCursor()); + cursorList.add(ContactsCursorRows.forGroupsHeader(getContext())); cursorList.add(groups); } } private void addNewNumberSection(@NonNull List cursorList) { - if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(filter)) { - cursorList.add(getPhoneNumberSearchHeaderCursor()); + if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) { + cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext())); cursorList.add(getNewNumberCursor()); - } else if (!FeatureFlags.usernames() && NumberUtil.isValidSmsOrEmail(filter)){ - cursorList.add(getPhoneNumberSearchHeaderCursor()); + } else if (!FeatureFlags.usernames() && NumberUtil.isValidSmsOrEmail(getFilter())) { + cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext())); cursorList.add(getNewNumberCursor()); } } private void addUsernameSearchSection(@NonNull List cursorList) { - if (FeatureFlags.usernames() && UsernameUtil.isValidUsernameForSearch(filter)) { - cursorList.add(getUsernameSearchHeaderCursor()); + if (FeatureFlags.usernames() && UsernameUtil.isValidUsernameForSearch(getFilter())) { + cursorList.add(ContactsCursorRows.forUsernameSearchHeader(getContext())); cursorList.add(getUsernameSearchCursor()); } } - private Cursor getRecentsHeaderCursor() { - MatrixCursor recentsHeader = new MatrixCursor(CONTACT_PROJECTION); - recentsHeader.addRow(new Object[]{ null, - getContext().getString(R.string.ContactsCursorLoader_recent_chats), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactRepository.DIVIDER_TYPE, - "" }); - return recentsHeader; - } - - private Cursor getContactsHeaderCursor() { - MatrixCursor contactsHeader = new MatrixCursor(CONTACT_PROJECTION, 1); - contactsHeader.addRow(new Object[] { null, - getContext().getString(R.string.ContactsCursorLoader_contacts), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactRepository.DIVIDER_TYPE, - "" }); - return contactsHeader; - } - - private Cursor getGroupsHeaderCursor() { - MatrixCursor groupHeader = new MatrixCursor(CONTACT_PROJECTION, 1); - groupHeader.addRow(new Object[]{ null, - getContext().getString(R.string.ContactsCursorLoader_groups), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactRepository.DIVIDER_TYPE, - "" }); - return groupHeader; - } - - private Cursor getPhoneNumberSearchHeaderCursor() { - MatrixCursor contactsHeader = new MatrixCursor(CONTACT_PROJECTION, 1); - contactsHeader.addRow(new Object[] { null, - getContext().getString(R.string.ContactsCursorLoader_phone_number_search), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactRepository.DIVIDER_TYPE, - "" }); - return contactsHeader; - } - - private Cursor getUsernameSearchHeaderCursor() { - MatrixCursor contactsHeader = new MatrixCursor(CONTACT_PROJECTION, 1); - contactsHeader.addRow(new Object[] { null, - getContext().getString(R.string.ContactsCursorLoader_username_search), - "", - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactRepository.DIVIDER_TYPE, - "" }); - return contactsHeader; - } - - private Cursor getRecentConversationsCursor() { return getRecentConversationsCursor(false); } @@ -278,21 +181,12 @@ public class ContactsCursorLoader extends CursorLoader { private Cursor getRecentConversationsCursor(boolean groupsOnly) { ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getContext()); - MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, RECENT_CONVERSATION_MAX); + MatrixCursor recentConversations = ContactsCursorRows.createMatrixCursor(RECENT_CONVERSATION_MAX); try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly, hideGroupsV1(mode), !smsEnabled(mode))) { ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations); - ThreadRecord threadRecord; + ThreadRecord threadRecord; while ((threadRecord = reader.getNext()) != null) { - Recipient recipient = threadRecord.getRecipient(); - String stringId = recipient.isGroup() ? recipient.requireGroupId().toString() : recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).or(recipient.getEmail()).or(""); - - recentConversations.addRow(new Object[] { recipient.getId().serialize(), - recipient.getDisplayName(getContext()), - stringId, - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, - "", - ContactRepository.RECENT_TYPE | (recipient.isRegistered() && !recipient.isForceSmsSelection() ? ContactRepository.PUSH_TYPE : 0), - recipient.getCombinedAboutAndEmoji() }); + recentConversations.addRow(ContactsCursorRows.forRecipient(getContext(), threadRecord.getRecipient())); } } return recentConversations; @@ -306,56 +200,34 @@ public class ContactsCursorLoader extends CursorLoader { } if (pushEnabled(mode)) { - cursorList.add(contactRepository.querySignalContacts(filter, selfEnabled(mode))); + cursorList.add(contactRepository.querySignalContacts(getFilter(), selfEnabled(mode))); } if (pushEnabled(mode) && smsEnabled(mode)) { - cursorList.add(contactRepository.queryNonSignalContacts(filter)); + cursorList.add(contactRepository.queryNonSignalContacts(getFilter())); } else if (smsEnabled(mode)) { - cursorList.add(filterNonPushContacts(contactRepository.queryNonSignalContacts(filter))); + cursorList.add(filterNonPushContacts(contactRepository.queryNonSignalContacts(getFilter()))); } return cursorList; } private Cursor getGroupsCursor() { - MatrixCursor groupContacts = new MatrixCursor(CONTACT_PROJECTION); - try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(filter, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode))) { + MatrixCursor groupContacts = ContactsCursorRows.createMatrixCursor(); + try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(getFilter(), flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode))) { GroupDatabase.GroupRecord groupRecord; while ((groupRecord = reader.getNext()) != null) { - groupContacts.addRow(new Object[] { groupRecord.getRecipientId().serialize(), - groupRecord.getTitle(), - groupRecord.getId(), - ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, - "", - ContactRepository.NORMAL_TYPE, - "" }); + groupContacts.addRow(ContactsCursorRows.forGroup(groupRecord)); } } return groupContacts; } private Cursor getNewNumberCursor() { - MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1); - newNumberCursor.addRow(new Object[] { null, - getUnknownContactTitle(), - filter, - ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, - "\u21e2", - ContactRepository.NEW_PHONE_TYPE, - "" }); - return newNumberCursor; + return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter()); } private Cursor getUsernameSearchCursor() { - MatrixCursor cursor = new MatrixCursor(CONTACT_PROJECTION, 1); - cursor.addRow(new Object[] { null, - getUnknownContactTitle(), - filter, - ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, - "\u21e2", - ContactRepository.NEW_USERNAME_TYPE, - "" }); - return cursor; + return ContactsCursorRows.forUsernameSearch(getUnknownContactTitle(), getFilter()); } private String getUnknownContactTitle() { @@ -370,20 +242,14 @@ public class ContactsCursorLoader extends CursorLoader { private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) { try { - final long startMillis = System.currentTimeMillis(); - final MatrixCursor matrix = new MatrixCursor(CONTACT_PROJECTION); + final long startMillis = System.currentTimeMillis(); + final MatrixCursor matrix = ContactsCursorRows.createMatrixCursor(); while (cursor.moveToNext()) { 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.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, - "" }); + matrix.addRow(ContactsCursorRows.forNonPushContact(cursor)); } } Log.i(TAG, "filterNonPushContacts() -> " + (System.currentTimeMillis() - startMillis) + "ms"); @@ -440,4 +306,24 @@ public class ContactsCursorLoader extends CursorLoader { private static boolean flagSet(int mode, int flag) { return (mode & flag) > 0; } + + public static class Factory implements AbstractContactsCursorLoader.Factory { + + private final Context context; + private final int displayMode; + private final String cursorFilter; + private final boolean displayRecents; + + public Factory(Context context, int displayMode, String cursorFilter, boolean displayRecents) { + this.context = context; + this.displayMode = displayMode; + this.cursorFilter = cursorFilter; + this.displayRecents = displayRecents; + } + + @Override + public @NonNull AbstractContactsCursorLoader create() { + return new ContactsCursorLoader(context, displayMode, cursorFilter, displayRecents); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorRows.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorRows.java new file mode 100644 index 0000000000..aaed145b6c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactsCursorRows.java @@ -0,0 +1,153 @@ +package org.thoughtcrime.securesms.contacts; + +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.provider.ContactsContract; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; +import org.thoughtcrime.securesms.recipients.Recipient; + +/** + * Helper utility for generating cursors and cursor rows for subclasses of {@link AbstractContactsCursorLoader}. + */ +public final class ContactsCursorRows { + + 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, + ContactRepository.ABOUT_COLUMN}; + + /** + * Create a {@link MatrixCursor} with the proper projection for a subclass of {@link AbstractContactsCursorLoader} + */ + public static @NonNull MatrixCursor createMatrixCursor() { + return new MatrixCursor(CONTACT_PROJECTION); + } + + /** + * Create a {@link MatrixCursor} with the proper projection for a subclass of {@link AbstractContactsCursorLoader} + * + * @param initialCapacity The initial capacity to hand to the {@link MatrixCursor} + */ + public static @NonNull MatrixCursor createMatrixCursor(int initialCapacity) { + return new MatrixCursor(CONTACT_PROJECTION, initialCapacity); + } + + /** + * Create a row for a contacts cursor based off the given recipient. + */ + public static @NonNull Object[] forRecipient(@NonNull Context context, @NonNull Recipient recipient) { + String stringId = recipient.isGroup() ? recipient.requireGroupId().toString() + : recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).or(recipient.getEmail()).or(""); + + return new Object[]{recipient.getId().serialize(), + recipient.getDisplayName(context), + stringId, + ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, + "", + ContactRepository.RECENT_TYPE | (recipient.isRegistered() && !recipient.isForceSmsSelection() ? ContactRepository.PUSH_TYPE : 0), + recipient.getCombinedAboutAndEmoji()}; + } + + /** + * Create a row for a contacts cursor based off the given system contact. + */ + public static @NonNull Object[] forNonPushContact(@NonNull Cursor systemContactCursor) { + return new Object[]{systemContactCursor.getLong(systemContactCursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN)), + systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN)), + systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN)), + systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN)), + systemContactCursor.getString(systemContactCursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN)), + ContactRepository.NORMAL_TYPE, + ""}; + } + + /** + * Create a row for a contacts cursor based off the given group record. + */ + public static @NonNull Object[] forGroup(@NonNull GroupDatabase.GroupRecord groupRecord) { + return new Object[]{groupRecord.getRecipientId().serialize(), + groupRecord.getTitle(), + groupRecord.getId(), + ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, + "", + ContactRepository.NORMAL_TYPE, + ""}; + } + + /** + * Create a row for a contacts cursor for a new number the user is entering or has entered. + */ + public static @NonNull MatrixCursor forNewNumber(@NonNull String unknownContactTitle, @NonNull String filter) { + MatrixCursor matrixCursor = createMatrixCursor(1); + + matrixCursor.addRow(new Object[]{null, + unknownContactTitle, + filter, + ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, + "\u21e2", + ContactRepository.NEW_PHONE_TYPE, + ""}); + + return matrixCursor; + } + + /** + * Create a row for a contacts cursor for a username the user is entering or has entered. + */ + public static @NonNull MatrixCursor forUsernameSearch(@NonNull String unknownContactTitle, @NonNull String filter) { + MatrixCursor matrixCursor = createMatrixCursor(1); + + matrixCursor.addRow(new Object[]{null, + unknownContactTitle, + filter, + ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, + "\u21e2", + ContactRepository.NEW_USERNAME_TYPE, + ""}); + + return matrixCursor; + } + + public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) { + return forHeader(context.getString(R.string.ContactsCursorLoader_username_search)); + } + + public static @NonNull MatrixCursor forPhoneNumberSearchHeader(@NonNull Context context) { + return forHeader(context.getString(R.string.ContactsCursorLoader_phone_number_search)); + } + + public static @NonNull MatrixCursor forGroupsHeader(@NonNull Context context) { + return forHeader(context.getString(R.string.ContactsCursorLoader_groups)); + } + + public static @NonNull MatrixCursor forRecentsHeader(@NonNull Context context) { + return forHeader(context.getString(R.string.ContactsCursorLoader_recent_chats)); + } + + public static @NonNull MatrixCursor forContactsHeader(@NonNull Context context) { + return forHeader(context.getString(R.string.ContactsCursorLoader_contacts)); + } + + public static @NonNull MatrixCursor forHeader(@NonNull String name) { + MatrixCursor matrixCursor = createMatrixCursor(1); + + matrixCursor.addRow(new Object[]{null, + name, + "", + ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, + "", + ContactRepository.DIVIDER_TYPE, + ""}); + + return matrixCursor; + } +}