99package com.datadog.android.compose.internal.utils
1010
1111import androidx.compose.ui.geometry.Rect
12+ import androidx.compose.ui.layout.LayoutCoordinates
1213import androidx.compose.ui.layout.boundsInWindow
1314import androidx.compose.ui.node.LayoutNode
1415import androidx.compose.ui.semantics.Role
@@ -22,10 +23,11 @@ import com.datadog.android.api.feature.FeatureSdkCore
2223import com.datadog.android.compose.DatadogSemanticsPropertyKey
2324import com.datadog.android.rum.RumAttributes.ACTION_TARGET_ROLE
2425import com.datadog.android.rum.RumAttributes.ACTION_TARGET_SELECTED
26+ import java.lang.reflect.Method
2527
2628internal class LayoutNodeUtils {
2729
28- private var reflectionFallbackModeActivated = false
30+ private val methodResolver = MethodResolver ()
2931
3032 @Suppress(" NestedBlockDepth" , " CyclomaticComplexMethod" )
3133 fun resolveLayoutNode (node : LayoutNode ): TargetNode ? {
@@ -99,33 +101,35 @@ internal class LayoutNodeUtils {
99101 }
100102 }
101103
102- fun getLayoutNodeBoundsInWindow (node : LayoutNode ): Rect ? = if (reflectionFallbackModeActivated) {
103- getLayoutNodeBoundsInWindowReflection(node)
104- } else {
105- getLayoutNodeBoundsInWindowInternal(node) ? : getLayoutNodeBoundsInWindowReflection(node)
104+ fun getLayoutNodeBoundsInWindow (node : LayoutNode ): Rect ? = when (methodResolver.state) {
105+ MethodResolver .State .UNKNOWN -> {
106+ getLayoutNodeBoundsInWindowInternal(node) ? : getLayoutNodeBoundsInWindowReflection(node)
107+ }
108+
109+ MethodResolver .State .MANGLING_FAILED -> getLayoutNodeBoundsInWindowReflection(node)
110+ MethodResolver .State .REFLECTION_FAILED -> getLayoutNodeBoundsInWindowInternal(node)
106111 }
107112
108- private fun getLayoutNodeBoundsInWindowInternal (node : LayoutNode ): Rect ? = runSafe(
113+ internal fun getLayoutNodeBoundsInWindowInternal (node : LayoutNode ): Rect ? = runSafe(
109114 " getLayoutNodeBoundsInWindow"
110115 ) { node.layoutDelegate.outerCoordinator.coordinates.boundsInWindow() }
111116
112- private fun getLayoutNodeBoundsInWindowReflection (node : LayoutNode ) = runSafe(
117+ internal fun getLayoutNodeBoundsInWindowReflection (node : LayoutNode ) = runSafe(
113118 " getLayoutNodeBoundsInWindow[reflection]"
114119 ) {
115120 // TODO RUM-13454 Update compose bom and remove this method
116- reflectionFallbackModeActivated = true
117- val coordinates = node.getMethod(" getLayoutDelegate" )
118- ?.getMethod(" getOuterCoordinator" )
119- ?.getMethod(" getCoordinates" )
120-
121- @Suppress(" UnsafeThirdPartyFunctionCall" ) // it's okay if exception will be thrown here
122- Class .forName(" androidx.compose.ui.layout.LayoutCoordinatesKt" )
123- .getMethod(" boundsInWindow" , Class .forName(" androidx.compose.ui.layout.LayoutCoordinates" ))
124- .invoke(null , coordinates) as ? Rect
121+ methodResolver.state = MethodResolver .State .MANGLING_FAILED
122+ val coordinates = node.invokeWithReflection(" getLayoutDelegate" )
123+ ?.invokeWithReflection(" getOuterCoordinator" )
124+ ?.invokeWithReflection(" getCoordinates" ) as ? LayoutCoordinates
125+ coordinates?.boundsInWindow()
125126 }
126127
127- private fun Any.getMethod (prefix : String ): Any? {
128- return this .javaClass.methods.firstOrNull { it.name == prefix || it.name.startsWith(" $prefix $" ) }
128+ @Suppress(" UnsafeThirdPartyFunctionCall" ) // runSafe in the caller swallows any Throwable
129+ private fun Any.invokeWithReflection (prefix : String ): Any? {
130+ if (methodResolver.state == MethodResolver .State .REFLECTION_FAILED ) return null
131+ return methodResolver
132+ .findMethod(javaClass, prefix)
129133 ?.invoke(this )
130134 }
131135
@@ -153,6 +157,35 @@ internal class LayoutNodeUtils {
153157 val customAttributes : Map <String , Any ?> = mapOf()
154158 )
155159
160+ internal class MethodResolver {
161+ // RUM-15813: Kotlin internal accessors in androidx.compose.ui are JVM-mangled with
162+ // a module suffix (plain / "$ui" / "$ui_release"). The mangling is stable per class,
163+ // but NOT necessarily the same across classes in the reflection chain: e.g. on Compose
164+ // UI 1.10 LayoutNode.getLayoutDelegate$ui is mangled while LayoutNodeLayoutDelegate
165+ // exposes getOuterCoordinator without a suffix. We therefore cache the resolved suffix
166+ // per owner-class rather than once globally.
167+ //
168+ enum class State {
169+ UNKNOWN , // — Try internal mangling resolution, if fails - reflection.
170+ MANGLING_FAILED , // Mangling resolution failed - allowing reflection attempt
171+ REFLECTION_FAILED // Reflection resolution failed - switching back to mangling resolution (even if it fails)
172+ }
173+
174+ var state: State = State .UNKNOWN
175+ set(value) {
176+ if (value.ordinal > field.ordinal) {
177+ field = value
178+ }
179+ }
180+
181+ val classPrefixMethodsCache: MutableMap <Class <* >, MutableMap <String , Method ?>> = mutableMapOf ()
182+
183+ fun findMethod (klass : Class <* >, prefix : String ): Method ? =
184+ classPrefixMethodsCache
185+ .resolveMethod(klass, prefix)
186+ .also { if (it == null ) state = State .REFLECTION_FAILED }
187+ }
188+
156189 companion object {
157190 private const val CLASS_NAME_CLICKABLE_ELEMENT =
158191 " androidx.compose.foundation.ClickableElement"
@@ -168,5 +201,33 @@ internal class LayoutNodeUtils {
168201 " androidx.compose.foundation.gestures.ScrollableElement"
169202 private const val CLASS_NAME_SELECTABLE_ELEMENT =
170203 " androidx.compose.foundation.selection.SelectableElement"
204+
205+ // Empty suffix covers public accessors; "$ui" and "$ui_release" cover Kotlin-internal
206+ // accessors in the androidx.compose.ui module (the exact suffix depends on build
207+ // configuration). Ordered by likelihood — public first, release build second.
208+ private val SUPPORTED_MANGLING_SUFFIXES = listOf (" " , " \$ ui_release" , " \$ ui" )
209+
210+ private fun MutableMap <Class <* >, MutableMap <String , Method ?>>.resolveMethod (
211+ klass : Class <* >,
212+ methodPrefix : String
213+ ): Method ? {
214+ val klassCache: MutableMap <String , Method ?> = getOrPut(klass) { mutableMapOf () }
215+
216+ if (! klassCache.containsKey(methodPrefix)) {
217+ klassCache[methodPrefix] = searchManglings(klass, methodPrefix)
218+ }
219+
220+ return klassCache[methodPrefix]
221+ }
222+
223+ private fun searchManglings (klass : Class <* >, prefix : String ): Method ? = SUPPORTED_MANGLING_SUFFIXES
224+ .firstNotNullOfOrNull {
225+ try {
226+ @Suppress(" UnsafeThirdPartyFunctionCall" ) // NoSuchMethodException is expected here
227+ klass.getMethod(" $prefix$it " )
228+ } catch (@Suppress(" SwallowedException" ) _: NoSuchMethodException ) {
229+ null
230+ }
231+ }
171232 }
172233}
0 commit comments