Skip to content

Commit 66db017

Browse files
committed
Improve LayoutNode iteration
1 parent 018df71 commit 66db017

File tree

3 files changed

+70
-65
lines changed

3 files changed

+70
-65
lines changed

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,25 @@ import java.lang.reflect.Method
3636
@SuppressLint("UseRequiresApi")
3737
@TargetApi(26)
3838
internal object ComposeViewHierarchyNode {
39-
private val getCollapsedSemanticsMethod: Method? by lazy {
40-
try {
41-
return@lazy LayoutNode::class.java.getDeclaredMethod("getCollapsedSemantics").apply {
42-
isAccessible = true
39+
private val getCollapsedSemanticsMethod: Method? by
40+
lazy(LazyThreadSafetyMode.NONE) {
41+
try {
42+
return@lazy LayoutNode::class.java.getDeclaredMethod("getCollapsedSemantics").apply {
43+
isAccessible = true
44+
}
45+
} catch (_: Throwable) {
46+
// ignore, as this method may not be available
4347
}
44-
} catch (_: Throwable) {
45-
// ignore, as this method may not be available
48+
return@lazy null
4649
}
47-
return@lazy null
48-
}
4950

5051
private var semanticsRetrievalErrorLogged: Boolean = false
5152

5253
@JvmStatic
5354
internal fun retrieveSemanticsConfiguration(node: LayoutNode): SemanticsConfiguration? {
5455
try {
5556
return node.semanticsConfiguration
56-
} catch (_: Exception) {
57+
} catch (_: Throwable) {
5758
// for backwards compatibility
5859
// Jetpack Compose 1.8 or older
5960
return getCollapsedSemanticsMethod?.let {
@@ -134,7 +135,7 @@ internal object ComposeViewHierarchyNode {
134135
"""
135136
Error retrieving semantics information from Compose tree. Most likely you're using
136137
an unsupported version of androidx.compose.ui:ui. The supported
137-
version range is 1.5.0 - 1.8.0.
138+
version range is 1.5.0 - 1.10.2.
138139
If you're using a newer version, please open a github issue with the version
139140
you're using, so we can add support for it.
140141
"""

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/SentryLayoutNodeHelper.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import java.lang.reflect.Method
1515
/**
1616
* Provides access to internal LayoutNode members that are subject to Kotlin name-mangling.
1717
*
18+
* This class is not thread-safe, as Compose UI operations are expected to be performed on the main
19+
* thread.
20+
*
1821
* Compiled against Compose >= 1.10 where the mangled names use the "ui" module suffix (e.g.
1922
* getChildren$ui()). For apps still on Compose < 1.10 (where the suffix is "$ui_release"), the
2023
* direct call will throw [NoSuchMethodError] and we fall back to reflection-based accessors that
@@ -23,8 +26,8 @@ import java.lang.reflect.Method
2326
internal object SentryLayoutNodeHelper {
2427
private class Fallback(val getChildren: Method?, val getOuterCoordinator: Method?)
2528

26-
@Volatile private var useFallback: Boolean? = null
27-
@Volatile private var fallback: Fallback? = null
29+
private var useFallback: Boolean? = null
30+
private var fallback: Fallback? = null
2831

2932
private fun tryResolve(clazz: Class<*>, name: String): Method? {
3033
return try {

sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -44,69 +44,57 @@ public class ComposeGestureTargetLocator(private val logger: ILogger) : GestureT
4444

4545
val rootLayoutNode = root.root
4646

47-
val queue: Queue<LayoutNode> = LinkedList()
48-
queue.add(rootLayoutNode)
47+
// Pair<Node, ParentTag>
48+
val queue: Queue<Pair<LayoutNode, String?>> = LinkedList()
49+
queue.add(Pair(rootLayoutNode, null))
4950

50-
// the final tag to return
51+
// the final tag to return, only relevant for clicks
52+
// as for scrolls, we return the first matching element
5153
var targetTag: String? = null
5254

53-
// the last known tag when iterating the node tree
54-
var lastKnownTag: String? = null
55-
var isClickable = false
56-
var isScrollable = false
57-
5855
while (!queue.isEmpty()) {
59-
val node = queue.poll() ?: continue
56+
val (node, parentTag) = queue.poll() ?: continue
6057
if (node.isPlaced && layoutNodeBoundsContain(rootLayoutNode, node, x, y)) {
61-
62-
val modifiers = node.getModifierInfo()
63-
for (index in modifiers.indices) {
64-
val modifierInfo = modifiers[index]
65-
val tag = composeHelper!!.extractTag(modifierInfo.modifier)
66-
if (tag != null) {
67-
lastKnownTag = tag
68-
}
69-
70-
if (modifierInfo.modifier is SemanticsModifier) {
71-
val semanticsModifierCore = modifierInfo.modifier as SemanticsModifier
72-
val semanticsConfiguration = semanticsModifierCore.semanticsConfiguration
73-
74-
for (item in semanticsConfiguration) {
75-
val key: String = item.key.name
76-
if ("ScrollBy" == key) {
77-
isScrollable = true
78-
} else if ("OnClick" == key) {
79-
isClickable = true
58+
val tag = extractTag(composeHelper!!, node) ?: parentTag
59+
if (tag != null) {
60+
val modifiers = node.getModifierInfo()
61+
for (index in modifiers.indices) {
62+
val modifierInfo = modifiers[index]
63+
if (modifierInfo.modifier is SemanticsModifier) {
64+
val semanticsModifierCore = modifierInfo.modifier as SemanticsModifier
65+
val semanticsConfiguration = semanticsModifierCore.semanticsConfiguration
66+
67+
for (item in semanticsConfiguration) {
68+
val key: String = item.key.name
69+
if (targetType == UiElement.Type.SCROLLABLE && "ScrollBy" == key) {
70+
return UiElement(null, null, null, tag, ORIGIN)
71+
} else if (targetType == UiElement.Type.CLICKABLE && "OnClick" == key) {
72+
targetTag = tag
73+
}
74+
}
75+
} else {
76+
// Jetpack Compose 1.5+: uses Node modifiers elements for clicks/scrolls
77+
val modifier = modifierInfo.modifier
78+
val type = modifier.javaClass.name
79+
if (
80+
targetType == UiElement.Type.CLICKABLE &&
81+
("androidx.compose.foundation.ClickableElement" == type ||
82+
"androidx.compose.foundation.CombinedClickableElement" == type)
83+
) {
84+
targetTag = tag
85+
} else if (
86+
targetType == UiElement.Type.SCROLLABLE &&
87+
("androidx.compose.foundation.ScrollingLayoutElement" == type ||
88+
"androidx.compose.foundation.ScrollingContainerElement" == type)
89+
) {
90+
return UiElement(null, null, null, tag, ORIGIN)
8091
}
81-
}
82-
} else {
83-
val modifier = modifierInfo.modifier
84-
// Newer Jetpack Compose 1.5 uses Node modifiers for clicks/scrolls
85-
val type = modifier.javaClass.name
86-
if (
87-
"androidx.compose.foundation.ClickableElement" == type ||
88-
"androidx.compose.foundation.CombinedClickableElement" == type
89-
) {
90-
isClickable = true
91-
} else if (
92-
"androidx.compose.foundation.ScrollingLayoutElement" == type ||
93-
"androidx.compose.foundation.ScrollingContainerElement" == type
94-
) {
95-
isScrollable = true
9692
}
9793
}
9894
}
99-
100-
if (isClickable && targetType == UiElement.Type.CLICKABLE) {
101-
targetTag = lastKnownTag
102-
}
103-
if (isScrollable && targetType == UiElement.Type.SCROLLABLE) {
104-
targetTag = lastKnownTag
105-
// skip any children for scrollable targets
106-
break
107-
}
10895
}
109-
queue.addAll(node.zSortedChildren.asMutableList())
96+
97+
queue.addAll(node.zSortedChildren.asMutableList().map { Pair(it, parentTag) })
11098
}
11199

112100
return if (targetTag == null) {
@@ -126,6 +114,19 @@ public class ComposeGestureTargetLocator(private val logger: ILogger) : GestureT
126114
return bounds.contains(Offset(x, y))
127115
}
128116

117+
private fun extractTag(composeHelper: SentryComposeHelper, node: LayoutNode): String? {
118+
var lastKnownTag: String? = null
119+
val modifiers = node.getModifierInfo()
120+
for (index in modifiers.indices) {
121+
val modifierInfo = modifiers[index]
122+
val tag = composeHelper.extractTag(modifierInfo.modifier)
123+
if (tag != null) {
124+
lastKnownTag = tag
125+
}
126+
}
127+
return lastKnownTag
128+
}
129+
129130
public companion object {
130131
private const val ORIGIN = "jetpack_compose"
131132
}

0 commit comments

Comments
 (0)