Refresh contact search views.

This commit is contained in:
Alex Hart
2021-07-12 15:44:59 -03:00
committed by Greyson Parrelli
parent a4d458f969
commit a157c1ae1d
51 changed files with 1004 additions and 513 deletions

View File

@@ -108,15 +108,31 @@ public class ContactRepository {
}
@WorkerThread
public Cursor querySignalContacts(@NonNull String query) {
public @NonNull Cursor querySignalContacts(@NonNull String query) {
return querySignalContacts(query, true);
}
@WorkerThread
public Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts(includeSelf)
: recipientDatabase.querySignalContacts(query, includeSelf);
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());
@@ -130,8 +146,7 @@ public class ContactRepository {
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}
}
return new SearchCursorWrapper(cursor, SEARCH_CURSOR_MAPPERS);
return cursor;
}
@WorkerThread

View File

@@ -1,16 +1,16 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@@ -21,6 +21,7 @@ import android.database.Cursor;
import android.provider.ContactsContract;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
@@ -29,7 +30,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.core.util.logging.Log;
@@ -40,12 +40,15 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolde
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CharacterIterable;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
import org.thoughtcrime.securesms.util.Util;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
/**
@@ -54,8 +57,8 @@ import java.util.Set;
* @author Jake McGinty
*/
public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewHolder>
implements FastScrollAdapter,
StickyHeaderAdapter<HeaderViewHolder>
implements FastScrollAdapter,
StickyHeaderAdapter<HeaderViewHolder>
{
@SuppressWarnings("unused")
private final static String TAG = Log.tag(ContactSelectionListAdapter.class);
@@ -98,14 +101,28 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
super(itemView);
}
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkboxVisible);
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, boolean checkboxVisible);
public abstract void unbind(@NonNull GlideRequests glideRequests);
public abstract void setChecked(boolean checked);
public void animateChecked(boolean checked) {
// Intentionally empty.
}
public abstract void setEnabled(boolean enabled);
public void setLetterHeaderCharacter(@Nullable String letterHeaderCharacter) {
// Intentionally empty.
}
}
public static class ContactViewHolder extends ViewHolder {
ContactViewHolder(@NonNull final View itemView,
public static class ContactViewHolder extends ViewHolder implements LetterHeaderDecoration.LetterHeaderItem {
private String letterHeader;
ContactViewHolder(@NonNull final View itemView,
@Nullable final ItemClickListener clickListener)
{
super(itemView);
@@ -118,8 +135,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return (ContactSelectionListItem) itemView;
}
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, about, color, checkBoxVisible);
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, about, checkBoxVisible);
}
@Override
@@ -129,13 +146,28 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public void setChecked(boolean checked) {
getView().setChecked(checked);
getView().setChecked(checked, false);
}
@Override
public void animateChecked(boolean checked) {
getView().setChecked(checked, true);
}
@Override
public void setEnabled(boolean enabled) {
getView().setEnabled(enabled);
}
@Override
public @Nullable String getHeaderLetter() {
return letterHeader;
}
@Override
public void setLetterHeaderCharacter(@Nullable String letterHeaderCharacter) {
this.letterHeader = letterHeaderCharacter;
}
}
public static class DividerViewHolder extends ViewHolder {
@@ -148,7 +180,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
@Override
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkboxVisible) {
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, boolean checkboxVisible) {
this.label.setText(name);
}
@@ -168,15 +200,15 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
}
public ContactSelectionListAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests,
public ContactSelectionListAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests,
@Nullable Cursor cursor,
@Nullable ItemClickListener clickListener,
boolean multiSelect,
@NonNull Set<RecipientId> currentContacts)
{
super(context, cursor);
this.layoutInflater = LayoutInflater.from(context);
this.layoutInflater = LayoutInflater.from(context);
this.glideRequests = glideRequests;
this.multiSelect = multiSelect;
this.clickListener = clickListener;
@@ -186,7 +218,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public long getHeaderId(int i) {
if (!isActiveCursor()) return -1;
else if (i == -1) return -1;
else if (i == -1) return -1;
int contactType = getContactType(i);
@@ -215,15 +247,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
String label = CursorUtil.requireString(cursor, ContactRepository.LABEL_COLUMN);
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
boolean isPush = (contactType & ContactRepository.PUSH_TYPE) > 0;
int color = isPush ? ContextCompat.getColor(getContext(), R.color.signal_text_primary)
: ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_60);
boolean currentContact = currentContacts.contains(id);
viewHolder.unbind(glideRequests);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, about, color, multiSelect || currentContact);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, about, multiSelect || currentContact);
viewHolder.setEnabled(true);
if (currentContact) {
@@ -234,6 +261,54 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
} else {
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
}
if (isContactRow(contactType)) {
int position = cursor.getPosition();
if (position == 0) {
viewHolder.setLetterHeaderCharacter(getHeaderLetterForDisplayName(cursor));
} else {
cursor.moveToPrevious();
int previousRowContactType = CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN);
if (!isContactRow(previousRowContactType)) {
cursor.moveToNext();
viewHolder.setLetterHeaderCharacter(getHeaderLetterForDisplayName(cursor));
} else {
String previousHeaderLetter = getHeaderLetterForDisplayName(cursor);
cursor.moveToNext();
String newHeaderLetter = getHeaderLetterForDisplayName(cursor);
if (Objects.equals(previousHeaderLetter, newHeaderLetter)) {
viewHolder.setLetterHeaderCharacter(null);
} else {
viewHolder.setLetterHeaderCharacter(newHeaderLetter);
}
}
}
}
}
private boolean isContactRow(int contactType) {
return (contactType & (ContactRepository.NEW_PHONE_TYPE | ContactRepository.NEW_USERNAME_TYPE | ContactRepository.DIVIDER_TYPE)) == 0;
}
private @Nullable String getHeaderLetterForDisplayName(@NonNull Cursor cursor) {
String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
Iterator<String> characterIterator = new CharacterIterable(name).iterator();
if (!TextUtils.isEmpty(name) && characterIterator.hasNext()) {
String next = characterIterator.next();
if (Character.isLetter(next.codePointAt(0))) {
return next.toUpperCase();
} else {
return "#";
}
} else {
return null;
}
}
@Override
@@ -250,12 +325,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
viewHolder.setEnabled(true);
if (currentContacts.contains(id)) {
viewHolder.setChecked(true);
viewHolder.animateChecked(true);
viewHolder.setEnabled(false);
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
viewHolder.animateChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
} else {
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
viewHolder.animateChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
}
}
@@ -275,7 +350,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position, int type) {
((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position));
((TextView) viewHolder.itemView).setText(getSpannedHeaderString(position));
}
@Override
@@ -301,6 +376,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return selectedContacts.size();
}
public int getCurrentContactsCount() {
return currentContacts.size();
}
private CharSequence getSpannedHeaderString(int position) {
final String headerString = getHeaderString(position);
if (isPush(position)) {

View File

@@ -34,6 +34,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
private FromTextView nameView;
private TextView labelView;
private CheckBox checkBox;
private View smsTag;
private String number;
private String chipName;
@@ -61,6 +62,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.labelView = findViewById(R.id.label);
this.nameView = findViewById(R.id.name);
this.checkBox = findViewById(R.id.check_box);
this.smsTag = findViewById(R.id.sms_tag);
ViewUtil.setTextViewGravityStart(this.nameView, getContext());
}
@@ -72,7 +74,6 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
String number,
String label,
String about,
int color,
boolean checkboxVisible)
{
this.glideRequests = glideRequests;
@@ -92,10 +93,15 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
name = this.recipient.get().getDisplayName(getContext());
}
boolean isPush = (contactType & ContactRepository.PUSH_TYPE) > 0;
if (isPush) {
smsTag.setVisibility(GONE);
} else {
smsTag.setVisibility(VISIBLE);
}
Recipient recipientSnapshot = recipient != null ? recipient.get() : null;
this.nameView.setTextColor(color);
this.numberView.setTextColor(color);
if (recipientSnapshot == null || recipientSnapshot.isResolving()) {
this.contactPhotoImage.setAvatar(glideRequests, null, false);
setText(null, type, name, number, label, about);
@@ -107,8 +113,20 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
}
public void setChecked(boolean selected) {
this.checkBox.setChecked(selected);
public void setChecked(boolean selected, boolean animate) {
boolean wasSelected = checkBox.isChecked();
if (wasSelected != selected) {
checkBox.setChecked(selected);
float alpha = selected ? 1f : 0f;
if (animate) {
checkBox.animate().setDuration(250L).alpha(alpha);
} else {
checkBox.animate().cancel();
checkBox.setAlpha(alpha);
}
}
}
@Override
@@ -146,7 +164,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
} else {
this.numberView.setText(!Util.isEmpty(about) ? about : number);
this.nameView.setEnabled(true);
this.labelView.setText(label != null && !label.equals("null") ? label : "");
this.labelView.setText(label != null && !label.equals("null") ? getResources().getString(R.string.ContactSelectionListItem__dot_s, label) : "");
this.labelView.setVisibility(View.VISIBLE);
}

