@@ -11,6 +11,7 @@ import androidx.compose.ui.layout.findRootCoordinates
11
11
import androidx.compose.ui.node.LayoutNode
12
12
import androidx.compose.ui.node.Owner
13
13
import androidx.compose.ui.semantics.SemanticsActions
14
+ import androidx.compose.ui.semantics.SemanticsConfiguration
14
15
import androidx.compose.ui.semantics.SemanticsProperties
15
16
import androidx.compose.ui.semantics.getOrNull
16
17
import androidx.compose.ui.text.TextLayoutResult
@@ -29,26 +30,51 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHiera
29
30
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
30
31
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
31
32
import java.lang.ref.WeakReference
33
+ import java.lang.reflect.Method
32
34
33
35
@TargetApi(26 )
34
36
internal object ComposeViewHierarchyNode {
35
37
38
+ private val getSemanticsConfigurationMethod: Method ? by lazy {
39
+ try {
40
+ return @lazy LayoutNode ::class .java.getDeclaredMethod(" getSemanticsConfiguration" ).apply {
41
+ isAccessible = true
42
+ }
43
+ } catch (_: Throwable ) {
44
+ // ignore, as this method may not be available
45
+ }
46
+ return @lazy null
47
+ }
48
+
49
+ private var semanticsRetrievalErrorLogged: Boolean = false
50
+
51
+ @JvmStatic
52
+ internal fun retrieveSemanticsConfiguration (node : LayoutNode ): SemanticsConfiguration ? {
53
+ // Jetpack Compose 1.8 or newer provides SemanticsConfiguration via SemanticsInfo
54
+ // See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
55
+ // and https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
56
+ getSemanticsConfigurationMethod?.let {
57
+ return it.invoke(node) as SemanticsConfiguration ?
58
+ }
59
+
60
+ // for backwards compatibility
61
+ return node.collapsedSemantics
62
+ }
63
+
36
64
/* *
37
65
* Since Compose doesn't have a concept of a View class (they are all composable functions),
38
66
* we need to map the semantics node to a corresponding old view system class.
39
67
*/
40
- private fun LayoutNode. getProxyClassName (isImage : Boolean ): String {
68
+ private fun getProxyClassName (isImage : Boolean , config : SemanticsConfiguration ? ): String {
41
69
return when {
42
70
isImage -> SentryReplayOptions .IMAGE_VIEW_CLASS_NAME
43
- collapsedSemantics?.contains(SemanticsProperties .Text ) == true ||
44
- collapsedSemantics?.contains(SemanticsActions .SetText ) == true ||
45
- collapsedSemantics?.contains(SemanticsProperties .EditableText ) == true -> SentryReplayOptions .TEXT_VIEW_CLASS_NAME
71
+ config != null && (config.contains(SemanticsProperties .Text ) || config.contains(SemanticsActions .SetText ) || config.contains(SemanticsProperties .EditableText )) -> SentryReplayOptions .TEXT_VIEW_CLASS_NAME
46
72
else -> " android.view.View"
47
73
}
48
74
}
49
75
50
- private fun LayoutNode .shouldMask (isImage : Boolean , options : SentryOptions ): Boolean {
51
- val sentryPrivacyModifier = collapsedSemantics ?.getOrNull(SentryReplayModifiers .SentryPrivacy )
76
+ private fun SemanticsConfiguration? .shouldMask (isImage : Boolean , options : SentryOptions ): Boolean {
77
+ val sentryPrivacyModifier = this ?.getOrNull(SentryReplayModifiers .SentryPrivacy )
52
78
if (sentryPrivacyModifier == " unmask" ) {
53
79
return false
54
80
}
@@ -57,7 +83,7 @@ internal object ComposeViewHierarchyNode {
57
83
return true
58
84
}
59
85
60
- val className = getProxyClassName(isImage)
86
+ val className = getProxyClassName(isImage, this )
61
87
if (options.sessionReplay.unmaskViewClasses.contains(className)) {
62
88
return false
63
89
}
@@ -83,16 +109,53 @@ internal object ComposeViewHierarchyNode {
83
109
_rootCoordinates = WeakReference (node.coordinates.findRootCoordinates())
84
110
}
85
111
86
- val semantics = node.collapsedSemantics
87
112
val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates ?.get())
113
+ val semantics: SemanticsConfiguration ?
114
+
115
+ try {
116
+ semantics = retrieveSemanticsConfiguration(node)
117
+ } catch (t: Throwable ) {
118
+ if (! semanticsRetrievalErrorLogged) {
119
+ semanticsRetrievalErrorLogged = true
120
+ options.logger.log(
121
+ SentryLevel .ERROR ,
122
+ t,
123
+ """
124
+ Error retrieving semantics information from Compose tree. Most likely you're using
125
+ an unsupported version of androidx.compose.ui:ui. The supported
126
+ version range is 1.5.0 - 1.8.0.
127
+ If you're using a newer version, please open a github issue with the version
128
+ you're using, so we can add support for it.
129
+ """ .trimIndent()
130
+ )
131
+ }
132
+
133
+ // If we're unable to retrieve the semantics configuration
134
+ // we should play safe and mask the whole node.
135
+ return GenericViewHierarchyNode (
136
+ x = visibleRect.left.toFloat(),
137
+ y = visibleRect.top.toFloat(),
138
+ width = node.width,
139
+ height = node.height,
140
+ elevation = (parent?.elevation ? : 0f ),
141
+ distance = distance,
142
+ parent = parent,
143
+ shouldMask = true ,
144
+ isImportantForContentCapture = false , /* will be set by children */
145
+ isVisible = ! node.outerCoordinator.isTransparent() && visibleRect.height() > 0 && visibleRect.width() > 0 ,
146
+ visibleRect = visibleRect
147
+ )
148
+ }
149
+
88
150
val isVisible = ! node.outerCoordinator.isTransparent() &&
89
151
(semantics == null || ! semantics.contains(SemanticsProperties .InvisibleToUser )) &&
90
152
visibleRect.height() > 0 && visibleRect.width() > 0
91
153
val isEditable = semantics?.contains(SemanticsActions .SetText ) == true ||
92
154
semantics?.contains(SemanticsProperties .EditableText ) == true
155
+
93
156
return when {
94
157
semantics?.contains(SemanticsProperties .Text ) == true || isEditable -> {
95
- val shouldMask = isVisible && node .shouldMask(isImage = false , options)
158
+ val shouldMask = isVisible && semantics .shouldMask(isImage = false , options)
96
159
97
160
parent?.setImportantForCaptureToAncestors(true )
98
161
// TODO: if we get reports that it's slow, we can drop this, and just mask
@@ -133,7 +196,7 @@ internal object ComposeViewHierarchyNode {
133
196
else -> {
134
197
val painter = node.findPainter()
135
198
if (painter != null ) {
136
- val shouldMask = isVisible && node .shouldMask(isImage = true , options)
199
+ val shouldMask = isVisible && semantics .shouldMask(isImage = true , options)
137
200
138
201
parent?.setImportantForCaptureToAncestors(true )
139
202
ImageViewHierarchyNode (
@@ -150,7 +213,7 @@ internal object ComposeViewHierarchyNode {
150
213
visibleRect = visibleRect
151
214
)
152
215
} else {
153
- val shouldMask = isVisible && node .shouldMask(isImage = false , options)
216
+ val shouldMask = isVisible && semantics .shouldMask(isImage = false , options)
154
217
155
218
// TODO: this currently does not support embedded AndroidViews, we'd have to
156
219
// TODO: traverse the ViewHierarchyNode here again. For now we can recommend
0 commit comments