Skip to content

Commit 505d1f7

Browse files
Merge pull request #795 from DataDog/carlosnogueira/RUM-5332/unresolved-rn-text-propertie-new-arch
[RUM-5332] Fix text properties for android new architecture
2 parents 2635371 + 33728a4 commit 505d1f7

File tree

13 files changed

+443
-407
lines changed

13 files changed

+443
-407
lines changed

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66

77
package com.datadog.reactnative.sessionreplay
88

9+
import android.annotation.SuppressLint
910
import com.datadog.android.api.feature.FeatureSdkCore
10-
import com.datadog.android.sessionreplay.ImagePrivacy
1111
import com.datadog.android.sessionreplay.SessionReplayConfiguration
12-
import com.datadog.android.sessionreplay.TextAndInputPrivacy
13-
import com.datadog.android.sessionreplay.TouchPrivacy
1412
import com.datadog.reactnative.DatadogSDKWrapperStorage
13+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
1514
import com.facebook.react.bridge.Promise
1615
import com.facebook.react.bridge.ReactContext
17-
import java.util.Locale
1816

1917
/**
2018
* The entry point to use Datadog's Session Replay feature.
@@ -33,6 +31,7 @@ class DdSessionReplayImplementation(
3331
* @param customEndpoint Custom server url for sending replay data.
3432
* @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled.
3533
*/
34+
@SuppressLint("VisibleForTests")
3635
fun enable(
3736
replaySampleRate: Double,
3837
customEndpoint: String,
@@ -42,12 +41,13 @@ class DdSessionReplayImplementation(
4241
) {
4342
val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
4443
val logger = sdkCore.internalLogger
44+
val textViewUtils = TextViewUtils.create(reactContext, logger)
4545
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
4646
.startRecordingImmediately(startRecordingImmediately)
4747
.setImagePrivacy(privacySettings.imagePrivacyLevel)
4848
.setTouchPrivacy(privacySettings.touchPrivacyLevel)
4949
.setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel)
50-
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext, logger))
50+
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(textViewUtils))
5151

5252
if (customEndpoint != "") {
5353
configuration.useCustomEndpoint(customEndpoint)

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
package com.datadog.reactnative.sessionreplay
88

9-
import androidx.annotation.VisibleForTesting
10-
import com.datadog.android.api.InternalLogger
119
import com.datadog.android.sessionreplay.ExtensionSupport
1210
import com.datadog.android.sessionreplay.MapperTypeWrapper
1311
import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
@@ -16,56 +14,34 @@ import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
1614
import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
1715
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
1816
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
19-
import com.facebook.react.bridge.ReactContext
20-
import com.facebook.react.uimanager.UIManagerModule
17+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
2118
import com.facebook.react.views.image.ReactImageView
2219
import com.facebook.react.views.text.ReactTextView
2320
import com.facebook.react.views.textinput.ReactEditText
2421
import com.facebook.react.views.view.ReactViewGroup
2522

23+
2624
internal class ReactNativeSessionReplayExtensionSupport(
27-
private val reactContext: ReactContext,
28-
private val logger: InternalLogger
25+
private val textViewUtils: TextViewUtils
2926
) : ExtensionSupport {
3027
override fun name(): String {
3128
return ReactNativeSessionReplayExtensionSupport::class.java.simpleName
3229
}
3330

3431
override fun getCustomViewMappers(): List<MapperTypeWrapper<*>> {
35-
val uiManagerModule = getUiManagerModule()
36-
3732
return listOf(
3833
MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()),
3934
MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()),
40-
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(reactContext, uiManagerModule)),
41-
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(reactContext, uiManagerModule)),
35+
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(textViewUtils)),
36+
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(textViewUtils)),
4237
)
4338
}
4439

45-
@VisibleForTesting
46-
internal fun getUiManagerModule(): UIManagerModule? {
47-
return try {
48-
reactContext.getNativeModule(UIManagerModule::class.java)
49-
} catch (e: IllegalStateException) {
50-
logger.log(
51-
level = InternalLogger.Level.WARN,
52-
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
53-
messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
54-
throwable = e
55-
)
56-
return null
57-
}
58-
}
59-
6040
override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
6141
return listOf()
6242
}
6343

