Skip to content

Commit 551b859

Browse files
authored
Merge pull request #2452 from DataDog/yl/compose/ensure-webview
RUM-7195: Support interop view from Compose
2 parents 8504c86 + c8d3254 commit 551b859

File tree

18 files changed

+236
-15
lines changed

18 files changed

+236
-15
lines changed

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/AndroidComposeViewMapper.kt

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
package com.datadog.android.sessionreplay.compose.internal.mappers.semantics
1010

11+
import androidx.annotation.UiThread
1112
import androidx.compose.ui.platform.AndroidComposeView
1213
import com.datadog.android.api.InternalLogger
1314
import com.datadog.android.sessionreplay.model.MobileSegment
@@ -31,6 +32,7 @@ internal class AndroidComposeViewMapper(
3132
viewBoundsResolver,
3233
drawableToColorMapper
3334
) {
35+
@UiThread
3436
override fun map(
3537
view: AndroidComposeView,
3638
mappingContext: MappingContext,

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/ComposeViewMapper.kt

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package com.datadog.android.sessionreplay.compose.internal.mappers.semantics
88

9+
import androidx.annotation.UiThread
910
import androidx.compose.ui.platform.ComposeView
1011
import com.datadog.android.api.InternalLogger
1112
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
@@ -31,6 +32,7 @@ internal class ComposeViewMapper(
3132
viewBoundsResolver,
3233
drawableToColorMapper
3334
) {
35+
@UiThread
3436
override fun map(
3537
view: ComposeView,
3638
mappingContext: MappingContext,

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/RootSemanticsNodeMapper.kt

+18-3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ internal class RootSemanticsNodeMapper(
5050
)
5151
) {
5252

53+
@UiThread
5354
internal fun createComposeWireframes(
5455
semanticsNode: SemanticsNode,
5556
density: Float,
@@ -69,18 +70,21 @@ internal class RootSemanticsNodeMapper(
6970
textAndInputPrivacy = mappingContext.textAndInputPrivacy,
7071
imageWireframeHelper = mappingContext.imageWireframeHelper
7172
),
72-
asyncJobStatusCallback = asyncJobStatusCallback
73+
asyncJobStatusCallback = asyncJobStatusCallback,
74+
mappingContext = mappingContext
7375
)
7476
}
7577
return wireframes
7678
}
7779

80+
@UiThread
7881
private fun createComposerWireframes(
7982
semanticsNode: SemanticsNode,
8083
touchPrivacyManager: TouchPrivacyManager,
8184
wireframes: MutableList<MobileSegment.Wireframe>,
8285
parentUiContext: UiContext,
83-
asyncJobStatusCallback: AsyncJobStatusCallback
86+
asyncJobStatusCallback: AsyncJobStatusCallback,
87+
mappingContext: MappingContext
8488
) {
8589
// If Hidden node is detected, add placeholder wireframe and return
8690
if (semanticsUtils.isNodeHidden(semanticsNode)) {
@@ -93,6 +97,16 @@ internal class RootSemanticsNodeMapper(
9397
}
9498
return
9599
}
100+
101+
val interopView = semanticsUtils.getInteropView(semanticsNode)
102+
103+
if (interopView != null) {
104+
val interopViewWireframes =
105+
mappingContext.interopViewCallback.map(interopView, mappingContext)
106+
wireframes.addAll(interopViewWireframes)
107+
return
108+
}
109+
96110
val mapper = getSemanticsNodeMapper(semanticsNode)
97111
updateTouchOverrideAreas(
98112
touchPrivacyManager = touchPrivacyManager,
@@ -119,7 +133,8 @@ internal class RootSemanticsNodeMapper(
119133
touchPrivacyManager = touchPrivacyManager,
120134
wireframes = wireframes,
121135
parentUiContext = currentUiContext,
122-
asyncJobStatusCallback = asyncJobStatusCallback
136+
asyncJobStatusCallback = asyncJobStatusCallback,
137+
mappingContext = mappingContext
123138
)
124139
}
125140
}

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/reflection/ComposeReflection.kt

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal object ComposeReflection {
2323
val OwnerField = WrappedCompositionClass?.getDeclaredFieldSafe("owner")
2424

2525
val LayoutNodeClass = getClassSafe("androidx.compose.ui.node.LayoutNode")
26+
val GetInteropViewMethod = LayoutNodeClass?.getDeclaredMethodSafe("getInteropView")
2627

2728
val SemanticsNodeClass = getClassSafe("androidx.compose.ui.semantics.SemanticsNode")
2829
val LayoutNodeField = SemanticsNodeClass?.getDeclaredFieldSafe("layoutNode")

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/ReflectionUtils.kt

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeRefl
2727
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ContentPainterElementClass
2828
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ContentPainterModifierClass
2929
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.GetInnerLayerCoordinatorMethod
30+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.GetInteropViewMethod
3031
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ImageField
3132
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.LayoutField
3233
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.LayoutNodeField
@@ -166,4 +167,8 @@ internal class ReflectionUtils {
166167
val staticLayout = StaticLayoutField?.getSafe(layout) as? StaticLayout
167168
return staticLayout?.text?.toString()
168169
}
170+
171+
fun getInteropView(semanticsNode: SemanticsNode): View? {
172+
return GetInteropViewMethod?.invoke(semanticsNode.layoutInfo) as? View
173+
}
169174
}

features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtils.kt

+4
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,8 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref
273273
internal fun isNodeHidden(semanticsNode: SemanticsNode): Boolean {
274274
return semanticsNode.config.getOrNull(SessionReplayHidePropertyKey) ?: false
275275
}
276+
277+
internal fun getInteropView(semanticsNode: SemanticsNode): View? {
278+
return reflectionUtils.getInteropView(semanticsNode)
279+
}
276280
}

features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/RootSemanticsNodeMapperTest.kt

+22
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package com.datadog.android.sessionreplay.compose.internal.mappers.semantics
88

9+
import android.view.View
910
import androidx.compose.ui.semantics.Role
1011
import androidx.compose.ui.semantics.SemanticsConfiguration
1112
import androidx.compose.ui.semantics.SemanticsNode
@@ -236,6 +237,27 @@ class RootSemanticsNodeMapperTest {
236237
)
237238
}
238239

