@datadog/mobile-react-native-session-replay 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +5 -5
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +5 -29
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ShadowNodeWrapper.kt +2 -2
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt +3 -26
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt +3 -25
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt +74 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/LegacyTextViewUtils.kt +118 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolver.kt → utils/text/TextViewUtils.kt} +60 -96
- package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -2
- package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
- package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -1
- package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
- package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +2 -7
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +3 -17
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolverTest.kt → utils/text/TextViewUtilsTest.kt} +171 -38
- package/ios/Sources/DdSessionReplay.mm +4 -4
- package/ios/Sources/DdSessionReplayImplementation.swift +13 -3
- package/ios/Sources/RCTFabricWrapper.h +13 -0
- package/ios/Sources/RCTFabricWrapper.mm +120 -0
- package/ios/Sources/RCTTextPropertiesWrapper.h +23 -0
- package/ios/Sources/RCTTextPropertiesWrapper.mm +28 -0
- package/ios/Sources/RCTTextViewRecorder.swift +69 -49
- package/ios/Sources/RCTVersion.h +8 -0
- package/package.json +5 -3
- package/scripts/set-ios-rn-version.js +47 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt +0 -22
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtils.kt +0 -40
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtilsTest.kt +0 -109
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Mobile Session Replay expands visibility into your mobile applications by visual
|
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
|
-
**Note
|
|
7
|
+
**Note**: Make sure you’ve setup and initialized the [Datadog React Native SDK][1] with views instrumentation enabled.
|
|
8
8
|
|
|
9
9
|
To install with NPM, run:
|
|
10
10
|
|
|
@@ -26,14 +26,14 @@ To enable Session Replay, import and call the `enable` method with your configur
|
|
|
26
26
|
import { SessionReplay } from "@datadog/mobile-react-native-session-replay";
|
|
27
27
|
|
|
28
28
|
SessionReplay.enable({
|
|
29
|
-
replaySampleRate: sampleRate // The percentage of sampled replays, in the range 0.0 - 100.0 (Default: 100.0).
|
|
29
|
+
replaySampleRate: sampleRate, // The percentage of sampled replays, in the range 0.0 - 100.0 (Default: 100.0).
|
|
30
30
|
textAndInputPrivacyLevel: TextAndInputPrivacyLevel.MASK_ALL, // Defines the way text and input (e.g text fields, checkboxes) should be masked (Default: `MASK_ALL`).
|
|
31
31
|
imagePrivacyLevel: ImagePrivacyLevel.MASK_ALL, // Defines the way images should be masked (Default: `MASK_ALL`).
|
|
32
32
|
touchPrivacyLevel: TouchPrivacyLevel.HIDE // Defines the way user touches (e.g tap) should be masked (Default: `HIDE`).
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
**Note
|
|
36
|
+
**Note**: All configuration properties are optional and should be adjusted based on your application's needs.
|
|
37
37
|
|
|
38
38
|
## Start or stop the recording manually
|
|
39
39
|
|
|
@@ -6,15 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay
|
|
8
8
|
|
|
9
|
+
import android.annotation.SuppressLint
|
|
9
10
|
import com.datadog.android.api.feature.FeatureSdkCore
|
|
10
|
-
import com.datadog.android.sessionreplay.ImagePrivacy
|
|
11
11
|
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
|
12
|
-
import com.datadog.android.sessionreplay.TextAndInputPrivacy
|
|
13
|
-
import com.datadog.android.sessionreplay.TouchPrivacy
|
|
14
12
|
import com.datadog.reactnative.DatadogSDKWrapperStorage
|
|
13
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
|
|
15
14
|
import com.facebook.react.bridge.Promise
|
|
16
15
|
import com.facebook.react.bridge.ReactContext
|
|
17
|
-
import java.util.Locale
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
18
|
* The entry point to use Datadog's Session Replay feature.
|
|
@@ -33,6 +31,7 @@ class DdSessionReplayImplementation(
|
|
|
33
31
|
* @param customEndpoint Custom server url for sending replay data.
|
|
34
32
|
* @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled.
|
|
35
33
|
*/
|
|
34
|
+
@SuppressLint("VisibleForTests")
|
|
36
35
|
fun enable(
|
|
37
36
|
replaySampleRate: Double,
|
|
38
37
|
customEndpoint: String,
|
|
@@ -42,12 +41,13 @@ class DdSessionReplayImplementation(
|
|
|
42
41
|
) {
|
|
43
42
|
val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
|
|
44
43
|
val logger = sdkCore.internalLogger
|
|
44
|
+
val textViewUtils = TextViewUtils.create(reactContext, logger)
|
|
45
45
|
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
|
|
46
46
|
.startRecordingImmediately(startRecordingImmediately)
|
|
47
47
|
.setImagePrivacy(privacySettings.imagePrivacyLevel)
|
|
48
48
|
.setTouchPrivacy(privacySettings.touchPrivacyLevel)
|
|
49
49
|
.setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel)
|
|
50
|
-
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(
|
|
50
|
+
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(textViewUtils))
|
|
51
51
|
|
|
52
52
|
if (customEndpoint != "") {
|
|
53
53
|
configuration.useCustomEndpoint(customEndpoint)
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay
|
|
8
8
|
|
|
9
|
-
import androidx.annotation.VisibleForTesting
|
|
10
|
-
import com.datadog.android.api.InternalLogger
|
|
11
9
|
import com.datadog.android.sessionreplay.ExtensionSupport
|
|
12
10
|
import com.datadog.android.sessionreplay.MapperTypeWrapper
|
|
13
11
|
import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
|
|
@@ -16,47 +14,29 @@ import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
|
|
|
16
14
|
import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
|
|
17
15
|
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
|
|
18
16
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
|
|
19
|
-
import com.
|
|
20
|
-
import com.facebook.react.uimanager.UIManagerModule
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
|
|
21
18
|
import com.facebook.react.views.image.ReactImageView
|
|
22
19
|
import com.facebook.react.views.text.ReactTextView
|
|
23
20
|
import com.facebook.react.views.textinput.ReactEditText
|
|
24
21
|
import com.facebook.react.views.view.ReactViewGroup
|
|
25
22
|
|
|
23
|
+
|
|
26
24
|
internal class ReactNativeSessionReplayExtensionSupport(
|
|
27
|
-
private val
|
|
28
|
-
private val logger: InternalLogger
|
|
25
|
+
private val textViewUtils: TextViewUtils
|
|
29
26
|
) : ExtensionSupport {
|
|
30
27
|
override fun name(): String {
|
|
31
28
|
return ReactNativeSessionReplayExtensionSupport::class.java.simpleName
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
override fun getCustomViewMappers(): List<MapperTypeWrapper<*>> {
|
|
35
|
-
val uiManagerModule = getUiManagerModule()
|
|
36
|
-
|
|
37
32
|
return listOf(
|
|
38
33
|
MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()),
|
|
39
34
|
MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()),
|
|
40
|
-
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(
|
|
41
|
-
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(
|
|
35
|
+
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(textViewUtils)),
|
|
36
|
+
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(textViewUtils)),
|
|
42
37
|
)
|
|
43
38
|
}
|
|
44
39
|
|
|
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
|
-
|
|
60
40
|
override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
|
|
61
41
|
return listOf()
|
|
62
42
|
}
|
|
@@ -64,8 +44,4 @@ internal class ReactNativeSessionReplayExtensionSupport(
|
|
|
64
44
|
override fun getCustomDrawableMapper(): List<DrawableToColorMapper> {
|
|
65
45
|
return emptyList()
|
|
66
46
|
}
|
|
67
|
-
|
|
68
|
-
internal companion object {
|
|
69
|
-
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
|
|
70
|
-
}
|
|
71
47
|
}
|
|
@@ -33,7 +33,7 @@ internal class ShadowNodeWrapper(
|
|
|
33
33
|
internal companion object {
|
|
34
34
|
internal fun getShadowNodeWrapper(
|
|
35
35
|
reactContext: ReactContext,
|
|
36
|
-
uiManagerModule: UIManagerModule
|
|
36
|
+
uiManagerModule: UIManagerModule?,
|
|
37
37
|
reflectionUtils: ReflectionUtils,
|
|
38
38
|
viewId: Int
|
|
39
39
|
): ShadowNodeWrapper? {
|
|
@@ -41,7 +41,7 @@ internal class ShadowNodeWrapper(
|
|
|
41
41
|
var target: ReactShadowNode<out ReactShadowNode<*>>? = null
|
|
42
42
|
|
|
43
43
|
val shadowNodeRunnable = Runnable {
|
|
44
|
-
val node = resolveShadowNode(reflectionUtils,
|
|
44
|
+
val node = uiManagerModule?.let { resolveShadowNode(reflectionUtils, it, viewId) }
|
|
45
45
|
if (node != null) {
|
|
46
46
|
target = node
|
|
47
47
|
}
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt
CHANGED
|
@@ -19,19 +19,11 @@ import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
|
|
|
19
19
|
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
|
|
20
20
|
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
|
|
21
21
|
import com.datadog.android.sessionreplay.utils.GlobalBounds
|
|
22
|
-
import com.datadog.reactnative.sessionreplay.
|
|
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
|
|
28
|
-
import com.facebook.react.views.image.ReactImageView
|
|
22
|
+
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
|
|
29
23
|
import com.facebook.react.views.textinput.ReactEditText
|
|
30
24
|
|
|
31
25
|
internal class ReactEditTextMapper(
|
|
32
|
-
private val
|
|
33
|
-
NoopTextPropertiesResolver(),
|
|
34
|
-
private val textViewUtils: TextViewUtils = TextViewUtils(),
|
|
26
|
+
private val textViewUtils: TextViewUtils
|
|
35
27
|
) : BaseAsyncBackgroundWireframeMapper<ReactEditText>(
|
|
36
28
|
viewIdentifierResolver = DefaultViewIdentifierResolver,
|
|
37
29
|
colorStringFormatter = DefaultColorStringFormatter,
|
|
@@ -47,20 +39,6 @@ internal class ReactEditTextMapper(
|
|
|
47
39
|
drawableToColorMapper = drawableToColorMapper,
|
|
48
40
|
)
|
|
49
41
|
|
|
50
|
-
internal constructor(
|
|
51
|
-
reactContext: ReactContext,
|
|
52
|
-
uiManagerModule: UIManagerModule?
|
|
53
|
-
) : this(
|
|
54
|
-
reactTextPropertiesResolver = if (uiManagerModule == null) {
|
|
55
|
-
NoopTextPropertiesResolver()
|
|
56
|
-
} else {
|
|
57
|
-
ReactTextPropertiesResolver(
|
|
58
|
-
reactContext = reactContext,
|
|
59
|
-
uiManagerModule = uiManagerModule
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
)
|
|
63
|
-
|
|
64
42
|
override fun map(
|
|
65
43
|
view: ReactEditText,
|
|
66
44
|
mappingContext: MappingContext,
|
|
@@ -89,7 +67,6 @@ internal class ReactEditTextMapper(
|
|
|
89
67
|
wireframes = backgroundWireframes,
|
|
90
68
|
view = view,
|
|
91
69
|
mappingContext = mappingContext,
|
|
92
|
-
reactTextPropertiesResolver = reactTextPropertiesResolver
|
|
93
70
|
)
|
|
94
71
|
}
|
|
95
72
|
|
|
@@ -102,7 +79,7 @@ internal class ReactEditTextMapper(
|
|
|
102
79
|
mappingContext: MappingContext,
|
|
103
80
|
asyncJobStatusCallback: AsyncJobStatusCallback
|
|
104
81
|
): MobileSegment.Wireframe? {
|
|
105
|
-
if (view !is
|
|
82
|
+
if (view !is ReactEditText) {
|
|
106
83
|
return super.resolveBackgroundAsImageWireframe(
|
|
107
84
|
view,
|
|
108
85
|
bounds,
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt
CHANGED
|
@@ -8,7 +8,6 @@ package com.datadog.reactnative.sessionreplay.mappers
|
|
|
8
8
|
|
|
9
9
|
import android.widget.TextView
|
|
10
10
|
import com.datadog.android.api.InternalLogger
|
|
11
|
-
import com.datadog.android.sessionreplay.SessionReplayPrivacy
|
|
12
11
|
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
13
12
|
import com.datadog.android.sessionreplay.recorder.MappingContext
|
|
14
13
|
import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
|
|
@@ -17,17 +16,10 @@ import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter
|
|
|
17
16
|
import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
|
|
18
17
|
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
|
|
19
18
|
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
|
|
20
|
-
import com.datadog.reactnative.sessionreplay.
|
|
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
|
|
26
20
|
|
|
27
21
|
internal class ReactTextMapper(
|
|
28
|
-
private val
|
|
29
|
-
NoopTextPropertiesResolver(),
|
|
30
|
-
private val textViewUtils: TextViewUtils = TextViewUtils(),
|
|
22
|
+
private val textViewUtils: TextViewUtils
|
|
31
23
|
): TextViewMapper<TextView>(
|
|
32
24
|
viewIdentifierResolver = DefaultViewIdentifierResolver,
|
|
33
25
|
colorStringFormatter = DefaultColorStringFormatter,
|
|
@@ -35,20 +27,6 @@ internal class ReactTextMapper(
|
|
|
35
27
|
drawableToColorMapper = DrawableToColorMapper.getDefault()
|
|
36
28
|
) {
|
|
37
29
|
|
|
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
|
-
|
|
52
30
|
override fun map(
|
|
53
31
|
view: TextView,
|
|
54
32
|
mappingContext: MappingContext,
|
|
@@ -56,11 +34,11 @@ internal class ReactTextMapper(
|
|
|
56
34
|
internalLogger: InternalLogger
|
|
57
35
|
): List<MobileSegment.Wireframe> {
|
|
58
36
|
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)
|
|
37
|
+
|
|
59
38
|
return textViewUtils.mapTextViewToWireframes(
|
|
60
39
|
wireframes = wireframes,
|
|
61
40
|
view = view,
|
|
62
41
|
mappingContext = mappingContext,
|
|
63
|
-
reactTextPropertiesResolver = reactTextPropertiesResolver
|
|
64
42
|
)
|
|
65
43
|
}
|
|
66
44
|
}
|
|
@@ -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,0 +1,118 @@
|
|
|
1
|
+
package com.datadog.reactnative.sessionreplay.utils.text
|
|
2
|
+
|
|
3
|
+
import android.widget.TextView
|
|
4
|
+
import androidx.annotation.VisibleForTesting
|
|
5
|
+
import com.datadog.android.api.InternalLogger
|
|
6
|
+
import com.datadog.android.internal.utils.densityNormalized
|
|
7
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
8
|
+
import com.datadog.reactnative.sessionreplay.ShadowNodeWrapper
|
|
9
|
+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
10
|
+
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
|
|
11
|
+
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
12
|
+
import com.facebook.react.bridge.ReactContext
|
|
13
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
14
|
+
import com.facebook.react.views.text.TextAttributes
|
|
15
|
+
import java.util.Locale
|
|
16
|
+
|
|
17
|
+
internal class LegacyTextViewUtils(
|
|
18
|
+
private val reactContext: ReactContext,
|
|
19
|
+
private val logger: InternalLogger,
|
|
20
|
+
private val reflectionUtils: ReflectionUtils,
|
|
21
|
+
drawableUtils: DrawableUtils,
|
|
22
|
+
) : TextViewUtils(reactContext, drawableUtils) {
|
|
23
|
+
|
|
24
|
+
private val uiManager: UIManagerModule? by lazy {
|
|
25
|
+
getUiManagerModule()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun resolveTextStyle(
|
|
29
|
+
textWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
30
|
+
pixelsDensity: Float,
|
|
31
|
+
view: TextView,
|
|
32
|
+
): MobileSegment.TextStyle? {
|
|
33
|
+
val shadowNodeWrapper: ShadowNodeWrapper =
|
|
34
|
+
ShadowNodeWrapper.getShadowNodeWrapper(
|
|
35
|
+
reactContext = reactContext,
|
|
36
|
+
uiManagerModule = uiManager,
|
|
37
|
+
reflectionUtils = reflectionUtils,
|
|
38
|
+
viewId = view.id,
|
|
39
|
+
) ?: return null
|
|
40
|
+
|
|
41
|
+
val fontFamily = getFontFamily(shadowNodeWrapper) ?: textWireframe.textStyle.family
|
|
42
|
+
|
|
43
|
+
val fontSize = getFontSize(shadowNodeWrapper)?.densityNormalized(pixelsDensity) ?: textWireframe.textStyle.size
|
|
44
|
+
|
|
45
|
+
val fontColor = getTextColor(shadowNodeWrapper) ?: textWireframe.textStyle.color
|
|
46
|
+
|
|
47
|
+
return MobileSegment.TextStyle(
|
|
48
|
+
family = fontFamily,
|
|
49
|
+
size = fontSize,
|
|
50
|
+
color = fontColor,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun getTextColor(shadowNodeWrapper: ShadowNodeWrapper?): String? {
|
|
55
|
+
if (shadowNodeWrapper == null) return null
|
|
56
|
+
|
|
57
|
+
val isColorSet =
|
|
58
|
+
shadowNodeWrapper
|
|
59
|
+
.getDeclaredShadowNodeField(IS_COLOR_SET_FIELD_NAME) as Boolean?
|
|
60
|
+
|
|
61
|
+
if (isColorSet != true) {
|
|
62
|
+
// Improvement: get default text color if different from black
|
|
63
|
+
return "#000000FF"
|
|
64
|
+
}
|
|
65
|
+
val resolvedColor =
|
|
66
|
+
shadowNodeWrapper
|
|
67
|
+
.getDeclaredShadowNodeField(COLOR_FIELD_NAME) as Int?
|
|
68
|
+
if (resolvedColor != null) {
|
|
69
|
+
return formatAsRgba(resolvedColor)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun getFontSize(shadowNodeWrapper: ShadowNodeWrapper?): Long? {
|
|
76
|
+
if (shadowNodeWrapper == null) return null
|
|
77
|
+
|
|
78
|
+
val textAttributes =
|
|
79
|
+
shadowNodeWrapper
|
|
80
|
+
.getDeclaredShadowNodeField(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
|
|
81
|
+
if (textAttributes != null) {
|
|
82
|
+
return textAttributes.effectiveFontSize.toLong()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun getFontFamily(shadowNodeWrapper: ShadowNodeWrapper?): String? {
|
|
89
|
+
if (shadowNodeWrapper == null) return null
|
|
90
|
+
|
|
91
|
+
val fontFamily =
|
|
92
|
+
shadowNodeWrapper
|
|
93
|
+
.getDeclaredShadowNodeField(FONT_FAMILY_FIELD_NAME) as? String
|
|
94
|
+
|
|
95
|
+
if (fontFamily != null) {
|
|
96
|
+
return resolveFontFamily(fontFamily.lowercase(Locale.US))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// store to avoid calling it multiple times
|
|
103
|
+
@VisibleForTesting
|
|
104
|
+
internal fun getUiManagerModule(): UIManagerModule? {
|
|
105
|
+
return try {
|
|
106
|
+
reactContext.getNativeModule(UIManagerModule::class.java)
|
|
107
|
+
} catch (e: IllegalStateException) {
|
|
108
|
+
logger.log(
|
|
109
|
+
level = InternalLogger.Level.WARN,
|
|
110
|
+
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
|
|
111
|
+
messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
|
|
112
|
+
throwable = e,
|
|
113
|
+
)
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|