Skip to content

Commit

Permalink
RUM-7215: Add AndroidComposeViewMapper to support popup
Browse files Browse the repository at this point in the history
  • Loading branch information
ambushwork committed Nov 15, 2024
1 parent 60631b4 commit ace54a7
Show file tree
Hide file tree
Showing 8 changed files with 512 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
* Copyright 2016-Present Datadog, Inc.
*/

@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

package com.datadog.android.sessionreplay.compose

import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.ComposeView
import com.datadog.android.sessionreplay.ExtensionSupport
import com.datadog.android.sessionreplay.MapperTypeWrapper
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.SemanticsWireframeMapper
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.AndroidComposeViewMapper
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.ComposeViewMapper
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.RootSemanticsNodeMapper
import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter
Expand All @@ -30,16 +35,28 @@ class ComposeExtensionSupport : ExtensionSupport {
private val colorStringFormatter: ColorStringFormatter = DefaultColorStringFormatter
private val viewBoundsResolver: ViewBoundsResolver = DefaultViewBoundsResolver
private val drawableToColorMapper: DrawableToColorMapper = DrawableToColorMapper.getDefault()
private val rootSemanticsNodeMapper = RootSemanticsNodeMapper(colorStringFormatter)

override fun getCustomViewMappers(): List<MapperTypeWrapper<*>> {
return listOf(
MapperTypeWrapper(
ComposeView::class.java,
SemanticsWireframeMapper(
ComposeViewMapper(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper,
rootSemanticsNodeMapper = rootSemanticsNodeMapper
)
),
MapperTypeWrapper(
AndroidComposeView::class.java,
AndroidComposeViewMapper(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
drawableToColorMapper,
rootSemanticsNodeMapper
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

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

import androidx.compose.ui.platform.AndroidComposeView
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
import com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMapper
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver

internal class AndroidComposeViewMapper(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper,
private val rootSemanticsNodeMapper: RootSemanticsNodeMapper
) : BaseWireframeMapper<AndroidComposeView>(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
) {
override fun map(
view: AndroidComposeView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): List<MobileSegment.Wireframe> {
val density =
mappingContext.systemInformation.screenDensity.let { if (it == 0.0f) 1.0f else it }
// TODO RUM 6192: Apply FGM for compose
val privacy = SessionReplayPrivacy.ALLOW
return rootSemanticsNodeMapper.createComposeWireframes(
view.semanticsOwner.unmergedRootSemanticsNode,
density,
mappingContext,
privacy,
asyncJobStatusCallback
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

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

import androidx.compose.ui.platform.ComposeView
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
import com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMapper
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver

internal class ComposeViewMapper(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper,
private val semanticsUtils: SemanticsUtils = SemanticsUtils(),
private val rootSemanticsNodeMapper: RootSemanticsNodeMapper
) : BaseWireframeMapper<ComposeView>(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
) {
override fun map(
view: ComposeView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): List<MobileSegment.Wireframe> {
val density =
mappingContext.systemInformation.screenDensity.let { if (it == 0.0f) 1.0f else it }
// TODO RUM 6192: Apply FGM for compose
val privacy = SessionReplayPrivacy.ALLOW
return semanticsUtils.findRootSemanticsNode(view)?.let { node ->
rootSemanticsNodeMapper.createComposeWireframes(
node,
density,
mappingContext,
privacy,
asyncJobStatusCallback
)
} ?: emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,20 @@

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

import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
import com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMapper
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver

internal class SemanticsWireframeMapper(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper,
internal class RootSemanticsNodeMapper(
private val colorStringFormatter: ColorStringFormatter,
private val semanticsUtils: SemanticsUtils = SemanticsUtils(),
private val semanticsNodeMapper: Map<Role, SemanticsNodeMapper> = mapOf(
// TODO RUM-6189 Add Mappers for each Semantics Role
Expand All @@ -45,46 +36,9 @@ internal class SemanticsWireframeMapper(
colorStringFormatter,
semanticsUtils
)
) : BaseWireframeMapper<ComposeView>(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
) {
override fun map(
view: ComposeView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): List<MobileSegment.Wireframe> {
val density = mappingContext.systemInformation.screenDensity.let { if (it == 0.0f) 1.0f else it }
// TODO RUM 6192: Apply FGM for compose
val privacy = SessionReplayPrivacy.ALLOW
return semanticsUtils.findRootSemanticsNode(view)?.let { node ->
createComposeWireframes(node, density, mappingContext, privacy, asyncJobStatusCallback)
} ?: emptyList()
}

private fun getSemanticsNodeMapper(
semanticsNode: SemanticsNode
): SemanticsNodeMapper {
val role = semanticsNode.config.getOrNull(SemanticsProperties.Role)
val mapper = semanticsNodeMapper[role]
if (mapper != null) {
return mapper
}
return if (isTextNode(semanticsNode)) {
textSemanticsNodeMapper
} else {
containerSemanticsNodeMapper
}
}

private fun isTextNode(semanticsNode: SemanticsNode): Boolean {
// Some text semantics nodes don't have an explicit `Role` but the text exists in the config
return semanticsNode.config.getOrNull(SemanticsProperties.Text)?.isNotEmpty() == true
}
private fun createComposeWireframes(
internal fun createComposeWireframes(
semanticsNode: SemanticsNode,
density: Float,
mappingContext: MappingContext,
Expand Down Expand Up @@ -127,4 +81,24 @@ internal class SemanticsWireframeMapper(
createComposerWireframes(it, wireframes, currentUiContext, asyncJobStatusCallback)
}
}

private fun getSemanticsNodeMapper(
semanticsNode: SemanticsNode
): SemanticsNodeMapper {
val role = semanticsNode.config.getOrNull(SemanticsProperties.Role)
val mapper = semanticsNodeMapper[role]
if (mapper != null) {
return mapper
}
return if (isTextNode(semanticsNode)) {
textSemanticsNodeMapper
} else {
containerSemanticsNodeMapper
}
}

private fun isTextNode(semanticsNode: SemanticsNode): Boolean {
// Some text semantics nodes don't have an explicit `Role` but the text exists in the config
return semanticsNode.config.getOrNull(SemanticsProperties.Text)?.isNotEmpty() == true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package com.datadog.android.sessionreplay.compose

import androidx.compose.ui.platform.ComposeView
import com.datadog.android.sessionreplay.ExtensionSupport
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.SemanticsWireframeMapper
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.ComposeViewMapper
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -41,7 +41,7 @@ class ComposeExtensionSupportTest {

// Then
val composeMapper = customMappers.firstOrNull { it.supportsView(mockView) }?.getUnsafeMapper()
assertThat(composeMapper).isInstanceOf(SemanticsWireframeMapper::class.java)
assertThat(composeMapper).isInstanceOf(ComposeViewMapper::class.java)
}

@Test
Expand Down
Loading

0 comments on commit ace54a7

Please sign in to comment.