240+
@Test
241+
fun `M call interop callback W semantics node has interop view`() {
242+
val mockSemanticsNode = mockSemanticsNode(null)
243+
val mockView = mock<View>()
244+
whenever(mockSemanticsUtils.getInteropView(mockSemanticsNode)) doReturn mockView
245+
246+
// When
247+
testedRootSemanticsNodeMapper.createComposeWireframes(
248+
mockSemanticsNode,
249+
fakeMappingContext.systemInformation.screenDensity,
250+
fakeMappingContext,
251+
mockAsyncJobStatusCallback
252+
)
253+
254+
// Then
255+
verify(fakeMappingContext.interopViewCallback).map(
256+
eq(mockView),
257+
eq(fakeMappingContext)
258+
)
259+
}
260+
239261
private fun mockSemanticsNode(role: Role?): SemanticsNode {
240262
return mock {
241263
whenever(mockSemanticsConfiguration.getOrNull(SemanticsProperties.Role)) doReturn role

features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/test/elmyr/MappingContextForgeryFactory.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ internal class MappingContextForgeryFactory : ForgeryFactory<MappingContext> {
1919
hasOptionSelectorParent = forge.aBool(),
2020
imagePrivacy = forge.getForgery(),
2121
textAndInputPrivacy = forge.getForgery(),
22-
touchPrivacyManager = mock()
22+
touchPrivacyManager = mock(),
23+
interopViewCallback = mock()
2324
)
2425
}
2526
}

