Skip to content

Commit a02fc56

Browse files
committed
RUM-6195: Implement caching logic for path based images
1 parent a98c047 commit a02fc56

File tree

17 files changed

+1075
-411
lines changed

17 files changed

+1075
-411
lines changed

detekt_custom.yml

+11
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,14 @@ datadog:
487487
- "android.graphics.drawable.Drawable.setTintList(android.content.res.ColorStateList?)"
488488
- "android.graphics.drawable.RippleDrawable.findIndexByLayerId(kotlin.Int)"
489489
- "android.graphics.drawable.DrawableContainer.DrawableContainerState.getChild(kotlin.Int)"
490+
- "android.graphics.Matrix.constructor()"
491+
- "android.graphics.Matrix.preScale(kotlin.Float, kotlin.Float)"
492+
- "android.graphics.Matrix.preTranslate(kotlin.Float, kotlin.Float)"
490493
- "android.graphics.Paint.constructor()"
494+
- "android.graphics.Path.computeBounds(android.graphics.RectF, kotlin.Boolean)"
495+
- "android.graphics.Path.transform(android.graphics.Matrix)"
496+
- "android.graphics.PathMeasure.constructor(android.graphics.Path?, kotlin.Boolean)"
497+
- "android.graphics.PathMeasure.nextContour()"
491498
- "android.graphics.Point.constructor()"
492499
- "android.graphics.Point.constructor(kotlin.Int, kotlin.Int)"
493500
- "android.graphics.Rect.centerX()"
@@ -496,6 +503,9 @@ datadog:
496503
- "android.graphics.Rect.constructor(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
497504
- "android.graphics.Rect.height()"
498505
- "android.graphics.Rect.width()"
506+
- "android.graphics.RectF.constructor()"
507+
- "android.graphics.RectF.width()"
508+
- "android.graphics.RectF.height()"
499509
# endregion
500510
# region Androidx APIs
501511
- "androidx.appcompat.widget.DatadogActionBarContainerAccessor.constructor(androidx.appcompat.widget.ActionBarContainer)"
@@ -1144,6 +1154,7 @@ datadog:
11441154
- "kotlin.Float.toFloat()"
11451155
- "kotlin.Float.toInt()"
11461156
- "kotlin.Float.toLong()"
1157+
- "kotlin.FloatArray.constructor(kotlin.Int)"
11471158
- "kotlin.Int.and(kotlin.Int)"
11481159
- "kotlin.Int.coerceAtMost(kotlin.Int)"
11491160
- "kotlin.Int.inv()"

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapper.kt

