-
-
Notifications
You must be signed in to change notification settings - Fork 467
Expand file tree
/
Copy pathSentryComposeHelper.kt
More file actions
155 lines (133 loc) · 5.91 KB
/
SentryComposeHelper.kt
File metadata and controls
155 lines (133 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // to access internal vals
package io.sentry.compose
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.semantics.SemanticsModifier
import io.sentry.ILogger
import io.sentry.SentryLevel
import java.lang.reflect.Field
internal class SentryComposeHelper(logger: ILogger) {
private val testTagElementField: Field? =
loadField(logger, "androidx.compose.ui.platform.TestTagElement", "tag")
private val sentryTagElementField: Field? =
loadField(logger, "io.sentry.compose.SentryModifier${'$'}SentryTagModifierNodeElement", "tag")
fun extractTag(modifier: Modifier): String? {
val type = modifier.javaClass.name
// Newer Jetpack Compose uses TestTagElement as node elements
// See
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/TestTag.kt;l=34;drc=dcaa116fbfda77e64a319e1668056ce3b032469f
try {
if ("androidx.compose.ui.platform.TestTagElement" == type && testTagElementField != null) {
val value = testTagElementField.get(modifier)
return value as String?
} else if (
"io.sentry.compose.SentryModifier${'$'}SentryTagModifierNodeElement" == type &&
sentryTagElementField != null
) {
val value = sentryTagElementField.get(modifier)
return value as String?
}
} catch (e: Throwable) {
// ignored
}
// Older versions use SemanticsModifier
if (modifier is SemanticsModifier) {
val semanticsConfiguration = modifier.semanticsConfiguration
for ((item, value) in semanticsConfiguration) {
val key = item.name
if ("SentryTag" == key || "TestTag" == key) {
if (value is String) {
return value
}
}
}
}
return null
}
companion object {
private fun loadField(logger: ILogger, className: String, fieldName: String): Field? {
try {
val clazz = Class.forName(className)
val field = clazz.getDeclaredField(fieldName)
field.isAccessible = true
return field
} catch (e: Exception) {
logger.log(SentryLevel.WARNING, "Could not load $className.$fieldName field")
}
return null
}
}
}
/**
* Copied from sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt
*
* A faster copy of
* https://github.com/androidx/androidx/blob/fc7df0dd68466ac3bb16b1c79b7a73dd0bfdd4c1/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt#L187
*
* Since we traverse the tree from the root, we don't need to find it again from the leaf node and
* just pass it as an argument.
*
* @return boundaries of this layout relative to the window's origin.
*/
public fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates?): Rect {
val root = rootCoordinates ?: findRootCoordinates()
val rootWidth = root.size.width.toFloat()
val rootHeight = root.size.height.toFloat()
// pass clipBounds explicitly to avoid the `localBoundingBoxOf$default` bridge that AGP 8.13's D8
// desugars inconsistently on minSdk < 24
val bounds = root.localBoundingBoxOf(this, true)
val boundsLeft = bounds.left.fastCoerceIn(0f, rootWidth)
val boundsTop = bounds.top.fastCoerceIn(0f, rootHeight)
val boundsRight = bounds.right.fastCoerceIn(0f, rootWidth)
val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)
if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
return Rect.Zero
}
val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))
val topLeftX = topLeft.x
val topRightX = topRight.x
val bottomLeftX = bottomLeft.x
val bottomRightX = bottomRight.x
val left = fastMinOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
val right = fastMaxOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
val topLeftY = topLeft.y
val topRightY = topRight.y
val bottomLeftY = bottomLeft.y
val bottomRightY = bottomRight.y
val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
return Rect(left, top, right, bottom)
}
/**
* Returns the smaller of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.minOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private fun fastMinOf(a: Float, b: Float, c: Float, d: Float): Float =
minOf(a, minOf(b, minOf(c, d)))
/**
* Returns the largest of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.maxOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private fun fastMaxOf(a: Float, b: Float, c: Float, d: Float): Float =
maxOf(a, maxOf(b, maxOf(c, d)))
/**
* Returns this float value clamped in the inclusive range defined by [minimumValue] and
* [maximumValue]. Unlike [Float.coerceIn], the range is not validated: the caller must ensure that
* [minimumValue] is less than [maximumValue].
*/
private fun Float.fastCoerceIn(minimumValue: Float, maximumValue: Float) =
this.fastCoerceAtLeast(minimumValue).fastCoerceAtMost(maximumValue)
/** Ensures that this value is not less than the specified [minimumValue]. */
private fun Float.fastCoerceAtLeast(minimumValue: Float): Float =
if (this < minimumValue) minimumValue else this
/** Ensures that this value is not greater than the specified [maximumValue]. */
private fun Float.fastCoerceAtMost(maximumValue: Float): Float =
if (this > maximumValue) maximumValue else this