Skip to content

Commit 7dfe501

Browse files
committed
Provide wireframe clipping property according with its parents bounds
1 parent 37407f7 commit 7dfe501

28 files changed

Lines changed: 1484 additions & 341 deletions

detekt.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ datadog:
899899
- "java.util.LinkedList.add(com.datadog.android.privacy.TrackingConsentProviderCallback)"
900900
- "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.Add)"
901901
- "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.Remove)"
902+
- "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)"
902903
- "java.util.LinkedList.add(com.datadog.android.sessionreplay.model.MobileSegment.WireframeUpdateMutation)"
903904
- "java.util.LinkedList.addFirst(android.view.View)"
904905
- "java.util.LinkedList.add(com.datadog.android.sessionreplay.recorder.Node)"

library/dd-sdk-android-session-replay/apiSurface

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ data class com.datadog.android.sessionreplay.model.MobileSegment
126126
sealed class Wireframe
127127
abstract fun toJson(): com.google.gson.JsonElement
128128
data class ShapeWireframe : Wireframe
129-
constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, ShapeStyle? = null, ShapeBorder? = null)
129+
constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null)
130130
val type: kotlin.String
131131
override fun toJson(): com.google.gson.JsonElement
132132
companion object
133133
fun fromJson(kotlin.String): ShapeWireframe
134134
data class TextWireframe : Wireframe
135-
constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, TextStyle, TextPosition? = null)
135+
constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, TextStyle, TextPosition? = null)
136136
val type: kotlin.String
137137
override fun toJson(): com.google.gson.JsonElement
138138
companion object
@@ -152,13 +152,13 @@ data class com.datadog.android.sessionreplay.model.MobileSegment
152152
sealed class WireframeUpdateMutation
153153
abstract fun toJson(): com.google.gson.JsonElement
154154
data class TextWireframeUpdate : WireframeUpdateMutation
155-
constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String? = null, TextStyle? = null, TextPosition? = null)
155+
constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String? = null, TextStyle? = null, TextPosition? = null)
156156
val type: kotlin.String
157157
override fun toJson(): com.google.gson.JsonElement
158158
companion object
159159
fun fromJson(kotlin.String): TextWireframeUpdate
160160
data class ShapeWireframeUpdate : WireframeUpdateMutation
161-
constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, ShapeStyle? = null, ShapeBorder? = null)
161+
constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null)
162162
val type: kotlin.String
163163
override fun toJson(): com.google.gson.JsonElement
164164
companion object
@@ -170,6 +170,11 @@ data class com.datadog.android.sessionreplay.model.MobileSegment
170170
fun toJson(): com.google.gson.JsonElement
171171
companion object
172172
fun fromJson(kotlin.String): Position
173+
data class WireframeClip
174+
constructor(kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null)
175+
fun toJson(): com.google.gson.JsonElement
176+
companion object
177+
fun fromJson(kotlin.String): WireframeClip
173178
data class ShapeStyle
174179
constructor(kotlin.String? = null, kotlin.Number? = null, kotlin.Number? = null)
175180
fun toJson(): com.google.gson.JsonElement

library/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/processor/MobileSegmentExt.kt

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,10 @@ package com.datadog.android.sessionreplay.processor
88

99
import com.datadog.android.sessionreplay.model.MobileSegment
1010

