Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeline recycling crash #4790

Merged
merged 5 commits into from
Jan 6, 2022
Merged

Timeline recycling crash #4790

merged 5 commits into from
Jan 6, 2022

Conversation

ouchadam
Copy link
Contributor

Tentatively fixes #4789 Timeline crashes when quickly scrolling or restoring state.

I was unable to reproduce this crash locally, the stack trace looks to be the same as when a TextView is modified after setting a TextFuture

Thread: main, Exception: java.lang.IllegalArgumentException: PrecomputedText's Parameters don't match the parameters of this TextView.
    at android.widget.TextView.setText(TextView.java:6206)
    at android.widget.TextView.setText(TextView.java:6124)
    at android.widget.TextView.setText(TextView.java:6076)
    at androidx.core.widget.TextViewCompat.setPrecomputedText(TextViewCompat.java:907)
    at androidx.appcompat.widget.AppCompatTextView.consumeTextFutureAndSetBlocking(AppCompatTextView.java:542)
    at androidx.appcompat.widget.AppCompatTextView.getText(AppCompatTextView.java:551)
    at androidx.emoji2.viewsintegration.EmojiInputFilter.filter(EmojiInputFilter.java:64)
    at android.widget.TextView.setText(TextView.java:6160)
    at android.widget.TextView.setText(TextView.java:6124)
    at android.widget.TextView.setText(TextView.java:6076)

My theory is that RecyclerView is recycling TextViews with non consumed futures which causes them to throw when the TextView options are rebound. This PR aims to solve the issue by resetting any present futures before calling setText which will allow AppCompatTextView.consumeTextFutureAndSetBlocking to be skipped if the view is rebound with setText directly (such is the case when emojis are present)

} else {
holder.messageView.text = if (bindingOptions?.preventMutation.orFalse()) {
setTextFuture(null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is the potential fix

@github-actions
Copy link

github-actions bot commented Dec 21, 2021

Unit Test Results

  66 files  ±0    66 suites  ±0   56s ⏱️ -2s
135 tests ±0  135 ✔️ ±0  0 💤 ±0  0 ±0 
418 runs  ±0  418 ✔️ ±0  0 💤 ±0  0 ±0 

Results for commit 22bab47. ± Comparison against base commit eb9fc57.

♻️ This comment has been updated with latest results.

Copy link
Member

@ganfra ganfra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, will see if we have still the crash after!

super.bind(holder)
holder.messageView.movementMethod = movementMethod
renderSendState(holder.messageView, holder.messageView)
holder.messageView.onClick(attributes.itemClickListener)
holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)

message?.let { holder.messageView.setTextWithEmojiSupport(it, bindingOptions) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that message should never be null, but the change is not strictly equivalent to what we had before: message ?: "".

Maybe continue to fallback to empty string, to ensure that the text is always updated, or change message to a lateinit CharSequence

(if message is null, the TextView content will not be reset and will display the previous bound value)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah good catch, resetting to null seems safer, I don't know the epoxy lifecycle well enough to trust using a late init

b5ce8dc

- tentatively fixes IllegalArgumentException when recycling the text views due to AppCompatTextView.consumeTextFutureAndSetBlocking attempting to consuming any futures, even if they may be invalid
…recycling and an item doesn't contain a message
@ouchadam ouchadam force-pushed the feature/adm/emoji-recycling branch from b5ce8dc to 6380ee9 Compare January 5, 2022 10:02
@ouchadam ouchadam requested a review from bmarty January 5, 2022 16:11
} else {
null
}
markwonPlugins?.forEach { plugin -> plugin.beforeSetText(holder.messageView, message?.charSequence as Spanned) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line (markwonPlugins...) has been removed by mistake during the merge I guess

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@bmarty bmarty Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 different methods beforeSetText and a afterSetText, I have no idea what they are doing TBH

Copy link
Contributor Author

@ouchadam ouchadam Jan 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

22bab47

from the default implementations in the markwon library, it seems they're mainly used for apply text movements (links) and measuring lists but custom plugins can make use of them if needed

/**
 * It can be useful to prepare a TextView for markdown. For example {@code ru.noties.markwon.image.ImagesPlugin}
 * uses this method to unregister previously registered {@link AsyncDrawableSpan}
 * (if there are such spans in this TextView at this point). Or {@link CorePlugin}
 * which measures ordered list numbers
**/ 

@@ -92,27 +91,22 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
it.bind(holder.messageView)
}
}
val textFuture = if (bindingOptions?.canUseTextFuture.orFalse()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering why we coputed that before the next line... Maybe there were a good reason, I do not remember

} else {
null
}
markwonPlugins?.forEach { plugin -> plugin.beforeSetText(holder.messageView, message?.charSequence as Spanned) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also noticed message?.charSequence as Spanned will throw if the message is null, will wrap with a .let

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm pretty sure message cannot be null but you are right, better safe than sorry.

… a nullable check to avoid attempt to cast a null to non null
@bmarty bmarty merged commit 296929e into develop Jan 6, 2022
@bmarty bmarty deleted the feature/adm/emoji-recycling branch January 6, 2022 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Crash - Emoji recycling attempting to use previous TextFuture
3 participants