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