Skip to content

Commit 08f38d8

Browse files
committed
PANA-8500: Map captured trees to wire models
1 parent cb76854 commit 08f38d8

2 files changed

Lines changed: 713 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
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+
@file:Suppress("TooManyFunctions") // This file contains stateless wire-model conversion extensions.
8+
9+
package com.datadog.android.sessionreplay.internal.composition
10+
11+
import com.datadog.android.sessionreplay.model.MobileSegment
12+
13+
internal sealed interface CaptureWireMappingResult<out T> {
14+
data class Success<T>(val value: T) : CaptureWireMappingResult<T>
15+
16+
data class Invalid(
17+
val failures: List<CaptureValidationFailure>
18+
) : CaptureWireMappingResult<Nothing>
19+
}
20+
21+
internal interface CapturedTreeWireMapper {
22+
fun mapFullSnapshot(
23+
snapshot: CapturedFullSnapshot
24+
): CaptureWireMappingResult<MobileSegment.MobileRecord.MobileFullSnapshotRecord>
25+
26+
fun mapMutation(
27+
mutation: CapturedMutationSet,
28+
base: CapturedFullSnapshot
29+
): CaptureWireMappingResult<MobileSegment.MobileRecord.MobileIncrementalSnapshotRecord>
30+
}
31+
32+
internal class DefaultCapturedTreeWireMapper(
33+
private val validator: CapturedTreeValidator = DefaultCapturedTreeValidator()
34+
) : CapturedTreeWireMapper {
35+
36+
@Suppress("ReturnCount") // Invalid capture data is returned at the point where it is detected.
37+
override fun mapFullSnapshot(
38+
snapshot: CapturedFullSnapshot
39+
): CaptureWireMappingResult<MobileSegment.MobileRecord.MobileFullSnapshotRecord> {
40+
val validation = validator.validate(snapshot)
41+
if (validation is CaptureValidationResult.Invalid) {
42+
return CaptureWireMappingResult.Invalid(validation.failures)
43+
}
44+
45+
val root = snapshot.root
46+
?: return invalidMapping(CaptureValidationErrorCode.MISSING_ROOT)
47+
val wireframes = snapshot.wireframes.map { captured ->
48+
captured.toWireframeOrNull()
49+
?: return invalidMapping(
50+
CaptureValidationErrorCode.UNRESOLVED_PIXEL_RESOURCE,
51+
captured.identity
52+
)
53+
}
54+
val tree = MobileSegment.CompositionTree(
55+
root = root.toWireLayer(),
56+
layers = snapshot.layers.map { it.toWireLayer() }.ifEmpty { null }
57+
)
58+
return CaptureWireMappingResult.Success(
59+
MobileSegment.MobileRecord.MobileFullSnapshotRecord(
60+
timestamp = snapshot.timestamp,
61+
data = MobileSegment.Data(
62+
wireframes = wireframes,
63+
compositionTree = tree
64+
)
65+
)
66+
)
67+
}
68+
69+
override fun mapMutation(
70+
mutation: CapturedMutationSet,
71+
base: CapturedFullSnapshot
72+
): CaptureWireMappingResult<MobileSegment.MobileRecord.MobileIncrementalSnapshotRecord> {
73+
val validation = validator.validate(mutation, base)
74+
if (validation is CaptureValidationResult.Invalid) {
75+
return CaptureWireMappingResult.Invalid(validation.failures)
76+
}
77+
78+
val data = MobileSegment.MobileIncrementalData.CompositionTreeMutationData(
79+
root = mutation.root.mapOrNull { it.toWireLayer() },
80+
adds = mutation.adds.mapOrNull { layers -> layers.map { it.toWireLayer() } },
81+
removes = mutation.removes.mapOrNull { identities -> identities.map(CapturedIdentity::wireId) },
82+
updates = mutation.updates.mapOrNull { updates -> updates.map { it.toWireUpdate() } }
83+
)
84+
return CaptureWireMappingResult.Success(
85+
MobileSegment.MobileRecord.MobileIncrementalSnapshotRecord(
86+
timestamp = mutation.timestamp,
87+
data = data
88+
)
89+
)
90+
}
91+
}
92+
93+
private fun CapturedLayer.toWireLayer(): MobileSegment.CompositionLayer =
94+
MobileSegment.CompositionLayer(
95+
id = identity.wireId,
96+
x = bounds.x,
97+
y = bounds.y,
98+
width = bounds.width,
99+
height = bounds.height,
100+
children = children.map { it.toWireChild() },
101+
modifiers = modifiers.map { it.toWireModifier() }.ifEmpty { null },
102+
compositeOperation = compositeOperation?.toWireCompositeOperation()
103+
)
104+
105+
private fun CapturedLayerUpdate.toWireUpdate(): MobileSegment.CompositionLayerUpdate =
106+
MobileSegment.CompositionLayerUpdate(
107+
id = identity.wireId,
108+
x = x.mapOrNull { it },
109+
y = y.mapOrNull { it },
110+
width = width.mapOrNull { it },
111+
height = height.mapOrNull { it },
112+
children = children.mapOrNull { children -> children.map { it.toWireChild() } },
113+
modifiers = modifiers.mapOrNull { modifiers -> modifiers.map { it.toWireModifier() } },
114+
compositeOperation = compositeOperation.mapOrNull {
115+
it?.toWireCompositeOperation() ?: MobileSegment.CompositeOperation.SOURCEOVER
116+
}
117+
)
118+
119+
private fun CapturedWireframe.toWireframeOrNull(): MobileSegment.Wireframe? = when (this) {
120+
is CapturedWireframe.Shape -> toWireframe()
121+
is CapturedWireframe.Text -> toWireframe()
122+
is CapturedWireframe.Pixel -> toWireframeOrNull()
123+
is CapturedWireframe.PrivacyPlaceholder -> toWireframe()
124+
is CapturedWireframe.WebView -> toWireframe()
125+
is CapturedWireframe.EmbeddedContent -> toWireframe()
126+
}
127+
128+
private fun CapturedWireframe.Shape.toWireframe() = MobileSegment.Wireframe.ShapeWireframe(
129+
id = identity.wireId,
130+
x = bounds.x,
131+
y = bounds.y,
132+
width = bounds.width,
133+
height = bounds.height,
134+
clip = clip?.toWireClip(),
135+
shapeStyle = style?.toWireShapeStyle(),
136+
border = border?.toWireBorder(),
137+
permanentId = permanentId
138+
)
139+
140+
private fun CapturedWireframe.Text.toWireframe() = MobileSegment.Wireframe.TextWireframe(
141+
id = identity.wireId,
142+
x = bounds.x,
143+
y = bounds.y,
144+
width = bounds.width,
145+
height = bounds.height,
146+
clip = clip?.toWireClip(),
147+
shapeStyle = style?.toWireShapeStyle(),
148+
border = border?.toWireBorder(),
149+
text = text,
150+
textStyle = textStyle.toWireTextStyle(),
151+
textPosition = textPosition?.toWireTextPosition(),
152+
permanentId = permanentId
153+
)
154+
155+
private fun CapturedWireframe.Pixel.toWireframeOrNull(): MobileSegment.Wireframe.ImageWireframe? =
156+
(resource as? PixelResource.Resolved)
157+
?.takeIf { it.resourceId.isNotBlank() }
158+
?.let { resolvedResource ->
159+
MobileSegment.Wireframe.ImageWireframe(
160+
id = identity.wireId,
161+
x = bounds.x,
162+
y = bounds.y,
163+
width = bounds.width,
164+
height = bounds.height,
165+
clip = clip?.toWireClip(),
166+
shapeStyle = style?.toWireShapeStyle(),
167+
border = border?.toWireBorder(),
168+
resourceId = resolvedResource.resourceId,
169+
mimeType = resolvedResource.mimeType,
170+
isEmpty = isEmpty,
171+
permanentId = permanentId
172+
)
173+
}
174+
175+
private fun CapturedWireframe.PrivacyPlaceholder.toWireframe() =
176+
MobileSegment.Wireframe.PlaceholderWireframe(
177+
id = identity.wireId,
178+
x = bounds.x,
179+
y = bounds.y,
180+
width = bounds.width,
181+
height = bounds.height,
182+
clip = clip?.toWireClip(),
183+
label = label,
184+
permanentId = permanentId
185+
)
186+
187+
private fun CapturedWireframe.WebView.toWireframe() = MobileSegment.Wireframe.WebviewWireframe(
188+
id = identity.wireId,
189+
x = bounds.x,
190+
y = bounds.y,
191+
width = bounds.width,
192+
height = bounds.height,
193+
clip = clip?.toWireClip(),
194+
shapeStyle = style?.toWireShapeStyle(),
195+
border = border?.toWireBorder(),
196+
slotId = identity.wireId.toString(),
197+
isVisible = isVisible,
198+
permanentId = permanentId
199+
)
200+
201+
private fun CapturedWireframe.EmbeddedContent.toWireframe() =
202+
MobileSegment.Wireframe.EmbeddedContentWireframe(
203+
id = identity.wireId,
204+
x = bounds.x,
205+
y = bounds.y,
206+
width = bounds.width,
207+
height = bounds.height,
208+
clip = clip?.toWireClip(),
209+
shapeStyle = style?.toWireShapeStyle(),
210+
border = border?.toWireBorder(),
211+
slotId = slotId,
212+
isVisible = isVisible,
213+
permanentId = permanentId
214+
)
215+
216+
private fun CapturedClip.toWireClip() = MobileSegment.WireframeClip(top, bottom, left, right)
217+
218+
private fun CapturedShapeStyle.toWireShapeStyle() = MobileSegment.ShapeStyle(
219+
backgroundColor = backgroundColor,
220+
backgroundGradient = backgroundGradient?.toWireGradient(),
221+
opacity = opacity,
222+
cornerRadius = cornerRadius
223+
)
224+
225+
private fun CapturedBackgroundGradient.toWireGradient() = MobileSegment.BackgroundGradient(
226+
stops = stops.map { MobileSegment.ShapeGradientStop(it.color, it.offset) },
227+
startPoint = MobileSegment.StartPoint(startPoint.x, startPoint.y),
228+
endPoint = MobileSegment.StartPoint(endPoint.x, endPoint.y)
229+
)
230+
231+
private fun CapturedShapeBorder.toWireBorder() = MobileSegment.ShapeBorder(color, width)
232+
233+
private fun CapturedTextStyle.toWireTextStyle() = MobileSegment.TextStyle(
234+
family = family,
235+
size = size,
236+
color = color,
237+
truncationMode = truncationMode?.toWireTruncationMode()
238+
)
239+
240+
private fun CapturedTruncationMode.toWireTruncationMode() = when (this) {
241+
CapturedTruncationMode.CLIP -> MobileSegment.TruncationMode.CLIP
242+
CapturedTruncationMode.HEAD -> MobileSegment.TruncationMode.HEAD
243+
CapturedTruncationMode.TAIL -> MobileSegment.TruncationMode.TAIL
244+
CapturedTruncationMode.MIDDLE -> MobileSegment.TruncationMode.MIDDLE
245+
}
246+
247+
private fun CapturedTextPosition.toWireTextPosition() = MobileSegment.TextPosition(
248+
padding = padding?.let { MobileSegment.Padding(it.top, it.bottom, it.left, it.right) },
249+
alignment = alignment?.let {
250+
MobileSegment.Alignment(
251+
horizontal = it.horizontal?.toWireHorizontal(),
252+
vertical = it.vertical?.toWireVertical()
253+
)
254+
}
255+
)
256+
257+
private fun CapturedHorizontalAlignment.toWireHorizontal() = when (this) {
258+
CapturedHorizontalAlignment.LEFT -> MobileSegment.Horizontal.LEFT
259+
CapturedHorizontalAlignment.CENTER -> MobileSegment.Horizontal.CENTER
260+
CapturedHorizontalAlignment.RIGHT -> MobileSegment.Horizontal.RIGHT
261+
}
262+
263+
private fun CapturedVerticalAlignment.toWireVertical() = when (this) {
264+
CapturedVerticalAlignment.TOP -> MobileSegment.Vertical.TOP
265+
CapturedVerticalAlignment.CENTER -> MobileSegment.Vertical.CENTER
266+
CapturedVerticalAlignment.BOTTOM -> MobileSegment.Vertical.BOTTOM
267+
}
268+
269+
private fun CapturedChild.toWireChild(): MobileSegment.CompositionLayerChild = when (this) {
270+
is CapturedChild.Layer -> MobileSegment.CompositionLayerChild(
271+
type = MobileSegment.Type.LAYER,
272+
id = identity.wireId
273+
)
274+
275+
is CapturedChild.Wireframe -> MobileSegment.CompositionLayerChild(
276+
type = MobileSegment.Type.WIREFRAME,
277+
id = identity.wireId
278+
)
279+
}
280+
281+
private fun CapturedModifier.toWireModifier(): MobileSegment.CompositionLayerModifier = when (this) {
282+
is CapturedModifier.BrightnessBias ->
283+
MobileSegment.CompositionLayerModifier.CompositionLayerBrightnessBiasModifier(value)
284+
285+
is CapturedModifier.Clip ->
286+
MobileSegment.CompositionLayerModifier.CompositionLayerClipModifier(
287+
path = path,
288+
fillRule = fillRule?.toWireFillRule()
289+
)
290+
291+
is CapturedModifier.ColorMatrix ->
292+
MobileSegment.CompositionLayerModifier.CompositionLayerColorMatrixModifier(values)
293+
294+
is CapturedModifier.GaussianBlur ->
295+
MobileSegment.CompositionLayerModifier.CompositionLayerGaussianBlurModifier(radius)
296+
297+
is CapturedModifier.MaskImage ->
298+
MobileSegment.CompositionLayerModifier.CompositionLayerMaskImageModifier(resourceId)
299+
300+
is CapturedModifier.Opacity ->
301+
MobileSegment.CompositionLayerModifier.CompositionLayerOpacityModifier(value)
302+
303+
is CapturedModifier.Saturate ->
304+
MobileSegment.CompositionLayerModifier.CompositionLayerSaturateModifier(value)
305+
306+
is CapturedModifier.Shadow ->
307+
MobileSegment.CompositionLayerModifier.CompositionLayerShadowModifier(
308+
color = color,
309+
offsetX = offsetX,
310+
offsetY = offsetY,
311+
radius = radius,
312+
path = path
313+
)
314+
}
315+
316+
private fun CapturedFillRule.toWireFillRule(): MobileSegment.FillRule = when (this) {
317+
CapturedFillRule.EVEN_ODD -> MobileSegment.FillRule.EVENODD
318+
CapturedFillRule.NON_ZERO -> MobileSegment.FillRule.NONZERO
319+
}
320+
321+
private fun CapturedCompositeOperation.toWireCompositeOperation(): MobileSegment.CompositeOperation =
322+
when (this) {
323+
CapturedCompositeOperation.DESTINATION_IN -> MobileSegment.CompositeOperation.DESTINATIONIN
324+
CapturedCompositeOperation.DESTINATION_OUT -> MobileSegment.CompositeOperation.DESTINATIONOUT
325+
CapturedCompositeOperation.PLUS_DARKER -> MobileSegment.CompositeOperation.PLUSDARKER
326+
CapturedCompositeOperation.SOURCE_OVER -> MobileSegment.CompositeOperation.SOURCEOVER
327+
}
328+
329+
private fun <T, R> CapturedChange<T>.mapOrNull(transform: (T) -> R): R? =
330+
when (this) {
331+
is CapturedChange.Set -> transform(value)
332+
CapturedChange.Unchanged -> null
333+
}
334+
335+
// The list contains exactly the single failure constructed here.
336+
@Suppress("UnsafeThirdPartyFunctionCall")
337+
private fun invalidMapping(
338+
code: CaptureValidationErrorCode,
339+
identity: CapturedIdentity? = null
340+
): CaptureWireMappingResult.Invalid = CaptureWireMappingResult.Invalid(
341+
listOf(CaptureValidationFailure(code, identity))
342+
)

0 commit comments

Comments
 (0)