Fix conversation list bug with pinned chats.
Co-authored-by: Alex Hart <alex@signal.org>master
parent
f84c8229de
commit
e428453835
|
@ -1,148 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
class CompositeConversationListAdapter extends RecyclerViewConcatenateAdapter {
|
||||
|
||||
private final FixedViewsAdapter pinnedHeaderAdapter;
|
||||
private final ConversationListAdapter pinnedAdapter;
|
||||
private final FixedViewsAdapter unpinnedHeaderAdapter;
|
||||
private final ConversationListAdapter unpinnedAdapter;
|
||||
|
||||
CompositeConversationListAdapter(@NonNull RecyclerView rv,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull ConversationListAdapter.OnConversationClickListener onConversationClickListener)
|
||||
{
|
||||
|
||||
TextView pinned = (TextView) LayoutInflater.from(rv.getContext()).inflate(R.layout.conversation_list_item_header, rv, false);
|
||||
TextView unpinned = (TextView) LayoutInflater.from(rv.getContext()).inflate(R.layout.conversation_list_item_header, rv, false);
|
||||
|
||||
pinned.setText(rv.getContext().getString(R.string.conversation_list__pinned));
|
||||
unpinned.setText(rv.getContext().getString(R.string.conversation_list__chats));
|
||||
|
||||
this.pinnedHeaderAdapter = new FixedViewsAdapter(pinned);
|
||||
this.pinnedAdapter = new ConversationListAdapter(this, glideRequests, onConversationClickListener);
|
||||
this.unpinnedHeaderAdapter = new FixedViewsAdapter(unpinned);
|
||||
this.unpinnedAdapter = new ConversationListAdapter(this, glideRequests, onConversationClickListener);
|
||||
|
||||
pinnedHeaderAdapter.hide();
|
||||
unpinnedHeaderAdapter.hide();
|
||||
|
||||
unpinnedAdapter.registerAdapterDataObserver(new UnpinnedAdapterDataObserver());
|
||||
pinnedAdapter.registerAdapterDataObserver(new PinnedAdapterDataObserver());
|
||||
|
||||
addAdapter(pinnedHeaderAdapter);
|
||||
addAdapter(pinnedAdapter);
|
||||
addAdapter(unpinnedHeaderAdapter);
|
||||
addAdapter(unpinnedAdapter);
|
||||
}
|
||||
|
||||
public void submitPinnedList(@NonNull PagedList<Conversation> pinnedConversations) {
|
||||
pinnedAdapter.submitList(pinnedConversations);
|
||||
}
|
||||
|
||||
public void submitUnpinnedList(@NonNull PagedList<Conversation> unpinnedConversations) {
|
||||
unpinnedAdapter.submitList(unpinnedConversations);
|
||||
}
|
||||
|
||||
public void setTypingThreads(@NonNull Set<Long> threads) {
|
||||
pinnedAdapter.setTypingThreads(threads);
|
||||
unpinnedAdapter.setTypingThreads(threads);
|
||||
}
|
||||
|
||||
public @NonNull Set<Long> getBatchSelectionIds() {
|
||||
HashSet<Long> hashSet = new HashSet();
|
||||
|
||||
hashSet.addAll(pinnedAdapter.getBatchSelectionIds());
|
||||
hashSet.addAll(unpinnedAdapter.getBatchSelectionIds());
|
||||
|
||||
return hashSet;
|
||||
}
|
||||
|
||||
public void selectAllThreads() {
|
||||
pinnedAdapter.selectAllThreads();
|
||||
unpinnedAdapter.selectAllThreads();
|
||||
}
|
||||
|
||||
public void updateArchived(int archivedCount) {
|
||||
unpinnedAdapter.updateArchived(archivedCount);
|
||||
}
|
||||
|
||||
public void toggleConversationInBatchSet(@NonNull Conversation conversation) {
|
||||
if (conversation.getThreadRecord().isPinned()) {
|
||||
pinnedAdapter.toggleConversationInBatchSet(conversation);
|
||||
} else {
|
||||
unpinnedAdapter.toggleConversationInBatchSet(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeBatchMode(boolean toggle) {
|
||||
pinnedAdapter.initializeBatchMode(toggle);
|
||||
unpinnedAdapter.initializeBatchMode(toggle);
|
||||
}
|
||||
|
||||
public long getPinnedItemCount() {
|
||||
return pinnedAdapter.getItemCount();
|
||||
}
|
||||
|
||||
public @NonNull Collection<Conversation> getBatchSelection() {
|
||||
Set<Conversation> conversations = new HashSet<>();
|
||||
|
||||
conversations.addAll(pinnedAdapter.getBatchSelection());
|
||||
conversations.addAll(unpinnedAdapter.getBatchSelection());
|
||||
|
||||
return conversations;
|
||||
}
|
||||
|
||||
private class UnpinnedAdapterDataObserver extends RecyclerView.AdapterDataObserver {
|
||||
@Override
|
||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
if (unpinnedAdapter.getItemCount() == 0) {
|
||||
unpinnedHeaderAdapter.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (itemCount > 0 && pinnedAdapter.getItemCount() > 0) {
|
||||
unpinnedHeaderAdapter.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PinnedAdapterDataObserver extends RecyclerView.AdapterDataObserver {
|
||||
@Override
|
||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
if (pinnedAdapter.getItemCount() == 0) {
|
||||
pinnedHeaderAdapter.hide();
|
||||
unpinnedHeaderAdapter.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (itemCount > 0) {
|
||||
pinnedHeaderAdapter.show();
|
||||
|
||||
if (unpinnedAdapter.getItemCount() > 0) {
|
||||
unpinnedHeaderAdapter.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.paging.PagedListAdapter;
|
||||
|
@ -13,12 +14,9 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -30,11 +28,12 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
class ConversationListAdapter extends PagedListAdapter<Conversation, ConversationListAdapter.BaseViewHolder> {
|
||||
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int TYPE_THREAD = 1;
|
||||
private static final int TYPE_ACTION = 2;
|
||||
private static final int TYPE_PLACEHOLDER = 3;
|
||||
private static final int TYPE_HEADER = 4;
|
||||
|
||||
private enum Payload {
|
||||
TYPING_INDICATOR,
|
||||
|
@ -46,26 +45,21 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new HashMap<>());
|
||||
private boolean batchMode = false;
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
private int archived;
|
||||
|
||||
private final RecyclerViewConcatenateAdapter parent;
|
||||
|
||||
protected ConversationListAdapter(@NonNull RecyclerViewConcatenateAdapter parent,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
protected ConversationListAdapter(@NonNull GlideRequests glideRequests,
|
||||
@NonNull OnConversationClickListener onConversationClickListener)
|
||||
{
|
||||
super(new ConversationDiffCallback());
|
||||
|
||||
this.parent = parent;
|
||||
this.glideRequests = glideRequests;
|
||||
this.onConversationClickListener = onConversationClickListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == TYPE_ACTION) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_action, parent, false), viewType);
|
||||
.inflate(R.layout.conversation_list_item_action, parent, false));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
|
@ -76,10 +70,10 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
return holder;
|
||||
} else if (viewType == TYPE_THREAD) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false), viewType);
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int position = this.parent.getLocalPosition(holder.getAdapterPosition()).getLocalPosition();
|
||||
int position = holder.getAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onConversationClickListener.onConversationClick(getItem(position));
|
||||
|
@ -87,7 +81,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
int position = this.parent.getLocalPosition(holder.getAdapterPosition()).getLocalPosition();
|
||||
int position = holder.getAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
return onConversationClickListener.onConversationLongClick(getItem(position));
|
||||
|
@ -100,16 +94,19 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
View v = new FrameLayout(parent.getContext());
|
||||
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
|
||||
return new PlaceholderViewHolder(v);
|
||||
} else if (viewType == TYPE_HEADER) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_list_item_header, parent, false);
|
||||
return new HeaderViewHolder(v);
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown type! " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
} else if (holder instanceof ConversationViewHolder) {
|
||||
for (Object payloadObject : payloads) {
|
||||
if (payloadObject instanceof Payload) {
|
||||
Payload payload = (Payload) payloadObject;
|
||||
|
@ -125,22 +122,8 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
|
||||
if (holder.getLocalViewType() == TYPE_ACTION) {
|
||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||
|
||||
casted.getConversationListItem().bind(new ThreadRecord.Builder(100)
|
||||
.setBody("")
|
||||
.setDate(100)
|
||||
.setRecipient(Recipient.UNKNOWN)
|
||||
.setCount(archived)
|
||||
.build(),
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
} else if (holder.getLocalViewType() == TYPE_THREAD) {
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder.getItemViewType() == TYPE_ACTION || holder.getItemViewType() == TYPE_THREAD) {
|
||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||
|
||||
|
@ -150,11 +133,24 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
} else if (holder.getItemViewType() == TYPE_HEADER) {
|
||||
HeaderViewHolder casted = (HeaderViewHolder) holder;
|
||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||
switch (conversation.getType()) {
|
||||
case PINNED_HEADER:
|
||||
casted.headerText.setText(R.string.conversation_list__pinned);
|
||||
break;
|
||||
case UNPINNED_HEADER:
|
||||
casted.headerText.setText(R.string.conversation_list__chats);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull BaseViewHolder holder) {
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof ConversationViewHolder) {
|
||||
((ConversationViewHolder) holder).getConversationListItem().unbind();
|
||||
}
|
||||
|
@ -181,35 +177,22 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
return batchSet.values();
|
||||
}
|
||||
|
||||
void updateArchived(int archived) {
|
||||
int oldArchived = this.archived;
|
||||
|
||||
this.archived = archived;
|
||||
|
||||
if (oldArchived != archived) {
|
||||
if (archived == 0) {
|
||||
notifyItemRemoved(getItemCount());
|
||||
} else if (oldArchived == 0) {
|
||||
notifyItemInserted(getItemCount() - 1);
|
||||
} else {
|
||||
notifyItemChanged(getItemCount() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return (archived > 0 ? 1 : 0) + super.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (archived > 0 && position == getItemCount() - 1) {
|
||||
return TYPE_ACTION;
|
||||
} else if (getItem(position) == null) {
|
||||
Conversation conversation = getItem(position);
|
||||
if (conversation == null) {
|
||||
return TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
return TYPE_THREAD;
|
||||
}
|
||||
switch (conversation.getType()) {
|
||||
case PINNED_HEADER:
|
||||
case UNPINNED_HEADER:
|
||||
return TYPE_HEADER;
|
||||
case ARCHIVED_FOOTER:
|
||||
return TYPE_ACTION;
|
||||
case THREAD:
|
||||
return TYPE_THREAD;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,7 +203,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
void selectAllThreads() {
|
||||
for (int i = 0; i < super.getItemCount(); i++) {
|
||||
Conversation conversation = getItem(i);
|
||||
if (conversation != null && conversation.getThreadRecord().getThreadId() != -1) {
|
||||
if (conversation != null && conversation.getThreadRecord().getThreadId() >= 0) {
|
||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
||||
}
|
||||
}
|
||||
|
@ -239,26 +222,12 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
static class BaseViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final int viewType;
|
||||
|
||||
public BaseViewHolder(@NonNull View itemView, int viewType) {
|
||||
super(itemView);
|
||||
this.viewType = viewType;
|
||||
}
|
||||
|
||||
public int getLocalViewType() {
|
||||
return viewType;
|
||||
}
|
||||
}
|
||||
|
||||
static final class ConversationViewHolder extends BaseViewHolder {
|
||||
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final BindableConversationListItem conversationListItem;
|
||||
|
||||
ConversationViewHolder(@NonNull View itemView, int viewType) {
|
||||
super(itemView, viewType);
|
||||
ConversationViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
conversationListItem = (BindableConversationListItem) itemView;
|
||||
}
|
||||
|
@ -281,9 +250,18 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, Conversatio
|
|||
}
|
||||
}
|
||||
|
||||
private static class PlaceholderViewHolder extends BaseViewHolder {
|
||||
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||
PlaceholderViewHolder(@NonNull View itemView) {
|
||||
super(itemView, TYPE_PLACEHOLDER);
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView headerText;
|
||||
|
||||
public HeaderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
headerText = (TextView) itemView;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,16 @@ package org.thoughtcrime.securesms.conversationlist;
|
|||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.PositionalDataSource;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
|
@ -57,10 +61,9 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
|
||||
}
|
||||
|
||||
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isPinned, boolean isArchived) {
|
||||
if (isPinned) return new PinnedConversationListDataSource(context, invalidator);
|
||||
else if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
||||
else return new ArchivedConversationListDataSource(context, invalidator);
|
||||
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
||||
else return new ArchivedConversationListDataSource(context, invalidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -72,7 +75,7 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
int effectiveCount = params.requestedStartPosition;
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
try (ConversationReader reader = new ConversationReader(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
ThreadRecord record;
|
||||
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
||||
conversations.add(new Conversation(record));
|
||||
|
@ -99,7 +102,7 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
List<Conversation> conversations = new ArrayList<>(params.loadSize);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.startPosition, params.loadSize))) {
|
||||
try (ConversationReader reader = new ConversationReader(getCursor(params.startPosition, params.loadSize))) {
|
||||
ThreadRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
conversations.add(new Conversation(record));
|
||||
|
@ -134,7 +137,13 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
}
|
||||
}
|
||||
|
||||
private static class UnarchivedConversationListDataSource extends ConversationListDataSource {
|
||||
@VisibleForTesting
|
||||
static class UnarchivedConversationListDataSource extends ConversationListDataSource {
|
||||
|
||||
private int totalCount;
|
||||
private int pinnedCount;
|
||||
private int archivedCount;
|
||||
private int unpinnedCount;
|
||||
|
||||
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||
super(context, invalidator);
|
||||
|
@ -142,29 +151,69 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getUnpinnedConversationListCount();
|
||||
int unarchivedCount = threadDatabase.getUnarchivedConversationListCount();
|
||||
|
||||
pinnedCount = threadDatabase.getPinnedConversationListCount();
|
||||
archivedCount = threadDatabase.getArchivedConversationListCount();
|
||||
unpinnedCount = unarchivedCount - pinnedCount;
|
||||
totalCount = unarchivedCount + (archivedCount != 0 ? 1 : 0) + (pinnedCount != 0 ? (unpinnedCount != 0 ? 2 : 1) : 0);
|
||||
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getUnpinnedConversationList(offset, limit);
|
||||
}
|
||||
}
|
||||
List<Cursor> cursors = new ArrayList<>(5);
|
||||
|
||||
private static class PinnedConversationListDataSource extends ConversationListDataSource {
|
||||
if (offset == 0 && hasPinnedHeader()) {
|
||||
MatrixCursor pinnedHeaderCursor = new MatrixCursor(ConversationReader.HEADER_COLUMN);
|
||||
pinnedHeaderCursor.addRow(ConversationReader.PINNED_HEADER);
|
||||
cursors.add(pinnedHeaderCursor);
|
||||
limit--;
|
||||
}
|
||||
|
||||
protected PinnedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||
super(context, invalidator);
|
||||
Cursor pinnedCursor = threadDatabase.getUnarchivedConversationList(true, offset, limit);
|
||||
cursors.add(pinnedCursor);
|
||||
limit -= pinnedCursor.getCount();
|
||||
|
||||
if (offset == 0 && hasUnpinnedHeader()) {
|
||||
MatrixCursor unpinnedHeaderCursor = new MatrixCursor(ConversationReader.HEADER_COLUMN);
|
||||
unpinnedHeaderCursor.addRow(ConversationReader.UNPINNED_HEADER);
|
||||
cursors.add(unpinnedHeaderCursor);
|
||||
limit--;
|
||||
}
|
||||
|
||||
long unpinnedOffset = Math.max(0, offset - pinnedCount - getHeaderOffset());
|
||||
Cursor unpinnedCursor = threadDatabase.getUnarchivedConversationList(false, unpinnedOffset, limit);
|
||||
cursors.add(unpinnedCursor);
|
||||
|
||||
if (offset + limit >= totalCount && hasArchivedFooter()) {
|
||||
MatrixCursor archivedFooterCursor = new MatrixCursor(ConversationReader.ARCHIVED_COLUMNS);
|
||||
archivedFooterCursor.addRow(ConversationReader.createArchivedFooterRow(archivedCount));
|
||||
cursors.add(archivedFooterCursor);
|
||||
}
|
||||
|
||||
return new MergeCursor(cursors.toArray(new Cursor[]{}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getPinnedConversationListCount();
|
||||
@VisibleForTesting
|
||||
int getHeaderOffset() {
|
||||
return (hasPinnedHeader() ? 1 : 0) + (hasUnpinnedHeader() ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getPinnedConversationList(offset, limit);
|
||||
@VisibleForTesting
|
||||
boolean hasPinnedHeader() {
|
||||
return pinnedCount != 0;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean hasUnpinnedHeader() {
|
||||
return hasPinnedHeader() && unpinnedCount != 0;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean hasArchivedFooter() {
|
||||
return archivedCount != 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,19 +221,17 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
|||
|
||||
private final Context context;
|
||||
private final Invalidator invalidator;
|
||||
private final boolean isPinned;
|
||||
private final boolean isArchived;
|
||||
|
||||
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isPinned, boolean isArchived) {
|
||||
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||
this.context = context;
|
||||
this.invalidator = invalidator;
|
||||
this.isPinned = isPinned;
|
||||
this.isArchived = isArchived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource<Integer, Conversation> create() {
|
||||
return ConversationListDataSource.create(context, invalidator, isPinned, isArchived);
|
||||
return ConversationListDataSource.create(context, invalidator, isArchived);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,7 +168,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
private View toolbarShadow;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private CompositeConversationListAdapter defaultAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private ConversationListSearchAdapter searchAdapter;
|
||||
private StickyHeaderDecoration searchAdapterDecoration;
|
||||
private ViewGroup megaphoneContainer;
|
||||
|
@ -213,7 +213,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
reminderView.setOnDismissListener(this::updateReminders);
|
||||
|
||||
list.setHasFixedSize(true);
|
||||
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
|
||||
list.setItemAnimator(new DeleteItemAnimator());
|
||||
list.addOnScrollListener(new ScrollListener());
|
||||
|
@ -458,7 +457,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
private void initializeListAdapters() {
|
||||
defaultAdapter = new CompositeConversationListAdapter(list, GlideApp.with(this), this);
|
||||
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
||||
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
||||
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
|
||||
|
||||
|
@ -503,8 +502,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
|
||||
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitUnpinnedList);
|
||||
viewModel.getPinnedConversations().observe(getViewLifecycleOwner(), this::onSubmitPinnedList);
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList);
|
||||
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
||||
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
|
@ -749,7 +747,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
.map(conversation -> conversation.getThreadRecord().getThreadId())
|
||||
.toList());
|
||||
|
||||
if (toPin.size() + defaultAdapter.getPinnedItemCount() > MAXIMUM_PINNED_CONVERSATIONS) {
|
||||
if (toPin.size() + viewModel.getPinnedCount() > MAXIMUM_PINNED_CONVERSATIONS) {
|
||||
Snackbar.make(fab,
|
||||
getString(R.string.conversation_list__you_can_only_pin_up_to_d_chats, MAXIMUM_PINNED_CONVERSATIONS),
|
||||
Snackbar.LENGTH_LONG)
|
||||
|
@ -789,13 +787,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||
}
|
||||
|
||||
private void onSubmitPinnedList(@NonNull PagedList<Conversation> pinnedConversations) {
|
||||
defaultAdapter.submitPinnedList(pinnedConversations);
|
||||
}
|
||||
|
||||
private void onSubmitUnpinnedList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||
defaultAdapter.submitUnpinnedList(conversationList.getConversations());
|
||||
defaultAdapter.updateArchived(conversationList.getArchivedCount());
|
||||
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||
defaultAdapter.submitList(conversationList.getConversations());
|
||||
|
||||
onPostSubmitList();
|
||||
}
|
||||
|
@ -926,7 +919,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
||||
boolean hasUnpinned = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned());
|
||||
boolean canPin = defaultAdapter.getPinnedItemCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
||||
boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS;
|
||||
|
||||
if (hasUnread) {
|
||||
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);
|
||||
|
|
|
@ -31,6 +31,8 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
|||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListViewModel.class);
|
||||
|
@ -38,7 +40,6 @@ class ConversationListViewModel extends ViewModel {
|
|||
private final Application application;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final LiveData<PagedList<Conversation>> pinnedList;
|
||||
private final LiveData<ConversationList> conversationList;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
|
@ -65,7 +66,7 @@ class ConversationListViewModel extends ViewModel {
|
|||
}
|
||||
};
|
||||
|
||||
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, false, isArchived);
|
||||
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
|
||||
PagedList.Config config = new PagedList.Config.Builder()
|
||||
.setPageSize(15)
|
||||
.setInitialLoadSizeHint(30)
|
||||
|
@ -87,36 +88,21 @@ class ConversationListViewModel extends ViewModel {
|
|||
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
|
||||
|
||||
if (isArchived) {
|
||||
updated.postValue(new ConversationList(conversation, 0));
|
||||
updated.postValue(new ConversationList(conversation, 0, 0));
|
||||
} else {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
|
||||
updated.postValue(new ConversationList(conversation, archiveCount));
|
||||
int pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
|
||||
updated.postValue(new ConversationList(conversation, archiveCount, pinnedCount));
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
if (!isArchived) {
|
||||
DataSource.Factory<Integer, Conversation> pinnedFactory = new ConversationListDataSource.Factory(application, invalidator, true, false);
|
||||
|
||||
this.pinnedList = new LivePagedListBuilder<>(pinnedFactory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
|
||||
.setInitialLoadKey(0)
|
||||
.build();
|
||||
} else {
|
||||
this.pinnedList = new MutableLiveData<>();
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<Boolean> hasNoConversations() {
|
||||
return LiveDataUtil.combineLatest(getPinnedConversations(),
|
||||
getConversationList(),
|
||||
(pinned, unpinned) -> pinned.isEmpty() && unpinned.isEmpty());
|
||||
}
|
||||
|
||||
@NonNull LiveData<PagedList<Conversation>> getPinnedConversations() {
|
||||
return pinnedList;
|
||||
return Transformations.map(getConversationList(), ConversationList::isEmpty);
|
||||
}
|
||||
|
||||
@NonNull LiveData<SearchResult> getSearchResult() {
|
||||
|
@ -131,6 +117,10 @@ class ConversationListViewModel extends ViewModel {
|
|||
return conversationList;
|
||||
}
|
||||
|
||||
public int getPinnedCount() {
|
||||
return Objects.requireNonNull(getConversationList().getValue()).pinnedCount;
|
||||
}
|
||||
|
||||
void onVisible() {
|
||||
megaphoneRepository.getNextMegaphone(megaphone::postValue);
|
||||
}
|
||||
|
@ -189,10 +179,12 @@ class ConversationListViewModel extends ViewModel {
|
|||
final static class ConversationList {
|
||||
private final PagedList<Conversation> conversations;
|
||||
private final int archivedCount;
|
||||
private final int pinnedCount;
|
||||
|
||||
ConversationList(PagedList<Conversation> conversations, int archivedCount) {
|
||||
ConversationList(PagedList<Conversation> conversations, int archivedCount, int pinnedCount) {
|
||||
this.conversations = conversations;
|
||||
this.archivedCount = archivedCount;
|
||||
this.pinnedCount = pinnedCount;
|
||||
}
|
||||
|
||||
PagedList<Conversation> getConversations() {
|
||||
|
@ -203,6 +195,10 @@ class ConversationListViewModel extends ViewModel {
|
|||
return archivedCount;
|
||||
}
|
||||
|
||||
public int getPinnedCount() {
|
||||
return pinnedCount;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return conversations.isEmpty() && archivedCount == 0;
|
||||
}
|
||||
|
|
|
@ -6,15 +6,25 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|||
|
||||
public class Conversation {
|
||||
private final ThreadRecord threadRecord;
|
||||
private final Type type;
|
||||
|
||||
public Conversation(@NonNull ThreadRecord threadRecord) {
|
||||
this.threadRecord = threadRecord;
|
||||
if (this.threadRecord.getThreadId() < 0) {
|
||||
type = Type.valueOf(this.threadRecord.getBody());
|
||||
} else {
|
||||
type = Type.THREAD;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull ThreadRecord getThreadRecord() {
|
||||
return threadRecord;
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -27,4 +37,11 @@ public class Conversation {
|
|||
public int hashCode() {
|
||||
return threadRecord.hashCode();
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
THREAD,
|
||||
PINNED_HEADER,
|
||||
UNPINNED_HEADER,
|
||||
ARCHIVED_FOOTER
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package org.thoughtcrime.securesms.conversationlist.model;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
|
||||
public class ConversationReader extends ThreadDatabase.StaticReader {
|
||||
|
||||
public static final String[] HEADER_COLUMN = {"header"};
|
||||
public static final String[] ARCHIVED_COLUMNS = {"header", "count"};
|
||||
public static final String[] PINNED_HEADER = {Conversation.Type.PINNED_HEADER.toString()};
|
||||
public static final String[] UNPINNED_HEADER = {Conversation.Type.UNPINNED_HEADER.toString()};
|
||||
|
||||
private final Cursor cursor;
|
||||
|
||||
public ConversationReader(@NonNull Cursor cursor) {
|
||||
super(cursor, ApplicationDependencies.getApplication());
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public static String[] createArchivedFooterRow(int archivedCount) {
|
||||
return new String[]{Conversation.Type.ARCHIVED_FOOTER.toString(), String.valueOf(archivedCount)};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThreadRecord getCurrent() {
|
||||
if (cursor.getColumnIndex(HEADER_COLUMN[0]) == -1) {
|
||||
return super.getCurrent();
|
||||
} else {
|
||||
return buildThreadRecordForHeader();
|
||||
}
|
||||
}
|
||||
|
||||
private ThreadRecord buildThreadRecordForHeader() {
|
||||
Conversation.Type type = Conversation.Type.valueOf(CursorUtil.requireString(cursor, HEADER_COLUMN[0]));
|
||||
int count = 0;
|
||||
if (type == Conversation.Type.ARCHIVED_FOOTER) {
|
||||
count = CursorUtil.requireInt(cursor, ARCHIVED_COLUMNS[1]);
|
||||
}
|
||||
return new ThreadRecord.Builder(-(100 + type.ordinal()))
|
||||
.setBody(type.toString())
|
||||
.setDate(100)
|
||||
.setRecipient(Recipient.UNKNOWN)
|
||||
.setCount(count)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -606,7 +606,7 @@ public final class GroupDatabase extends Database {
|
|||
}
|
||||
|
||||
public @Nullable GroupRecord getCurrent() {
|
||||
if (cursor == null || cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)) == null) {
|
||||
if (cursor == null || cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)) == null || cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -143,6 +143,8 @@ public class ThreadDatabase extends Database {
|
|||
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
|
||||
.toList();
|
||||
|
||||
private static final String ORDER_BY_DEFAULT = TABLE_NAME + "." + DATE + " DESC";
|
||||
|
||||
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
@ -542,7 +544,12 @@ public class ThreadDatabase extends Database {
|
|||
String query = RECIPIENT_ID + " = ?";
|
||||
|
||||
for (Map.Entry<RecipientId, Boolean> entry : status.entrySet()) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
||||
if (entry.getValue()) {
|
||||
values.put(PINNED, "0");
|
||||
}
|
||||
|
||||
values.put(ARCHIVED, entry.getValue() ? "1" : "0");
|
||||
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
|
||||
}
|
||||
|
@ -584,14 +591,6 @@ public class ThreadDatabase extends Database {
|
|||
return positions;
|
||||
}
|
||||
|
||||
public Cursor getPinnedConversationList(long offset, long limit) {
|
||||
return getUnarchivedConversationList("1", offset, limit);
|
||||
}
|
||||
|
||||
public Cursor getUnpinnedConversationList(long offset, long limit) {
|
||||
return getUnarchivedConversationList("0", offset, limit);
|
||||
}
|
||||
|
||||
public Cursor getArchivedConversationList(long offset, long limit) {
|
||||
return getConversationList("1", offset, limit);
|
||||
}
|
||||
|
@ -600,10 +599,10 @@ public class ThreadDatabase extends Database {
|
|||
return getConversationList(archived, 0, 0);
|
||||
}
|
||||
|
||||
private Cursor getUnarchivedConversationList(@NonNull String pinned, long offset, long limit) {
|
||||
public Cursor getUnarchivedConversationList(boolean pinned, long offset, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = createQuery(ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?", offset, limit);
|
||||
Cursor cursor = db.rawQuery(query, new String[]{pinned});
|
||||
Cursor cursor = db.rawQuery(query, new String[]{pinned ? "1" : "0"});
|
||||
|
||||
setNotifyConversationListListeners(cursor);
|
||||
|
||||
|
@ -620,14 +619,6 @@ public class ThreadDatabase extends Database {
|
|||
return cursor;
|
||||
}
|
||||
|
||||
public int getPinnedConversationListCount() {
|
||||
return getUnarchivedConversationListCount(true);
|
||||
}
|
||||
|
||||
public int getUnpinnedConversationListCount() {
|
||||
return getUnarchivedConversationListCount(false);
|
||||
}
|
||||
|
||||
public int getArchivedConversationListCount() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] columns = new String[] { "COUNT(*)" };
|
||||
|
@ -643,13 +634,26 @@ public class ThreadDatabase extends Database {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private int getUnarchivedConversationListCount(boolean pinned) {
|
||||
public int getPinnedConversationListCount() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] columns = new String[] { "COUNT(*)" };
|
||||
String query = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0 AND " + PINNED + " = ?";
|
||||
String[] args = new String[] { pinned ? "1" : "0" };
|
||||
String query = ARCHIVED + " = 0 AND " + PINNED + " = 1 AND " + MESSAGE_COUNT + " != 0";
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null)) {
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getUnarchivedConversationListCount() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] columns = new String[] { "COUNT(*)" };
|
||||
String query = ARCHIVED + " = 0 AND " + MESSAGE_COUNT + " != 0";
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
|
@ -686,6 +690,7 @@ public class ThreadDatabase extends Database {
|
|||
public void archiveConversation(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(PINNED, 0);
|
||||
contentValues.put(ARCHIVED, 1);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||
|
@ -1110,12 +1115,20 @@ public class ThreadDatabase extends Database {
|
|||
public static final int INBOX_ZERO = 4;
|
||||
}
|
||||
|
||||
public class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
||||
public class Reader extends StaticReader {
|
||||
public Reader(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
super(cursor, context);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StaticReader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final Context context;
|
||||
|
||||
public StaticReader(Cursor cursor, Context context) {
|
||||
this.cursor = cursor;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ThreadRecord getNext() {
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
@PowerMockIgnore({ "org.powermock.*", "org.mockito.*", "org.robolectric.*", "android.*", "androidx.*" })
|
||||
@PrepareForTest({ApplicationDependencies.class, DatabaseFactory.class, ThreadDatabase.class})
|
||||
public class UnarchivedConversationListDataSourceTest {
|
||||
|
||||
@Rule
|
||||
public PowerMockRule rule = new PowerMockRule();
|
||||
|
||||
private ConversationListDataSource.UnarchivedConversationListDataSource testSubject;
|
||||
|
||||
private ThreadDatabase threadDatabase;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mockStatic(ApplicationDependencies.class);
|
||||
mockStatic(DatabaseFactory.class);
|
||||
|
||||
final Context context = mock(Context.class);
|
||||
final ContentResolver contentResolver = mock(ContentResolver.class);
|
||||
threadDatabase = mock(ThreadDatabase.class);
|
||||
|
||||
when(DatabaseFactory.getThreadDatabase(any())).thenReturn(threadDatabase);
|
||||
when(context.getContentResolver()).thenReturn(contentResolver);
|
||||
|
||||
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(context, mock(Invalidator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNoConversations_whenIGetTotalCount_thenIExpectZero() {
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
||||
// THEN
|
||||
assertEquals(0, result);
|
||||
assertEquals(0, testSubject.getHeaderOffset());
|
||||
assertFalse(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenArchivedConversations_whenIGetTotalCount_thenIExpectOne() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
||||
// THEN
|
||||
assertEquals(1, result);
|
||||
assertEquals(0, testSubject.getHeaderOffset());
|
||||
assertFalse(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertTrue(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSinglePinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectThree() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
||||
// THEN
|
||||
assertEquals(3, result);
|
||||
assertEquals(1, testSubject.getHeaderOffset());
|
||||
assertTrue(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertTrue(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSingleUnpinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
||||
// THEN
|
||||
assertEquals(2, result);
|
||||
assertEquals(0, testSubject.getHeaderOffset());
|
||||
assertFalse(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertTrue(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSinglePinnedAndSingleUnpinned_whenIGetTotalCount_thenIExpectFour() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(2);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
||||
// THEN
|
||||
assertEquals(4, result);
|
||||
assertEquals(2, testSubject.getHeaderOffset());
|
||||
assertTrue(testSubject.hasPinnedHeader());
|
||||
assertTrue(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenNoConversations_whenIGetCursor_thenIExpectAnEmptyCursor() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 0);
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||
assertEquals(0, cursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenArchivedConversations_whenIGetCursor_thenIExpectOne() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 0);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||
assertEquals(1, cursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSinglePinnedAndArchivedConversations_whenIGetCursor_thenIExpectThree() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(1, 0);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 99);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 98);
|
||||
assertEquals(3, cursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSingleUnpinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||
assertEquals(2, cursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSinglePinnedAndSingleUnpinned_whenIGetCursor_thenIExpectFour() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(1, 1);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(2);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 99);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 97);
|
||||
assertEquals(4, cursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenLoadingSecondPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 100);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(4);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(104);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(50, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 50, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 44, 100);
|
||||
assertEquals(100, cursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenHasArchivedAndLoadingLastPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 99);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(4);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(103);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(50, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 50, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 44, 100);
|
||||
assertEquals(100, cursor.getCount());
|
||||
|
||||
cursor.moveToLast();
|
||||
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]));
|
||||
}
|
||||
|
||||
|
||||
private void setupThreadDatabaseCursors(int pinned, int unpinned) {
|
||||
Cursor pinnedCursor = mock(Cursor.class);
|
||||
when(pinnedCursor.getCount()).thenReturn(pinned);
|
||||
|
||||
Cursor unpinnedCursor = mock(Cursor.class);
|
||||
when(unpinnedCursor.getCount()).thenReturn(unpinned);
|
||||
|
||||
when(threadDatabase.getUnarchivedConversationList(eq(true), anyLong(), anyLong())).thenReturn(pinnedCursor);
|
||||
when(threadDatabase.getUnarchivedConversationList(eq(false), anyLong(), anyLong())).thenReturn(unpinnedCursor);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue