Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUM-7215: Add AndroidComposeViewMapper to support popup #2395

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
ambushwork marked this conversation as resolved.
Show resolved Hide resolved

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,20 @@ internal class SemanticsWireframeMapper(
createComposerWireframes(it, wireframes, currentUiContext, asyncJobStatusCallback)
}
}

private fun getSemanticsNodeMapper(
semanticsNode: SemanticsNode
): SemanticsNodeMapper {
val role = semanticsNode.config.getOrNull(SemanticsProperties.Role)
return semanticsNodeMapper[role] ?: 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