mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Fix ANR when trying to load a message composed of 10,000 random emoji.
This commit is contained in:
committed by
Cody Henthorne
parent
f8eaa96412
commit
c9a59a7417
@@ -8,6 +8,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Annotation;
|
||||
import android.text.Layout;
|
||||
import android.text.PrecomputedText;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -29,10 +30,12 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.PrecomputedTextCompat;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
@@ -42,10 +45,14 @@ import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
@@ -85,6 +92,12 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private int lastSizeChangedWidth = -1;
|
||||
private int lastSizeChangedHeight = -1;
|
||||
|
||||
// Utilized for async text loading when a large number of emoji is present.
|
||||
private int taskNumber = 0;
|
||||
private Executor backgroundExecutor = new SerialMonoLifoExecutor(SignalExecutors.UNBOUNDED);
|
||||
private CharSequence requestedText = null;
|
||||
private BufferType requestedType = null;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
|
||||
@@ -128,7 +141,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
public void setMaxLength(int maxLength) {
|
||||
this.maxLength = maxLength;
|
||||
setText(getText());
|
||||
setTextAsync(getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -162,8 +175,115 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommended method for calling through to reset the text flow within this file.
|
||||
* Doing so will ensure we call setTextAsync with the requested arguments as necessary.
|
||||
*/
|
||||
private void resetText() {
|
||||
if (requestedText == null || requestedType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTextAsync(requestedText, requestedType);
|
||||
}
|
||||
|
||||
public void setTextAsync(@Nullable CharSequence text) {
|
||||
setTextAsync(text, BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text. If there are more than 100 emoji candidates, we utilize PrecomputedTextCompat.
|
||||
*/
|
||||
public void setTextAsync(@Nullable CharSequence text, BufferType type) {
|
||||
taskNumber++;
|
||||
final int number = taskNumber;
|
||||
|
||||
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||
if (candidates == null || candidates.size() <= 100) {
|
||||
setText(text, type);
|
||||
|
||||
if (sizeChangeInProgress) {
|
||||
sizeChangeInProgress = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final PrecomputedTextCompat.Params params = getTextMetricsParamsCompat();
|
||||
final Reference<EmojiTextView> ref = new WeakReference<>(this);
|
||||
|
||||
backgroundExecutor.execute(() -> {
|
||||
EmojiTextView textView = ref.get();
|
||||
if (textView != null) {
|
||||
|
||||
final CharSequence textToSet;
|
||||
synchronized (textView) {
|
||||
textToSet = getTextToSet(text, type);
|
||||
}
|
||||
|
||||
if (textToSet == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PrecomputedTextCompat precomputedTextCompat = PrecomputedTextCompat.create(textToSet, params);
|
||||
|
||||
textView.post(() -> {
|
||||
if (textView.taskNumber != number) {
|
||||
return;
|
||||
}
|
||||
|
||||
textView.setPrecomputedText(precomputedTextCompat);
|
||||
|
||||
if (textView.sizeChangeInProgress) {
|
||||
textView.sizeChangeInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Note if you aren't sure how many emoji are going to be displayed, it may be better to utilize [setTextAsync]
|
||||
*/
|
||||
@Override
|
||||
public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
boolean isPrecomputed = (text instanceof PrecomputedTextCompat || (Build.VERSION.SDK_INT >= 28 && text instanceof PrecomputedText));
|
||||
if (!isPrecomputed) {
|
||||
text = getTextToSet(text, type);
|
||||
}
|
||||
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setText(text, BufferType.SPANNABLE);
|
||||
|
||||
previousText = text;
|
||||
previousBufferType = type;
|
||||
previousOverflowText = overflowText;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousTransformationMethod = getTransformationMethod();
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
|
||||
if (getMaxLines() > 0 && getMaxLines() != Integer.MAX_VALUE) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
} else if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
}
|
||||
}
|
||||
|
||||
if (getLayoutParams() != null && getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable CharSequence getTextToSet(@Nullable CharSequence text, BufferType type) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis &&
|
||||
@@ -187,15 +307,9 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
|
||||
if (unchanged(text, overflowText, type)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
previousText = text;
|
||||
previousOverflowText = overflowText;
|
||||
previousBufferType = type;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousTransformationMethod = getTransformationMethod();
|
||||
|
||||
Spannable textToSet;
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
textToSet = new SpannableStringBuilder(Optional.ofNullable(text).orElse(""));
|
||||
@@ -203,21 +317,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
textToSet = new SpannableStringBuilder(EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji));
|
||||
}
|
||||
|
||||
super.setText(textToSet, BufferType.SPANNABLE);
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
|
||||
if (getMaxLines() > 0 && getMaxLines() != Integer.MAX_VALUE) {
|
||||
ellipsizeEmojiTextForMaxLines();
|
||||
} else if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
}
|
||||
}
|
||||
|
||||
if (getLayoutParams() != null && getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
requestLayout();
|
||||
}
|
||||
return textToSet;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,12 +388,15 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
CharSequence text = getText();
|
||||
if (text != null) {
|
||||
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
if (widthSpecMode != MeasureSpec.AT_MOST) {
|
||||
return widthMeasureSpec;
|
||||
}
|
||||
|
||||
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
float measuredTextWidth = hasMetricAffectingSpan(text) ? Layout.getDesiredWidth(text, getPaint()) : getLongestLineWidth(text);
|
||||
int desiredWidth = (int) measuredTextWidth + getPaddingLeft() + getPaddingRight();
|
||||
|
||||
if (widthSpecMode == MeasureSpec.AT_MOST && desiredWidth < widthSpecSize) {
|
||||
if (desiredWidth < widthSpecSize) {
|
||||
return MeasureSpec.makeMeasureSpec(desiredWidth + 3, MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
@@ -338,7 +441,8 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
public void setOverflowText(@Nullable CharSequence overflowText) {
|
||||
this.overflowText = overflowText;
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
this.requestedType = BufferType.SPANNABLE;
|
||||
resetText();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -472,8 +576,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
if (!sizeChangeInProgress) {
|
||||
sizeChangeInProgress = true;
|
||||
setText(previousText, previousBufferType);
|
||||
sizeChangeInProgress = false;
|
||||
resetText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1089,7 +1089,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
bodyText.setMaxLines(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
bodyText.setText(StringUtil.trim(styledText));
|
||||
bodyText.setTextAsync(StringUtil.trim(styledText));
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
|
||||
if (conversationMessage.getBottomButton() != null) {
|
||||
|
||||
@@ -129,9 +129,10 @@ public class LongMessageFragment extends FullScreenDialogFragment {
|
||||
CommunicationActions.handlePotentialProxyLinkUrl(requireActivity(), url));
|
||||
|
||||
bubble.setVisibility(View.VISIBLE);
|
||||
text.setText(body);
|
||||
text.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageFontSize());
|
||||
text.setTextAsync(body);
|
||||
|
||||
if (!message.get().getMessageRecord().isOutgoing()) {
|
||||
text.setMentionBackgroundTint(ContextCompat.getColor(requireContext(), ThemeUtil.isDarkTheme(requireActivity()) ? R.color.core_grey_60 : R.color.core_grey_20));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user