Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ambushwork committed Oct 21, 2024
1 parent 71c250b commit a0e962d
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 147 deletions.
16 changes: 11 additions & 5 deletions features/dd-sdk-android-session-replay-compose/consumer-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
-keepnames class androidx.compose.runtime.Composition
-keepnames class androidx.compose.ui.platform.ComposeView
-keepnames class androidx.compose.material.DefaultButtonColors
-keepclassmembers class androidx.compose.ui.platform.WrappedComposition {
-keep class androidx.compose.ui.platform.WrappedComposition {
<fields>;
}
-keepclassmembers class androidx.compose.ui.platform.AbstractComposeView {
Expand All @@ -22,8 +22,8 @@
-keepclassmembers class androidx.compose.foundation.BackgroundElement {
<fields>;
}
-keepclassmembers class androidx.compose.ui.node.LayoutNode {
<fields>;
-keep class androidx.compose.ui.node.LayoutNode {
*;
}
-keepclassmembers class androidx.compose.ui.draw.PainterElement {
<fields>;
Expand All @@ -43,10 +43,16 @@
-keepclassmembers class androidx.compose.ui.graphics.AndroidImageBitmap {
<fields>;
}
-keepclassmembers class coil.compose.ContentPainterModifier {
-keep class coil.compose.ContentPainterModifier {
<fields>;
}
-keepclassmembers class coil.compose.AsyncImagePainter {
-keep class coil.compose.AsyncImagePainter {
<fields>;
}
-keepclassmembers class androidx.compose.foundation.layout.PaddingElement{
<fields>;
}
-keepclassmembers class "androidx.compose.ui.graphics.GraphicsLayerElement"{
<fields>;
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,43 @@ package com.datadog.android.sessionreplay.compose.internal.mappers.semantics
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.semantics.SemanticsNode
import com.datadog.android.sessionreplay.compose.internal.utils.BackgroundInfo
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.GlobalBounds
import kotlin.math.roundToInt

internal abstract class AbstractSemanticsNodeMapper(
private val colorStringFormatter: ColorStringFormatter
private val colorStringFormatter: ColorStringFormatter,
private val semanticsUtils: SemanticsUtils = SemanticsUtils()
) : SemanticsNodeMapper {

protected fun resolveBounds(semanticsNode: SemanticsNode): GlobalBounds {
val rect = semanticsNode.boundsInRoot
val density = semanticsNode.layoutInfo.density.density
val width = ((rect.right - rect.left) / density).toLong()
val height = ((rect.bottom - rect.top) / density).toLong()
val x = (rect.left / density).toLong()
val y = (rect.top / density).toLong()
return GlobalBounds(x, y, width, height)
return semanticsUtils.resolveInnerBounds(semanticsNode)
}

protected fun resolveModifierWireframes(semanticsNode: SemanticsNode): List<MobileSegment.Wireframe> {
return semanticsUtils.resolveBackgroundInfo(semanticsNode).map {
convertBackgroundInfoToWireframes(backgroundInfo = it)
}
}

private fun convertBackgroundInfoToWireframes(
backgroundInfo: BackgroundInfo
): MobileSegment.Wireframe {
val shapeStyle = MobileSegment.ShapeStyle(
backgroundColor = backgroundInfo.color?.let { convertColor(it) },
cornerRadius = backgroundInfo.cornerRadius
)
return MobileSegment.Wireframe.ShapeWireframe(
id = semanticsUtils.resolveBackgroundInfoId(backgroundInfo),
x = backgroundInfo.globalBounds.x,
y = backgroundInfo.globalBounds.y,
width = backgroundInfo.globalBounds.width,
height = backgroundInfo.globalBounds.height,
shapeStyle = shapeStyle
)
}

protected fun convertColor(color: Long): String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,25 @@
package com.datadog.android.sessionreplay.compose.internal.mappers.semantics

import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.unit.Density
import com.datadog.android.sessionreplay.compose.internal.data.SemanticsWireframe
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.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.GlobalBounds

internal class ButtonSemanticsNodeMapper(
colorStringFormatter: ColorStringFormatter,
private val semanticsUtils: SemanticsUtils = SemanticsUtils()
) : AbstractSemanticsNodeMapper(colorStringFormatter) {
semanticsUtils: SemanticsUtils,
) : AbstractSemanticsNodeMapper(colorStringFormatter, semanticsUtils) {

override fun map(
semanticsNode: SemanticsNode,
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): SemanticsWireframe {
val density = semanticsNode.layoutInfo.density
val bounds = resolveBounds(semanticsNode)
val buttonStyle = resolveSemanticsButtonStyle(semanticsNode, bounds, density)
return SemanticsWireframe(
wireframes = MobileSegment.Wireframe.ShapeWireframe(
id = semanticsNode.id.toLong(),
x = bounds.x,
y = bounds.y,
width = bounds.width,
height = bounds.height,
shapeStyle = buttonStyle
).let { listOf(it) },
uiContext = parentContext.copy(
parentContentColor = buttonStyle.backgroundColor ?: parentContext.parentContentColor
)
)
}

private fun resolveSemanticsButtonStyle(
semanticsNode: SemanticsNode,
globalBounds: GlobalBounds,
density: Density
): MobileSegment.ShapeStyle {
val color = semanticsUtils.resolveSemanticsModifierColor(semanticsNode)
val cornerRadius = semanticsUtils.resolveSemanticsModifierCornerRadius(semanticsNode, globalBounds, density)
return MobileSegment.ShapeStyle(
backgroundColor = color?.let { convertColor(it) },
cornerRadius = cornerRadius
wireframes = resolveModifierWireframes(semanticsNode),
uiContext = parentContext
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal class ImageSemanticsNodeMapper(
): SemanticsWireframe {
val bounds = resolveBounds(semanticsNode)
val bitmapInfo = resolveSemanticsPainter(semanticsNode)
val containerFrames = resolveModifierWireframes(semanticsNode).toMutableList()
val imageWireframe = if (bitmapInfo != null) {
parentContext.imageWireframeHelper.createImageWireframeByBitmap(
id = semanticsNode.id.toLong(),
Expand All @@ -54,8 +55,11 @@ internal class ImageSemanticsNodeMapper(
} else {
null
}
imageWireframe?.let {
containerFrames.add(it)
}
return SemanticsWireframe(
wireframes = listOfNotNull(imageWireframe),
wireframes = containerFrames,
uiContext = null
)
}
Expand All @@ -72,6 +76,10 @@ internal class ImageSemanticsNodeMapper(
}
}
// TODO RUM-6535: support more painters.
if (ComposeReflection.AsyncImagePainterClass?.isInstance(painter) == true) {
isContextualImage = true
painter = PainterFieldOfAsyncImagePainter?.getSafe(painter) as? Painter
}
val bitmap = when (painter) {
is BitmapPainter -> tryParseBitmapPainterToBitmap(painter)
is VectorPainter -> tryParseVectorPainterToBitmap(painter)
Expand All @@ -82,7 +90,7 @@ internal class ImageSemanticsNodeMapper(

val newBitmap = bitmap?.let {
@Suppress("UnsafeThirdPartyFunctionCall") // isMutable is always false
it.copy(it.config, false)
it.copy(Bitmap.Config.ARGB_8888, false)
}
return newBitmap?.let {
BitmapInfo(it, isContextualImage)
Expand All @@ -93,19 +101,24 @@ internal class ImageSemanticsNodeMapper(
val vector = ComposeReflection.VectorField?.getSafe(vectorPainter)
val cacheDrawScope = ComposeReflection.CacheDrawScopeField?.getSafe(vector)
val mCachedImage = ComposeReflection.CachedImageField?.getSafe(cacheDrawScope)
return BitmapField?.getSafe(mCachedImage) as? Bitmap
return mCachedImage?.let {
BitmapField?.getSafe(it) as? Bitmap
}

}

private fun tryParseBitmapPainterToBitmap(bitmapPainter: BitmapPainter): Bitmap? {
val image = ImageField?.getSafe(bitmapPainter)
return BitmapField?.getSafe(image) as? Bitmap
return image?.let {
BitmapField?.getSafe(image) as? Bitmap
}
}

private fun tryParseLocalImagePainter(semanticsNode: SemanticsNode): Painter? {
val modifier = semanticsNode.layoutInfo.getModifierInfo().firstOrNull {
PainterElementClass?.isInstance(it.modifier) == true
}?.modifier
return PainterField?.getSafe(modifier) as? Painter
return modifier?.let { PainterField?.getSafe(it) as? Painter }
}

private fun tryParseAsyncImagePainter(semanticsNode: SemanticsNode): Painter? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.datadog.android.Datadog
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.feature.FeatureSdkCore
import java.lang.reflect.Field
import java.lang.reflect.Method

internal object ComposeReflection {
val WrappedCompositionClass = getClassSafe("androidx.compose.ui.platform.WrappedComposition")
Expand All @@ -36,7 +37,9 @@ internal object ComposeReflection {
val OwnerField = WrappedCompositionClass?.getDeclaredFieldSafe("owner")

val LayoutNodeClass = getClassSafe("androidx.compose.ui.node.LayoutNode")
val LayoutNodeOwnerField = LayoutNodeClass?.getDeclaredFieldSafe("owner")

val GetInnerLayerCoordinatorMethod = LayoutNodeClass?.getDeclaredMethodSafe("getInnerLayerCoordinator")

val AndroidComposeViewClass = getClassSafe("androidx.compose.ui.platform.AndroidComposeView")
val SemanticsOwner = AndroidComposeViewClass?.getDeclaredFieldSafe("semanticsOwner")

Expand All @@ -45,7 +48,15 @@ internal object ComposeReflection {

val BackgroundElementClass = getClassSafe("androidx.compose.foundation.BackgroundElement")
val ColorField = BackgroundElementClass?.getDeclaredFieldSafe("color")
val ShapeField = BackgroundElementClass?.getDeclaredFieldSafe("shape")

val PaddingElementClass = getClassSafe("androidx.compose.foundation.layout.PaddingElement")
val StartField = PaddingElementClass?.getDeclaredFieldSafe("start")
val EndField = PaddingElementClass?.getDeclaredFieldSafe("end")
val BottomField = PaddingElementClass?.getDeclaredFieldSafe("bottom")
val TopField = PaddingElementClass?.getDeclaredFieldSafe("top")

val GraphicsLayerElementClass = getClassSafe("androidx.compose.ui.graphics.GraphicsLayerElement")
val ClipShapeField = GraphicsLayerElementClass?.getDeclaredFieldSafe("shape")

val PainterElementClass = getClassSafe("androidx.compose.ui.draw.PainterElement")
val PainterField = PainterElementClass?.getDeclaredFieldSafe("painter")
Expand Down Expand Up @@ -77,6 +88,11 @@ internal fun Field.accessible(): Field {
return this
}

internal fun Method.accessible(): Method {
isAccessible = true
return this
}

@Suppress("TooGenericExceptionCaught")
internal fun Field.getSafe(target: Any?): Any? {
return try {
Expand Down Expand Up @@ -186,3 +202,42 @@ internal fun Class<*>.getDeclaredFieldSafe(fieldName: String): Field? {
null
}
}

@Suppress("TooGenericExceptionCaught")
internal fun Class<*>.getDeclaredMethodSafe(methodName: String): Method? {
return try {
getDeclaredMethod(methodName).accessible()
} catch (e: SecurityException) {
(Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.MAINTAINER,
{
"Unable to get method $methodName through reflection"
},
e
)
null
} catch (e: NullPointerException) {
(Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.MAINTAINER,
{
"Unable to get method $methodName through reflection, name is null"
},
e
)
null
} catch (e: NoSuchMethodException) {
(Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.MAINTAINER,
{
"Unable to get field $methodName through reflection, " +
"either because of obfuscation or dependency version mismatch"
},
e
)
null
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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.utils

import com.datadog.android.sessionreplay.utils.GlobalBounds

internal data class BackgroundInfo(
val globalBounds: GlobalBounds = GlobalBounds(0L, 0L, 0L, 0L),
val color: Long? = null,
val cornerRadius: Float = 0f
)
Loading

0 comments on commit a0e962d

Please sign in to comment.