Skip to content

Commit

Permalink
creating dedicated model for charsequences with binding options to at…
Browse files Browse the repository at this point in the history
…tempt to enforce correct setText usage
  • Loading branch information
ouchadam committed Dec 22, 2021
1 parent fbe1e49 commit de84a0b
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,50 +38,50 @@ import java.util.concurrent.TimeUnit

@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class SpanUtilsTest : InstrumentedTest {
class VectorCharSequenceFactoryTest : InstrumentedTest {

private val spanUtils = SpanUtils {
private val factory = VectorCharSequenceFactory {
val emojiCompat = EmojiCompat.get()
emojiCompat.waitForInit()
emojiCompat.process(it) ?: it
}

private fun SpanUtils.canUseTextFuture(message: CharSequence): Boolean {
return getBindingOptions(message).canUseTextFuture
private fun VectorCharSequenceFactory.canUseTextFuture(message: CharSequence): Boolean {
return create(message).bindingOptions.canUseTextFuture
}

@Test
fun canUseTextFutureString() {
spanUtils.canUseTextFuture("test").shouldBeTrue()
factory.canUseTextFuture("test").shouldBeTrue()
}

@Test
fun canUseTextFutureCharSequenceOK() {
spanUtils.canUseTextFuture(SpannableStringBuilder().append("hello")).shouldBeTrue()
factory.canUseTextFuture(SpannableStringBuilder().append("hello")).shouldBeTrue()
}

@Test
fun canUseTextFutureCharSequenceWithSpanOK() {
val string = SpannableString("Text with strikethrough, underline, red spans")
string.setSpan(ForegroundColorSpan(Color.RED), 36, 39, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

spanUtils.canUseTextFuture(string) shouldBeEqualTo true
factory.canUseTextFuture(string) shouldBeEqualTo true
}

@Test
fun canUseTextFutureCharSequenceWithSpanKOStrikethroughSpan() {
val string = SpannableString("Text with strikethrough, underline, red spans")
string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
factory.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
}

@Test
fun canUseTextFutureCharSequenceWithSpanKOUnderlineSpan() {
val string = SpannableString("Text with strikethrough, underline, red spans")
string.setSpan(UnderlineSpan(), 25, 34, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
factory.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
}

@Test
Expand All @@ -90,7 +90,7 @@ class SpanUtilsTest : InstrumentedTest {
string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
string.setSpan(UnderlineSpan(), 25, 34, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
factory.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
}

@Test
Expand All @@ -100,32 +100,32 @@ class SpanUtilsTest : InstrumentedTest {
string.setSpan(UnderlineSpan(), 25, 34, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
string.setSpan(ForegroundColorSpan(Color.RED), 36, 39, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

spanUtils.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
factory.canUseTextFuture(string) shouldBeEqualTo trueIfAlwaysAllowed()
}

@Test
fun testGetBindingOptionsRegular() {
val string = SpannableString("Text")
val result = spanUtils.getBindingOptions(string)
result.canUseTextFuture shouldBeEqualTo true
result.preventMutation shouldBeEqualTo false
val result = factory.create(string)
result.bindingOptions.canUseTextFuture shouldBeEqualTo true
result.bindingOptions.preventMutation shouldBeEqualTo false
}

@Test
fun testGetBindingOptionsStrikethrough() {
val string = SpannableString("Text with strikethrough")
string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val result = spanUtils.getBindingOptions(string)
result.canUseTextFuture shouldBeEqualTo false
result.preventMutation shouldBeEqualTo false
val result = factory.create(string)
result.bindingOptions.canUseTextFuture shouldBeEqualTo false
result.bindingOptions.preventMutation shouldBeEqualTo false
}

@Test
fun testGetBindingOptionsMetricAffectingSpan() {
val string = SpannableString("Emoji \uD83D\uDE2E\u200D\uD83D\uDCA8")
val result = spanUtils.getBindingOptions(string)
result.canUseTextFuture shouldBeEqualTo false
result.preventMutation shouldBeEqualTo true
val result = factory.create(string)
result.bindingOptions.canUseTextFuture shouldBeEqualTo false
result.bindingOptions.preventMutation shouldBeEqualTo true
}

private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package im.vector.app.core.epoxy.bottomsheet
import android.text.method.MovementMethod
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
Expand All @@ -27,14 +28,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.core.extensions.setVectorText
import im.vector.app.features.displayname.getBestName
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.html.VectorCharSequence
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.util.MatrixItem

/**
Expand All @@ -50,10 +50,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
lateinit var matrixItem: MatrixItem

@EpoxyAttribute
lateinit var body: CharSequence

@EpoxyAttribute
var bindingOptions: BindingOptions? = null
lateinit var body: VectorCharSequence

@EpoxyAttribute
var bodyDetails: CharSequence? = null
Expand Down Expand Up @@ -84,13 +81,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
}
holder.imagePreview.isVisible = data != null
holder.body.movementMethod = movementMethod
holder.body.text = if (bindingOptions?.preventMutation.orFalse()) {
body.preventMutation()
} else {
body
}
holder.body.setVectorText(body)
holder.bodyDetails.setTextOrHide(bodyDetails)
body.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
body.value.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.timestamp.setTextOrHide(time)
}

Expand All @@ -102,7 +95,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
class Holder : VectorEpoxyHolder() {
val avatar by bind<ImageView>(R.id.bottom_sheet_message_preview_avatar)
val sender by bind<TextView>(R.id.bottom_sheet_message_preview_sender)
val body by bind<TextView>(R.id.bottom_sheet_message_preview_body)
val body by bind<AppCompatTextView>(R.id.bottom_sheet_message_preview_body)
val bodyDetails by bind<TextView>(R.id.bottom_sheet_message_preview_body_details)
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
val imagePreview by bind<ImageView>(R.id.bottom_sheet_message_preview_image)
Expand Down
21 changes: 10 additions & 11 deletions vector/src/main/java/im/vector/app/core/extensions/TextView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ import im.vector.app.R
import im.vector.app.core.epoxy.util.preventMutation
import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import im.vector.app.features.html.VectorCharSequence
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.extensions.orFalse

/**
* Set a text in the TextView, or set visibility to GONE if the text is null
Expand Down Expand Up @@ -147,24 +146,24 @@ fun TextView.copyOnLongClick() {
}

/**
* Safely set text containing emojis whilst still enabling text optimisations
* Safely set text with optimisations containing spans and emojis
* For use in epoxy models
*/
fun AppCompatTextView.setTextWithEmojiSupport(message: CharSequence?, bindingOptions: BindingOptions?) {
fun AppCompatTextView.setVectorText(message: VectorCharSequence?) {
setTextFuture(null)
when {
message == null -> {
message?.value == null -> {
text = null
}
bindingOptions?.canUseTextFuture.orFalse() -> {
val textFuture = PrecomputedTextCompat.getTextFuture(message, TextViewCompat.getTextMetricsParams(this), null)
message.bindingOptions.canUseTextFuture -> {
val textFuture = PrecomputedTextCompat.getTextFuture(message.value, TextViewCompat.getTextMetricsParams(this), null)
setTextFuture(textFuture)
}
bindingOptions?.preventMutation.orFalse() -> {
text = message.preventMutation()
message.bindingOptions.preventMutation -> {
text = message.value.preventMutation()
}
else -> {
text = message
else -> {
text = message.value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +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.html.VectorCharSequenceFactory
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
Expand All @@ -54,7 +54,7 @@ class MessageActionsEpoxyController @Inject constructor(
private val imageContentRenderer: ImageContentRenderer,
private val dimensionConverter: DimensionConverter,
private val errorFormatter: ErrorFormatter,
private val spanUtils: SpanUtils,
private val vectorCharSequenceFactory: VectorCharSequenceFactory,
private val eventDetailsFormatter: EventDetailsFormatter,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<MessageActionState>() {
Expand All @@ -66,8 +66,7 @@ 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)
val body = vectorCharSequenceFactory.create(state.messageBody.linkify(host.listener))
bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(host.avatarRenderer)
Expand All @@ -76,7 +75,6 @@ class MessageActionsEpoxyController @Inject constructor(
imageContentRenderer(host.imageContentRenderer)
data(state.timelineEvent()?.buildImageContentRendererData(host.dimensionConverter.dpToPx(66)))
userClicked { host.listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
bindingOptions(bindingOptions)
body(body)
bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root))
time(formattedDate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.html.VectorCharSequenceFactory
import im.vector.app.features.settings.VectorPreferences
import me.gujun.android.span.image
import me.gujun.android.span.span
Expand All @@ -42,7 +43,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val avatarSizeProvider: AvatarSizeProvider,
private val drawableProvider: DrawableProvider,
private val attributesFactory: MessageItemAttributesFactory,
private val vectorPreferences: VectorPreferences) {
private val vectorPreferences: VectorPreferences,
private val vectorCharSequenceFactory: VectorCharSequenceFactory) {

fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
Expand Down Expand Up @@ -110,7 +112,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(params.isHighlighted)
.attributes(attributes)
.message(spannableStr)
.message(vectorCharSequenceFactory.create(spannableStr))
.movementMethod(createLinkMovementMethod(params.callback))
}
else -> null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.CodeVisitor
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.html.SpanUtils
import im.vector.app.features.html.VectorCharSequenceFactory
import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
Expand Down Expand Up @@ -114,7 +114,7 @@ class MessageItemFactory @Inject constructor(
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val spanUtils: SpanUtils,
private val vectorCharSequenceFactory: VectorCharSequenceFactory,
private val session: Session,
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker) {

Expand Down Expand Up @@ -498,19 +498,17 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val bindingOptions = spanUtils.getBindingOptions(body)
val linkifiedBody = body.linkify(callback)

return MessageTextItem_().apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(linkifiedBody, callback, informationData)
val spannable = vectorCharSequenceFactory.create(annotateWithEdited(linkifiedBody, callback, informationData))
message(spannable)
} else {
message(linkifiedBody)
message(vectorCharSequenceFactory.create(linkifiedBody))
}
}
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.bindingOptions(bindingOptions)
.searchForPills(isFormatted)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
Expand All @@ -533,11 +531,10 @@ class MessageItemFactory @Inject constructor(
editedSpan(spannable)
}
}
.bindingOptions(spanUtils.getBindingOptions(formattedBody))
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.message(formattedBody)
.message(vectorCharSequenceFactory.create(formattedBody))
}

private fun annotateWithEdited(linkifiedBody: CharSequence,
Expand Down Expand Up @@ -591,7 +588,6 @@ class MessageItemFactory @Inject constructor(
textStyle = "italic"
}

val bindingOptions = spanUtils.getBindingOptions(htmlBody)
val message = formattedBody.linkify(callback)

return MessageTextItem_()
Expand All @@ -600,8 +596,7 @@ class MessageItemFactory @Inject constructor(
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.attributes(attributes)
.message(message)
.bindingOptions(bindingOptions)
.message(vectorCharSequenceFactory.create(message))
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
}
Expand All @@ -614,19 +609,17 @@ class MessageItemFactory @Inject constructor(
val formattedBody = SpannableStringBuilder()
formattedBody.append("* ${informationData.memberName} ")
formattedBody.append(messageContent.getHtmlBody())
val bindingOptions = spanUtils.getBindingOptions(formattedBody)
val message = formattedBody.linkify(callback)

return MessageTextItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited(message, callback, informationData)
message(spannable)
message(vectorCharSequenceFactory.create(spannable))
} else {
message(message)
message(vectorCharSequenceFactory.create(message))
}
}
.bindingOptions(bindingOptions)
.leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
Expand Down
Loading

0 comments on commit de84a0b

Please sign in to comment.