-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a2ff0ef
commit c63ffcb
Showing
6 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
53 changes: 53 additions & 0 deletions
53
...ession-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ColorUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
...e-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/LongExt.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
.../kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
113 changes: 113 additions & 0 deletions
113
...lay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactViewGroupMapper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
...on-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ColorUtilsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |