diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 8d833e098..2ec53c556 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -134,6 +134,12 @@ android { } else { java.srcDirs += ['src/oldarch/kotlin'] } + + if (reactNativeMinorVersion >= 75) { + java.srcDirs += ['src/rn75/kotlin'] + } else { + java.srcDirs += ['src/rnlegacy/kotlin'] + } } test { java.srcDir("src/test/kotlin") diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt index 7e39bac47..91a9d6560 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt @@ -6,28 +6,26 @@ package com.datadog.reactnative.sessionreplay +import ReactViewBackgroundDrawableUtils import android.view.Gravity import android.widget.TextView import androidx.annotation.VisibleForTesting import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized import com.datadog.reactnative.sessionreplay.utils.DrawableUtils -import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils import com.datadog.reactnative.sessionreplay.utils.formatAsRgba import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerModule import com.facebook.react.views.text.TextAttributes -import com.facebook.react.views.view.ReactViewBackgroundDrawable import java.util.Locale internal class ReactTextPropertiesResolver( private val reactContext: ReactContext, private val uiManagerModule: UIManagerModule, private val reflectionUtils: ReflectionUtils = ReflectionUtils(), - private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils = - ReactViewBackgroundDrawableUtils(), - private val drawableUtils: DrawableUtils = DrawableUtils() + private val drawableUtils: DrawableUtils = + ReactViewBackgroundDrawableUtils() ): TextPropertiesResolver { override fun addReactNativeProperties( originalWireframe: MobileSegment.Wireframe.TextWireframe, @@ -94,14 +92,14 @@ internal class ReactTextPropertiesResolver( view: TextView, pixelDensity: Float, ): Pair? { - val backgroundDrawable: ReactViewBackgroundDrawable = - drawableUtils.getReactBackgroundFromDrawable(view.background) ?: return null + val backgroundDrawable = drawableUtils + .getReactBackgroundFromDrawable(view.background) ?: return null // view.alpha is the value of the opacity prop on the js side val opacity = view.alpha val (shapeStyle, border) = - reactViewBackgroundDrawableUtils + drawableUtils .resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity) return shapeStyle to border diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt index a65fc18ad..91fd44e86 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt @@ -6,6 +6,7 @@ package com.datadog.reactnative.sessionreplay.mappers +import ReactViewBackgroundDrawableUtils import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext @@ -18,13 +19,11 @@ import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver.resolve import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver import com.datadog.android.sessionreplay.utils.DrawableToColorMapper import com.datadog.reactnative.sessionreplay.utils.DrawableUtils -import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils import com.facebook.react.views.view.ReactViewGroup internal class ReactViewGroupMapper( - private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils = - ReactViewBackgroundDrawableUtils(), - private val drawableUtils: DrawableUtils = DrawableUtils() + private val drawableUtils: DrawableUtils = + ReactViewBackgroundDrawableUtils() ) : BaseWireframeMapper( viewIdentifierResolver = DefaultViewIdentifierResolver, @@ -49,7 +48,7 @@ internal class ReactViewGroupMapper( val (shapeStyle, border) = if (backgroundDrawable != null) { - reactViewBackgroundDrawableUtils + drawableUtils .resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity) } else { null to null diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt index bf6ac4983..d15a42270 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt @@ -7,29 +7,16 @@ package com.datadog.reactnative.sessionreplay.utils import android.graphics.drawable.Drawable -import android.graphics.drawable.InsetDrawable -import android.graphics.drawable.LayerDrawable -import com.facebook.react.views.view.ReactViewBackgroundDrawable +import com.datadog.android.sessionreplay.model.MobileSegment -internal class DrawableUtils { - internal fun getReactBackgroundFromDrawable(drawable: Drawable?): ReactViewBackgroundDrawable? { - if (drawable is ReactViewBackgroundDrawable) { - return drawable - } +internal abstract class DrawableUtils( + protected val reflectionUtils: ReflectionUtils = ReflectionUtils() +) { + internal abstract fun resolveShapeAndBorder( + drawable: Drawable, + opacity: Float, + pixelDensity: Float + ): Pair - if (drawable is InsetDrawable) { - return getReactBackgroundFromDrawable(drawable.drawable) - } - - if (drawable is LayerDrawable) { - for (layerNumber in 0 until drawable.numberOfLayers) { - val layer = drawable.getDrawable(layerNumber) - if (layer is ReactViewBackgroundDrawable) { - return layer - } - } - } - - return null - } -} + internal abstract fun getReactBackgroundFromDrawable(drawable: Drawable?): Drawable? +} \ No newline at end of file diff --git a/packages/react-native-session-replay/android/src/rn75/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt b/packages/react-native-session-replay/android/src/rn75/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt new file mode 100644 index 000000000..a324e4b78 --- /dev/null +++ b/packages/react-native-session-replay/android/src/rn75/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt @@ -0,0 +1,101 @@ +import android.graphics.drawable.Drawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized +import com.datadog.reactnative.sessionreplay.utils.DrawableUtils +import com.datadog.reactnative.sessionreplay.utils.formatAsRgba +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.Spacing +import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable + +internal class ReactViewBackgroundDrawableUtils : DrawableUtils() { + @OptIn(UnstableReactNativeAPI::class) + override fun resolveShapeAndBorder( + drawable: Drawable, + opacity: Float, + pixelDensity: Float + ): Pair { + if (drawable !is CSSBackgroundDrawable) { + return null to null + } + + val borderProps = resolveBorder(drawable, pixelDensity) + val backgroundColor = getBackgroundColor(drawable) + val colorHexString = if (backgroundColor != null) { + formatAsRgba(backgroundColor) + } else { + return null to borderProps + } + + return MobileSegment.ShapeStyle( + colorHexString, + opacity, + getBorderRadius(drawable) + ) to borderProps + } + + @OptIn(UnstableReactNativeAPI::class) + override fun getReactBackgroundFromDrawable(drawable: Drawable?): Drawable? { + if (drawable is CSSBackgroundDrawable) { + return drawable + } + + if (drawable is InsetDrawable) { + return getReactBackgroundFromDrawable(drawable.drawable) + } + + if (drawable is LayerDrawable) { + for (layerNumber in 0 until drawable.numberOfLayers) { + val layer = drawable.getDrawable(layerNumber) + if (layer is CSSBackgroundDrawable) { + return layer + } + } + } + + return null + } + + @OptIn(UnstableReactNativeAPI::class) + private fun getBorderRadius(drawable: CSSBackgroundDrawable): Float { + val width = drawable.intrinsicWidth.toFloat() + val height = drawable.intrinsicHeight.toFloat() + return drawable.borderRadius.uniform?.getRadius(width, height) ?: 0f + } + + @OptIn(UnstableReactNativeAPI::class) + private fun getBackgroundColor( + backgroundDrawable: CSSBackgroundDrawable + ): Int? { + return reflectionUtils.getDeclaredField( + backgroundDrawable, + COLOR_FIELD_NAME + ) as Int? + } + + @OptIn(UnstableReactNativeAPI::class) + private fun resolveBorder( + backgroundDrawable: CSSBackgroundDrawable, + pixelDensity: Float + ): MobileSegment.ShapeBorder { + val borderWidth = + backgroundDrawable.fullBorderWidth.toLong().convertToDensityNormalized(pixelDensity) + val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL)) + + return MobileSegment.ShapeBorder( + color = borderColor, + width = borderWidth + ) + } + + private fun LengthPercentage?.getRadius(width: Float, height: Float) = this + ?.resolve(width, height) + ?.let { (it.horizontal + it.vertical) / 2f } + ?: 0f + + private companion object { + private const val COLOR_FIELD_NAME = "mColor" + } +} diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt b/packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt similarity index 54% rename from packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt rename to packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt index fabacf4ba..0306bb72b 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +++ b/packages/react-native-session-replay/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt @@ -1,25 +1,29 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.reactnative.sessionreplay.utils - +import android.graphics.drawable.Drawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized +import com.datadog.reactnative.sessionreplay.utils.DrawableUtils +import com.datadog.reactnative.sessionreplay.utils.formatAsRgba import com.facebook.react.uimanager.Spacing import com.facebook.react.views.view.ReactViewBackgroundDrawable -internal class ReactViewBackgroundDrawableUtils( - private val reflectionUtils: ReflectionUtils = ReflectionUtils() -) { - internal fun resolveShapeAndBorder( - drawable: ReactViewBackgroundDrawable, +internal class ReactViewBackgroundDrawableUtils() : DrawableUtils() { + override fun resolveShapeAndBorder( + drawable: Drawable, opacity: Float, pixelDensity: Float ): Pair { + if (drawable !is ReactViewBackgroundDrawable) { + return null to null + } + val borderProps = resolveBorder(drawable, pixelDensity) + val cornerRadius = drawable + .fullBorderRadius + .toLong() + .convertToDensityNormalized(pixelDensity) + val backgroundColor = getBackgroundColor(drawable) val colorHexString = if (backgroundColor != null) { formatAsRgba(backgroundColor) @@ -27,9 +31,6 @@ internal class ReactViewBackgroundDrawableUtils( return null to borderProps } - val cornerRadius = - drawable.fullBorderRadius.toLong().convertToDensityNormalized(pixelDensity) - return MobileSegment.ShapeStyle( colorHexString, opacity, @@ -37,13 +38,25 @@ internal class ReactViewBackgroundDrawableUtils( ) to borderProps } - private fun getBackgroundColor( - backgroundDrawable: ReactViewBackgroundDrawable, - ): Int? { - return reflectionUtils.getDeclaredField( - backgroundDrawable, - COLOR_FIELD_NAME - ) as Int? + override fun getReactBackgroundFromDrawable(drawable: Drawable?): Drawable? { + if (drawable is ReactViewBackgroundDrawable) { + return drawable + } + + if (drawable is InsetDrawable) { + return getReactBackgroundFromDrawable(drawable.drawable) + } + + if (drawable is LayerDrawable) { + for (layerNumber in 0 until drawable.numberOfLayers) { + val layer = drawable.getDrawable(layerNumber) + if (layer is ReactViewBackgroundDrawable) { + return layer + } + } + } + + return null } private fun resolveBorder( @@ -60,7 +73,16 @@ internal class ReactViewBackgroundDrawableUtils( ) } + private fun getBackgroundColor( + backgroundDrawable: ReactViewBackgroundDrawable + ): Int? { + return reflectionUtils.getDeclaredField( + backgroundDrawable, + COLOR_FIELD_NAME + ) as Int? + } + private companion object { private const val COLOR_FIELD_NAME = "mColor" } -} \ No newline at end of file +} diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt index 8e65720fa..93ab816f7 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt @@ -15,7 +15,6 @@ import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Compani import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.TEXT_ATTRIBUTES_FIELD_NAME import com.datadog.reactnative.sessionreplay.ShadowNodeWrapper.Companion.UI_IMPLEMENTATION_FIELD_NAME import com.datadog.reactnative.sessionreplay.utils.DrawableUtils -import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils import com.datadog.reactnative.sessionreplay.utils.formatAsRgba import com.datadog.reactnative.tools.unit.forge.ForgeConfigurator @@ -64,14 +63,11 @@ internal class ReactTextPropertiesResolverTest { @Mock lateinit var mockTextView: TextView - @Mock - lateinit var mockDrawableUtils: DrawableUtils - @Mock lateinit var mockReactViewBackgroundDrawable: ReactViewBackgroundDrawable @Mock - lateinit var mockReactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils + lateinit var mockDrawableUtils: DrawableUtils @Mock lateinit var mockShadowNodeWrapper: ShadowNodeWrapper @@ -108,7 +104,6 @@ internal class ReactTextPropertiesResolverTest { testedResolver = ReactTextPropertiesResolver( reactContext = mockReactContext, uiManagerModule = mockUiManagerModule, - reactViewBackgroundDrawableUtils = mockReactViewBackgroundDrawableUtils, drawableUtils = mockDrawableUtils, reflectionUtils = mockReflectionUtils ) @@ -145,7 +140,7 @@ internal class ReactTextPropertiesResolverTest { ) ).thenReturn(mockReactViewBackgroundDrawable) whenever( - mockReactViewBackgroundDrawableUtils.resolveShapeAndBorder( + mockDrawableUtils.resolveShapeAndBorder( drawable = eq(mockReactViewBackgroundDrawable), opacity = eq(0f), pixelDensity = eq(0f) diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt index 23662f149..617ba13ef 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt @@ -15,7 +15,6 @@ import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.SystemInformation import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.reactnative.sessionreplay.utils.DrawableUtils -import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils import com.facebook.react.views.view.ReactViewBackgroundDrawable import com.facebook.react.views.view.ReactViewGroup import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -40,7 +39,7 @@ internal class ReactViewGroupMapperTest { private lateinit var testedMapper: ReactViewGroupMapper @Mock - private lateinit var mockReactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils + private lateinit var mockDrawableUtils: DrawableUtils @Mock private lateinit var mockReactViewGroup: ReactViewGroup @@ -57,9 +56,6 @@ internal class ReactViewGroupMapperTest { @Mock private lateinit var mockSystemInformation: SystemInformation - @Mock - private lateinit var mockDrawableUtils: DrawableUtils - @Mock private lateinit var mockReactViewBackgroundDrawable: ReactViewBackgroundDrawable @@ -75,7 +71,6 @@ internal class ReactViewGroupMapperTest { whenever(mockSystemInformation.screenDensity).thenReturn(0f) testedMapper = ReactViewGroupMapper( - reactViewBackgroundDrawableUtils = mockReactViewBackgroundDrawableUtils, drawableUtils = mockDrawableUtils ) } @@ -115,7 +110,7 @@ internal class ReactViewGroupMapperTest { ) ).thenReturn(mockReactViewBackgroundDrawable) whenever( - mockReactViewBackgroundDrawableUtils.resolveShapeAndBorder( + mockDrawableUtils.resolveShapeAndBorder( drawable = eq(mockReactViewBackgroundDrawable), pixelDensity = eq(0f), opacity = eq(0f) diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt index f71c351d7..a52414009 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt @@ -6,6 +6,7 @@ package com.datadog.reactnative.sessionreplay.utils +import ReactViewBackgroundDrawableUtils import android.graphics.drawable.ColorDrawable import android.graphics.drawable.InsetDrawable import android.graphics.drawable.LayerDrawable @@ -47,7 +48,7 @@ internal class DrawableUtilsTest { whenever(mockLayerDrawable.numberOfLayers).thenReturn(3) whenever(mockLayerDrawable.getDrawable(0)).thenReturn(mockReactViewBackgroundDrawable) - testedDrawableUtils = DrawableUtils() + testedDrawableUtils = ReactViewBackgroundDrawableUtils() } @Test