Skip to content

Commit b84746f

Browse files
authored
Merge pull request #2648 from DataDog/yl/compose/coil3-support-pr
RUM-9053: Support Coil3 for Session Replay image recording
2 parents eaeb158 + 64ac577 commit b84746f

File tree

4 files changed

+84
-1
lines changed

4 files changed

+84
-1
lines changed

features/dd-sdk-android-session-replay-compose/consumer-rules.pro

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@
3232
-keep class androidx.compose.ui.node.LayoutNode {
3333
*;
3434
}
35+
-keep class androidx.compose.ui.node.NodeChain {
36+
<fields>;
37+
}
38+
-keep class androidx.compose.ui.Modifier$Node{
39+
<fields>;
40+
}
41+
-keep class coil3.compose.internal.ContentPainterNode{
42+
<fields>;
43+
}
44+
-keep class coil3.compose.AsyncImagePainter{
45+
<methods>;
46+
}
47+
3548
-keep class androidx.compose.ui.semantics.SemanticsNode {
3649
<fields>;
3750
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ internal object ComposeReflection {
2424

2525
val LayoutNodeClass = getClassSafe("androidx.compose.ui.node.LayoutNode")
2626
val GetInteropViewMethod = LayoutNodeClass?.getDeclaredMethodSafe("getInteropView")
27+
val NodesFieldOfLayoutNode = LayoutNodeClass?.getDeclaredFieldSafe("nodes")
28+
val NodeChainClass = getClassSafe("androidx.compose.ui.node.NodeChain")
29+
val HeadFieldOfNodeChain = NodeChainClass?.getDeclaredFieldSafe("head")
30+
val ModifierNodeClass = getClassSafe("androidx.compose.ui.Modifier\$Node")
31+
val ChildFieldOfModifierNode = ModifierNodeClass?.getDeclaredFieldSafe("child")
2732

2833
val SemanticsNodeClass = getClassSafe("androidx.compose.ui.semantics.SemanticsNode")
2934
val LayoutNodeField = SemanticsNodeClass?.getDeclaredFieldSafe("layoutNode")
@@ -80,6 +85,8 @@ internal object ComposeReflection {
8085
val AndroidImageBitmapClass = getClassSafe("androidx.compose.ui.graphics.AndroidImageBitmap")
8186
val BitmapField = AndroidImageBitmapClass?.getDeclaredFieldSafe("bitmap")
8287

88+
// Region of Coil
89+
8390
val ContentPainterModifierClass = getClassSafe("coil.compose.ContentPainterModifier")
8491
val PainterFieldOfContentPainterModifier =
8592
ContentPainterModifierClass?.getDeclaredFieldSafe("painter")
@@ -97,6 +104,26 @@ internal object ComposeReflection {
97104
)
98105
val PainterFieldOfAsyncImagePainter = AsyncImagePainterClass?.getDeclaredFieldSafe("_painter")
99106

107+
// End region of Coil
108+
109+
// Region of Coil3
110+
111+
val PainterNodeClass = getClassSafe(
112+
"coil3.compose.internal.ContentPainterNode",
113+
isCritical = false
114+
)
115+
116+
val PainterFieldOfPainterNode = PainterNodeClass?.getDeclaredFieldSafe("painter")
117+
118+
val AsyncImagePainter3Class = getClassSafe(
119+
"coil3.compose.AsyncImagePainter",
120+
isCritical = false
121+
)
122+
val PainterMethodOfAsync3ImagePainter =
123+
AsyncImagePainter3Class?.getDeclaredMethodSafe("getPainter")
124+
125+
// End region of Coil3
126+
100127
// Region of MultiParagraph text
101128
val ParagraphInfoListField = MultiParagraph::class.java.getDeclaredFieldSafe(
102129
"paragraphInfoList",

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,27 @@ import androidx.compose.ui.semantics.SemanticsNode
2424
import androidx.compose.ui.semantics.SemanticsOwner
2525
import androidx.compose.ui.text.MultiParagraph
2626
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection
27+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.AsyncImagePainterClass
2728
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.BitmapField
29+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ChildFieldOfModifierNode
2830
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.CompositionField
2931
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ContentPainterElementClass
3032
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ContentPainterModifierClass
3133
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.GetInnerLayerCoordinatorMethod
3234
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.GetInteropViewMethod
35+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.HeadFieldOfNodeChain
3336
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ImageField
3437
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.LayoutField
3538
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.LayoutNodeField
39+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.NodesFieldOfLayoutNode
3640
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterElementClass
3741
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterField
3842
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfAsyncImagePainter
3943
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfContentPainterElement
4044
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfContentPainterModifier
45+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfPainterNode
46+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterMethodOfAsync3ImagePainter
47+
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterNodeClass
4148
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.StaticLayoutField
4249
import com.datadog.android.sessionreplay.compose.internal.reflection.getSafe
4350

@@ -141,6 +148,31 @@ internal class ReflectionUtils {
141148
}
142149
}
143150

151+
fun getCoil3AsyncImagePainter(semanticsNode: SemanticsNode): Painter? {
152+
// Check if Coil3 ContentPainterNode is present first to optimize the performance
153+
// by skipping the node chain iteration
154+
if (PainterNodeClass == null) {
155+
return null
156+
}
157+
val layoutNode = LayoutNodeField?.getSafe(semanticsNode)
158+
val nodeChain = NodesFieldOfLayoutNode?.getSafe(layoutNode)
159+
val headNode = HeadFieldOfNodeChain?.getSafe(nodeChain) as? Modifier.Node
160+
var currentNode = headNode
161+
var painterNode: Modifier.Node? = null
162+
// Iterate NodeChain to find Coil3 `ContentPainterNode`
163+
while (currentNode != null) {
164+
if (currentNode::class.java == PainterNodeClass) {
165+
painterNode = currentNode
166+
break
167+
}
168+
currentNode = ChildFieldOfModifierNode?.getSafe(currentNode) as? Modifier.Node
169+
}
170+
val asyncImagePainter = PainterFieldOfPainterNode?.getSafe(painterNode)
171+
val painter =
172+
asyncImagePainter?.let { PainterMethodOfAsync3ImagePainter?.invoke(it) }
173+
return painter as? Painter
174+
}
175+
144176
fun getLocalImagePainter(semanticsNode: SemanticsNode): Painter? {
145177
val modifier = semanticsNode.layoutInfo.getModifierInfo().firstOrNull {
146178
PainterElementClass?.isInstance(it.modifier) == true
@@ -149,6 +181,11 @@ internal class ReflectionUtils {
149181
}
150182

151183
fun getAsyncImagePainter(semanticsNode: SemanticsNode): Painter? {
184+
// Check if Coil AsyncImagePainter is present first to optimize the performance
185+
// by skipping the modifier iteration
186+
if (AsyncImagePainterClass == null) {
187+
return null
188+
}
152189
val asyncPainter = semanticsNode.layoutInfo.getModifierInfo().firstNotNullOfOrNull {
153190
if (ContentPainterModifierClass?.isInstance(it.modifier) == true) {
154191
PainterFieldOfContentPainterModifier?.getSafe(it.modifier)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,21 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref
249249
): BitmapInfo? {
250250
var isContextualImage = false
251251
var painter = reflectionUtils.getLocalImagePainter(semanticsNode)
252+
253+
// Try to resolve Coil AsyncImagePainter.
252254
if (painter == null) {
253255
isContextualImage = true
254256
painter = reflectionUtils.getAsyncImagePainter(semanticsNode)
255257
}
256-
// TODO RUM-6535: support more painters.
258+
// In some versions of Coil, bitmap painter is nested in `AsyncImagePainter`
257259
if (painter != null && reflectionUtils.isAsyncImagePainter(painter)) {
258260
isContextualImage = true
259261
painter = reflectionUtils.getNestedPainter(painter)
260262
}
263+
// Try to resolve Coil3 painter if is still null.
264+
if (painter == null) {
265+
painter = reflectionUtils.getCoil3AsyncImagePainter(semanticsNode)
266+
}
261267
val bitmap = when (painter) {
262268
is BitmapPainter -> reflectionUtils.getBitmapInBitmapPainter(painter)
263269
is VectorPainter -> reflectionUtils.getBitmapInVectorPainter(painter)

0 commit comments

Comments
 (0)