Fix a crash on Epoxy if text contains a MetricAffectingSpan
This commit is contained in:
parent
04d23ce7f6
commit
808c401675
@ -27,10 +27,13 @@ import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.epoxy.util.preventMutation
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
/**
|
||||
@ -48,6 +51,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
||||
@EpoxyAttribute
|
||||
lateinit var body: CharSequence
|
||||
|
||||
@EpoxyAttribute
|
||||
var bindingOptions: BindingOptions? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var bodyDetails: CharSequence? = null
|
||||
|
||||
@ -77,7 +83,11 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
||||
}
|
||||
holder.imagePreview.isVisible = data != null
|
||||
holder.body.movementMethod = movementMethod
|
||||
holder.body.text = body
|
||||
holder.body.text = if (bindingOptions?.preventMutation.orFalse()) {
|
||||
body.preventMutation()
|
||||
} else {
|
||||
body
|
||||
}
|
||||
holder.bodyDetails.setTextOrHide(bodyDetails)
|
||||
body.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
|
||||
holder.timestamp.setTextOrHide(time)
|
||||
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.epoxy.util
|
||||
|
||||
import android.text.SpannableString
|
||||
|
||||
fun CharSequence?.preventMutation(): CharSequence? = this?.let { SpannableString(it) }
|
@ -37,6 +37,7 @@ import im.vector.app.features.home.room.detail.timeline.image.buildImageContentR
|
||||
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import im.vector.app.features.html.SpanUtils
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
@ -53,6 +54,7 @@ class MessageActionsEpoxyController @Inject constructor(
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val dimensionConverter: DimensionConverter,
|
||||
private val errorFormatter: ErrorFormatter,
|
||||
private val spanUtils: SpanUtils,
|
||||
private val eventDetailsFormatter: EventDetailsFormatter,
|
||||
private val dateFormatter: VectorDateFormatter
|
||||
) : TypedEpoxyController<MessageActionState>() {
|
||||
@ -64,6 +66,8 @@ class MessageActionsEpoxyController @Inject constructor(
|
||||
// Message preview
|
||||
val date = state.timelineEvent()?.root?.originServerTs
|
||||
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
|
||||
val body = state.messageBody.linkify(host.listener)
|
||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
||||
bottomSheetMessagePreviewItem {
|
||||
id("preview")
|
||||
avatarRenderer(host.avatarRenderer)
|
||||
@ -72,7 +76,8 @@ class MessageActionsEpoxyController @Inject constructor(
|
||||
imageContentRenderer(host.imageContentRenderer)
|
||||
data(state.timelineEvent()?.buildImageContentRendererData(host.dimensionConverter.dpToPx(66)))
|
||||
userClicked { host.listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
|
||||
body(state.messageBody.linkify(host.listener))
|
||||
bindingOptions(bindingOptions)
|
||||
body(body)
|
||||
bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root))
|
||||
time(formattedDate)
|
||||
}
|
||||
|
@ -473,7 +473,7 @@ class MessageItemFactory @Inject constructor(
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
val canUseTextFuture = spanUtils.canUseTextFuture(body)
|
||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
||||
val linkifiedBody = body.linkify(callback)
|
||||
|
||||
return MessageTextItem_().apply {
|
||||
@ -485,7 +485,7 @@ class MessageItemFactory @Inject constructor(
|
||||
}
|
||||
}
|
||||
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
||||
.canUseTextFuture(canUseTextFuture)
|
||||
.bindingOptions(bindingOptions)
|
||||
.searchForPills(isFormatted)
|
||||
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
|
||||
.imageContentRenderer(imageContentRenderer)
|
||||
@ -565,7 +565,7 @@ class MessageItemFactory @Inject constructor(
|
||||
textStyle = "italic"
|
||||
}
|
||||
|
||||
val canUseTextFuture = spanUtils.canUseTextFuture(htmlBody)
|
||||
val bindingOptions = spanUtils.getBindingOptions(htmlBody)
|
||||
val message = formattedBody.linkify(callback)
|
||||
|
||||
return MessageTextItem_()
|
||||
@ -575,7 +575,7 @@ class MessageItemFactory @Inject constructor(
|
||||
.previewUrlCallback(callback)
|
||||
.attributes(attributes)
|
||||
.message(message)
|
||||
.canUseTextFuture(canUseTextFuture)
|
||||
.bindingOptions(bindingOptions)
|
||||
.highlighted(highlight)
|
||||
.movementMethod(createLinkMovementMethod(callback))
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
data class BindingOptions(
|
||||
val canUseTextFuture: Boolean,
|
||||
val preventMutation: Boolean
|
||||
)
|
@ -26,12 +26,14 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.epoxy.onLongClickIgnoringLinks
|
||||
import im.vector.app.core.epoxy.util.preventMutation
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
@ -43,7 +45,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
var message: CharSequence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var canUseTextFuture: Boolean = true
|
||||
var bindingOptions: BindingOptions? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var useBigFont: Boolean = false
|
||||
@ -85,7 +87,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
it.bind(holder.messageView)
|
||||
}
|
||||
}
|
||||
val textFuture = if (canUseTextFuture) {
|
||||
val textFuture = if (bindingOptions?.canUseTextFuture.orFalse()) {
|
||||
PrecomputedTextCompat.getTextFuture(
|
||||
message ?: "",
|
||||
TextViewCompat.getTextMetricsParams(holder.messageView),
|
||||
@ -99,10 +101,14 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
holder.messageView.onClick(attributes.itemClickListener)
|
||||
holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
|
||||
|
||||
if (canUseTextFuture) {
|
||||
if (bindingOptions?.canUseTextFuture.orFalse()) {
|
||||
holder.messageView.setTextFuture(textFuture)
|
||||
} else {
|
||||
holder.messageView.text = message
|
||||
holder.messageView.text = if (bindingOptions?.preventMutation.orFalse()) {
|
||||
message.preventMutation()
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,23 +22,42 @@ import android.text.style.MetricAffectingSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import androidx.emoji2.text.EmojiCompat
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
|
||||
import javax.inject.Inject
|
||||
|
||||
class SpanUtils @Inject constructor() {
|
||||
fun getBindingOptions(charSequence: CharSequence): BindingOptions {
|
||||
val emojiCharSequence = EmojiCompat.get().process(charSequence)
|
||||
|
||||
if (emojiCharSequence !is Spanned) {
|
||||
return BindingOptions(
|
||||
canUseTextFuture = true,
|
||||
preventMutation = false
|
||||
)
|
||||
}
|
||||
|
||||
return BindingOptions(
|
||||
canUseTextFuture = canUseTextFuture(emojiCharSequence),
|
||||
preventMutation = preventMutation(emojiCharSequence)
|
||||
)
|
||||
}
|
||||
|
||||
// Workaround for https://issuetracker.google.com/issues/188454876
|
||||
fun canUseTextFuture(charSequence: CharSequence): Boolean {
|
||||
private fun canUseTextFuture(charSequence: Spanned): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
// On old devices, it works correctly
|
||||
return true
|
||||
}
|
||||
|
||||
val emojiCharSequence = EmojiCompat.get().process(charSequence)
|
||||
if (emojiCharSequence !is Spanned) {
|
||||
return true
|
||||
}
|
||||
|
||||
return emojiCharSequence
|
||||
.getSpans(0, emojiCharSequence.length, Any::class.java)
|
||||
return charSequence
|
||||
.getSpans(0, charSequence.length, Any::class.java)
|
||||
.all { it !is StrikethroughSpan && it !is UnderlineSpan && it !is MetricAffectingSpan }
|
||||
}
|
||||
|
||||
// Workaround for setting text during binding which mutate the text itself
|
||||
private fun preventMutation(spanned: Spanned): Boolean {
|
||||
return spanned
|
||||
.getSpans(0, spanned.length, Any::class.java)
|
||||
.any { it is MetricAffectingSpan }
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user