kopia lustrzana https://github.com/ryukoposting/Signal-Android
791 wiersze
28 KiB
Java
791 wiersze
28 KiB
Java
/*
|
|
* Copyright (C) 2011 Whisper Systems
|
|
*
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms.conversation;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.ColorInt;
|
|
import androidx.annotation.DrawableRes;
|
|
import androidx.annotation.LayoutRes;
|
|
import androidx.annotation.MainThread;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.core.content.ContextCompat;
|
|
import androidx.lifecycle.LifecycleOwner;
|
|
import androidx.recyclerview.widget.DiffUtil;
|
|
import androidx.recyclerview.widget.ListAdapter;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
import com.google.android.exoplayer2.MediaItem;
|
|
|
|
import org.signal.core.util.logging.Log;
|
|
import org.signal.paging.PagingController;
|
|
import org.thoughtcrime.securesms.BindableConversationItem;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
|
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.util.CachedInflater;
|
|
import org.thoughtcrime.securesms.util.DateUtils;
|
|
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
|
import org.thoughtcrime.securesms.util.Projection;
|
|
import org.thoughtcrime.securesms.util.ProjectionList;
|
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.Calendar;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Adapter that renders a conversation.
|
|
*
|
|
* Important spacial thing to keep in mind: The adapter is intended to be shown on a reversed layout
|
|
* manager, so position 0 is at the bottom of the screen. That's why the "header" is at the bottom,
|
|
* the "footer" is at the top, and we refer to the "next" record as having a lower index.
|
|
*/
|
|
public class ConversationAdapter
|
|
extends ListAdapter<ConversationMessage, RecyclerView.ViewHolder>
|
|
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
|
|
{
|
|
|
|
private static final String TAG = Log.tag(ConversationAdapter.class);
|
|
|
|
public static final int HEADER_TYPE_POPOVER_DATE = 1;
|
|
public static final int HEADER_TYPE_INLINE_DATE = 2;
|
|
public static final int HEADER_TYPE_LAST_SEEN = 3;
|
|
|
|
private static final int MESSAGE_TYPE_OUTGOING_MULTIMEDIA = 0;
|
|
private static final int MESSAGE_TYPE_OUTGOING_TEXT = 1;
|
|
private static final int MESSAGE_TYPE_INCOMING_MULTIMEDIA = 2;
|
|
private static final int MESSAGE_TYPE_INCOMING_TEXT = 3;
|
|
private static final int MESSAGE_TYPE_UPDATE = 4;
|
|
private static final int MESSAGE_TYPE_HEADER = 5;
|
|
public static final int MESSAGE_TYPE_FOOTER = 6;
|
|
private static final int MESSAGE_TYPE_PLACEHOLDER = 7;
|
|
|
|
private static final int PAYLOAD_TIMESTAMP = 0;
|
|
public static final int PAYLOAD_NAME_COLORS = 1;
|
|
public static final int PAYLOAD_SELECTED = 2;
|
|
|
|
private final ItemClickListener clickListener;
|
|
private final Context context;
|
|
private final LifecycleOwner lifecycleOwner;
|
|
private final GlideRequests glideRequests;
|
|
private final Locale locale;
|
|
private final Recipient recipient;
|
|
|
|
private final Set<MultiselectPart> selected;
|
|
private final Calendar calendar;
|
|
private final MessageDigest digest;
|
|
|
|
private String searchQuery;
|
|
private ConversationMessage recordToPulse;
|
|
private View typingView;
|
|
private View footerView;
|
|
private PagingController pagingController;
|
|
private boolean hasWallpaper;
|
|
private boolean isMessageRequestAccepted;
|
|
private ConversationMessage inlineContent;
|
|
private Colorizer colorizer;
|
|
private boolean isTypingViewEnabled;
|
|
private boolean condensedMode;
|
|
|
|
public ConversationAdapter(@NonNull Context context,
|
|
@NonNull LifecycleOwner lifecycleOwner,
|
|
@NonNull GlideRequests glideRequests,
|
|
@NonNull Locale locale,
|
|
@Nullable ItemClickListener clickListener,
|
|
@NonNull Recipient recipient,
|
|
@NonNull Colorizer colorizer)
|
|
{
|
|
super(new DiffUtil.ItemCallback<ConversationMessage>() {
|
|
@Override
|
|
public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
|
|
return oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId();
|
|
}
|
|
|
|
@Override
|
|
public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.lifecycleOwner = lifecycleOwner;
|
|
this.context = context;
|
|
|
|
this.glideRequests = glideRequests;
|
|
this.locale = locale;
|
|
this.clickListener = clickListener;
|
|
this.recipient = recipient;
|
|
this.selected = new HashSet<>();
|
|
this.calendar = Calendar.getInstance();
|
|
this.digest = getMessageDigestOrThrow();
|
|
this.hasWallpaper = recipient.hasWallpaper();
|
|
this.isMessageRequestAccepted = true;
|
|
this.colorizer = colorizer;
|
|
}
|
|
|
|
@Override
|
|
public int getItemViewType(int position) {
|
|
if (isTypingViewEnabled() && position == 0) {
|
|
return MESSAGE_TYPE_HEADER;
|
|
}
|
|
|
|
if (hasFooter() && position == getItemCount() - 1) {
|
|
return MESSAGE_TYPE_FOOTER;
|
|
}
|
|
|
|
ConversationMessage conversationMessage = getItem(position);
|
|
MessageRecord messageRecord = (conversationMessage != null) ? conversationMessage.getMessageRecord() : null;
|
|
|
|
if (messageRecord == null) {
|
|
return MESSAGE_TYPE_PLACEHOLDER;
|
|
} else if (messageRecord.isUpdate()) {
|
|
return MESSAGE_TYPE_UPDATE;
|
|
} else if (messageRecord.isOutgoing()) {
|
|
return MessageRecordUtil.isTextOnly(messageRecord, context) && !conversationMessage.hasBeenQuoted() ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA;
|
|
} else {
|
|
return MessageRecordUtil.isTextOnly(messageRecord, context) && !conversationMessage.hasBeenQuoted() ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA;
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
@Override
|
|
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
switch (viewType) {
|
|
case MESSAGE_TYPE_INCOMING_TEXT:
|
|
case MESSAGE_TYPE_INCOMING_MULTIMEDIA:
|
|
case MESSAGE_TYPE_OUTGOING_TEXT:
|
|
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
|
case MESSAGE_TYPE_UPDATE:
|
|
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
|
BindableConversationItem bindable = (BindableConversationItem) itemView;
|
|
|
|
itemView.setOnClickListener((v) -> {
|
|
if (clickListener != null) {
|
|
clickListener.onItemClick(bindable.getMultiselectPartForLatestTouch());
|
|
}
|
|
});
|
|
|
|
itemView.setOnLongClickListener((v) -> {
|
|
if (clickListener != null) {
|
|
clickListener.onItemLongClick(itemView, bindable.getMultiselectPartForLatestTouch());
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
bindable.setEventListener(clickListener);
|
|
|
|
return new ConversationViewHolder(itemView);
|
|
case MESSAGE_TYPE_PLACEHOLDER:
|
|
View v = new FrameLayout(parent.getContext());
|
|
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
|
|
return new PlaceholderViewHolder(v);
|
|
case MESSAGE_TYPE_HEADER:
|
|
return new HeaderViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
|
|
case MESSAGE_TYPE_FOOTER:
|
|
return new FooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
|
|
default:
|
|
throw new IllegalStateException("Cannot create viewholder for type: " + viewType);
|
|
}
|
|
}
|
|
|
|
private boolean containsValidPayload(@NonNull List<Object> payloads) {
|
|
return payloads.contains(PAYLOAD_TIMESTAMP) || payloads.contains(PAYLOAD_NAME_COLORS) || payloads.contains(PAYLOAD_SELECTED);
|
|
}
|
|
|
|
@Override
|
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
|
if (containsValidPayload(payloads)) {
|
|
switch (getItemViewType(position)) {
|
|
case MESSAGE_TYPE_INCOMING_TEXT:
|
|
case MESSAGE_TYPE_INCOMING_MULTIMEDIA:
|
|
case MESSAGE_TYPE_OUTGOING_TEXT:
|
|
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
|
case MESSAGE_TYPE_UPDATE:
|
|
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
|
|
if (payloads.contains(PAYLOAD_TIMESTAMP)) {
|
|
conversationViewHolder.getBindable().updateTimestamps();
|
|
}
|
|
|
|
if (payloads.contains(PAYLOAD_NAME_COLORS)) {
|
|
conversationViewHolder.getBindable().updateContactNameColor();
|
|
}
|
|
|
|
if (payloads.contains(PAYLOAD_SELECTED)) {
|
|
conversationViewHolder.getBindable().updateSelectedState();
|
|
}
|
|
|
|
default:
|
|
return;
|
|
}
|
|
} else {
|
|
super.onBindViewHolder(holder, position, payloads);
|
|
}
|
|
}
|
|
|
|
public void setCondensedMode(boolean condensedMode) {
|
|
this.condensedMode = condensedMode;
|
|
notifyDataSetChanged();
|
|
}
|
|
|
|
@Override
|
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
|
switch (getItemViewType(position)) {
|
|
case MESSAGE_TYPE_INCOMING_TEXT:
|
|
case MESSAGE_TYPE_INCOMING_MULTIMEDIA:
|
|
case MESSAGE_TYPE_OUTGOING_TEXT:
|
|
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
|
case MESSAGE_TYPE_UPDATE:
|
|
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
|
|
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
|
int adapterPosition = holder.getAdapterPosition();
|
|
|
|
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
|
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
|
|
|
conversationViewHolder.getBindable().bind(lifecycleOwner,
|
|
conversationMessage,
|
|
Optional.ofNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
|
|
Optional.ofNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
|
|
glideRequests,
|
|
locale,
|
|
selected,
|
|
recipient,
|
|
searchQuery,
|
|
conversationMessage == recordToPulse,
|
|
hasWallpaper && !condensedMode,
|
|
isMessageRequestAccepted,
|
|
conversationMessage == inlineContent,
|
|
colorizer,
|
|
condensedMode);
|
|
|
|
if (conversationMessage == recordToPulse) {
|
|
recordToPulse = null;
|
|
}
|
|
break;
|
|
case MESSAGE_TYPE_HEADER:
|
|
((HeaderViewHolder) holder).bind(typingView);
|
|
break;
|
|
case MESSAGE_TYPE_FOOTER:
|
|
((HeaderFooterViewHolder) holder).bind(footerView);
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getItemCount() {
|
|
boolean hasFooter = footerView != null;
|
|
return super.getItemCount() + (isTypingViewEnabled ? 1 : 0) + (hasFooter ? 1 : 0);
|
|
}
|
|
|
|
@Override
|
|
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
|
if (holder instanceof ConversationViewHolder) {
|
|
((ConversationViewHolder) holder).getBindable().unbind();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long getHeaderId(int position) {
|
|
if (isHeaderPosition(position)) return -1;
|
|
if (isFooterPosition(position)) return -1;
|
|
if (position >= getItemCount()) return -1;
|
|
if (position < 0) return -1;
|
|
|
|
ConversationMessage conversationMessage = getItem(position);
|
|
|
|
if (conversationMessage == null) return -1;
|
|
|
|
calendar.setTimeInMillis(conversationMessage.getMessageRecord().getDateSent());
|
|
return calendar.get(Calendar.YEAR) * 1000L + calendar.get(Calendar.DAY_OF_YEAR);
|
|
}
|
|
|
|
@Override
|
|
public StickyHeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
|
|
return new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_header, parent, false));
|
|
}
|
|
|
|
@Override
|
|
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position, int type) {
|
|
Context context = viewHolder.itemView.getContext();
|
|
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
|
|
|
|
viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateSent()));
|
|
|
|
if (type == HEADER_TYPE_POPOVER_DATE) {
|
|
if (hasWallpaper) {
|
|
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18);
|
|
} else {
|
|
viewHolder.setBackgroundRes(R.drawable.sticky_date_header_background);
|
|
}
|
|
} else if (type == HEADER_TYPE_INLINE_DATE) {
|
|
if (hasWallpaper) {
|
|
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18);
|
|
} else {
|
|
viewHolder.clearBackground();
|
|
}
|
|
}
|
|
|
|
if (hasWallpaper && ThemeUtil.isDarkTheme(context)) {
|
|
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_colorNeutralInverse));
|
|
} else {
|
|
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant));
|
|
}
|
|
}
|
|
|
|
public @Nullable ConversationMessage getItem(int position) {
|
|
position = isTypingViewEnabled() ? position - 1 : position;
|
|
|
|
if (position < 0) {
|
|
return null;
|
|
} else {
|
|
if (pagingController != null) {
|
|
pagingController.onDataNeededAroundIndex(position);
|
|
}
|
|
|
|
if (position < super.getItemCount()) {
|
|
return super.getItem(position);
|
|
} else {
|
|
Log.d(TAG, "Could not access corrected position " + position + " as it is out of bounds.");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setPagingController(@Nullable PagingController pagingController) {
|
|
this.pagingController = pagingController;
|
|
}
|
|
|
|
public boolean isForRecipientId(@NonNull RecipientId recipientId) {
|
|
return recipient.getId().equals(recipientId);
|
|
}
|
|
|
|
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
|
int messagePosition = isTypingViewEnabled ? position - 1 : position;
|
|
int count = messagePosition + 1;
|
|
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, count, count));
|
|
|
|
if (hasWallpaper) {
|
|
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18);
|
|
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.transparent_black_80));
|
|
} else {
|
|
viewHolder.clearBackground();
|
|
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.core_grey_45));
|
|
}
|
|
}
|
|
|
|
boolean hasNoConversationMessages() {
|
|
return super.getItemCount() == 0;
|
|
}
|
|
|
|
/**
|
|
* The presence of a header may throw off the position you'd like to jump to. This will return
|
|
* an adjusted message position based on adapter state.
|
|
*/
|
|
@MainThread
|
|
int getAdapterPositionForMessagePosition(int messagePosition) {
|
|
return isTypingViewEnabled() ? messagePosition + 1 : messagePosition;
|
|
}
|
|
|
|
/**
|
|
* Finds the received timestamp for the item at the requested adapter position. Will return 0 if
|
|
* the position doesn't refer to an incoming message.
|
|
*/
|
|
@MainThread
|
|
long getReceivedTimestamp(int position) {
|
|
if (isHeaderPosition(position)) return 0;
|
|
if (isFooterPosition(position)) return 0;
|
|
if (position >= getItemCount()) return 0;
|
|
if (position < 0) return 0;
|
|
|
|
ConversationMessage conversationMessage = getItem(position);
|
|
|
|
if (conversationMessage == null || conversationMessage.getMessageRecord().isOutgoing()) {
|
|
return 0;
|
|
} else {
|
|
return conversationMessage.getMessageRecord().getDateReceived();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the view the appears at the top of the list (because the list is reversed).
|
|
*/
|
|
void setFooterView(@Nullable View view) {
|
|
boolean hadFooter = hasFooter();
|
|
|
|
this.footerView = view;
|
|
|
|
if (view == null && hadFooter) {
|
|
notifyItemRemoved(getItemCount());
|
|
} else if (view != null && hadFooter) {
|
|
notifyItemChanged(getItemCount() - 1);
|
|
} else if (view != null) {
|
|
notifyItemInserted(getItemCount() - 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the view that appears at the bottom of the list (because the list is reversed).
|
|
*/
|
|
void setTypingView(@NonNull View view) {
|
|
this.typingView = view;
|
|
}
|
|
|
|
void setTypingViewEnabled(boolean isTypingViewEnabled) {
|
|
if (typingView == null && isTypingViewEnabled) {
|
|
throw new IllegalStateException("Must set header before enabling.");
|
|
}
|
|
|
|
if (this.isTypingViewEnabled && !isTypingViewEnabled) {
|
|
this.isTypingViewEnabled = false;
|
|
notifyItemRemoved(0);
|
|
} else if (this.isTypingViewEnabled) {
|
|
notifyItemChanged(0);
|
|
} else if (isTypingViewEnabled) {
|
|
this.isTypingViewEnabled = true;
|
|
notifyItemInserted(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Momentarily highlights a mention at the requested position.
|
|
*/
|
|
void pulseAtPosition(int position) {
|
|
if (position >= 0 && position < getItemCount()) {
|
|
int correctedPosition = isHeaderPosition(position) ? position + 1 : position;
|
|
|
|
recordToPulse = getItem(correctedPosition);
|
|
notifyItemChanged(correctedPosition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Conversation search query updated. Allows rendering of text highlighting.
|
|
*/
|
|
void onSearchQueryUpdated(String query) {
|
|
if (!Objects.equals(query, this.searchQuery)) {
|
|
this.searchQuery = query;
|
|
notifyDataSetChanged();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lets the adapter know that the wallpaper state has changed.
|
|
* @return True if the internal wallpaper state changed, otherwise false.
|
|
*/
|
|
boolean onHasWallpaperChanged(boolean hasWallpaper) {
|
|
if (this.hasWallpaper != hasWallpaper) {
|
|
Log.d(TAG, "Resetting adapter due to wallpaper change.");
|
|
this.hasWallpaper = hasWallpaper;
|
|
notifyDataSetChanged();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns set of records that are selected in multi-select mode.
|
|
*/
|
|
public Set<MultiselectPart> getSelectedItems() {
|
|
return new HashSet<>(selected);
|
|
}
|
|
|
|
public void removeFromSelection(@NonNull Set<MultiselectPart> parts) {
|
|
selected.removeAll(parts);
|
|
updateSelected();
|
|
}
|
|
|
|
/**
|
|
* Clears all selected records from multi-select mode.
|
|
*/
|
|
void clearSelection() {
|
|
selected.clear();
|
|
updateSelected();
|
|
}
|
|
|
|
/**
|
|
* Toggles the selected state of a record in multi-select mode.
|
|
*/
|
|
void toggleSelection(MultiselectPart multiselectPart) {
|
|
if (selected.contains(multiselectPart)) {
|
|
selected.remove(multiselectPart);
|
|
} else {
|
|
selected.add(multiselectPart);
|
|
}
|
|
updateSelected();
|
|
}
|
|
|
|
private void updateSelected() {
|
|
notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED);
|
|
}
|
|
|
|
/**
|
|
* Provided a pool, this will initialize it with view counts that make sense.
|
|
*/
|
|
@MainThread
|
|
static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) {
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 25);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_MULTIMEDIA, 15);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 25);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_MULTIMEDIA, 15);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_PLACEHOLDER, 15);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_HEADER, 1);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_FOOTER, 1);
|
|
pool.setMaxRecycledViews(MESSAGE_TYPE_UPDATE, 5);
|
|
}
|
|
|
|
public boolean isTypingViewEnabled() {
|
|
return isTypingViewEnabled;
|
|
}
|
|
|
|
public boolean hasFooter() {
|
|
return footerView != null;
|
|
}
|
|
|
|
private boolean isHeaderPosition(int position) {
|
|
return isTypingViewEnabled() && position == 0;
|
|
}
|
|
|
|
private boolean isFooterPosition(int position) {
|
|
return hasFooter() && position == (getItemCount() - 1);
|
|
}
|
|
|
|
private static @LayoutRes int getLayoutForViewType(int viewType) {
|
|
switch (viewType) {
|
|
case MESSAGE_TYPE_OUTGOING_TEXT: return R.layout.conversation_item_sent_text_only;
|
|
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: return R.layout.conversation_item_sent_multimedia;
|
|
case MESSAGE_TYPE_INCOMING_TEXT: return R.layout.conversation_item_received_text_only;
|
|
case MESSAGE_TYPE_INCOMING_MULTIMEDIA: return R.layout.conversation_item_received_multimedia;
|
|
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
|
|
default: throw new IllegalArgumentException("Unknown type!");
|
|
}
|
|
}
|
|
|
|
private static MessageDigest getMessageDigestOrThrow() {
|
|
try {
|
|
return MessageDigest.getInstance("SHA1");
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) {
|
|
try {
|
|
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
|
|
} catch (IndexOutOfBoundsException e) {
|
|
Log.w(TAG, "Race condition changed size of conversation", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void setMessageRequestAccepted(boolean messageRequestAccepted) {
|
|
if (this.isMessageRequestAccepted != messageRequestAccepted) {
|
|
this.isMessageRequestAccepted = messageRequestAccepted;
|
|
notifyDataSetChanged();
|
|
}
|
|
}
|
|
|
|
public void playInlineContent(@Nullable ConversationMessage conversationMessage) {
|
|
if (this.inlineContent != conversationMessage) {
|
|
this.inlineContent = conversationMessage;
|
|
notifyDataSetChanged();
|
|
}
|
|
}
|
|
|
|
public void updateTimestamps() {
|
|
notifyItemRangeChanged(0, getItemCount(), PAYLOAD_TIMESTAMP);
|
|
}
|
|
|
|
final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
|
|
public ConversationViewHolder(final @NonNull View itemView) {
|
|
super(itemView);
|
|
}
|
|
|
|
public BindableConversationItem getBindable() {
|
|
return (BindableConversationItem) itemView;
|
|
}
|
|
|
|
@Override
|
|
public void showProjectionArea() {
|
|
getBindable().showProjectionArea();
|
|
}
|
|
|
|
@Override
|
|
public void hideProjectionArea() {
|
|
getBindable().hideProjectionArea();
|
|
}
|
|
|
|
@Override
|
|
public @Nullable MediaItem getMediaItem() {
|
|
return getBindable().getMediaItem();
|
|
}
|
|
|
|
@Override
|
|
public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
|
|
return getBindable().getPlaybackPolicyEnforcer();
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
|
|
return getBindable().getGiphyMp4PlayableProjection(recyclerView);
|
|
}
|
|
|
|
@Override
|
|
public boolean canPlayContent() {
|
|
return getBindable().canPlayContent();
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldProjectContent() {
|
|
return getBindable().shouldProjectContent();
|
|
}
|
|
|
|
@Override
|
|
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
|
return getBindable().getColorizerProjections(coordinateRoot);
|
|
}
|
|
}
|
|
|
|
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
|
|
TextView textView;
|
|
View divider;
|
|
|
|
StickyHeaderViewHolder(View itemView) {
|
|
super(itemView);
|
|
textView = itemView.findViewById(R.id.text);
|
|
divider = itemView.findViewById(R.id.last_seen_divider);
|
|
}
|
|
|
|
StickyHeaderViewHolder(TextView textView) {
|
|
super(textView);
|
|
this.textView = textView;
|
|
}
|
|
|
|
public void setText(CharSequence text) {
|
|
textView.setText(text);
|
|
}
|
|
|
|
public void setTextColor(@ColorInt int color) {
|
|
textView.setTextColor(color);
|
|
}
|
|
|
|
public void setBackgroundRes(@DrawableRes int resId) {
|
|
textView.setBackgroundResource(resId);
|
|
}
|
|
|
|
public void setDividerColor(@ColorInt int color) {
|
|
if (divider != null) {
|
|
divider.setBackgroundColor(color);
|
|
}
|
|
}
|
|
|
|
public void clearBackground() {
|
|
textView.setBackground(null);
|
|
}
|
|
}
|
|
|
|
public abstract static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
|
|
|
|
private ViewGroup container;
|
|
|
|
HeaderFooterViewHolder(@NonNull View itemView) {
|
|
super(itemView);
|
|
this.container = (ViewGroup) itemView;
|
|
}
|
|
|
|
void bind(@Nullable View view) {
|
|
unbind();
|
|
|
|
if (view != null) {
|
|
removeViewFromParent(view);
|
|
container.addView(view);
|
|
}
|
|
}
|
|
|
|
void unbind() {
|
|
container.removeAllViews();
|
|
}
|
|
|
|
private void removeViewFromParent(@NonNull View view) {
|
|
if (view.getParent() != null) {
|
|
((ViewGroup) view.getParent()).removeView(view);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class FooterViewHolder extends HeaderFooterViewHolder {
|
|
FooterViewHolder(@NonNull View itemView) {
|
|
super(itemView);
|
|
setPaddingTop();
|
|
}
|
|
|
|
@Override
|
|
void bind(@Nullable View view) {
|
|
super.bind(view);
|
|
setPaddingTop();
|
|
}
|
|
|
|
private void setPaddingTop() {
|
|
if (Build.VERSION.SDK_INT <= 23) {
|
|
int addToPadding = ViewUtil.getStatusBarHeight(itemView) + (int) ThemeUtil.getThemedDimen(itemView.getContext(), android.R.attr.actionBarSize);
|
|
ViewUtil.setPaddingTop(itemView, itemView.getPaddingTop() + addToPadding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class HeaderViewHolder extends HeaderFooterViewHolder {
|
|
HeaderViewHolder(@NonNull View itemView) {
|
|
super(itemView);
|
|
}
|
|
}
|
|
|
|
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
|
PlaceholderViewHolder(@NonNull View itemView) {
|
|
super(itemView);
|
|
}
|
|
}
|
|
|
|
public interface ItemClickListener extends BindableConversationItem.EventListener {
|
|
void onItemClick(MultiselectPart item);
|
|
void onItemLongClick(View itemView, MultiselectPart item);
|
|
}
|
|
}
|