Skip to content

Commit

Permalink
RUM-6195: Implement caching logic for path based images
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Dec 18, 2024
1 parent c5216f9 commit 6bd6400
Show file tree
Hide file tree
Showing 17 changed files with 1,076 additions and 413 deletions.
11 changes: 11 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,14 @@ datadog:
- "android.graphics.drawable.Drawable.setTintList(android.content.res.ColorStateList?)"
- "android.graphics.drawable.RippleDrawable.findIndexByLayerId(kotlin.Int)"
- "android.graphics.drawable.DrawableContainer.DrawableContainerState.getChild(kotlin.Int)"
- "android.graphics.Matrix.constructor()"
- "android.graphics.Matrix.preScale(kotlin.Float, kotlin.Float)"
- "android.graphics.Matrix.preTranslate(kotlin.Float, kotlin.Float)"
- "android.graphics.Paint.constructor()"
- "android.graphics.Path.computeBounds(android.graphics.RectF, kotlin.Boolean)"
- "android.graphics.Path.transform(android.graphics.Matrix)"
- "android.graphics.PathMeasure.constructor(android.graphics.Path?, kotlin.Boolean)"
- "android.graphics.PathMeasure.nextContour()"
- "android.graphics.Point.constructor()"
- "android.graphics.Point.constructor(kotlin.Int, kotlin.Int)"
- "android.graphics.Rect.centerX()"
Expand All @@ -496,6 +503,9 @@ datadog:
- "android.graphics.Rect.constructor(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
- "android.graphics.Rect.height()"
- "android.graphics.Rect.width()"
- "android.graphics.RectF.constructor()"
- "android.graphics.RectF.width()"
- "android.graphics.RectF.height()"
# endregion
# region Androidx APIs
- "androidx.appcompat.widget.DatadogActionBarContainerAccessor.constructor(androidx.appcompat.widget.ActionBarContainer)"
Expand Down Expand Up @@ -1150,6 +1160,7 @@ datadog:
- "kotlin.Float.toFloat()"
- "kotlin.Float.toInt()"
- "kotlin.Float.toLong()"
- "kotlin.FloatArray.constructor(kotlin.Int)"
- "kotlin.Int.and(kotlin.Int)"
- "kotlin.Int.coerceAtMost(kotlin.Int)"
- "kotlin.Int.inv()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.state.ToggleableState
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.compose.internal.data.SemanticsWireframe
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.compose.internal.utils.ColorUtils
import com.datadog.android.sessionreplay.compose.internal.utils.PathUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.model.MobileSegment
Expand All @@ -24,7 +26,9 @@ import com.datadog.android.sessionreplay.utils.GlobalBounds
internal class CheckboxSemanticsNodeMapper(
colorStringFormatter: ColorStringFormatter,
private val semanticsUtils: SemanticsUtils = SemanticsUtils(),
private val pathUtils: PathUtils = PathUtils()
private val colorUtils: ColorUtils = ColorUtils(),
private val logger: InternalLogger = InternalLogger.UNBOUND,
private val pathUtils: PathUtils = PathUtils(logger)
) : AbstractSemanticsNodeMapper(colorStringFormatter, semanticsUtils) {

override fun map(
Expand All @@ -35,9 +39,11 @@ internal class CheckboxSemanticsNodeMapper(
val globalBounds = resolveBounds(semanticsNode)

val checkableWireframes = if (parentContext.textAndInputPrivacy != TextAndInputPrivacy.MASK_SENSITIVE_INPUTS) {
resolveMaskedCheckable(
semanticsNode = semanticsNode,
globalBounds = globalBounds
listOf(
resolveMaskedCheckable(
semanticsNode = semanticsNode,
globalBounds = globalBounds
)
)
} else {
// Resolves checkable view regardless the state
Expand All @@ -58,9 +64,9 @@ internal class CheckboxSemanticsNodeMapper(
private fun resolveMaskedCheckable(
semanticsNode: SemanticsNode,
globalBounds: GlobalBounds
): List<MobileSegment.Wireframe> {
): MobileSegment.Wireframe {
// TODO RUM-5118: Decide how to display masked checkbox, Currently use old unchecked shape wireframe,
return createUncheckedWireframes(
return createUncheckedState(
semanticsNode = semanticsNode,
globalBounds = globalBounds,
backgroundColor = DEFAULT_COLOR_WHITE,
Expand All @@ -74,13 +80,14 @@ internal class CheckboxSemanticsNodeMapper(
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
globalBounds: GlobalBounds
): List<MobileSegment.Wireframe> =
if (isCheckboxChecked(semanticsNode)) {
createCheckedWireframes(
): List<MobileSegment.Wireframe> {
return if (isCheckboxChecked(semanticsNode)) {
createCheckedState(
parentContext = parentContext,
asyncJobStatusCallback = asyncJobStatusCallback,
semanticsNode = semanticsNode,
globalBounds = globalBounds
globalBounds = globalBounds,
currentIndex = 0
)
} else {
val borderColor =
Expand All @@ -89,20 +96,24 @@ internal class CheckboxSemanticsNodeMapper(
convertColor(rawColor)
} ?: DEFAULT_COLOR_BLACK

createUncheckedWireframes(
semanticsNode = semanticsNode,
globalBounds = globalBounds,
backgroundColor = DEFAULT_COLOR_WHITE,
borderColor = borderColor,
currentIndex = 0
listOf(
createUncheckedState(
semanticsNode = semanticsNode,
globalBounds = globalBounds,
backgroundColor = DEFAULT_COLOR_WHITE,
borderColor = borderColor,
currentIndex = 0
)
)
}
}

private fun createCheckedWireframes(
private fun createCheckedState(
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
semanticsNode: SemanticsNode,
globalBounds: GlobalBounds
globalBounds: GlobalBounds,
currentIndex: Int
): List<MobileSegment.Wireframe> {
val rawFillColor = semanticsUtils.resolveCheckboxFillColor(semanticsNode)
val rawCheckmarkColor = semanticsUtils.resolveCheckmarkColor(semanticsNode)
Expand All @@ -114,24 +125,25 @@ internal class CheckboxSemanticsNodeMapper(
convertColor(it)
} ?: getFallbackCheckmarkColor(DEFAULT_COLOR_WHITE)

val parsedFillColor = pathUtils.parseColorSafe(fillColorRgba)
val parsedCheckmarkColor = pathUtils.parseColorSafe(checkmarkColorRgba)
val parsedFillColor = colorUtils.parseColorSafe(fillColorRgba)
val parsedCheckmarkColor = colorUtils.parseColorSafe(checkmarkColorRgba)
val wireframes = mutableListOf<MobileSegment.Wireframe>()

if (parsedFillColor != null && parsedCheckmarkColor != null) {
val checkMarkBitmap = semanticsUtils
val androidPath = semanticsUtils
.resolveCheckPath(semanticsNode)?.let { checkPath ->
pathUtils.convertPathToBitmap(
checkPath = checkPath,
fillColor = parsedFillColor,
checkmarkColor = parsedCheckmarkColor
)
pathUtils.asAndroidPathSafe(checkPath)
}

if (checkMarkBitmap != null) {
parentContext.imageWireframeHelper.createImageWireframeByBitmap(
id = resolveId(semanticsNode, 0),
if (androidPath != null) {
parentContext.imageWireframeHelper.createImageWireframeByPath(
id = resolveId(semanticsNode, currentIndex),
globalBounds = globalBounds,
bitmap = checkMarkBitmap,
path = androidPath,
strokeColor = parsedCheckmarkColor,
strokeWidth = STROKE_WIDTH_DP.toInt(),
targetWidth = CHECKBOX_SIZE_DP,
targetHeight = CHECKBOX_SIZE_DP,
density = parentContext.density,
isContextualImage = false,
imagePrivacy = ImagePrivacy.MASK_NONE,
Expand All @@ -145,15 +157,24 @@ internal class CheckboxSemanticsNodeMapper(
border = MobileSegment.ShapeBorder(
color = fillColorRgba,
width = BOX_BORDER_WIDTH_DP
)
),
customResourceIdCacheKey = null
)?.let { imageWireframe ->
return listOf(imageWireframe)
wireframes.add(imageWireframe)
}
}
}

if (wireframes.isNotEmpty()) {
return wireframes
}

// if we failed to create a wireframe from the path
return createManualCheckedWireframe(semanticsNode, globalBounds, fillColorRgba)
return createManualCheckedWireframe(
semanticsNode = semanticsNode,
globalBounds = globalBounds,
backgroundColor = fillColorRgba
)
}

private fun createManualCheckedWireframe(
Expand All @@ -164,31 +185,29 @@ internal class CheckboxSemanticsNodeMapper(
val strokeColor = getFallbackCheckmarkColor(backgroundColor)

val wireframesList = mutableListOf<MobileSegment.Wireframe>()
var index = 0

val borderColor =
semanticsUtils.resolveBorderColor(semanticsNode)
?.let { rawColor ->
convertColor(rawColor)
} ?: DEFAULT_COLOR_BLACK

createUncheckedWireframes(
val background = createUncheckedState(
semanticsNode = semanticsNode,
globalBounds = globalBounds,
backgroundColor = backgroundColor,
borderColor = borderColor,
currentIndex = 0
).firstOrNull()?.let {
wireframesList.add(it)
index++
}
)

wireframesList.add(background)

val checkmarkWidth = globalBounds.width * CHECKMARK_SIZE_FACTOR
val checkmarkHeight = globalBounds.height * CHECKMARK_SIZE_FACTOR
val xPos = globalBounds.x + ((globalBounds.width / 2) - (checkmarkWidth / 2))
val yPos = globalBounds.y + ((globalBounds.height / 2) - (checkmarkHeight / 2))
val foreground = MobileSegment.Wireframe.ShapeWireframe(
id = resolveId(semanticsNode, index),
id = resolveId(semanticsNode, 1),
x = xPos.toLong(),
y = yPos.toLong(),
width = checkmarkWidth.toLong(),
Expand All @@ -208,29 +227,27 @@ internal class CheckboxSemanticsNodeMapper(
return wireframesList
}

private fun createUncheckedWireframes(
private fun createUncheckedState(
semanticsNode: SemanticsNode,
globalBounds: GlobalBounds,
backgroundColor: String,
borderColor: String,
currentIndex: Int
): List<MobileSegment.Wireframe> {
return listOf(
MobileSegment.Wireframe.ShapeWireframe(
id = resolveId(semanticsNode, currentIndex),
x = globalBounds.x,
y = globalBounds.y,
width = globalBounds.width,
height = globalBounds.height,
shapeStyle = MobileSegment.ShapeStyle(
backgroundColor = backgroundColor,
opacity = 1f,
cornerRadius = CHECKBOX_CORNER_RADIUS
),
border = MobileSegment.ShapeBorder(
color = borderColor,
width = BOX_BORDER_WIDTH_DP
)
): MobileSegment.Wireframe {
return MobileSegment.Wireframe.ShapeWireframe(
id = resolveId(semanticsNode, currentIndex),
x = globalBounds.x,
y = globalBounds.y,
width = CHECKBOX_SIZE_DP.toLong(),
height = CHECKBOX_SIZE_DP.toLong(),
shapeStyle = MobileSegment.ShapeStyle(
backgroundColor = backgroundColor,
opacity = 1f,
cornerRadius = CHECKBOX_CORNER_RADIUS
),
border = MobileSegment.ShapeBorder(
color = borderColor,
width = BOX_BORDER_WIDTH_DP
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.android.sessionreplay.compose.internal.utils

import android.graphics.Color
import com.datadog.android.api.InternalLogger
import java.util.Locale

internal class ColorUtils(
private val logger: InternalLogger = InternalLogger.UNBOUND
) {
internal fun parseColorSafe(color: String): Int? {
return try {
@Suppress("UnsafeThirdPartyFunctionCall") // handling IllegalArgumentException
Color.parseColor(color)
} catch (e: IllegalArgumentException) {
logger.log(
target = InternalLogger.Target.MAINTAINER,
level = InternalLogger.Level.WARN,
messageBuilder = { COLOR_PARSE_ERROR.format(Locale.US, color) },
throwable = e
)
null
}
}

internal fun convertRgbaToArgb(rgbaString: String): String {
if (rgbaString.length < 2) return rgbaString

// for takeLast: n > 0
@Suppress("UnsafeThirdPartyFunctionCall")
val alphaValue = rgbaString.takeLast(2)

// for substring: length is necessarily > 1 at this point
// for dropLast: n > 0
@Suppress("UnsafeThirdPartyFunctionCall")
val rgbColor = rgbaString
.substring(1)
.dropLast(2)
return "#$alphaValue$rgbColor"
}

internal companion object {
internal const val COLOR_PARSE_ERROR = "Failed to parse color: %s"
}
}
Loading

0 comments on commit 6bd6400

Please sign in to comment.