Skip to content

Commit b83e7b6

Browse files
authored
Merge pull request #2364 from DataDog/yl/sr-mnt/add-chip-support
RUM-6871: Add Material Chip mapper and improve CompoundButton telemetry
2 parents 51ab074 + 92876f8 commit b83e7b6

File tree

10 files changed

+337
-13
lines changed

10 files changed

+337
-13
lines changed

detekt_custom.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,7 @@ datadog:
926926
- "kotlin.collections.MutableList.removeAll(kotlin.Function1)"
927927
- "kotlin.collections.MutableList.removeFirstOrNull()"
928928
- "kotlin.collections.MutableList.toMutableList()"
929+
- "kotlin.collections.MutableList.toList()"
929930
- "kotlin.collections.MutableList.toSet()"
930931
- "kotlin.collections.MutableList.toTypedArray()"
931932
- "kotlin.collections.MutableList.withIndex()"

features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.cardview.widget.CardView
1010
import com.datadog.android.sessionreplay.ExtensionSupport
1111
import com.datadog.android.sessionreplay.MapperTypeWrapper
1212
import com.datadog.android.sessionreplay.material.internal.CardWireframeMapper
13+
import com.datadog.android.sessionreplay.material.internal.ChipWireframeMapper
1314
import com.datadog.android.sessionreplay.material.internal.MaterialDrawableToColorMapper
1415
import com.datadog.android.sessionreplay.material.internal.MaterialOptionSelectorDetector
1516
import com.datadog.android.sessionreplay.material.internal.SliderWireframeMapper
@@ -23,6 +24,7 @@ import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
2324
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
2425
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
2526
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver
27+
import com.google.android.material.chip.Chip
2628
import com.google.android.material.slider.Slider
2729
import com.google.android.material.tabs.TabLayout
2830

@@ -63,11 +65,18 @@ class MaterialExtensionSupport : ExtensionSupport {
6365
viewBoundsResolver,
6466
drawableToColorMapper
6567
)
68+
val chipWireframeMapper = ChipWireframeMapper(
69+
viewIdentifierResolver,
70+
colorStringFormatter,
71+
viewBoundsResolver,
72+
drawableToColorMapper
73+
)
6674

