Skip to content

Commit 5376af1

Browse files
authored
Merge pull request #2667 from DataDog/yl/fix-action-tracking-crash
RUM-9345: Use reflection to retrieve semantics information in modifier
2 parents b9f6fc2 + d88757b commit 5376af1

File tree

3 files changed

+46
-23
lines changed

3 files changed

+46
-23
lines changed

integrations/dd-sdk-android-compose/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ plugins {
3939

4040
android {
4141
namespace = "com.datadog.android.compose"
42+
defaultConfig {
43+
consumerProguardFiles("consumer-rules.pro")
44+
}
4245
buildFeatures {
4346
compose = true
4447
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Keep the Compose internals class name. We need this in the RUM actions tracking.
2+
-keep class androidx.compose.foundation.ClickableElement {
3+
<fields>;
4+
}
5+
-keep class androidx.compose.foundation.CombinedClickableElement {
6+
<fields>;
7+
}

integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/internal/utils/LayoutNodeUtils.kt

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package com.datadog.android.compose.internal.utils
1111
import androidx.compose.ui.geometry.Rect
1212
import androidx.compose.ui.layout.boundsInWindow
1313
import androidx.compose.ui.node.LayoutNode
14+
import androidx.compose.ui.semantics.Role
1415
import androidx.compose.ui.semantics.SemanticsActions
1516
import androidx.compose.ui.semantics.SemanticsModifier
1617
import androidx.compose.ui.semantics.SemanticsProperties
@@ -24,12 +25,15 @@ import com.datadog.android.rum.RumAttributes.ACTION_TARGET_SELECTED
2425

2526
internal class LayoutNodeUtils {
2627

27-
@Suppress("NestedBlockDepth")
28+
@Suppress("NestedBlockDepth", "CyclomaticComplexMethod")
2829
fun resolveLayoutNode(node: LayoutNode): TargetNode? {
2930
return runSafe {
3031
var isClickable = false
3132
var isScrollable = false
3233
var datadogTag: String? = null
34+
var role: Role? = null
35+
var selected: Boolean? = null
36+
val customAttributes = mutableMapOf<String, Any?>()
3337
for (info in node.getModifierInfo()) {
3438
val modifier = info.modifier
3539
if (modifier is SemanticsModifier) {
@@ -43,42 +47,51 @@ internal class LayoutNodeUtils {
4347
getOrNull(DatadogSemanticsPropertyKey)?.let {
4448
datadogTag = it
4549
}
50+
selected = selected ?: getOrNull(SemanticsProperties.Selected)
51+
role = role ?: getOrNull(SemanticsProperties.Role)
4652
}
4753
} else {
48-
val className = modifier::class.qualifiedName
49-
if (className == CLASS_NAME_CLICKABLE_ELEMENT ||
50-
className == CLASS_NAME_COMBINED_CLICKABLE_ELEMENT ||
51-
className == CLASS_NAME_TOGGLEABLE_ELEMENT
52-
) {
53-
isClickable = true
54-
} else if (className == CLASS_NAME_SCROLLING_LAYOUT_ELEMENT ||
55-
className == CLASS_NAME_SCROLLABLE_ELEMENT
56-
) {
57-
isScrollable = true
54+
when (modifier::class.qualifiedName) {
55+
CLASS_NAME_CLICKABLE_ELEMENT,
56+
CLASS_NAME_COMBINED_CLICKABLE_ELEMENT -> {
57+
role = role ?: getRole(modifier)
58+
isClickable = true
59+
}
60+
61+
CLASS_NAME_TOGGLEABLE_ELEMENT -> {
62+
isClickable = true
63+
}
64+
65+
CLASS_NAME_SCROLLING_LAYOUT_ELEMENT,
66+
CLASS_NAME_SCROLLABLE_ELEMENT -> {
67+
isScrollable = true
68+
}
5869
}
5970
}
6071
}
61-
72+
selected?.let {
73+
customAttributes[ACTION_TARGET_SELECTED] = it
74+
}
75+
role?.let {
76+
customAttributes[ACTION_TARGET_ROLE] = it
77+
}
6278
datadogTag?.let {
6379
TargetNode(
6480
tag = it,
6581
isClickable = isClickable,
6682
isScrollable = isScrollable,
67-
customAttributes = resolveCustomAttributes(node)
83+
customAttributes = customAttributes.toMap()
6884
)
6985
}
7086
}
7187
}
7288

73-
private fun resolveCustomAttributes(node: LayoutNode): Map<String, Any?> {
74-
return node.collapsedSemantics?.let { configuration ->
75-
val selected = configuration.getOrNull(SemanticsProperties.Selected)
76-
val role = configuration.getOrNull(SemanticsProperties.Role)
77-
mapOf(
78-
ACTION_TARGET_SELECTED to selected,
79-
ACTION_TARGET_ROLE to role
80-
)
81-
}.orEmpty()
89+
@Suppress("UnsafeThirdPartyFunctionCall")
90+
// Function is wrapped with `runSafe` in the call site.
91+
private fun getRole(obj: Any): Role {
92+
val roleField = obj::class.java.getDeclaredField("role")
93+
roleField.isAccessible = true
94+
return roleField.get(obj) as Role
8295
}
8396

8497
fun getLayoutNodeBoundsInWindow(node: LayoutNode): Rect? {
@@ -90,7 +103,7 @@ internal class LayoutNodeUtils {
90103
private fun <T> runSafe(action: () -> T): T? {
91104
try {
92105
return action()
93-
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
106+
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
94107
// We rely on visibility suppression to access internal field,
95108
// any runtime exception must be caught here.
96109
(Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log(

0 commit comments

Comments
 (0)