Skip to content

Commit adf6f50

Browse files
authored
Merge 339c0cd into b4edb46
2 parents b4edb46 + 339c0cd commit adf6f50

5 files changed

Lines changed: 105 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
### Fixes
1414

1515
- Android: Remove the dependency on protobuf-lite for tombstones ([#5157](https://github.com/getsentry/sentry-java/pull/5157))
16+
- Support masking/unmasking and click/scroll detection for Jetpack Compose 1.10+ ([#5189](https://github.com/getsentry/sentry-java/pull/5189))
1617

1718
### Features
1819

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ androidx-compose-material-icons-core = { module = "androidx.compose.material:mat
8585
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version="1.7.8" }
8686
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" }
8787
# Note: don't change without testing forwards compatibility
88-
androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.5.0" }
88+
androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.10.2" }
8989
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.3" }
9090
androidx-core = { module = "androidx.core:core", version = "1.3.2" }
9191
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.7.0" }

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

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ import java.lang.reflect.Method
3636
@SuppressLint("UseRequiresApi")
3737
@TargetApi(26)
3838
internal object ComposeViewHierarchyNode {
39-
private val getSemanticsConfigurationMethod: Method? by lazy {
39+
private val getCollapsedSemanticsMethod: Method? by lazy {
4040
try {
41-
return@lazy LayoutNode::class.java.getDeclaredMethod("getSemanticsConfiguration").apply {
41+
return@lazy LayoutNode::class.java.getDeclaredMethod("getCollapsedSemantics").apply {
4242
isAccessible = true
4343
}
4444
} catch (_: Throwable) {
@@ -51,17 +51,15 @@ internal object ComposeViewHierarchyNode {
5151

5252
@JvmStatic
5353
internal fun retrieveSemanticsConfiguration(node: LayoutNode): SemanticsConfiguration? {
54-
// Jetpack Compose 1.8 or newer provides SemanticsConfiguration via SemanticsInfo
55-
// See
56-
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
57-
// and
58-
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
59-
getSemanticsConfigurationMethod?.let {
60-
return it.invoke(node) as SemanticsConfiguration?
54+
try {
55+
return node.semanticsConfiguration
56+
} catch (_: Exception) {
57+
// for backwards compatibility
58+
// Jetpack Compose 1.8 or older
59+
return getCollapsedSemanticsMethod?.let {
60+
return it.invoke(node) as SemanticsConfiguration?
61+
}
6162
}
62-
63-
// for backwards compatibility
64-
return node.collapsedSemantics
6563
}
6664

6765
/**
@@ -157,15 +155,15 @@ internal object ComposeViewHierarchyNode {
157155
shouldMask = true,
158156
isImportantForContentCapture = false, // will be set by children
159157
isVisible =
160-
!node.outerCoordinator.isTransparent() &&
158+
!SentryLayoutNodeHelper.isTransparent(node) &&
161159
visibleRect.height() > 0 &&
162160
visibleRect.width() > 0,
163161
visibleRect = visibleRect,
164162
)
165163
}
166164

167165
val isVisible =
168-
!node.outerCoordinator.isTransparent() &&
166+
!SentryLayoutNodeHelper.isTransparent(node) &&
169167
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
170168
visibleRect.height() > 0 &&
171169
visibleRect.width() > 0
@@ -301,7 +299,7 @@ internal object ComposeViewHierarchyNode {
301299
options: SentryMaskingOptions,
302300
logger: ILogger,
303301
) {
304-
val children = this.children
302+
val children = SentryLayoutNodeHelper.getChildren(this)
305303
if (children.isEmpty()) {
306304
return
307305
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
@file:Suppress(
2+
"INVISIBLE_MEMBER",
3+
"INVISIBLE_REFERENCE",
4+
"EXPOSED_PARAMETER_TYPE",
5+
"EXPOSED_RETURN_TYPE",
6+
"EXPOSED_FUNCTION_RETURN_TYPE",
7+
)
8+
9+
package io.sentry.android.replay.viewhierarchy
10+
11+
import androidx.compose.ui.node.LayoutNode
12+
import androidx.compose.ui.node.NodeCoordinator
13+
import java.lang.reflect.Method
14+
15+
/**
16+
* Provides access to internal LayoutNode members that are subject to Kotlin name-mangling.
17+
*
18+
* Compiled against Compose >= 1.10 where the mangled names use the "ui" module suffix (e.g.
19+
* getChildren$ui()). For apps still on Compose < 1.10 (where the suffix is "$ui_release"), the
20+
* direct call will throw [NoSuchMethodError] and we fall back to reflection-based accessors that
21+
* are resolved and cached on first use.
22+
*/
23+
internal object SentryLayoutNodeHelper {
24+
private class Fallback(val getChildren: Method?, val getOuterCoordinator: Method?)
25+
26+
@Volatile private var useFallback: Boolean? = null
27+
@Volatile private var fallback: Fallback? = null
28+
29+
private fun tryResolve(clazz: Class<*>, name: String): Method? {
30+
return try {
31+
clazz.getDeclaredMethod(name).apply { isAccessible = true }
32+
} catch (_: NoSuchMethodException) {
33+
null
34+
}
35+
}
36+
37+
@Suppress("UNCHECKED_CAST")
38+
fun getChildren(node: LayoutNode): List<LayoutNode> {
39+
when (useFallback) {
40+
false -> return node.children
41+
true -> {
42+
return getFallback().getChildren!!.invoke(node) as List<LayoutNode>
43+
}
44+
null -> {
45+
try {
46+
return node.children.also { useFallback = false }
47+
} catch (_: NoSuchMethodError) {
48+
useFallback = true
49+
return getFallback().getChildren!!.invoke(node) as List<LayoutNode>
50+
}
51+
}
52+
}
53+
}
54+
55+
fun isTransparent(node: LayoutNode): Boolean {
56+
when (useFallback) {
57+
false -> return node.outerCoordinator.isTransparent()
58+
true -> {
59+
val fb = getFallback()
60+
val coordinator = fb.getOuterCoordinator!!.invoke(node) as NodeCoordinator
61+
return coordinator.isTransparent()
62+
}
63+
null -> {
64+
try {
65+
return node.outerCoordinator.isTransparent().also { useFallback = false }
66+
} catch (_: NoSuchMethodError) {
67+
useFallback = true
68+
val fb = getFallback()
69+
val coordinator = fb.getOuterCoordinator!!.invoke(node) as NodeCoordinator
70+
return coordinator.isTransparent()
71+
}
72+
}
73+
}
74+
}
75+
76+
private fun getFallback(): Fallback {
77+
fallback?.let {
78+
return it
79+
}
80+
81+
val layoutNodeClass = LayoutNode::class.java
82+
val getChildren = tryResolve(layoutNodeClass, "getChildren\$ui_release")
83+
val getOuterCoordinator = tryResolve(layoutNodeClass, "getOuterCoordinator\$ui_release")
84+
85+
return Fallback(getChildren, getOuterCoordinator).also { fallback = it }
86+
}
87+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@ public class ComposeGestureTargetLocator(private val logger: ILogger) : GestureT
5252

5353
// the last known tag when iterating the node tree
5454
var lastKnownTag: String? = null
55+
var isClickable = false
56+
var isScrollable = false
57+
5558
while (!queue.isEmpty()) {
5659
val node = queue.poll() ?: continue
5760
if (node.isPlaced && layoutNodeBoundsContain(rootLayoutNode, node, x, y)) {
58-
var isClickable = false
59-
var isScrollable = false
6061

6162
val modifiers = node.getModifierInfo()
6263
for (index in modifiers.indices) {

0 commit comments

Comments
 (0)