Skip to content

Commit 339c0cd

Browse files
committed
Switch to reflection
1 parent d9d89d9 commit 339c0cd

File tree

5 files changed

+67
-139
lines changed

5 files changed

+67
-139
lines changed

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/build.gradle.kts

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import io.gitlab.arturbosch.detekt.Detekt
22
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
3-
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
43

54
plugins {
65
id("com.android.library")
@@ -92,80 +91,6 @@ dependencies {
9291
testImplementation(libs.coil.compose)
9392
}
9493

95-
// Compile Compose110Helper.kt against Compose 1.10 where internal LayoutNode accessors
96-
// are mangled with module name "ui" (e.g. getChildren$ui()) instead of "ui_release"
97-
val compose110Classpath by
98-
configurations.creating {
99-
isCanBeConsumed = false
100-
isCanBeResolved = true
101-
attributes {
102-
attribute(Attribute.of("artifactType", String::class.java), "android-classes-jar")
103-
}
104-
}
105-
106-
val compose110KotlinCompiler by
107-
configurations.creating {
108-
isCanBeConsumed = false
109-
isCanBeResolved = true
110-
}
111-
112-
dependencies {
113-
//noinspection UseTomlInstead
114-
compose110Classpath("androidx.compose.ui:ui-android:1.10.0")
115-
//noinspection UseTomlInstead
116-
compose110KotlinCompiler("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0")
117-
}
118-
119-
val compileCompose110 by
120-
tasks.registering(JavaExec::class) {
121-
val sourceDir = file("src/compose110/kotlin")
122-
val outputDir = layout.buildDirectory.dir("classes/kotlin/compose110")
123-
val compileClasspathFiles = compose110Classpath.incoming.files
124-
125-
inputs.dir(sourceDir)
126-
inputs.files(compileClasspathFiles)
127-
outputs.dir(outputDir)
128-
129-
classpath = compose110KotlinCompiler
130-
mainClass.set("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler")
131-
132-
argumentProviders.add(
133-
CommandLineArgumentProvider {
134-
val cp = compileClasspathFiles.files.joinToString(File.pathSeparator)
135-
outputDir.get().asFile.mkdirs()
136-
listOf(
137-
sourceDir.absolutePath,
138-
"-classpath",
139-
cp,
140-
"-d",
141-
outputDir.get().asFile.absolutePath,
142-
"-jvm-target",
143-
"1.8",
144-
"-language-version",
145-
"1.9",
146-
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
147-
"-Xsuppress-version-warnings",
148-
"-no-stdlib",
149-
)
150-
}
151-
)
152-
}
153-
154-
// Make compose110 output available to the Android Kotlin compilation
155-
val compose110Output = files(compileCompose110.map { it.outputs.files })
156-
157-
tasks.withType<KotlinCompile>().configureEach {
158-
if (name == "compileReleaseKotlin" || name == "compileDebugKotlin") {
159-
dependsOn(compileCompose110)
160-
libraries.from(compose110Output)
161-
}
162-
}
163-
164-
// Include compose110 classes in the AAR
165-
android.libraryVariants.all {
166-
registerPreJavacGeneratedBytecode(project.files(compileCompose110.map { it.outputs.files }))
167-
}
168-
16994
tasks.withType<Detekt>().configureEach {
17095
// Target version of the generated JVM bytecode. It is used for type resolution.
17196
jvmTarget = JavaVersion.VERSION_1_8.toString()

sentry-android-replay/src/compose110/kotlin/io/sentry/android/replay/viewhierarchy/Compose110Helper.kt

Lines changed: 0 additions & 23 deletions
This file was deleted.

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

Lines changed: 10 additions & 12 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
/**

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

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,79 @@
99
package io.sentry.android.replay.viewhierarchy
1010

1111
import androidx.compose.ui.node.LayoutNode
12+
import androidx.compose.ui.node.NodeCoordinator
13+
import java.lang.reflect.Method
1214

1315
/**
1416
* Provides access to internal LayoutNode members that are subject to Kotlin name-mangling.
1517
*
16-
* LayoutNode.children and LayoutNode.outerCoordinator are Kotlin `internal`, so their getters are
17-
* mangled with the module name: getChildren$ui_release() in Compose < 1.10 vs getChildren$ui() in
18-
* Compose >= 1.10. This class detects the version on first use and delegates to the correct
19-
* accessor.
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.
2022
*/
2123
internal object SentryLayoutNodeHelper {
22-
@Volatile private var compose110Helper: Compose110Helper? = null
23-
@Volatile private var useCompose110: Boolean? = null
24+
private class Fallback(val getChildren: Method?, val getOuterCoordinator: Method?)
2425

25-
private fun getHelper(): Compose110Helper {
26-
compose110Helper?.let {
27-
return it
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
2834
}
29-
val helper = Compose110Helper()
30-
compose110Helper = helper
31-
return helper
3235
}
3336

37+
@Suppress("UNCHECKED_CAST")
3438
fun getChildren(node: LayoutNode): List<LayoutNode> {
35-
return if (useCompose110 == false) {
36-
node.children
37-
} else {
38-
try {
39-
getHelper().getChildren(node).also { useCompose110 = true }
40-
} catch (_: NoSuchMethodError) {
41-
useCompose110 = false
42-
node.children
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+
}
4351
}
4452
}
4553
}
4654

4755
fun isTransparent(node: LayoutNode): Boolean {
48-
return if (useCompose110 == false) {
49-
node.outerCoordinator.isTransparent()
50-
} else {
51-
try {
52-
getHelper().getOuterCoordinator(node).isTransparent().also { useCompose110 = true }
53-
} catch (_: NoSuchMethodError) {
54-
useCompose110 = false
55-
node.outerCoordinator.isTransparent()
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()
5662
}
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
5779
}
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 }
5886
}
5987
}

0 commit comments

Comments
 (0)