features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ internal class MappingContextForgeryFactory : ForgeryFactory<MappingContext> {
1919
textAndInputPrivacy = forge.getForgery(),
2020
imagePrivacy = forge.getForgery(),
2121
hasOptionSelectorParent = forge.aBool(),
22-
touchPrivacyManager = mock()
22+
touchPrivacyManager = mock(),
23+
interopViewCallback = mock()
2324
)
2425
}
2526
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ class com.datadog.android.sessionreplay.internal.recorder.resources.DefaultDrawa
6666
override fun copy(android.graphics.drawable.Drawable, android.content.res.Resources): android.graphics.drawable.Drawable?
6767
interface com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier
6868
fun copy(android.graphics.drawable.Drawable, android.content.res.Resources): android.graphics.drawable.Drawable?
69+
interface com.datadog.android.sessionreplay.recorder.InteropViewCallback
70+
fun map(android.view.View, MappingContext): List<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
6971
data class com.datadog.android.sessionreplay.recorder.MappingContext
70-
constructor(SystemInformation, com.datadog.android.sessionreplay.utils.ImageWireframeHelper, com.datadog.android.sessionreplay.TextAndInputPrivacy, com.datadog.android.sessionreplay.ImagePrivacy, com.datadog.android.sessionreplay.internal.TouchPrivacyManager, Boolean = false)
72+
constructor(SystemInformation, com.datadog.android.sessionreplay.utils.ImageWireframeHelper, com.datadog.android.sessionreplay.TextAndInputPrivacy, com.datadog.android.sessionreplay.ImagePrivacy, com.datadog.android.sessionreplay.internal.TouchPrivacyManager, Boolean = false, InteropViewCallback)
7173
interface com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
7274
fun isOptionSelector(android.view.ViewGroup): Boolean
7375
data class com.datadog.android.sessionreplay.recorder.SystemInformation

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -1414,21 +1414,27 @@ public final class com/datadog/android/sessionreplay/model/ResourceMetadata$Comp
14141414
public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/ResourceMetadata;
14151415
}
14161416

