Skip to content

Commit baf0cfa

Browse files
Android: added ReactImageViewMapper for SR Image Recording
1 parent 7e201e3 commit baf0cfa

File tree

12 files changed

+557
-18
lines changed

12 files changed

+557
-18
lines changed

packages/core/android/build.gradle

+4-4
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,10 @@ dependencies {
192192
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
193193
compileOnly "com.squareup.okhttp3:okhttp:3.12.13"
194194

195-
implementation "com.datadoghq:dd-sdk-android-rum:2.14.0"
196-
implementation "com.datadoghq:dd-sdk-android-logs:2.14.0"
197-
implementation "com.datadoghq:dd-sdk-android-trace:2.14.0"
198-
implementation "com.datadoghq:dd-sdk-android-webview:2.14.0"
195+
implementation "com.datadoghq:dd-sdk-android-rum:2.17.0-SNAPSHOT"
196+
implementation "com.datadoghq:dd-sdk-android-logs:2.17.0-SNAPSHOT"
197+
implementation "com.datadoghq:dd-sdk-android-trace:2.17.0-SNAPSHOT"
198+
implementation "com.datadoghq:dd-sdk-android-webview:2.17.0-SNAPSHOT"
199199
implementation "com.google.code.gson:gson:2.10.0"
200200
testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
201201
testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"

packages/react-native-session-replay/android/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ dependencies {
188188
api "com.facebook.react:react-android:$reactNativeVersion"
189189
}
190190
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
191-
implementation "com.datadoghq:dd-sdk-android-session-replay:2.14.0"
191+
implementation "com.datadoghq:dd-sdk-android-session-replay:2.17.0-SNAPSHOT"
192192
implementation project(path: ':datadog_mobile-react-native')
193193

194194
testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"

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

+15-4
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import com.datadog.android.api.InternalLogger
1111
import com.datadog.android.sessionreplay.ExtensionSupport
1212
import com.datadog.android.sessionreplay.MapperTypeWrapper
1313
import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
14+
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
1415
import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
16+
import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
1517
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
1618
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
1719
import com.facebook.react.bridge.ReactContext
1820
import com.facebook.react.uimanager.UIManagerModule
21+
import com.facebook.react.views.image.ReactImageView
1922
import com.facebook.react.views.text.ReactTextView
2023
import com.facebook.react.views.textinput.ReactEditText
2124
import com.facebook.react.views.view.ReactViewGroup
@@ -24,21 +27,21 @@ internal class ReactNativeSessionReplayExtensionSupport(
2427
private val reactContext: ReactContext,
2528
private val logger: InternalLogger
2629
) : ExtensionSupport {
30+
override fun name(): String {
31+
return ReactNativeSessionReplayExtensionSupport::class.java.simpleName
32+
}
2733

2834
override fun getCustomViewMappers(): List<MapperTypeWrapper<*>> {
2935
val uiManagerModule = getUiManagerModule()
3036

3137
return listOf(
38+
MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()),
3239
MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()),
3340
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(reactContext, uiManagerModule)),
3441
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(reactContext, uiManagerModule)),
3542
)
3643
}
3744

