@@ -11,10 +11,12 @@ import androidx.compose.ui.layout.findRootCoordinates
1111import androidx.compose.ui.node.LayoutNode
1212import androidx.compose.ui.node.Owner
1313import androidx.compose.ui.semantics.SemanticsActions
14+ import androidx.compose.ui.semantics.SemanticsConfiguration
1415import androidx.compose.ui.semantics.SemanticsProperties
1516import androidx.compose.ui.semantics.getOrNull
1617import androidx.compose.ui.text.TextLayoutResult
1718import androidx.compose.ui.unit.TextUnit
19+ import io.sentry.ILogger
1820import io.sentry.SentryLevel
1921import io.sentry.SentryOptions
2022import io.sentry.SentryReplayOptions
@@ -29,26 +31,55 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHiera
2931import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
3032import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
3133import java.lang.ref.WeakReference
34+ import java.lang.reflect.Method
3235
3336@TargetApi(26 )
3437internal object ComposeViewHierarchyNode {
3538
39+ private val getSemanticsConfigurationMethod: Method ? by lazy {
40+ try {
41+ return @lazy LayoutNode ::class .java.getDeclaredMethod(" getSemanticsConfiguration" ).apply {
42+ isAccessible = true
43+ }
44+ } catch (_: Throwable ) {
45+ // ignore, as this method may not be available
46+ }
47+ return @lazy null
48+ }
49+
50+ private fun LayoutNode.retrieveSemanticsConfiguration (logger : ILogger ): SemanticsConfiguration ? {
51+ // Jetpack Compose 1.8 or newer provides SemanticsConfiguration via SemanticsInfo
52+ // See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
53+ // and https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
54+ try {
55+ getSemanticsConfigurationMethod?.let {
56+ return it.invoke(this ) as SemanticsConfiguration ?
57+ }
58+ } catch (_: Throwable ) {
59+ logger.log(
60+ SentryLevel .WARNING ,
61+ " Failed to invoke LayoutNode.getSemanticsConfiguration"
62+ )
63+ }
64+
65+ // for backwards compatibility
66+ return collapsedSemantics
67+ }
68+
3669 /* *
3770 * Since Compose doesn't have a concept of a View class (they are all composable functions),
3871 * we need to map the semantics node to a corresponding old view system class.
3972 */
40- private fun LayoutNode .getProxyClassName (isImage : Boolean ): String {
73+ private fun SemanticsConfiguration .getProxyClassName (isImage : Boolean ): String {
4174 return when {
4275 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
76+ contains(SemanticsProperties .Text ) || contains(SemanticsActions .SetText ) || contains(SemanticsProperties .EditableText ) -> SentryReplayOptions .TEXT_VIEW_CLASS_NAME
4677 else -> " android.view.View"
4778 }
4879 }
4980
50- private fun LayoutNode .shouldMask (isImage : Boolean , options : SentryOptions ): Boolean {
51- val sentryPrivacyModifier = collapsedSemantics ?.getOrNull(SentryReplayModifiers .SentryPrivacy )
81+ private fun SemanticsConfiguration? .shouldMask (isImage : Boolean , options : SentryOptions ): Boolean {
82+ val sentryPrivacyModifier = this ?.getOrNull(SentryReplayModifiers .SentryPrivacy )
5283 if (sentryPrivacyModifier == " unmask" ) {
5384 return false
5485 }
@@ -57,7 +88,7 @@ internal object ComposeViewHierarchyNode {
5788 return true
5889 }
5990
60- val className = getProxyClassName(isImage)
91+ val className = this ?. getProxyClassName(isImage)
6192 if (options.sessionReplay.unmaskViewClasses.contains(className)) {
6293 return false
6394 }
@@ -83,7 +114,7 @@ internal object ComposeViewHierarchyNode {
83114 _rootCoordinates = WeakReference (node.coordinates.findRootCoordinates())
84115 }
85116
86- val semantics = node.collapsedSemantics
117+ val semantics = node.retrieveSemanticsConfiguration(options.logger)
87118 val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates ?.get())
88119 val isVisible = ! node.outerCoordinator.isTransparent() &&
89120 (semantics == null || ! semantics.contains(SemanticsProperties .InvisibleToUser )) &&
@@ -92,7 +123,7 @@ internal object ComposeViewHierarchyNode {
92123 semantics?.contains(SemanticsProperties .EditableText ) == true
93124 return when {
94125 semantics?.contains(SemanticsProperties .Text ) == true || isEditable -> {
95- val shouldMask = isVisible && node .shouldMask(isImage = false , options)
126+ val shouldMask = isVisible && semantics .shouldMask(isImage = false , options)
96127
97128 parent?.setImportantForCaptureToAncestors(true )
98129 // TODO: if we get reports that it's slow, we can drop this, and just mask
@@ -133,7 +164,7 @@ internal object ComposeViewHierarchyNode {
133164 else -> {
134165 val painter = node.findPainter()
135166 if (painter != null ) {
136- val shouldMask = isVisible && node .shouldMask(isImage = true , options)
167+ val shouldMask = isVisible && semantics .shouldMask(isImage = true , options)
137168
138169 parent?.setImportantForCaptureToAncestors(true )
139170 ImageViewHierarchyNode (
@@ -150,7 +181,7 @@ internal object ComposeViewHierarchyNode {
150181 visibleRect = visibleRect
151182 )
152183 } else {
153- val shouldMask = isVisible && node .shouldMask(isImage = false , options)
184+ val shouldMask = isVisible && semantics .shouldMask(isImage = false , options)
154185
155186 // TODO: this currently does not support embedded AndroidViews, we'd have to
156187 // TODO: traverse the ViewHierarchyNode here again. For now we can recommend
0 commit comments