+75-58
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import androidx.compose.ui.semantics.SemanticsNode
1010
import androidx.compose.ui.semantics.SemanticsProperties
1111
import androidx.compose.ui.semantics.getOrNull
1212
import androidx.compose.ui.state.ToggleableState
13+
import com.datadog.android.api.InternalLogger
1314
import com.datadog.android.sessionreplay.ImagePrivacy
1415
import com.datadog.android.sessionreplay.TextAndInputPrivacy
1516
import com.datadog.android.sessionreplay.compose.internal.data.SemanticsWireframe
1617
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
18+
import com.datadog.android.sessionreplay.compose.internal.utils.ColorUtils
1719
import com.datadog.android.sessionreplay.compose.internal.utils.PathUtils
1820
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
1921
import com.datadog.android.sessionreplay.model.MobileSegment
@@ -24,7 +26,9 @@ import com.datadog.android.sessionreplay.utils.GlobalBounds
2426
internal class CheckboxSemanticsNodeMapper(
2527
colorStringFormatter: ColorStringFormatter,
2628
private val semanticsUtils: SemanticsUtils = SemanticsUtils(),
27-
private val pathUtils: PathUtils = PathUtils()
29+
private val colorUtils: ColorUtils = ColorUtils(),
30+
private val logger: InternalLogger = InternalLogger.UNBOUND,
31+
private val pathUtils: PathUtils = PathUtils(logger)
2832
) : AbstractSemanticsNodeMapper(colorStringFormatter, semanticsUtils) {
2933

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

3741
val checkableWireframes = if (parentContext.textAndInputPrivacy != TextAndInputPrivacy.MASK_SENSITIVE_INPUTS) {
38-
resolveMaskedCheckable(
39-
semanticsNode = semanticsNode,
40-
globalBounds = globalBounds
42+
listOf(
43+
resolveMaskedCheckable(
44+
semanticsNode = semanticsNode,
45+
globalBounds = globalBounds
46+
)
4147
)
4248
} else {
4349
// Resolves checkable view regardless the state
@@ -58,9 +64,9 @@ internal class CheckboxSemanticsNodeMapper(
5864
private fun resolveMaskedCheckable(
5965
semanticsNode: SemanticsNode,
6066
globalBounds: GlobalBounds
61-
): List<MobileSegment.Wireframe> {
67+
): MobileSegment.Wireframe {
6268
// TODO RUM-5118: Decide how to display masked checkbox, Currently use old unchecked shape wireframe,
63-
return createUncheckedWireframes(
69+
return createUncheckedState(
6470
semanticsNode = semanticsNode,
6571
globalBounds = globalBounds,
6672
backgroundColor = DEFAULT_COLOR_WHITE,
@@ -74,13 +80,14 @@ internal class CheckboxSemanticsNodeMapper(
7480
parentContext: UiContext,
7581
asyncJobStatusCallback: AsyncJobStatusCallback,
7682
globalBounds: GlobalBounds
77-
): List<MobileSegment.Wireframe> =
78-
if (isCheckboxChecked(semanticsNode)) {
79-
createCheckedWireframes(
83+
): List<MobileSegment.Wireframe> {
84+
return if (isCheckboxChecked(semanticsNode)) {
85+
createCheckedState(
8086
parentContext = parentContext,
8187
asyncJobStatusCallback = asyncJobStatusCallback,
8288
semanticsNode = semanticsNode,
83-
globalBounds = globalBounds
89+
globalBounds = globalBounds,
90+
currentIndex = 0
8491
)
8592
} else {
8693
val borderColor =
@@ -89,20 +96,24 @@ internal class CheckboxSemanticsNodeMapper(
8996
convertColor(rawColor)
9097
} ?: DEFAULT_COLOR_BLACK
9198

92-
createUncheckedWireframes(
93-
semanticsNode = semanticsNode,
94-
globalBounds = globalBounds,
95-
backgroundColor = DEFAULT_COLOR_WHITE,
96-
borderColor = borderColor,
97-
currentIndex = 0
99+
listOf(
100+
createUncheckedState(
101+
semanticsNode = semanticsNode,
102+
globalBounds = globalBounds,
103+
backgroundColor = DEFAULT_COLOR_WHITE,
104+
borderColor = borderColor,
105+
currentIndex = 0
106+
)
98107
)
99108
}
109+
}
100110

101-
private fun createCheckedWireframes(
111+
private fun createCheckedState(
102112
parentContext: UiContext,
103113
asyncJobStatusCallback: AsyncJobStatusCallback,
104114
semanticsNode: SemanticsNode,
105-
globalBounds: GlobalBounds
115+
globalBounds: GlobalBounds,
116+
currentIndex: Int
106117
): List<MobileSegment.Wireframe> {
107118
val rawFillColor = semanticsUtils.resolveCheckboxFillColor(semanticsNode)
108119
val rawCheckmarkColor = semanticsUtils.resolveCheckmarkColor(semanticsNode)
@@ -114,24 +125,25 @@ internal class CheckboxSemanticsNodeMapper(
114125
convertColor(it)
115126
} ?: getFallbackCheckmarkColor(DEFAULT_COLOR_WHITE)
116127

117-
val parsedFillColor = pathUtils.parseColorSafe(fillColorRgba)
118-
val parsedCheckmarkColor = pathUtils.parseColorSafe(checkmarkColorRgba)
128+
val parsedFillColor = colorUtils.parseColorSafe(fillColorRgba)
129+
val parsedCheckmarkColor = colorUtils.parseColorSafe(checkmarkColorRgba)
130+
val wireframes = mutableListOf<MobileSegment.Wireframe>()
119131

120132
if (parsedFillColor != null && parsedCheckmarkColor != null) {
121-
val checkMarkBitmap = semanticsUtils
133+
val androidPath = semanticsUtils
122134
.resolveCheckPath(semanticsNode)?.let { checkPath ->
123-
pathUtils.convertPathToBitmap(
124-
checkPath = checkPath,
125-
fillColor = parsedFillColor,
126-
checkmarkColor = parsedCheckmarkColor
127-
)
135+
pathUtils.asAndroidPathSafe(checkPath)
128136
}
129137

130-
if (checkMarkBitmap != null) {
131-
parentContext.imageWireframeHelper.createImageWireframeByBitmap(
132-
id = resolveId(semanticsNode, 0),
138+
if (androidPath != null) {
139+
parentContext.imageWireframeHelper.createImageWireframeByPath(
140+
id = resolveId(semanticsNode, currentIndex),
133141
globalBounds = globalBounds,
134-
bitmap = checkMarkBitmap,
142+
path = androidPath,
143+
strokeColor = parsedCheckmarkColor,
144+
strokeWidth = STROKE_WIDTH_DP.toInt(),
145+
targetWidth = CHECKBOX_SIZE_DP,
146+
targetHeight = CHECKBOX_SIZE_DP,
135147
density = parentContext.density,
136148
isContextualImage = false,
137149
imagePrivacy = ImagePrivacy.MASK_NONE,
@@ -145,15 +157,24 @@ internal class CheckboxSemanticsNodeMapper(
145157
border = MobileSegment.ShapeBorder(
146158
color = fillColorRgba,
147159
width = BOX_BORDER_WIDTH_DP
148-
)
160+
),
161+
customResourceIdCacheKey = null
149162
)?.let { imageWireframe ->
150-
return listOf(imageWireframe)
163+
wireframes.add(imageWireframe)
151164
}
152165
}
153166
}
154167

168+
if (wireframes.isNotEmpty()) {
169+
return wireframes
170+
}
171+
155172
// if we failed to create a wireframe from the path
156-
return createManualCheckedWireframe(semanticsNode, globalBounds, fillColorRgba)
173+
return createManualCheckedWireframe(
174+
semanticsNode = semanticsNode,
175+
globalBounds = globalBounds,
176+
backgroundColor = fillColorRgba
177+
)
157178
}
158179

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

166187
val wireframesList = mutableListOf<MobileSegment.Wireframe>()
167-
var index = 0
168188

169189
val borderColor =
170190
semanticsUtils.resolveBorderColor(semanticsNode)
171191
?.let { rawColor ->
172192
convertColor(rawColor)
173193
} ?: DEFAULT_COLOR_BLACK
174194

175-
createUncheckedWireframes(
195+
val background = createUncheckedState(
176196
semanticsNode = semanticsNode,
177197
globalBounds = globalBounds,
178198
backgroundColor = backgroundColor,
179199
borderColor = borderColor,
180200
currentIndex = 0
181-
).firstOrNull()?.let {
182-
wireframesList.add(it)
183-
index++
184-
}
201+
)
202+
203+
wireframesList.add(background)
185204

186205
val checkmarkWidth = globalBounds.width * CHECKMARK_SIZE_FACTOR
187206
val checkmarkHeight = globalBounds.height * CHECKMARK_SIZE_FACTOR
188207
val xPos = globalBounds.x + ((globalBounds.width / 2) - (checkmarkWidth / 2))
189208
val yPos = globalBounds.y + ((globalBounds.height / 2) - (checkmarkHeight / 2))
190209
val foreground = MobileSegment.Wireframe.ShapeWireframe(
191-
id = resolveId(semanticsNode, index),
210+
id = resolveId(semanticsNode, 1),
192211
x = xPos.toLong(),
193212
y = yPos.toLong(),
194213
width = checkmarkWidth.toLong(),
@@ -208,29 +227,27 @@ internal class CheckboxSemanticsNodeMapper(
208227
return wireframesList
209228
}
210229

211-
private fun createUncheckedWireframes(
230+
private fun createUncheckedState(
212231
semanticsNode: SemanticsNode,
213232
globalBounds: GlobalBounds,
214233
backgroundColor: String,
215234
borderColor: String,
216235
currentIndex: Int
217-
): List<MobileSegment.Wireframe> {
218-
return listOf(
219-
MobileSegment.Wireframe.ShapeWireframe(
220-
id = resolveId(semanticsNode, currentIndex),
221-
x = globalBounds.x,
222-
y = globalBounds.y,
223-
width = globalBounds.width,
224-
height = globalBounds.height,
225-
shapeStyle = MobileSegment.ShapeStyle(
226-
backgroundColor = backgroundColor,
227-
opacity = 1f,
228-
cornerRadius = CHECKBOX_CORNER_RADIUS
229-
),
230-
border = MobileSegment.ShapeBorder(
231-
color = borderColor,
232-
width = BOX_BORDER_WIDTH_DP
233-
)
236+
): MobileSegment.Wireframe {
237+
return MobileSegment.Wireframe.ShapeWireframe(
238+
id = resolveId(semanticsNode, currentIndex),
239+
x = globalBounds.x,
240+
y = globalBounds.y,
241+
width = CHECKBOX_SIZE_DP.toLong(),
242+
height = CHECKBOX_SIZE_DP.toLong(),
243+
shapeStyle = MobileSegment.ShapeStyle(
244+
backgroundColor = backgroundColor,
245+
opacity = 1f,
246+
cornerRadius = CHECKBOX_CORNER_RADIUS
247+
),
248+
border = MobileSegment.ShapeBorder(
249+
color = borderColor,
250+
width = BOX_BORDER_WIDTH_DP
234251
)
235252
)
236253
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.sessionreplay.compose.internal.utils
8+
9+
import android.graphics.Color
10+
import com.datadog.android.api.InternalLogger
11+
import java.util.Locale
12+
13+
internal class ColorUtils(
14+
private val logger: InternalLogger = InternalLogger.UNBOUND
15+
) {
16+
internal fun parseColorSafe(color: String): Int? {
17+
return try {
18+
@Suppress("UnsafeThirdPartyFunctionCall") // handling IllegalArgumentException
19+
Color.parseColor(color)
20+
} catch (e: IllegalArgumentException) {
21+
logger.log(
22+
target = InternalLogger.Target.MAINTAINER,
23+
level = InternalLogger.Level.WARN,
24+
messageBuilder = { COLOR_PARSE_ERROR.format(Locale.US, color) },
25+
throwable = e
26+
)
27+
null
28+
}
29+
}
30+
31+
internal fun convertRgbaToArgb(rgbaString: String): String {
32+
if (rgbaString.length < 2) return rgbaString
33+
34+
// for takeLast: n > 0
35+
@Suppress("UnsafeThirdPartyFunctionCall")
36+
val alphaValue = rgbaString.takeLast(2)
37+
38+
// for substring: length is necessarily > 1 at this point
39+
// for dropLast: n > 0
40+
@Suppress("UnsafeThirdPartyFunctionCall")
41+
val rgbColor = rgbaString
42+
.substring(1)
43+
.dropLast(2)
44+
return "#$alphaValue$rgbColor"
45+
}
46+
47+
internal companion object {
48+
internal const val COLOR_PARSE_ERROR = "Failed to parse color: %s"
49+
}
50+
}

0 commit comments

Comments
 (0)