6444
override fun getCustomDrawableMapper(): List<DrawableToColorMapper> {
6545
return emptyList()
6646
}
67-
68-
internal companion object {
69-
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
70-
}
7147
}

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ShadowNodeWrapper.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ internal class ShadowNodeWrapper(
3333
internal companion object {
3434
internal fun getShadowNodeWrapper(
3535
reactContext: ReactContext,
36-
uiManagerModule: UIManagerModule,
36+
uiManagerModule: UIManagerModule?,
3737
reflectionUtils: ReflectionUtils,
3838
viewId: Int
3939
): ShadowNodeWrapper? {
4040
val countDownLatch = CountDownLatch(1)
4141
var target: ReactShadowNode<out ReactShadowNode<*>>? = null
4242

4343
val shadowNodeRunnable = Runnable {
44-
val node = resolveShadowNode(reflectionUtils, uiManagerModule, viewId)
44+
val node = uiManagerModule?.let { resolveShadowNode(reflectionUtils, it, viewId) }
4545
if (node != null) {
4646
target = node
4747
}

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,11 @@ import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
1919
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
2020
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
2121
import com.datadog.android.sessionreplay.utils.GlobalBounds
22-
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
23-
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
24-
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
25-
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
26-
import com.facebook.react.bridge.ReactContext
27-
import com.facebook.react.uimanager.UIManagerModule
22+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
2823
import com.facebook.react.views.textinput.ReactEditText
2924

3025
internal class ReactEditTextMapper(
31-
private val reactTextPropertiesResolver: TextPropertiesResolver =
32-
NoopTextPropertiesResolver(),
33-
private val textViewUtils: TextViewUtils = TextViewUtils(),
26+
private val textViewUtils: TextViewUtils
3427
) : BaseAsyncBackgroundWireframeMapper<ReactEditText>(
3528
viewIdentifierResolver = DefaultViewIdentifierResolver,
3629
colorStringFormatter = DefaultColorStringFormatter,
@@ -46,20 +39,6 @@ internal class ReactEditTextMapper(
4639
drawableToColorMapper = drawableToColorMapper,
4740
)
4841

49-
internal constructor(
50-
reactContext: ReactContext,
51-
uiManagerModule: UIManagerModule?
52-
) : this(
53-
reactTextPropertiesResolver = if (uiManagerModule == null) {
54-
NoopTextPropertiesResolver()
55-
} else {
56-
ReactTextPropertiesResolver(
57-
reactContext = reactContext,
58-
uiManagerModule = uiManagerModule
59-
)
60-
}
61-
)
62-
6342
override fun map(
6443
view: ReactEditText,
6544
mappingContext: MappingContext,
@@ -88,7 +67,6 @@ internal class ReactEditTextMapper(
8867
wireframes = backgroundWireframes,
8968
view = view,
9069
mappingContext = mappingContext,
91-
reactTextPropertiesResolver = reactTextPropertiesResolver
9270
)
9371
}
9472

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package com.datadog.reactnative.sessionreplay.mappers
88

99
import android.widget.TextView
1010
import com.datadog.android.api.InternalLogger
11-
import com.datadog.android.sessionreplay.SessionReplayPrivacy
1211
import com.datadog.android.sessionreplay.model.MobileSegment
1312
import com.datadog.android.sessionreplay.recorder.MappingContext
1413
import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
@@ -17,50 +16,29 @@ import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter
1716
import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
1817
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
1918
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
20-
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
21-
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
22-
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
23-
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
24-
import com.facebook.react.bridge.ReactContext
25-
import com.facebook.react.uimanager.UIManagerModule
19+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
2620

2721
internal class ReactTextMapper(
28-
private val reactTextPropertiesResolver: TextPropertiesResolver =
29-
NoopTextPropertiesResolver(),
30-
private val textViewUtils: TextViewUtils = TextViewUtils(),
22+
private val textViewUtils: TextViewUtils
3123
): TextViewMapper<TextView>(
3224
viewIdentifierResolver = DefaultViewIdentifierResolver,
3325
colorStringFormatter = DefaultColorStringFormatter,
3426
viewBoundsResolver = DefaultViewBoundsResolver,
3527
drawableToColorMapper = DrawableToColorMapper.getDefault()
3628
) {
3729

38-
internal constructor(
39-
reactContext: ReactContext,
40-
uiManagerModule: UIManagerModule?
41-
): this(
42-
reactTextPropertiesResolver = if (uiManagerModule == null) {
43-
NoopTextPropertiesResolver()
44-
} else {
45-
ReactTextPropertiesResolver(
46-
reactContext = reactContext,
47-
uiManagerModule = uiManagerModule
48-
)
49-
}
50-
)
51-
5230
override fun map(
5331
view: TextView,
5432
mappingContext: MappingContext,
5533
asyncJobStatusCallback: AsyncJobStatusCallback,
5634
internalLogger: InternalLogger
5735
): List<MobileSegment.Wireframe> {
5836
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)
37+
5938
return textViewUtils.mapTextViewToWireframes(
6039
wireframes = wireframes,
6140
view = view,
6241
mappingContext = mappingContext,
63-
reactTextPropertiesResolver = reactTextPropertiesResolver
6442
)
6543
}
6644
}

packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtils.kt

Lines changed: 0 additions & 40 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.datadog.reactnative.sessionreplay.utils.text
2+
3+
import android.text.Spannable
4+
import android.text.style.ForegroundColorSpan
5+
import android.view.View
6+
import android.widget.TextView
7+
import com.datadog.android.api.InternalLogger
8+
import com.datadog.android.sessionreplay.model.MobileSegment
9+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
10+
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
11+
import com.facebook.react.bridge.ReactContext
12+
import java.util.Locale
13+
14+
internal class FabricTextViewUtils(private val reactContext: ReactContext, private val logger: InternalLogger, drawableUtils: DrawableUtils): TextViewUtils(reactContext, drawableUtils) {
15+
16+
override fun resolveTextStyle(
17+
textWireframe: MobileSegment.Wireframe.TextWireframe,
18+
pixelsDensity: Float,
19+
view: TextView
20+
): MobileSegment.TextStyle {
21+
22+
val fontColor = getTextColor(view, textWireframe)
23+
val fontSize = getFontSize(view, pixelsDensity)
24+
val fontFamily = getFontFamily(textWireframe)
25+
26+
return MobileSegment.TextStyle(
27+
family = fontFamily,
28+
size = fontSize,
29+
color = fontColor
30+
)
31+
}
32+
33+
private fun getTextColor(view: TextView, textWireframe: MobileSegment.Wireframe.TextWireframe): String {
34+
val spanned = getFieldFromView(view, SPANNED_FIELD_NAME) as? Spannable
35+
val spans = spanned?.getSpans(0, spanned.length, ForegroundColorSpan::class.java)
36+
val fontColor = spans?.firstOrNull()?.foregroundColor?.let { formatAsRgba(it) } ?: textWireframe.textStyle.color
37+
38+
return fontColor
39+
}
40+
41+
private fun getFontSize(view: TextView, pixelsDensity: Float): Long {
42+
val fontSize = (view.textSize / pixelsDensity).toLong()
43+
return fontSize
44+
}
45+
46+
private fun getFontFamily(textWireframe: MobileSegment.Wireframe.TextWireframe): String {
47+
val fontFamily = textWireframe.textStyle.family
48+
return resolveFontFamily(fontFamily.lowercase(Locale.US))
49+
}
50+
51+
internal fun getFieldFromView(view: View, value: String): Any? {
52+
try {
53+
val field = view.javaClass.getDeclaredField(value)
54+
field.isAccessible = true
55+
return field.get(view)
56+
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
57+
when (e) {
58+
is NoSuchFieldException -> handleError(e, RESOLVE_FABRICFIELD_ERROR)
59+
is NullPointerException -> handleError(e, NULL_FABRICFIELD_ERROR)
60+
else -> handleError(e, RESOLVE_FABRICFIELD_ERROR)
61+
}
62+
return null
63+
}
64+
}
65+
66+
private fun handleError(e: Exception, message: String) {
67+
logger.log(
68+
level = InternalLogger.Level.WARN,
69+
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
70+
messageBuilder = { message },
71+
throwable = e
72+
)
73+
}
74+
}

0 commit comments

Comments
 (0)