From c69d39fef5af1e3700d706437f1571bedcb6dd6f Mon Sep 17 00:00:00 2001 From: Jonathan Moskovich <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:48:27 +0300 Subject: [PATCH] RUM-18048: Fix Fabric text mapping in minified Android builds --- .../utils/text/FabricTextViewUtils.kt | 36 ++++--------------- .../sessionreplay/utils/text/TextViewUtils.kt | 10 ++---- .../utils/text/TextViewUtilsTest.kt | 18 +++++++--- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt index 425bcd281..4de631a90 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt @@ -1,17 +1,15 @@ package com.datadog.reactnative.sessionreplay.utils.text -import android.text.Spannable import android.text.style.ForegroundColorSpan -import android.view.View import android.widget.TextView -import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.reactnative.sessionreplay.utils.DrawableUtils import com.datadog.reactnative.sessionreplay.utils.formatAsRgba import com.facebook.react.bridge.ReactContext +import com.facebook.react.views.text.ReactTextView import java.util.Locale -internal class FabricTextViewUtils(private val reactContext: ReactContext, private val logger: InternalLogger, drawableUtils: DrawableUtils): TextViewUtils(reactContext, drawableUtils) { +internal class FabricTextViewUtils(reactContext: ReactContext, drawableUtils: DrawableUtils): TextViewUtils(reactContext, drawableUtils) { override fun resolveTextStyle( textWireframe: MobileSegment.Wireframe.TextWireframe, @@ -31,7 +29,9 @@ internal class FabricTextViewUtils(private val reactContext: ReactContext, priva } private fun getTextColor(view: TextView, textWireframe: MobileSegment.Wireframe.TextWireframe): String { - val spanned = getFieldFromView(view, SPANNED_FIELD_NAME) as? Spannable + // Use the public accessor so R8 can rewrite the reference when it obfuscates ReactTextView. + // Looking up the private mSpanned field by name breaks in minified applications. + val spanned = (view as? ReactTextView)?.spanned val spans = spanned?.getSpans(0, spanned.length, ForegroundColorSpan::class.java) val fontColor = spans?.firstOrNull()?.foregroundColor?.let { formatAsRgba(it) } ?: textWireframe.textStyle.color @@ -47,28 +47,4 @@ internal class FabricTextViewUtils(private val reactContext: ReactContext, priva val fontFamily = textWireframe.textStyle.family return resolveFontFamily(fontFamily.lowercase(Locale.US)) } - - internal fun getFieldFromView(view: View, value: String): Any? { - try { - val field = view.javaClass.getDeclaredField(value) - field.isAccessible = true - return field.get(view) - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - when (e) { - is NoSuchFieldException -> handleError(e, RESOLVE_FABRICFIELD_ERROR) - is NullPointerException -> handleError(e, NULL_FABRICFIELD_ERROR) - else -> handleError(e, RESOLVE_FABRICFIELD_ERROR) - } - return null - } - } - - private fun handleError(e: Exception, message: String) { - logger.log( - level = InternalLogger.Level.WARN, - targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), - messageBuilder = { message }, - throwable = e - ) - } -} \ No newline at end of file +} diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtils.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtils.kt index d02ed7624..9a4571d61 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtils.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtils.kt @@ -141,23 +141,17 @@ internal abstract class TextViewUtils(private val reactContext: ReactContext, pr @VisibleForTesting companion object { - internal const val SPANNED_FIELD_NAME = "mSpanned" - private const val ROBOTO_TYPEFACE_NAME = "roboto" private const val SERIF_FAMILY_NAME = "serif" private const val SANS_SERIF_FAMILY_NAME = "roboto, sans-serif" internal const val MONOSPACE_FAMILY_NAME = "monospace" - internal const val RESOLVE_FABRICFIELD_ERROR = "Unable to resolve field from fabric view" - internal const val NULL_FABRICFIELD_ERROR = "Null value found when trying to resolve field from fabric view" - - fun create(reactContext: ReactContext, logger: InternalLogger): TextViewUtils { return when (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - true -> FabricTextViewUtils(reactContext, logger, ReactViewBackgroundDrawableUtils()) + true -> FabricTextViewUtils(reactContext, ReactViewBackgroundDrawableUtils()) false -> LegacyTextViewUtils(reactContext, logger, ReactViewBackgroundDrawableUtils()) } } } -} \ No newline at end of file +} diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtilsTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtilsTest.kt index 494d4fabd..a4be7eded 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtilsTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/text/TextViewUtilsTest.kt @@ -23,6 +23,7 @@ import com.datadog.reactnative.sessionreplay.utils.formatAsRgba import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils.Companion.MONOSPACE_FAMILY_NAME import com.datadog.reactnative.tools.unit.forge.ForgeConfigurator import com.facebook.react.bridge.ReactContext +import com.facebook.react.views.text.ReactTextView import com.facebook.react.views.text.internal.span.CustomStyleSpan import com.facebook.react.views.view.ReactViewBackgroundDrawable import fr.xgouchet.elmyr.Forge @@ -42,7 +43,6 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -60,6 +60,9 @@ internal class TextViewUtilsTest { @Mock lateinit var mockTextView: TextView + @Mock + lateinit var mockReactTextView: ReactTextView + @Mock lateinit var mockReactViewBackgroundDrawable: ReactViewBackgroundDrawable @@ -116,12 +119,11 @@ internal class TextViewUtilsTest { val realFabricUtils = FabricTextViewUtils( mockReactContext, - mockLogger, mockDrawableUtils ) testedUtils = spy(realUtils) - fabricTestedUtils = spy(realFabricUtils) + fabricTestedUtils = realFabricUtils } @Test @@ -455,14 +457,20 @@ internal class TextViewUtilsTest { whenever(mockForegroundColorSpan.foregroundColor).thenReturn(-1) val spannable = mock(Spannable::class.java) - doReturn(spannable).whenever(fabricTestedUtils).getFieldFromView(any(), any()) + whenever(mockReactTextView.spanned).thenReturn(spannable) + whenever(mockReactTextView.background).thenReturn(null) + whenever(mockReactTextView.textSize).thenReturn(16f) whenever(spannable.getSpans(anyInt(), anyInt(), eq(ForegroundColorSpan::class.java))) .thenReturn( arrayOf(mockForegroundColorSpan) ) - val result = fabricTestedUtils.addReactNativeProperties(fakeWireframe, mockTextView, 0f) + val result = fabricTestedUtils.addReactNativeProperties( + fakeWireframe, + mockReactTextView, + 0f + ) assertThat(result.textStyle.color).isEqualTo("#ffffffff") }