Inline useNewLinkifier flag.

This commit is contained in:
Greyson Parrelli
2026-06-15 10:51:05 -04:00
parent 933b799266
commit 39679ebfc3
9 changed files with 10 additions and 97 deletions
@@ -11,10 +11,10 @@ import android.text.Spanned
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import org.signal.core.util.addDetectedLinks
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan
import org.thoughtcrime.securesms.util.LinkUtil
import org.thoughtcrime.securesms.util.Linkification
import org.thoughtcrime.securesms.util.UrlClickHandler
import org.thoughtcrime.securesms.util.hasOnlyThumbnail
@@ -34,7 +34,7 @@ object V2ConversationItemUtils {
}
LinkifyCompat.addLinks(messageBody, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
Linkification.applyWebUrlSpans(messageBody)
messageBody.addDetectedLinks()
messageBody.getSpans(0, messageBody.length, URLSpan::class.java).forEach { urlSpan ->
val url = urlSpan.url
@@ -15,10 +15,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.util.LinkifyCompat;
import org.signal.core.util.LinkifierSpannableExtensionsKt;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Linkification;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
public final class GroupDescriptionUtil {
@@ -38,7 +38,7 @@ public final class GroupDescriptionUtil {
if (linkify) {
LinkifyCompat.addLinks(descriptionSpannable, Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS);
Linkification.applyWebUrlSpans(descriptionSpannable);
LinkifierSpannableExtensionsKt.addDetectedLinks(descriptionSpannable);
for (URLSpan urlSpan : descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class)) {
String url = urlSpan.getURL();
@@ -11,7 +11,6 @@ import java.util.stream.Collectors;
import org.signal.core.util.Linkifier;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Linkification;
import org.signal.core.util.Util;
import org.whispersystems.signalservice.api.util.OptionalUtil;
@@ -51,7 +50,7 @@ public final class LinkPreviewUtil {
* @return All URLs allowed as previews in the source text.
*/
public static @NonNull Links findValidPreviewUrls(@NonNull String text) {
List<Linkifier.DetectedLink> detected = Linkification.findWebLinks(text);
List<Linkifier.DetectedLink> detected = Linkifier.findLinks(text);
if (detected.isEmpty()) {
return Links.EMPTY;
}
@@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
import org.thoughtcrime.securesms.components.ProgressCard;
import org.thoughtcrime.securesms.components.SearchView;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Linkification;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.signal.core.ui.util.ThemeUtil;
@@ -458,7 +457,7 @@ public class SubmitDebugLogActivity extends BaseActivity {
TextView dialogView = new TextView(builder.getContext());
LongClickCopySpan longClickUrl = new LongClickCopySpan(url);
for (Linkifier.DetectedLink link : Linkification.findWebLinks(dialogText)) {
for (Linkifier.DetectedLink link : Linkifier.findLinks(dialogText)) {
spannableDialogText.setSpan(longClickUrl, link.getStart(), link.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@@ -37,11 +37,11 @@ import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.util.addDetectedLinks
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Linkification
import org.thoughtcrime.securesms.util.viewModel
import org.signal.core.ui.R as CoreUiR
@@ -163,7 +163,7 @@ private fun ViewNoteBottomSheetContent(
if (!isInspection) {
LinkifyCompat.addLinks(spannable, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
}
Linkification.applyWebUrlSpans(spannable)
spannable.addDetectedLinks()
it.text = spannable
}
}
@@ -52,6 +52,7 @@ import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.Debouncer
import org.signal.core.util.DimensionUnit
import org.signal.core.util.ServiceUtil
import org.signal.core.util.addDetectedLinks
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.dp
import org.signal.core.util.getParcelableCompat
@@ -95,7 +96,6 @@ import org.thoughtcrime.securesms.stories.viewer.views.StoryViewsBottomSheetDial
import org.thoughtcrime.securesms.util.AvatarUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.LinkUtil
import org.thoughtcrime.securesms.util.Linkification
import org.thoughtcrime.securesms.util.LongClickCopySpan
import org.thoughtcrime.securesms.util.LongClickMovementMethod
import org.thoughtcrime.securesms.util.Projection
@@ -995,7 +995,7 @@ class StoryViewerPageFragment :
fun linkifyUrlLinks(spannable: Spannable) {
LinkifyCompat.addLinks(spannable, Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
Linkification.applyWebUrlSpans(spannable)
spannable.addDetectedLinks()
spannable.getSpans(0, spannable.length, URLSpan::class.java).forEach { urlSpan ->
val url = urlSpan.url
@@ -1,57 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import android.text.Spannable
import android.text.SpannableString
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import org.signal.core.util.Linkifier
import org.signal.core.util.Linkifier.DetectedLink
import org.signal.core.util.addDetectedLinks
/**
* Temporary abstraction while we switch over to the new [Linkifier] via remote config.
* When the remote config is off, we fallback to the pre-existing android link logic.
*/
object Linkification {
/**
* Adds [URLSpan]s for web URLs in [spannable]. Returns `true` if at least one span was added.
*/
@JvmStatic
fun applyWebUrlSpans(spannable: Spannable): Boolean {
return if (RemoteConfig.useNewLinkifier) {
spannable.addDetectedLinks()
} else {
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
}
}
/**
* Finds web URLs in [text].
*/
@JvmStatic
fun findWebLinks(text: CharSequence): List<DetectedLink> {
if (RemoteConfig.useNewLinkifier) {
return Linkifier.findLinks(text)
}
val spannable = SpannableString(text)
if (!LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)) {
return emptyList()
}
return spannable.getSpans(0, spannable.length, URLSpan::class.java).map { span ->
DetectedLink(
start = spannable.getSpanStart(span),
end = spannable.getSpanEnd(span),
url = span.url
)
}
}
}
@@ -1402,17 +1402,6 @@ object RemoteConfig {
hotSwappable = true
)
/**
* Whether to use our custom [org.signal.core.util.Linkifier] for web URL detection.
*/
@JvmStatic
@get:JvmName("useNewLinkifier")
val useNewLinkifier: Boolean by remoteBoolean(
key = "android.useNewLinkifier",
defaultValue = false,
hotSwappable = true
)
/**
* Whether screen sharing is available during calls.
*/
@@ -1,34 +1,17 @@
package org.thoughtcrime.securesms.linkpreview
import android.app.Application
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.util.RemoteConfig
@Suppress("ClassName")
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class LinkPreviewUtilTest_findValidPreviewUrls {
@Before
fun setup() {
mockkStatic(RemoteConfig::class)
every { RemoteConfig.useNewLinkifier } returns true
}
@After
fun tearDown() {
unmockkStatic(RemoteConfig::class)
}
@Test
fun no_links() {
val links = LinkPreviewUtil.findValidPreviewUrls("No links")