Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RUM-6028, 0.75 Support] DrawableUtils based on RN version #751

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Conditional DrawableUtils based on RN version
  • Loading branch information
marco-saia-datadog committed Dec 17, 2024

Verified

This commit was signed with the committer’s verified signature.
commit caf42353e45c6fcdd16b3db6cb41a61589b15ee3
6 changes: 6 additions & 0 deletions packages/react-native-session-replay/android/build.gradle
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -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<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?>? {
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
Original file line number Diff line number Diff line change
@@ -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<ReactViewGroup>(
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
Original file line number Diff line number Diff line change
@@ -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<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?>

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?
}
Original file line number Diff line number Diff line change
@@ -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<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,62 @@
/*
* 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<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?> {
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)
} else {
return null to borderProps
}

val cornerRadius =
drawable.fullBorderRadius.toLong().convertToDensityNormalized(pixelDensity)

return MobileSegment.ShapeStyle(
colorHexString,
opacity,
cornerRadius
) 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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
Loading