|
| 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