11-
internal fun MobileSegment.Wireframe.bounds(): Bounds {
11+
internal fun MobileSegment.Wireframe.copy(clip: MobileSegment.WireframeClip?):
12+
MobileSegment.Wireframe {
1213
return when (this) {
13-
is MobileSegment.Wireframe.ShapeWireframe -> this.bounds()
14-
is MobileSegment.Wireframe.TextWireframe -> this.bounds()
14+
is MobileSegment.Wireframe.ShapeWireframe -> this.copy(clip = clip)
15+
is MobileSegment.Wireframe.TextWireframe -> this.copy(clip = clip)
1516
}
1617
}
17-
18-
internal fun MobileSegment.Wireframe.ShapeWireframe.bounds(): Bounds {
19-
return Bounds(x, y, width, height)
20-
}
21-
22-
internal fun MobileSegment.Wireframe.TextWireframe.bounds(): Bounds {
23-
return Bounds(x, y, width, height)
24-
}
25-
26-
internal data class Bounds(val x: Long, val y: Long, val width: Long, val height: Long)

library/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/processor/MutationResolver.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ internal class MutationResolver {
218218
if (prevWireframe.shapeStyle != currentWireframe.shapeStyle) {
219219
mutation = mutation.copy(shapeStyle = currentWireframe.shapeStyle)
220220
}
221+
if (prevWireframe.clip != currentWireframe.clip) {
222+
mutation = mutation.copy(
223+
clip = currentWireframe.clip
224+
?: MobileSegment.WireframeClip(0, 0, 0, 0)
225+
)
226+
}
221227

222228
return mutation
223229
}
@@ -280,6 +286,12 @@ internal class MutationResolver {
280286
if (prevWireframe.textPosition != currentWireframe.textPosition) {
281287
mutation = mutation.copy(textPosition = currentWireframe.textPosition)
282288
}
289+
if (prevWireframe.clip != currentWireframe.clip) {
290+
mutation = mutation.copy(
291+
clip = currentWireframe.clip
292+
?: MobileSegment.WireframeClip(0, 0, 0, 0)
293+
)
294+
}
283295

284296
return mutation
285297
}

library/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/processor/NodeFlattener.kt

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ import com.datadog.android.sessionreplay.recorder.Node
1111
import java.util.LinkedList
1212
import java.util.Stack
1313

14-
internal class NodeFlattener {
14+
internal class NodeFlattener(private val wireframeUtils: WireframeUtils = WireframeUtils()) {
1515

1616
internal fun flattenNode(root: Node): List<MobileSegment.Wireframe> {
1717
val stack = Stack<Node>()
1818
val list = LinkedList<MobileSegment.Wireframe>()
1919
stack.push(root)
2020
while (stack.isNotEmpty()) {
2121
val node = stack.pop()
22-
list.addAll(node.wireframes)
22+
var wireframe = node.wireframe
23+
wireframeUtils.resolveWireframeClip(wireframe, node.parents)?.let { clip ->
24+
wireframe = wireframe.copy(clip = clip)
25+
}
26+
list.add(wireframe)
27+
2328
for (i in node.children.count() - 1 downTo 0) {
2429
stack.push(node.children[i])
2530
}
@@ -30,30 +35,7 @@ internal class NodeFlattener {
3035
private fun filterOutInvalidWireframes(wireframes: List<MobileSegment.Wireframe>):
3136
List<MobileSegment.Wireframe> {
3237
return wireframes.filterIndexed { index, wireframe ->
33-
isValidWireframe(wireframe, wireframes.drop(index + 1))
34-
}
35-
}
36-
37-
private fun isValidWireframe(
38-
wireframe: MobileSegment.Wireframe,
39-
topWireframes: List<MobileSegment.Wireframe>
40-
): Boolean {
41-
val wireframeBounds = wireframe.bounds()
42-
if (wireframeBounds.width <= 0 || wireframeBounds.height <= 0) {
43-
return false
38+
wireframeUtils.checkIsValidWireframe(wireframe, wireframes.drop(index + 1))
4439
}
45-
topWireframes.forEach {
46-
if (it.bounds().isCovering(wireframeBounds)) {
47-
return false
48-
}
49-
}
50-
return true
51-
}
52-
53-
private fun Bounds.isCovering(other: Bounds): Boolean {
54-
return this.x <= other.x &&
55-
x + width >= other.x + other.width &&
56-
y <= other.y &&
57-
y + height >= other.y + other.height
5840
}
5941
}

library/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/processor/SnapshotProcessor.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@ internal class SnapshotProcessor(
3636

3737
@MainThread
3838
override fun process(node: Node, orientationChanged: OrientationChanged?) {
39-
if (node.wireframes.isEmpty()) {
40-
// TODO: RUMM-2397 Add the proper logs here once the sdkLogger will be added
41-
return
42-
}
43-
4439
buildRunnable { timestamp, newContext, currentContext ->
4540
Runnable {
4641
@Suppress("ThreadSafety") // this runs inside an executor
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.processor
8+
9+
import com.datadog.android.sessionreplay.model.MobileSegment
10+
import kotlin.math.max
11+
12+
internal class WireframeUtils {
13+
14+
internal fun resolveWireframeClip(
15+
wireframe: MobileSegment.Wireframe,
16+
parents: List<MobileSegment.Wireframe>
17+
): MobileSegment.WireframeClip? {
18+
var clipTop = 0L
19+
var clipLeft = 0L
20+
var clipRight = 0L
21+
var clipBottom = 0L
22+
val wireframeBounds = wireframe.bounds()
23+
24+
parents.map { it.bounds() }.forEach {
25+
clipTop = max(it.top - wireframeBounds.top, clipTop)
26+
clipBottom = max(wireframeBounds.bottom - it.bottom, clipBottom)
27+
clipLeft = max(it.left - wireframeBounds.left, clipLeft)
28+
clipRight = max(wireframeBounds.right - it.right, clipRight)
29+
}
30+
31+
@Suppress("ComplexCondition")
32+
return if (clipTop > 0 || clipBottom > 0 || clipLeft > 0 || clipRight > 0) {
33+
MobileSegment.WireframeClip(
34+
top = clipTop,
35+
bottom = clipBottom,
36+
left = clipLeft,
37+
right = clipRight
38+
)
39+
} else {
40+
null
41+
}
42+
}
43+
44+
internal fun checkIsValidWireframe(
45+
wireframe: MobileSegment.Wireframe,
46+
topWireframes: List<MobileSegment.Wireframe>
47+
): Boolean {
48+
val wireframeBounds = wireframe.bounds()
49+
if (wireframeBounds.width <= 0 || wireframeBounds.height <= 0) {
50+
return false
51+
}
52+
topWireframes.forEach {
53+
if (it.bounds().isCovering(wireframeBounds)) {
54+
return false
55+
}
56+
}
57+
return true
58+
}
59+
60+
private fun Bounds.isCovering(other: Bounds): Boolean {
61+
return left <= other.left &&
62+
right >= other.right &&
63+
top <= other.top &&
64+
bottom >= other.bottom
65+
}
66+
67+
private fun MobileSegment.Wireframe.bounds(): Bounds {
68+
return when (this) {
69+
is MobileSegment.Wireframe.ShapeWireframe -> this.bounds()
70+
is MobileSegment.Wireframe.TextWireframe -> this.bounds()
71+
}
72+
}
73+
74+
private fun MobileSegment.Wireframe.ShapeWireframe.bounds(): Bounds {
75+
return Bounds(
76+
left = x + (clip?.left ?: 0),
77+
right = x + width - (clip?.right ?: 0),
78+
top = y + (clip?.top ?: 0),
79+
bottom = y + height - (clip?.bottom ?: 0),
80+
width = width,
81+
height = height
82+
)
83+
}
84+
85+
private fun MobileSegment.Wireframe.TextWireframe.bounds(): Bounds {
86+
return Bounds(
87+
left = x + (clip?.left ?: 0),
88+
right = x + width - (clip?.right ?: 0),
89+
top = y + (clip?.top ?: 0),
90+
bottom = y + height - (clip?.bottom ?: 0),
91+
width = width,
92+
height = height
93+
)
94+
}
95+
96+
internal data class Bounds(
97+
val left: Long,
98+
val right: Long,
99+
val top: Long,
100+
val bottom: Long,
101+
val width: Long,
102+
val height: Long
103+
)
104+
}

library/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/Node.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.datadog.android.sessionreplay.recorder
99
import com.datadog.android.sessionreplay.model.MobileSegment
1010

1111
internal data class Node(
12-
val wireframes: List<MobileSegment.Wireframe>,
13-
val children: List<Node> = emptyList()
12+
val wireframe: MobileSegment.Wireframe,
13+
val children: List<Node> = emptyList(),
14+
val parents: List<MobileSegment.Wireframe> = emptyList()
1415
)

library/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/SnapshotProducer.kt

Lines changed: 21 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,60 @@
66

77
package com.datadog.android.sessionreplay.recorder
88

9-
import android.os.Build
109
import android.view.View
1110
import android.view.ViewGroup
12-
import android.view.ViewStub
13-
import androidx.appcompat.widget.ActionBarContextView
14-
import androidx.appcompat.widget.Toolbar
11+
import com.datadog.android.sessionreplay.model.MobileSegment
1512
import com.datadog.android.sessionreplay.recorder.mapper.GenericWireframeMapper
1613
import java.util.LinkedList
1714

1815
internal class SnapshotProducer(
19-
private val wireframeMapper: GenericWireframeMapper
16+
private val wireframeMapper: GenericWireframeMapper,
17+
private val viewUtils: ViewUtils = ViewUtils()
2018
) {
2119

22-
private val systemViewIds by lazy {
23-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
24-
setOf(android.R.id.navigationBarBackground, android.R.id.statusBarBackground)
25-
} else {
26-
emptySet()
27-
}
28-
}
29-
3020
fun produce(rootView: View, pixelsDensity: Float): Node? {
31-
return convertViewToNode(rootView, pixelsDensity)
21+
return convertViewToNode(rootView, pixelsDensity, LinkedList())
3222
}
3323

3424
@Suppress("ComplexMethod")
35-
private fun convertViewToNode(view: View, pixelsDensity: Float): Node? {
36-
if (view.isNotVisible()) {
25+
private fun convertViewToNode(
26+
view: View,
27+
pixelsDensity: Float,
28+
parents: LinkedList<MobileSegment.Wireframe>
29+
): Node? {
30+
if (viewUtils.checkIfNotVisible(view)) {
3731
return null
3832
}
3933

40-
if (view.isSystemNoise()) {
34+
if (viewUtils.checkIfSystemNoise(view)) {
4135
return null
4236
}
4337

44-
if (view.isToolbar()) {
38+
if (viewUtils.checkIsToolbar(view)) {
4539
// skip adding the children and just take a screenshot of the toolbar.
4640
// It is too complex to de - structure this in multiple wireframes
4741
// and we cannot actually get all the details here.
4842
return Node(
49-
wireframes = listOf(wireframeMapper.imageMapper.map(view, pixelsDensity))
43+
wireframe = wireframeMapper.imageMapper.map(view, pixelsDensity)
5044
)
5145
}
5246

5347
val childNodes = LinkedList<Node>()
48+
val wireframe = wireframeMapper.map(view, pixelsDensity)
5449
if (view is ViewGroup && view.childCount > 0) {
50+
val parentsCopy = LinkedList(parents).apply { add(wireframe) }
5551
for (i in 0 until view.childCount) {
5652
val viewChild = view.getChildAt(i) ?: continue
57-
convertViewToNode(viewChild, pixelsDensity)?.let {
53+
convertViewToNode(viewChild, pixelsDensity, parentsCopy)?.let {
5854
childNodes.add(it)
5955
}
6056
}
6157
}
6258

63-
return convertViewToNode(view, childNodes, pixelsDensity)
64-
}
65-
66-
private fun convertViewToNode(
67-
view: View,
68-
childNodes: LinkedList<Node>,
69-
pixelsDensity: Float
70-
) = Node(
71-
children = childNodes,
72-
wireframes = listOf(wireframeMapper.map(view, pixelsDensity))
73-
)
74-
75-
private fun View.isToolbar(): Boolean {
76-
return (
77-
Toolbar::class.java.isAssignableFrom(this::class.java) &&
78-
this.id == androidx.appcompat.R.id.action_bar
79-
) ||
80-
(
81-
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
82-
android.widget.Toolbar::class.java
83-
.isAssignableFrom(this::class.java)
84-
)
85-
}
86-
87-
private fun View.isNotVisible(): Boolean {
88-
return !this.isShown || this.width <= 0 || this.height <= 0
89-
}
90-
91-
private fun View.isSystemNoise(): Boolean {
92-
return id in systemViewIds ||
93-
this is ViewStub ||
94-
this is ActionBarContextView
59+
return Node(
60+
children = childNodes,
61+
wireframe = wireframe,
62+
parents = parents
63+
)
9564
}
9665
}

0 commit comments

Comments
 (0)