From 05b2a5d70cbb4883b181d2254c9aeea46f51bb55 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 29 Nov 2024 17:10:06 +0100 Subject: [PATCH] fix crash when post is single line with hashtags (#4778) ``` java.lang.StringIndexOutOfBoundsException at android.text.SpannableStringBuilder.(SpannableStringBuilder.java:63) at android.text.SpannableStringBuilder.subSequence(SpannableStringBuilder.java:1198) at com.keylesspalace.tusky.util.LinkHelper.setClickableText(LinkHelper.kt:99) at com.keylesspalace.tusky.adapter.StatusBaseViewHolder.setTextVisible(StatusBaseViewHolder.java:289) at com.keylesspalace.tusky.adapter.StatusBaseViewHolder.setSpoilerAndContent(StatusBaseViewHolder.java:244) at com.keylesspalace.tusky.adapter.StatusBaseViewHolder.setupWithStatus(StatusBaseViewHolder.java:820) at com.keylesspalace.tusky.adapter.StatusViewHolder.setupWithStatus(StatusViewHolder.java:91) at com.keylesspalace.tusky.components.timeline.TimelinePagingAdapter.bindViewHolder(TimelinePagingAdapter.kt:100) at com.keylesspalace.tusky.components.timeline.TimelinePagingAdapter.onBindViewHolder(TimelinePagingAdapter.kt:82) at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7847) at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6646) at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6917) at androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:288) at androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:345) at androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:361) at androidx.recyclerview.widget.GapWorker.prefetch(GapWorker.java:368) at androidx.recyclerview.widget.GapWorker.run(GapWorker.java:399) at android.os.Handler.handleCallback(Handler.java:959) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loopOnce(Looper.java:232) at android.os.Looper.loop(Looper.java:317) at android.app.ActivityThread.main(ActivityThread.java:8705) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886) ``` --- .../keylesspalace/tusky/util/LinkHelper.kt | 2 +- .../tusky/util/LinkHelperTest.kt | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt index 6694b7fc1..a2f26107a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt @@ -137,7 +137,7 @@ internal fun getTrailingHashtags(content: Spanned): Pair> { return when (trailingContentLength) { 0 -> Pair(content.length, emptyList()) else -> { - val trailingContentOffset = content.length - trailingContentLength + val trailingContentOffset = (content.length - trailingContentLength).coerceAtLeast(0) Pair( trailingContentOffset, content.getSpans(trailingContentOffset, content.length, URLSpan::class.java) diff --git a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt index b1d7cad90..de5b7b768 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt @@ -317,17 +317,30 @@ class LinkHelperTest { @Test fun `get trailing hashtags with empty content returns empty list`() { - assert(getTrailingHashtags(SpannableStringBuilder("")).second.isEmpty()) + val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("")) + assertEquals(0, endOfContent) + assert(trailingHashtags.isEmpty()) } @Test fun `get trailing hashtags with no hashtags returns empty list`() { - assert(getTrailingHashtags(SpannableStringBuilder("some untagged content")).second.isEmpty()) + val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("some untagged content")) + assertEquals(21, endOfContent) + assert(trailingHashtags.isEmpty()) } @Test fun `get trailing hashtags with all inline hashtags returns empty list`() { - assert(getTrailingHashtags(SpannableStringBuilder("some #inline #tagged #content")).second.isEmpty()) + val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("some #inline #tagged #content")) + assertEquals(29, endOfContent) + assert(trailingHashtags.isEmpty()) + } + + @Test + fun `get trailing hashtags with only hashtags returns empty list`() { + val (endOfContent, trailingHashtags) = getTrailingHashtags(SpannableStringBuilder("#some #inline #tagged #content")) + assertEquals(0, endOfContent) + assert(trailingHashtags.isEmpty()) } @Test @@ -336,7 +349,8 @@ class LinkHelperTest { tags.first().let { append("#${it.name}", URLSpan(it.url), 0) } } - val (_, trailingHashtags) = getTrailingHashtags(content) + val (endOfContent, trailingHashtags) = getTrailingHashtags(content) + assertEquals(30, endOfContent) assertEquals(tags.first().name, trailingHashtags.single().name) assertEquals(tags.first().url, trailingHashtags.single().url) } @@ -352,7 +366,8 @@ class LinkHelperTest { } } - val (_, trailingHashtags) = getTrailingHashtags(content) + val (endOfContent, trailingHashtags) = getTrailingHashtags(content) + assertEquals(30, endOfContent) assertEquals(tags.size, trailingHashtags.size) tags.forEachIndexed { index, tag -> assertEquals(tag.name, trailingHashtags[index].name)