6775
return listOf(
6876
MapperTypeWrapper(Slider::class.java, sliderWireframeMapper),
6977
MapperTypeWrapper(TabLayout.TabView::class.java, tabWireframeMapper),
70-
MapperTypeWrapper(CardView::class.java, cardWireframeMapper)
78+
MapperTypeWrapper(CardView::class.java, cardWireframeMapper),
79+
MapperTypeWrapper(Chip::class.java, chipWireframeMapper)
7180
)
7281
}
7382

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.material.internal
8+
9+
import com.datadog.android.api.InternalLogger
10+
import com.datadog.android.sessionreplay.ImagePrivacy
11+
import com.datadog.android.sessionreplay.model.MobileSegment
12+
import com.datadog.android.sessionreplay.recorder.MappingContext
13+
import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
14+
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
15+
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
16+
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
17+
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
18+
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver
19+
import com.google.android.material.chip.Chip
20+
21+
internal class ChipWireframeMapper(
22+
viewIdentifierResolver: ViewIdentifierResolver,
23+
colorStringFormatter: ColorStringFormatter,
24+
viewBoundsResolver: ViewBoundsResolver,
25+
drawableToColorMapper: DrawableToColorMapper
26+
) : TextViewMapper<Chip>(
27+
viewIdentifierResolver,
28+
colorStringFormatter,
29+
viewBoundsResolver,
30+
drawableToColorMapper
31+
) {
32+
override fun map(
33+
view: Chip,
34+
mappingContext: MappingContext,
35+
asyncJobStatusCallback: AsyncJobStatusCallback,
36+
internalLogger: InternalLogger
37+
): List<MobileSegment.Wireframe> {
38+
val wireframes = mutableListOf<MobileSegment.Wireframe>()
39+
40+
val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(
41+
view,
42+
mappingContext.systemInformation.screenDensity
43+
)
44+
val density = mappingContext.systemInformation.screenDensity
45+
val drawableBounds = view.chipDrawable.bounds
46+
val backgroundWireframe = mappingContext.imageWireframeHelper.createImageWireframe(
47+
view = view,
48+
// Background drawable doesn't need to be masked.
49+
imagePrivacy = ImagePrivacy.MASK_NONE,
50+
currentWireframeIndex = 0,
51+
x = viewGlobalBounds.x + drawableBounds.left.toLong().densityNormalized(density),
52+
y = viewGlobalBounds.y + drawableBounds.top.toLong().densityNormalized(density),
53+
width = view.chipDrawable.intrinsicWidth,
54+
height = view.chipDrawable.intrinsicHeight,
55+
usePIIPlaceholder = false,
56+
drawable = view.chipDrawable,
57+
asyncJobStatusCallback = asyncJobStatusCallback
58+
)
59+
backgroundWireframe?.let {
60+
wireframes.add(it)
61+
}
62+
// Text wireframe
63+
wireframes.add(super.createTextWireframe(view, mappingContext, viewGlobalBounds))
64+
return wireframes.toList()
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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.material
8+
9+
import android.graphics.Rect
10+
import android.graphics.Typeface
11+
import android.graphics.drawable.Drawable
12+
import android.text.Layout
13+
import com.datadog.android.api.InternalLogger
14+
import com.datadog.android.sessionreplay.ImagePrivacy
15+
import com.datadog.android.sessionreplay.material.forge.ForgeConfigurator
16+
import com.datadog.android.sessionreplay.material.internal.ChipWireframeMapper
17+
import com.datadog.android.sessionreplay.material.internal.densityNormalized
18+
import com.datadog.android.sessionreplay.recorder.MappingContext
19+
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
20+
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
21+
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
22+
import com.datadog.android.sessionreplay.utils.GlobalBounds
23+
import com.datadog.android.sessionreplay.utils.OPAQUE_ALPHA_VALUE
24+
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
25+
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver
26+
import com.google.android.material.chip.Chip
27+
import fr.xgouchet.elmyr.Forge
28+
import fr.xgouchet.elmyr.annotation.FloatForgery
29+
import fr.xgouchet.elmyr.annotation.Forgery
30+
import fr.xgouchet.elmyr.annotation.IntForgery
31+
import fr.xgouchet.elmyr.annotation.LongForgery
32+
import fr.xgouchet.elmyr.annotation.StringForgery
33+
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
34+
import fr.xgouchet.elmyr.junit5.ForgeExtension
35+
import org.junit.jupiter.api.BeforeEach
36+
import org.junit.jupiter.api.Test
37+
import org.junit.jupiter.api.extension.ExtendWith
38+
import org.junit.jupiter.api.extension.Extensions
39+
import org.mockito.ArgumentMatchers.anyInt
40+
import org.mockito.Mock
41+
import org.mockito.junit.jupiter.MockitoExtension
42+
import org.mockito.junit.jupiter.MockitoSettings
43+
import org.mockito.kotlin.any
44+
import org.mockito.kotlin.doReturn
45+
import org.mockito.kotlin.eq
46+
import org.mockito.kotlin.isNull
47+
import org.mockito.kotlin.mock
48+
import org.mockito.kotlin.times
49+
import org.mockito.kotlin.verify
50+
import org.mockito.kotlin.whenever
51+
import org.mockito.quality.Strictness
52+
53+
@Extensions(
54+
ExtendWith(MockitoExtension::class),
55+
ExtendWith(ForgeExtension::class)
56+
)
57+
@MockitoSettings(strictness = Strictness.LENIENT)
58+
@ForgeConfiguration(value = ForgeConfigurator::class)
59+
class ChipWireframeMapperTest {
60+
61+
private lateinit var testedChipWireframeMapper: ChipWireframeMapper
62+
63+
@Mock
64+
lateinit var mockChipView: Chip
65+
66+
@Mock
67+
lateinit var mockViewBoundsResolver: ViewBoundsResolver
68+
69+
@Mock
70+
lateinit var mockAsyncJobStatusCallback: AsyncJobStatusCallback
71+
72+
@Mock
73+
lateinit var mockInternalLogger: InternalLogger
74+
75+
@Mock
76+
lateinit var mockViewIdentifierResolver: ViewIdentifierResolver
77+
78+
@Mock
79+
lateinit var mockColorStringFormatter: ColorStringFormatter
80+
81+
@Mock
82+
lateinit var mockDrawableToColorMapper: DrawableToColorMapper
83+
84+
@Mock
85+
lateinit var mockChipDrawable: Drawable
86+
87+
@LongForgery
88+
var fakeViewId: Long = 0L
89+
90+
@StringForgery
91+
var fakeText: String = ""
92+
93+
@Forgery
94+
lateinit var fakeMappingContext: MappingContext
95+
96+
@Forgery
97+
lateinit var fakeGlobalBounds: GlobalBounds
98+
99+
lateinit var fakeDrawableBounds: Rect
100+
101+
@IntForgery
102+
private var fakeDrawableHeight: Int = 0
103+
104+
@IntForgery
105+
private var fakeDrawableWidth: Int = 0
106+
107+
@Mock
108+
lateinit var mockLayout: Layout
109+
110+
@FloatForgery(0f, 255f)
111+
var fakeFontSize: Float = 0f
112+
113+
@IntForgery(min = 0, max = 0xffffff)
114+
var fakeTextColor: Int = 0
115+
116+
@BeforeEach
117+
fun `set up`(forge: Forge) {
118+
fakeDrawableBounds = Rect(
119+
forge.aSmallInt(),
120+
forge.aSmallInt(),
121+
forge.aSmallInt(),
122+
forge.aSmallInt()
123+
)
124+
mockChipView = mockChip()
125+
whenever(
126+
mockViewBoundsResolver.resolveViewGlobalBounds(
127+
mockChipView,
128+
fakeMappingContext.systemInformation.screenDensity
129+
)
130+
).thenReturn(fakeGlobalBounds)
131+
whenever(
132+
mockViewIdentifierResolver.resolveViewId(
133+
mockChipView
134+
)
135+
).thenReturn(fakeViewId)
136+
testedChipWireframeMapper = ChipWireframeMapper(
137+
viewIdentifierResolver = mockViewIdentifierResolver,
138+
colorStringFormatter = mockColorStringFormatter,
139+
viewBoundsResolver = mockViewBoundsResolver,
140+
drawableToColorMapper = mockDrawableToColorMapper
141+
)
142+
}
143+
144+
@Test
145+
fun `M resolves card view wireframe W map`() {
146+
// When
147+
testedChipWireframeMapper.map(
148+
mockChipView,
149+
fakeMappingContext,
150+
mockAsyncJobStatusCallback,
151+
mockInternalLogger
152+
)
153+
154+
// Then
155+
val density = fakeMappingContext.systemInformation.screenDensity
156+
157+
verify(fakeMappingContext.imageWireframeHelper).createImageWireframe(
158+
view = eq(mockChipView),
159+
// Background drawable doesn't need to be masked.
160+
imagePrivacy = eq(ImagePrivacy.MASK_NONE),
161+
currentWireframeIndex = anyInt(),
162+
x = eq(
163+
fakeGlobalBounds.x + fakeDrawableBounds.left.toLong()
164+
.densityNormalized(density)
165+
),
166+
y = eq(
167+
fakeGlobalBounds.y + fakeDrawableBounds.top.toLong()
168+
.densityNormalized(density)
169+
),
170+
width = eq(fakeDrawableWidth),
171+
height = eq(fakeDrawableHeight),
172+
usePIIPlaceholder = eq(false),
173+
drawable = eq(mockChipDrawable),
174+
drawableCopier = any(),
175+
asyncJobStatusCallback = eq(mockAsyncJobStatusCallback),
176+
clipping = isNull(),
177+
shapeStyle = isNull(),
178+
border = isNull(),
179+
prefix = any()
180+
)
181+
}
182+
183+
private fun mockChip(): Chip {
184+
return mock {
185+
whenever(it.text).thenReturn(fakeText)
186+
whenever(it.chipDrawable).thenReturn(mockChipDrawable)
187+
whenever(mockChipDrawable.bounds).thenReturn(fakeDrawableBounds)
188+
whenever(mockChipDrawable.intrinsicWidth).thenReturn(fakeDrawableWidth)
189+
whenever(mockChipDrawable.intrinsicHeight).thenReturn(fakeDrawableHeight)
190+
191+
whenever(it.layout) doReturn mockLayout
192+
whenever(it.typeface) doReturn Typeface.SERIF
193+
whenever(it.textSize) doReturn fakeFontSize
194+
whenever(it.currentTextColor) doReturn fakeTextColor
195+
whenever(it.textAlignment) doReturn 0
196+
whenever(it.gravity) doReturn 0
197+
whenever(
198+
mockColorStringFormatter.formatColorAndAlphaAsHexString(
199+
fakeTextColor,
200+
OPAQUE_ALPHA_VALUE
201+
)
202+
) doReturn ""
203+
}
204+
}
205+
}

features/dd-sdk-android-session-replay/api/apiSurface

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ open class com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper<T: a
7878
constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper)
7979
override fun map(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
8080
protected open fun resolveCapturedText(T, com.datadog.android.sessionreplay.TextAndInputPrivacy, Boolean): String
81+
protected fun createTextWireframe(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.GlobalBounds): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.TextWireframe
8182
interface com.datadog.android.sessionreplay.recorder.mapper.TraverseAllChildrenMapper<T: android.view.ViewGroup> : WireframeMapper<T>
8283
interface com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper<T: android.view.View>
8384
fun map(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>

features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,7 @@ public final class com/datadog/android/sessionreplay/recorder/mapper/EditTextMap
14741474

14751475
public class com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper : com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper {
14761476
public fun <init> (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V
1477+
protected final fun createTextWireframe (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe;
14771478
public synthetic fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List;
14781479
public fun map (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List;
14791480
protected fun resolveCapturedText (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Z)Ljava/lang/String;

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,24 @@ internal abstract class CheckableCompoundButtonMapper<T : CompoundButton>(
6363
} else {
6464
CHECK_BOX_NOT_CHECKED_DRAWABLE_INDEX
6565
}
66-
(view.buttonDrawable?.constantState as? DrawableContainer.DrawableContainerState)?.getChild(
67-
checkableDrawableIndex
68-
)
66+
view.buttonDrawable?.let {
67+
(it.constantState as? DrawableContainer.DrawableContainerState)?.getChild(
68+
checkableDrawableIndex
69+
)
70+
} ?: kotlin.run {
71+
internalLogger.log(
72+
level = InternalLogger.Level.ERROR,
73+
targets = listOf(
74+
InternalLogger.Target.MAINTAINER,
75+
InternalLogger.Target.TELEMETRY
76+
),
77+
messageBuilder = { NULL_BUTTON_DRAWABLE_MSG },
78+
additionalProperties = mapOf(
79+
"replay.compound.view" to view.javaClass.canonicalName
80+
)
81+
)
82+
null
83+
}
6984
} else {
7085
// view.buttonDrawable is not available below API 23, so reflection is used to retrieve it.
7186
try {
@@ -88,14 +103,7 @@ internal abstract class CheckableCompoundButtonMapper<T : CompoundButton>(
88103
null
89104
}
90105
}
91-
return originCheckableDrawable?.let { cloneCheckableDrawable(view, it) } ?: run {
92-
internalLogger.log(
93-
level = InternalLogger.Level.ERROR,
94-
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
95-
messageBuilder = { GET_DRAWABLE_FAIL_MESSAGE }
96-
)
97-
null
98-
}
106+
return originCheckableDrawable?.let { cloneCheckableDrawable(view, it) }
99107
}
100108

101109
private fun cloneCheckableDrawable(view: T, drawable: Drawable): Drawable? {
@@ -115,6 +123,8 @@ internal abstract class CheckableCompoundButtonMapper<T : CompoundButton>(
115123
internal const val DEFAULT_CHECKABLE_HEIGHT_IN_DP = 32L
116124
internal const val GET_DRAWABLE_FAIL_MESSAGE =
117125
"Failed to get buttonDrawable from the checkable compound button."
126+
internal const val NULL_BUTTON_DRAWABLE_MSG =
127+
"ButtonDrawable of the compound button is null"
118128

119129
// Reflects the field at the initialization of the class instead of reflecting it for every wireframe generation
120130
@Suppress("PrivateApi", "SwallowedException", "TooGenericExceptionCaught")

0 commit comments

Comments
 (0)