Skip to content

Commit

Permalink
Implement SR for ReactViewGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Nov 29, 2023
1 parent a2ff0ef commit c63ffcb
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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

import androidx.annotation.VisibleForTesting
import com.facebook.react.views.view.ReactViewGroup

private const val ALPHA_CODE_LENGTH: Int = 2
private const val HEX_RADIX: Int = 16
private const val OPAQUE_ALPHA_VALUE: Int = 255
private const val HEX_COLOR_INCLUDING_ALPHA_LENGTH: Int = 8

internal fun resolveAlpha(opacity: Float, backgroundColor: Int?): Int {
// must not be toHexString because we'll lose the sign bit
val bgColorAsHexCode = backgroundColor?.toString(HEX_RADIX)
var opacityPercentage = opacity

if (bgColorAsHexCode?.length == HEX_COLOR_INCLUDING_ALPHA_LENGTH) {
val opacityCode = bgColorAsHexCode.take(ALPHA_CODE_LENGTH)
opacityPercentage = alphaCodeToPct(opacityCode)
}

return (opacityPercentage * OPAQUE_ALPHA_VALUE).toInt()
}

@VisibleForTesting
internal fun alphaCodeToPct(hexAlpha: String): Float {
return try {
val alphaDecimal = hexAlpha.toInt(HEX_RADIX)
(alphaDecimal / 255.0).toFloat()
} catch (e: NumberFormatException) {
// this shouldn't happen, but if it does we'll just return 1f
1f
}
}

internal fun resolveOpacity(view: ReactViewGroup, currentOpacity: Float): Float {
return if (view.alpha == 0f) {
0f
} else {
val combinedOpacity = view.alpha * currentOpacity

if (view.parent != null && view.parent is ReactViewGroup) {
resolveOpacity(view.parent as ReactViewGroup, combinedOpacity)
} else {
combinedOpacity
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class DdSessionReplayImplementation(
fun enable(replaySampleRate: Double, defaultPrivacyLevel: String, promise: Promise) {
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
.setPrivacy(buildPrivacy(defaultPrivacyLevel))
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport())
.build()
sessionReplayProvider().enable(configuration)
promise.resolve(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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

internal fun Long.convertToDensityNormalized(density: Float): Long {
return if (density == 0f) {
this
} else {
(this / density).toLong()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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

import android.view.View
import com.datadog.android.sessionreplay.ExtensionSupport
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
import com.facebook.react.views.view.ReactViewGroup

internal class ReactNativeSessionReplayExtensionSupport : ExtensionSupport {

override fun getCustomViewMappers(): Map<SessionReplayPrivacy, Map<Class<*>, WireframeMapper<View, *>>> {
return mapOf(SessionReplayPrivacy.ALLOW to mapOf(
ReactViewGroup::class.java to ReactViewGroupMapper() as WireframeMapper<View, *>
))
}

override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
return listOf()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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

import android.annotation.SuppressLint
import android.graphics.Color
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.BaseWireframeMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper
import com.datadog.android.sessionreplay.model.MobileSegment
import com.facebook.react.uimanager.Spacing
import com.facebook.react.views.view.ReactViewBackgroundDrawable
import com.facebook.react.views.view.ReactViewGroup

internal class ReactViewGroupMapper :
BaseWireframeMapper<ReactViewGroup, MobileSegment.Wireframe>(),
TraverseAllChildrenMapper<ReactViewGroup, MobileSegment.Wireframe> {

@SuppressLint("VisibleForTests")
override fun map(
view: ReactViewGroup,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
val viewGlobalBounds = resolveViewGlobalBounds(
view,
mappingContext.systemInformation.screenDensity
)

val pixelDensity = mappingContext.systemInformation.screenDensity
val backgroundDrawable = view.background as? ReactViewBackgroundDrawable ?: return emptyList()

val backgroundColor = view.backgroundColor
if (backgroundColor == Color.TRANSPARENT) {
return emptyList()
}
val opacity = resolveOpacity(view, view.alpha)

// view.alpha is the value of the opacity prop on the js side
// this should override the alpha value of the background color if it is explicitly 0
if (opacity == 0f) {
return emptyList()
}

val viewAlpha = resolveAlpha(opacity, backgroundColor)

val backgroundColorAndAlpha = colorAndAlphaAsStringHexa(
color = backgroundColor,
alphaAsHexa = viewAlpha
)

val (shapeStyle, border) = resolveRNShapeStyleAndBorder(
backgroundDrawable = backgroundDrawable,
viewAlpha = viewAlpha,
viewColor = backgroundColorAndAlpha,
opacity = opacity,
pixelDensity = pixelDensity
)

return listOf(
MobileSegment.Wireframe.ShapeWireframe(
resolveViewId(view),
viewGlobalBounds.x,
viewGlobalBounds.y,
viewGlobalBounds.width,
viewGlobalBounds.height,
shapeStyle = shapeStyle,
border = border
)
)
}

private fun resolveRNShapeStyleAndBorder(
backgroundDrawable: ReactViewBackgroundDrawable?,
viewAlpha: Int,
viewColor: String,
opacity: Float,
pixelDensity: Float
):
Pair<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?> {
if (backgroundDrawable == null) {
return MobileSegment.ShapeStyle(
backgroundColor = viewColor,
opacity = viewAlpha,
) to null
} else {
val cornerRadius = backgroundDrawable.fullBorderRadius
val borderColor = resolveBorderColor(backgroundDrawable, opacity)
val borderWidth = backgroundDrawable.fullBorderWidth
val cornerRadiusDp = cornerRadius.toLong().convertToDensityNormalized(pixelDensity)

return MobileSegment.ShapeStyle(
backgroundColor = viewColor,
opacity = viewAlpha,
cornerRadius = cornerRadiusDp
) to MobileSegment.ShapeBorder(
color = borderColor,
width = borderWidth.toLong().convertToDensityNormalized(pixelDensity)
)
}
}

private fun resolveBorderColor(backgroundDrawable: ReactViewBackgroundDrawable, opacity: Float): String {
val borderColor = backgroundDrawable.getBorderColor(Spacing.ALL)
val alpha = resolveAlpha(opacity, borderColor)
return colorAndAlphaAsStringHexa(borderColor, alpha)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +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

import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.quality.Strictness

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
internal class ColorUtilsTest {

@Test
fun `M use default alpha W resolveAlphaValue { color without opacity }`() {
// When
val alphaValue = resolveAlpha(0.1f, 0x000000)

// 0.1f = 10% opacity -> 10% of 255 is 25
// Then
assertThat(alphaValue)
.isEqualTo(25)
}

@Test
fun `M use opacity code W resolveAlphaValue { color with opacity }`() {
// Given
val color = 1717960806

// When
val alphaValue = resolveAlpha(0.1f, color)

// decimal color 1717960806 translates to hex 66660066
// 66 being the hex alpha, which is 40% opacity -> 40% of 255 is 102
// Then
assertThat(alphaValue).isEqualTo(102)
}

@Test
fun `M return alpha of 1f W alphaCodeToPct { NumberFormatException }`() {
// Given
val color = "not a number"

// When
val alphaValue = alphaCodeToPct(color)

// Then
assertThat(alphaValue)
.isEqualTo(1f)
}
}

0 comments on commit c63ffcb

Please sign in to comment.