38-
override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
39-
return listOf()
40-
}
41-
4245
@VisibleForTesting
4346
internal fun getUiManagerModule(): UIManagerModule? {
4447
return try {
@@ -54,6 +57,14 @@ internal class ReactNativeSessionReplayExtensionSupport(
5457
}
5558
}
5659

60+
override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
61+
return listOf()
62+
}
63+
64+
override fun getCustomDrawableMapper(): List<DrawableToColorMapper> {
65+
return emptyList()
66+
}
67+
5768
internal companion object {
5869
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
5970
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import android.view.Gravity
1010
import android.widget.TextView
1111
import androidx.annotation.VisibleForTesting
1212
import com.datadog.android.sessionreplay.model.MobileSegment
13-
import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized
13+
import com.datadog.reactnative.sessionreplay.extensions.densityNormalized
1414
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
1515
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
1616
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
@@ -136,7 +136,7 @@ internal class ReactTextPropertiesResolver(
136136
val fontFamily = getFontFamily(shadowNodeWrapper)
137137
?: textWireframe.textStyle.family
138138
val fontSize = getFontSize(shadowNodeWrapper)
139-
?.convertToDensityNormalized(pixelsDensity)
139+
?.densityNormalized(pixelsDensity)
140140
?: textWireframe.textStyle.size
141141
val fontColor = getTextColor(shadowNodeWrapper)
142142
?: textWireframe.textStyle.color
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.reactnative.sessionreplay.extensions
8+
9+
internal fun Int.densityNormalized(density: Float): Int {
10+
if (density == 0f) {
11+
return this
12+
}
13+
return (this / density).toInt()
14+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
package com.datadog.reactnative.sessionreplay.extensions
88

9-
internal fun Long.convertToDensityNormalized(density: Float): Long {
9+
internal fun Long.densityNormalized(density: Float): Long {
1010
return if (density == 0f) {
1111
this
1212
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.reactnative.sessionreplay.extensions
8+
9+
import android.content.res.Resources
10+
import android.graphics.Bitmap
11+
import android.graphics.drawable.BitmapDrawable
12+
import android.graphics.drawable.Drawable
13+
import android.graphics.drawable.ShapeDrawable
14+
import android.graphics.drawable.VectorDrawable
15+
import android.widget.ImageView
16+
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
17+
import androidx.core.graphics.drawable.toBitmapOrNull
18+
import com.facebook.drawee.drawable.ArrayDrawable
19+
import com.facebook.drawee.drawable.ForwardingDrawable
20+
import com.facebook.drawee.drawable.RoundedBitmapDrawable
21+
import com.facebook.drawee.drawable.ScaleTypeDrawable
22+
import com.facebook.drawee.drawable.ScalingUtils
23+
24+
internal fun ScaleTypeDrawable.imageViewScaleType(): ImageView.ScaleType? {
25+
return when (this.scaleType) {
26+
ScalingUtils.ScaleType.CENTER -> ImageView.ScaleType.CENTER
27+
ScalingUtils.ScaleType.CENTER_CROP -> ImageView.ScaleType.CENTER_CROP
28+
ScalingUtils.ScaleType.CENTER_INSIDE -> ImageView.ScaleType.CENTER_INSIDE
29+
ScalingUtils.ScaleType.FIT_CENTER -> ImageView.ScaleType.FIT_CENTER
30+
ScalingUtils.ScaleType.FIT_START -> ImageView.ScaleType.FIT_START
31+
ScalingUtils.ScaleType.FIT_END -> ImageView.ScaleType.FIT_END
32+
ScalingUtils.ScaleType.FIT_XY -> ImageView.ScaleType.FIT_XY
33+
else -> null
34+
}
35+
}
36+
37+
internal fun ArrayDrawable.getScaleTypeDrawable(): ScaleTypeDrawable? {
38+
for (i in 0 until this.numberOfLayers) {
39+
try {
40+
(this.getDrawable(i) as? ScaleTypeDrawable)?.let {
41+
return it
42+
}
43+
} catch(_: IllegalArgumentException) { }
44+
}
45+
46+
return null
47+
}
48+
49+
internal fun ArrayDrawable.getDrawableOrNull(index: Int): Drawable? {
50+
return try {
51+
this.getDrawable(index)
52+
} catch (_: IllegalArgumentException) {
53+
null
54+
}
55+
}
56+
57+
internal fun ForwardingDrawable.tryToExtractBitmap(resources: Resources): Bitmap? {
58+
val forwardedDrawable = this.drawable
59+
return if (forwardedDrawable != null) {
60+
forwardedDrawable.tryToExtractBitmap(resources)
61+
} else {
62+
this.toBitmapOrNull(
63+
this.intrinsicWidth,
64+
this.intrinsicHeight,
65+
Bitmap.Config.ARGB_8888
66+
)
67+
}
68+
}
69+
70+
internal fun RoundedBitmapDrawable.tryToExtractBitmap(): Bitmap? {
71+
val privateBitmap = try {
72+
val field = RoundedBitmapDrawable::class.java.getDeclaredField("mBitmap")
73+
field.isAccessible = true
74+
field.get(this) as? Bitmap
75+
} catch (_: NoSuchFieldException) {
76+
null
77+
} catch (_: IllegalAccessException) {
78+
null
79+
} catch (_: Exception) {
80+
null
81+
}
82+
83+
return privateBitmap ?: this.toBitmapOrNull(
84+
this.intrinsicWidth,
85+
this.intrinsicHeight,
86+
Bitmap.Config.ARGB_8888
87+
)
88+
}
89+
90+
internal fun BitmapDrawable.tryToExtractBitmap(resources: Resources): Bitmap? {
91+
if (this.bitmap != null) {
92+
return this.bitmap
93+
}
94+
95+
if (this.constantState != null) {
96+
val copy = this.constantState?.newDrawable(resources)
97+
return (copy as? BitmapDrawable)?.bitmap ?: copy?.toBitmapOrNull(
98+
this.intrinsicWidth,
99+
this.intrinsicHeight,
100+
Bitmap.Config.ARGB_8888
101+
)
102+
}
103+
104+
return null
105+
}
106+
107+
internal fun ArrayDrawable.tryToExtractBitmap(resources: Resources): Bitmap? {
108+
var width = 0
109+
var height = 0
110+
for (index in 0 until this.numberOfLayers) {
111+
val drawable = this.getDrawableOrNull(index) ?: continue
112+
113+
if (drawable is ScaleTypeDrawable) {
114+
return drawable.tryToExtractBitmap(resources)
115+
}
116+
117+
if (drawable.intrinsicWidth * drawable.intrinsicHeight > width * height) {
118+
width = drawable.intrinsicWidth
119+
height = drawable.intrinsicHeight
120+
}
121+
}
122+
123+
return if (width > 0 && height > 0)
124+
this.toBitmapOrNull(width, height, Bitmap.Config.ARGB_8888)
125+
else
126+
null
127+
}
128+
129+
internal fun Drawable.tryToExtractBitmap(
130+
resources: Resources
131+
): Bitmap? {
132+
when (this) {
133+
is ArrayDrawable -> {
134+
return this.tryToExtractBitmap(resources)
135+
}
136+
is ForwardingDrawable -> {
137+
return this.tryToExtractBitmap(resources)
138+
}
139+
is RoundedBitmapDrawable -> {
140+
return this.tryToExtractBitmap()
141+
}
142+
is BitmapDrawable -> {
143+
return this.tryToExtractBitmap(resources)
144+
}
145+
is VectorDrawable, is ShapeDrawable, is DrawerArrowDrawable -> {
146+
return this.toBitmapOrNull(
147+
this.intrinsicWidth,
148+
this.intrinsicHeight,
149+
Bitmap.Config.ARGB_8888
150+
)
151+
}
152+
else -> return null
153+
}
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.reactnative.sessionreplay.mappers
8+
9+
import com.datadog.android.api.InternalLogger
10+
import com.datadog.android.sessionreplay.model.MobileSegment
11+
import com.datadog.android.sessionreplay.recorder.MappingContext
12+
import com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper
13+
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
14+
import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter
15+
import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
16+
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
17+
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
18+
import com.datadog.reactnative.sessionreplay.extensions.densityNormalized
19+
import com.datadog.reactnative.sessionreplay.extensions.getScaleTypeDrawable
20+
import com.datadog.reactnative.sessionreplay.extensions.imageViewScaleType
21+
import com.datadog.reactnative.sessionreplay.resources.ReactDrawableCopier
22+
import com.datadog.reactnative.sessionreplay.utils.ImageViewUtils
23+
import com.facebook.drawee.drawable.FadeDrawable
24+
import com.facebook.react.views.image.ReactImageView
25+
26+
internal class ReactNativeImageViewMapper: BaseAsyncBackgroundWireframeMapper<ReactImageView>(
27+
viewIdentifierResolver = DefaultViewIdentifierResolver,
28+
colorStringFormatter = DefaultColorStringFormatter,
29+
viewBoundsResolver = DefaultViewBoundsResolver,
30+
drawableToColorMapper = DrawableToColorMapper.getDefault()
31+
) {
32+
private val drawableCopier = ReactDrawableCopier()
33+
34+
override fun map(
35+
view: ReactImageView,
36+
mappingContext: MappingContext,
37+
asyncJobStatusCallback: AsyncJobStatusCallback,
38+
internalLogger: InternalLogger
39+
): List<MobileSegment.Wireframe> {
40+
val wireframes = mutableListOf<MobileSegment.Wireframe>()
41+
wireframes.addAll(super.map(view, mappingContext, asyncJobStatusCallback, internalLogger))
42+
43+
val drawable = view.drawable?.current ?: return wireframes
44+
45+
val parentRect = ImageViewUtils.resolveParentRectAbsPosition(view)
46+
val scaleType = (drawable as? FadeDrawable)
47+
?.getScaleTypeDrawable()
48+
?.imageViewScaleType() ?: view.scaleType
49+
val contentRect = ImageViewUtils.resolveContentRectWithScaling(view, drawable, scaleType)
50+
51+
val resources = view.resources
52+
val density = resources.displayMetrics.density
53+
54+
val clipping = if (view.cropToPadding) {
55+
ImageViewUtils.calculateClipping(parentRect, contentRect, density)
56+
} else {
57+
null
58+
}
59+
60+
val contentXPosInDp = contentRect.left.densityNormalized(density).toLong()
61+
val contentYPosInDp = contentRect.top.densityNormalized(density).toLong()
62+
val contentWidthPx = contentRect.width()
63+
val contentHeightPx = contentRect.height()
64+
65+
// resolve foreground
66+
mappingContext.imageWireframeHelper.createImageWireframeByDrawable(
67+
view = view,
68+
imagePrivacy = mappingContext.imagePrivacy,
69+
currentWireframeIndex = wireframes.size,
70+
x = contentXPosInDp,
71+
y = contentYPosInDp,
72+
width = contentWidthPx,
73+
height = contentHeightPx,
74+
usePIIPlaceholder = true,
75+
drawable = drawable,
76+
drawableCopier = drawableCopier,
77+
asyncJobStatusCallback = asyncJobStatusCallback,
78+
clipping = clipping,
79+
shapeStyle = null,
80+
border = null,
81+
prefix = "drawable",
82+
resourceIdCacheKey = generateUUID(view)
83+
)?.let {
84+
wireframes.add(it)
85+
}
86+
87+
return wireframes
88+
}
89+
90+
private fun generateUUID(reactImageView: ReactImageView): String {
91+
val source = reactImageView.imageSource?.source ?:
92+
System.identityHashCode(reactImageView).toString()
93+
val drawableType = reactImageView.drawable.current::class.java.name
94+
return "${drawableType}-${source}"
95+
}
96+
}
97+

0 commit comments

Comments
 (0)