Fix conversation list memory leak.

fork-5.53.8
Cody Henthorne 2022-05-24 16:09:55 -04:00 zatwierdzone przez Alex Hart
rodzic 2a91c67c51
commit 5dba1067d6
7 zmienionych plików z 119 dodań i 142 usunięć

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
@ -11,7 +12,8 @@ import java.util.Set;
public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull ThreadRecord thread,
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations);

Wyświetl plik

@ -8,6 +8,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
@ -39,6 +40,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
SELECTION
}
private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests;
private final OnConversationClickListener onConversationClickListener;
private ConversationSet selectedConversations = new ConversationSet();
@ -46,11 +48,13 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
private PagingController pagingController;
protected ConversationListAdapter(@NonNull GlideRequests glideRequests,
protected ConversationListAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull OnConversationClickListener onConversationClickListener)
{
super(new ConversationDiffCallback());
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
this.onConversationClickListener = onConversationClickListener;
}
@ -127,7 +131,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
ConversationViewHolder casted = (ConversationViewHolder) holder;
Conversation conversation = Objects.requireNonNull(getItem(position));
casted.getConversationListItem().bind(conversation.getThreadRecord(),
casted.getConversationListItem().bind(lifecycleOwner,
conversation.getThreadRecord(),
glideRequests,
Locale.getDefault(),
typingSet,

Wyświetl plik

@ -341,6 +341,30 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}));
}
@Override
public void onDestroyView() {
coordinator = null;
list = null;
searchEmptyState = null;
toolbarShadow = null;
bottomActionBar = null;
reminderView = null;
emptyState = null;
megaphoneContainer = null;
paymentNotificationView = null;
voiceNotePlayerViewStub = null;
fab = null;
cameraFab = null;
snapToTopDataObserver = null;
itemAnimator = null;
activeAdapter = null;
defaultAdapter = null;
searchAdapter = null;
super.onDestroyView();
}
@Override
public void onResume() {
super.onResume();
@ -646,8 +670,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void initializeListAdapters() {
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
defaultAdapter = new ConversationListAdapter(getViewLifecycleOwner(), GlideApp.with(this), this);
searchAdapter = new ConversationListSearchAdapter(getViewLifecycleOwner(), GlideApp.with(this), this, Locale.getDefault());
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false, 0);
setAdapter(defaultAdapter);
@ -1527,7 +1551,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
float dX, float dY, int actionState,
boolean isCurrentlyActive)
{
if (viewHolder.itemView instanceof ConversationListItemInboxZero) return;
float absoluteDx = Math.abs(dX);
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {

Wyświetl plik

@ -36,6 +36,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
@ -69,7 +70,6 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.search.MessageResult;
import org.thoughtcrime.securesms.util.DateUtils;
@ -84,20 +84,17 @@ import java.util.Set;
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
public final class ConversationListItem extends ConstraintLayout
implements RecipientForeverObserver,
BindableConversationListItem,
Unbindable,
Observer<SpannableString>
{
public final class ConversationListItem extends ConstraintLayout implements BindableConversationListItem, Unbindable {
@SuppressWarnings("unused")
private final static String TAG = Log.tag(ConversationListItem.class);
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
private final Rect conversationAvatarTouchDelegateBounds = new Rect();
private final Rect newConversationAvatarTouchDelegateBounds = new Rect();
private final Rect conversationAvatarTouchDelegateBounds = new Rect();
private final Rect newConversationAvatarTouchDelegateBounds = new Rect();
private final Observer<Recipient> recipientObserver = this::onRecipientChanged;
private final Observer<SpannableString> displayBodyObserver = this::onDisplayBodyChanged;
private Set<Long> typingThreads;
private LiveRecipient recipient;
@ -174,7 +171,7 @@ public final class ConversationListItem extends ConstraintLayout
newConversationAvatarTouchDelegateBounds.right = right;
}
newConversationAvatarTouchDelegateBounds.top = top;
newConversationAvatarTouchDelegateBounds.top = top;
newConversationAvatarTouchDelegateBounds.bottom = bottom;
TouchDelegate currentDelegate = getTouchDelegate();
@ -187,25 +184,24 @@ public final class ConversationListItem extends ConstraintLayout
}
@Override
public void bind(@NonNull ThreadRecord thread,
public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations)
{
bindThread(thread, glideRequests, locale, typingThreads, selectedConversations, null);
bindThread(lifecycleOwner, thread, glideRequests, locale, typingThreads, selectedConversations, null);
}
public void bindThread(@NonNull ThreadRecord thread,
public void bindThread(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations,
@Nullable String highlightSubstring)
{
observeRecipient(thread.getRecipient().live());
observeDisplayBody(null);
this.threadId = thread.getThreadId();
this.glideRequests = glideRequests;
this.unreadCount = thread.getUnreadCount();
@ -214,6 +210,9 @@ public final class ConversationListItem extends ConstraintLayout
this.locale = locale;
this.highlightSubstring = highlightSubstring;
observeRecipient(lifecycleOwner, thread.getRecipient().live());
observeDisplayBody(null, null);
if (highlightSubstring != null) {
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
@ -227,7 +226,7 @@ public final class ConversationListItem extends ConstraintLayout
LiveData<SpannableString> displayBody = getThreadDisplayBody(getContext(), thread, glideRequests, thumbSize, thumbTarget);
setSubjectViewText(displayBody.getValue());
observeDisplayBody(displayBody);
observeDisplayBody(lifecycleOwner, displayBody);
if (thread.getDate() > 0) {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
@ -259,19 +258,20 @@ public final class ConversationListItem extends ConstraintLayout
}
}
public void bindContact(@NonNull Recipient contact,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable String highlightSubstring)
public void bindContact(@NonNull LifecycleOwner lifecycleOwner,
@NonNull Recipient contact,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
observeRecipient(contact.live());
observeDisplayBody(null);
setSubjectViewText(null);
this.glideRequests = glideRequests;
this.locale = locale;
this.highlightSubstring = highlightSubstring;
observeRecipient(lifecycleOwner, contact.live());
observeDisplayBody(null, null);
setSubjectViewText(null);
fromView.setText(contact, SearchUtil.getHighlightedSpan(locale, SpanUtil::getMediumBoldSpan, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, contact.getE164().orElse(""), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText("");
@ -285,19 +285,20 @@ public final class ConversationListItem extends ConstraintLayout
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
}
public void bindMessage(@NonNull MessageResult messageResult,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable String highlightSubstring)
public void bindMessage(@NonNull LifecycleOwner lifecycleOwner,
@NonNull MessageResult messageResult,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
observeRecipient(messageResult.getConversationRecipient().live());
observeDisplayBody(null);
setSubjectViewText(null);
this.glideRequests = glideRequests;
this.locale = locale;
this.highlightSubstring = highlightSubstring;
observeRecipient(lifecycleOwner, messageResult.getConversationRecipient().live());
observeDisplayBody(null, null);
setSubjectViewText(null);
fromView.setText(recipient.get(), false);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
@ -314,12 +315,12 @@ public final class ConversationListItem extends ConstraintLayout
@Override
public void unbind() {
if (this.recipient != null) {
observeRecipient(null);
observeRecipient(null, null);
setSelectedConversations(new ConversationSet());
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
}
observeDisplayBody(null);
observeDisplayBody(null, null);
}
@Override
@ -332,7 +333,7 @@ public final class ConversationListItem extends ConstraintLayout
if (recipient != null) {
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
}
if (batchMode && selected) {
checkContainer.setVisibility(VISIBLE);
uncheckedView.setVisibility(GONE);
@ -383,31 +384,31 @@ public final class ConversationListItem extends ConstraintLayout
return lastSeen;
}
private void observeRecipient(@Nullable LiveRecipient newRecipient) {
private void observeRecipient(@Nullable LifecycleOwner lifecycleOwner, @Nullable LiveRecipient newRecipient) {
if (this.recipient != null) {
this.recipient.removeForeverObserver(this);
this.recipient.getLiveData().removeObserver(recipientObserver);
}
this.recipient = newRecipient;
if (this.recipient != null) {
this.recipient.observeForever(this);
if (lifecycleOwner != null && this.recipient != null) {
this.recipient.getLiveData().observe(lifecycleOwner, recipientObserver);
}
}
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
private void observeDisplayBody(@Nullable LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
if (displayBody == null && glideRequests != null) {
glideRequests.clear(thumbTarget);
}
if (this.displayBody != null) {
this.displayBody.removeObserver(this);
this.displayBody.removeObserver(displayBodyObserver);
}
this.displayBody = displayBody;
if (this.displayBody != null) {
this.displayBody.observeForever(this);
if (lifecycleOwner != null && this.displayBody != null) {
this.displayBody.observe(lifecycleOwner, displayBodyObserver);
}
}
@ -424,7 +425,7 @@ public final class ConversationListItem extends ConstraintLayout
if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) {
deliveryStatusIndicator.setNone();
alertView.setFailed();
} else if (!thread.isOutgoing() ||
} else if (!thread.isOutgoing() ||
thread.isOutgoingAudioCall() ||
thread.isOutgoingVideoCall() ||
thread.isVerificationStatusChange())
@ -470,8 +471,7 @@ public final class ConversationListItem extends ConstraintLayout
unreadIndicator.setVisibility(View.VISIBLE);
}
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
private void onRecipientChanged(@NonNull Recipient recipient) {
if (this.recipient == null || !this.recipient.getId().equals(recipient.getId())) {
Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId());
return;
@ -531,7 +531,7 @@ public final class ConversationListItem extends ConstraintLayout
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context)))));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
int seconds = (int)(thread.getExpiresIn() / 1000);
int seconds = (int) (thread.getExpiresIn() / 1000);
if (seconds <= 0) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_messages_disabled), R.drawable.ic_update_timer_disabled_16, defaultTint);
}
@ -649,7 +649,9 @@ public final class ConversationListItem extends ConstraintLayout
return new SpannableString(builder);
}
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
/**
* After a short delay, if the main data hasn't shown yet, then a loading message is displayed.
*/
private static @NonNull LiveData<SpannableString> whileLoadingShow(@NonNull String loading, @NonNull LiveData<SpannableString> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(loading)));
}
@ -682,9 +684,9 @@ public final class ConversationListItem extends ConstraintLayout
return Transformations.map(description, sequence -> {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new StyleSpan(Typeface.ITALIC),
0,
sequence.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
0,
sequence.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
});
}
@ -699,8 +701,7 @@ public final class ConversationListItem extends ConstraintLayout
}
}
@Override
public void onChanged(SpannableString spannableString) {
private void onDisplayBodyChanged(SpannableString spannableString) {
setSubjectViewText(spannableString);
if (typingThreads != null) {

Wyświetl plik

@ -6,6 +6,7 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
@ -39,7 +40,8 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
}
@Override
public void bind(@NonNull ThreadRecord thread,
public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<Long> typingThreads,

Wyświetl plik

@ -1,63 +0,0 @@
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.Locale;
import java.util.Set;
public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem {
public ConversationListItemInboxZero(Context context) {
super(context);
}
public ConversationListItemInboxZero(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ConversationListItemInboxZero(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ConversationListItemInboxZero(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void unbind() {
}
@Override
public void bind(@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations)
{
}
@Override
public void setSelectedConversations(@NonNull ConversationSet conversations) {
}
@Override
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
}
}

Wyświetl plik

@ -7,6 +7,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
@ -28,20 +29,23 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
private static final int TYPE_CONTACTS = 2;
private static final int TYPE_MESSAGES = 3;
private final GlideRequests glideRequests;
private final EventListener eventListener;
private final Locale locale;
private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests;
private final EventListener eventListener;
private final Locale locale;
@NonNull
private SearchResult searchResult = SearchResult.EMPTY;
ConversationListSearchAdapter(@NonNull GlideRequests glideRequests,
ConversationListSearchAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull Locale locale)
{
this.glideRequests = glideRequests;
this.eventListener = eventListener;
this.locale = locale;
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
this.eventListener = eventListener;
this.locale = locale;
}
@Override
@ -55,21 +59,21 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
ThreadRecord conversationResult = getConversationResult(position);
if (conversationResult != null) {
holder.bind(conversationResult, glideRequests, eventListener, locale, searchResult.getQuery());
holder.bind(lifecycleOwner, conversationResult, glideRequests, eventListener, locale, searchResult.getQuery());
return;
}
Recipient contactResult = getContactResult(position);
if (contactResult != null) {
holder.bind(contactResult, glideRequests, eventListener, locale, searchResult.getQuery());
holder.bind(lifecycleOwner, contactResult, glideRequests, eventListener, locale, searchResult.getQuery());
return;
}
MessageResult messageResult = getMessageResult(position);
if (messageResult != null) {
holder.bind(messageResult, glideRequests, eventListener, locale, searchResult.getQuery());
holder.bind(lifecycleOwner, messageResult, glideRequests, eventListener, locale, searchResult.getQuery());
}
}
@ -157,33 +161,36 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
root = (ConversationListItem) itemView;
}
void bind(@NonNull ThreadRecord conversationResult,
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord conversationResult,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull Locale locale,
@Nullable String query)
{
root.bindThread(conversationResult, glideRequests, locale, Collections.emptySet(), new ConversationSet(), query);
root.bindThread(lifecycleOwner, conversationResult, glideRequests, locale, Collections.emptySet(), new ConversationSet(), query);
root.setOnClickListener(view -> eventListener.onConversationClicked(conversationResult));
}
void bind(@NonNull Recipient contactResult,
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull Recipient contactResult,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull Locale locale,
@Nullable String query)
{
root.bindContact(contactResult, glideRequests, locale, query);
root.bindContact(lifecycleOwner, contactResult, glideRequests, locale, query);
root.setOnClickListener(view -> eventListener.onContactClicked(contactResult));
}
void bind(@NonNull MessageResult messageResult,
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull MessageResult messageResult,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull Locale locale,
@Nullable String query)
{
root.bindMessage(messageResult, glideRequests, locale, query);
root.bindMessage(lifecycleOwner, messageResult, glideRequests, locale, query);
root.setOnClickListener(view -> eventListener.onMessageClicked(messageResult));
}