View File

@@ -16,7 +16,6 @@
*/
package org.thoughtcrime.securesms.contacts;
import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
@@ -30,7 +29,6 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
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.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -126,7 +124,9 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
List<Cursor> contacts = getContactsCursors();
if (!isCursorListEmpty(contacts)) {
cursorList.add(ContactsCursorRows.forContactsHeader(getContext()));
if (!getFilter().isEmpty() || recents) {
cursorList.add(ContactsCursorRows.forContactsHeader(getContext()));
}
cursorList.addAll(contacts);
}
}
@@ -195,19 +195,14 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
private List<Cursor> getContactsCursors() {
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(contactRepository.querySignalContacts(getFilter(), selfEnabled(mode)));
}
if (pushEnabled(mode) && smsEnabled(mode)) {
cursorList.add(contactRepository.queryNonSignalContacts(getFilter()));
cursorList.add(contactRepository.queryNonGroupContacts(getFilter(), selfEnabled(mode)));
} else if (pushEnabled(mode)) {
cursorList.add(contactRepository.querySignalContacts(getFilter(), selfEnabled(mode)));
} else if (smsEnabled(mode)) {
cursorList.add(filterNonPushContacts(contactRepository.queryNonSignalContacts(getFilter())));
cursorList.add(contactRepository.queryNonSignalContacts(getFilter()));
}
return cursorList;
}
@@ -240,25 +235,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
}
}
private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) {
try {
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(ContactsCursorRows.forNonPushContact(cursor));
}
}
Log.i(TAG, "filterNonPushContacts() -> " + (System.currentTimeMillis() - startMillis) + "ms");
return matrix;
} finally {
cursor.close();
}
}
private static boolean isCursorListEmpty(List<Cursor> list) {
int sum = 0;
for (Cursor cursor : list) {

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.contacts
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ViewUtil
/**
* ItemDecoration which paints a letter header at the appropriate location above a LetterHeaderItem.
*/
class LetterHeaderDecoration(private val context: Context, private val hideDecoration: () -> Boolean) : RecyclerView.ItemDecoration() {
private val textBounds = Rect()
private val bounds = Rect()
private val padTop = ViewUtil.dpToPx(16)
private val padStart = context.resources.getDimensionPixelSize(R.dimen.dsl_settings_gutter)
private var dividerHeight = -1
private val textPaint = Paint().apply {
color = ContextCompat.getColor(context, R.color.signal_text_primary)
isAntiAlias = true
style = Paint.Style.FILL
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
textAlign = Paint.Align.LEFT
textSize = ViewUtil.spToPx(16f).toFloat()
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val viewHolder = parent.getChildViewHolder(view)
if (hideDecoration() || viewHolder !is LetterHeaderItem || viewHolder.getHeaderLetter() == null) {
outRect.set(0, 0, 0, 0)
return
}
if (dividerHeight == -1) {
val v = LayoutInflater.from(context).inflate(R.layout.dsl_section_header, parent, false)
v.measure(0, 0)
dividerHeight = v.measuredHeight
}
outRect.set(0, dividerHeight, 0, 0)
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (hideDecoration()) {
return
}
val childCount = parent.childCount
val isRtl = parent.layoutDirection == View.LAYOUT_DIRECTION_RTL
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val holder = parent.getChildViewHolder(child)
val headerLetter = if (holder is LetterHeaderItem) holder.getHeaderLetter() else null
if (headerLetter != null) {
parent.getDecoratedBoundsWithMargins(child, bounds)
textPaint.getTextBounds(headerLetter, 0, headerLetter.length, textBounds)
val x = if (isRtl) getLayoutBoundsRTL() else getLayoutBoundsLTR()
val y = bounds.top + padTop - textBounds.top
canvas.save()
canvas.drawText(headerLetter, x.toFloat(), y.toFloat(), textPaint)
canvas.restore()
}
}
}
private fun getLayoutBoundsLTR() = bounds.left + padStart
private fun getLayoutBoundsRTL() = bounds.right - padStart - textBounds.width()
interface LetterHeaderItem {
fun getHeaderLetter(): String?
}
}