1417+
public abstract interface class com/datadog/android/sessionreplay/recorder/InteropViewCallback {
1418+
public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;)Ljava/util/List;
1419+
}
1420+
14171421
public final class com/datadog/android/sessionreplay/recorder/MappingContext {
1418-
public fun <init> (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;Z)V
1419-
public synthetic fun <init> (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
1422+
public fun <init> (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;)V
1423+
public synthetic fun <init> (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
14201424
public final fun component1 ()Lcom/datadog/android/sessionreplay/recorder/SystemInformation;
14211425
public final fun component2 ()Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;
14221426
public final fun component3 ()Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
14231427
public final fun component4 ()Lcom/datadog/android/sessionreplay/ImagePrivacy;
14241428
public final fun component5 ()Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;
14251429
public final fun component6 ()Z
1426-
public final fun copy (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;Z)Lcom/datadog/android/sessionreplay/recorder/MappingContext;
1427-
public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/recorder/MappingContext;
1430+
public final fun component7 ()Lcom/datadog/android/sessionreplay/recorder/InteropViewCallback;
1431+
public final fun copy (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;)Lcom/datadog/android/sessionreplay/recorder/MappingContext;
1432+
public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/recorder/MappingContext;
14281433
public fun equals (Ljava/lang/Object;)Z
14291434
public final fun getHasOptionSelectorParent ()Z
14301435
public final fun getImagePrivacy ()Lcom/datadog/android/sessionreplay/ImagePrivacy;
14311436
public final fun getImageWireframeHelper ()Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;
1437+
public final fun getInteropViewCallback ()Lcom/datadog/android/sessionreplay/recorder/InteropViewCallback;
14321438
public final fun getSystemInformation ()Lcom/datadog/android/sessionreplay/recorder/SystemInformation;
14331439
public final fun getTextAndInputPrivacy ()Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
14341440
public final fun getTouchPrivacyManager ()Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.datadog.android.sessionreplay.R
1515
import com.datadog.android.sessionreplay.TextAndInputPrivacy
1616
import com.datadog.android.sessionreplay.internal.TouchPrivacyManager
1717
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
18+
import com.datadog.android.sessionreplay.internal.recorder.callback.DefaultInteropViewCallback
1819
import com.datadog.android.sessionreplay.model.MobileSegment
1920
import com.datadog.android.sessionreplay.recorder.MappingContext
2021
import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
@@ -45,7 +46,11 @@ internal class SnapshotProducer(
4546
imageWireframeHelper = imageWireframeHelper,
4647
textAndInputPrivacy = textAndInputPrivacy,
4748
imagePrivacy = imagePrivacy,
48-
touchPrivacyManager = touchPrivacyManager
49+
touchPrivacyManager = touchPrivacyManager,
50+
interopViewCallback = DefaultInteropViewCallback(
51+
treeViewTraversal,
52+
recordedDataQueueRefs
53+
)
4954
),
5055
LinkedList(),
5156
recordedDataQueueRefs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.recorder.callback
8+
9+
import android.view.View
10+
import androidx.annotation.UiThread
11+
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
12+
import com.datadog.android.sessionreplay.internal.recorder.TreeViewTraversal
13+
import com.datadog.android.sessionreplay.model.MobileSegment
14+
import com.datadog.android.sessionreplay.recorder.InteropViewCallback
15+
import com.datadog.android.sessionreplay.recorder.MappingContext
16+
17+
internal class DefaultInteropViewCallback(
18+
private val treeViewTraversal: TreeViewTraversal,
19+
private val recordedDataQueueRefs: RecordedDataQueueRefs
20+
) : InteropViewCallback {
21+
22+
@UiThread
23+
override fun map(view: View, mappingContext: MappingContext): List<MobileSegment.Wireframe> {
24+
return treeViewTraversal.traverse(
25+
view,
26+
mappingContext,
27+
recordedDataQueueRefs
28+
).mappedWireframes
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.recorder
8+
9+
import android.view.View
10+
import androidx.annotation.UiThread
11+
import com.datadog.android.sessionreplay.model.MobileSegment
12+
13+
/**
14+
* Interface to define the callback for Jetpack Compose semantics tree to call
15+
* when there is an interop view to map.
16+
*/
17+
interface InteropViewCallback {
18+
19+
/**
20+
* Called when an interop view needs to be mapped.
21+
*/
22+
@UiThread
23+
fun map(view: View, mappingContext: MappingContext): List<MobileSegment.Wireframe>
24+
}

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ import com.datadog.android.sessionreplay.utils.ImageWireframeHelper
2222
* @param touchPrivacyManager the manager to handle touch privacy area.
2323
* @param hasOptionSelectorParent tells if one of the parents of the current [android.view.View]
2424
* is an option selector type (e.g. time picker, date picker, drop - down list)
25+
* @param interopViewCallback the callback for Jetpack Compose semantics tree to call
26+
* when there is an interop view to map.
2527
*/
2628
data class MappingContext(
2729
val systemInformation: SystemInformation,
2830
val imageWireframeHelper: ImageWireframeHelper,
2931
val textAndInputPrivacy: TextAndInputPrivacy,
3032
val imagePrivacy: ImagePrivacy,
3133
val touchPrivacyManager: TouchPrivacyManager,
32-
val hasOptionSelectorParent: Boolean = false
34+
val hasOptionSelectorParent: Boolean = false,
35+
val interopViewCallback: InteropViewCallback
3336
)

features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ internal class MappingContextForgeryFactory : ForgeryFactory<MappingContext> {
1919
hasOptionSelectorParent = forge.aBool(),
2020
textAndInputPrivacy = forge.getForgery(),
2121
imagePrivacy = forge.getForgery(),
22-
touchPrivacyManager = mock()
22+
touchPrivacyManager = mock(),
23+
interopViewCallback = mock()
2324
)
2425
}
2526
}

0 commit comments

Comments
 (0)