diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 60d38c0ae0a..ffcd91c70ff 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -70,6 +70,7 @@ kotlin { explicitApi() } dependencies { api(projects.sentry) + api(projects.sentryCompose) compileOnly(libs.androidx.compose.ui.replay) implementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid)) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index 2e58418c3ac..9aa21b16dc8 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -30,6 +30,7 @@ import io.sentry.android.replay.util.toOpaque import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode +import io.sentry.compose.SentryLayoutNodeHelper import java.lang.ref.WeakReference import java.lang.reflect.Method @@ -157,7 +158,7 @@ internal object ComposeViewHierarchyNode { shouldMask = true, isImportantForContentCapture = false, // will be set by children isVisible = - !node.outerCoordinator.isTransparent() && + !SentryLayoutNodeHelper.isTransparent(node) && visibleRect.height() > 0 && visibleRect.width() > 0, visibleRect = visibleRect, @@ -165,7 +166,7 @@ internal object ComposeViewHierarchyNode { } val isVisible = - !node.outerCoordinator.isTransparent() && + !SentryLayoutNodeHelper.isTransparent(node) && (semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) && visibleRect.height() > 0 && visibleRect.width() > 0 @@ -301,7 +302,7 @@ internal object ComposeViewHierarchyNode { options: SentryMaskingOptions, logger: ILogger, ) { - val children = this.children + val children = SentryLayoutNodeHelper.getChildren(this) if (children.isEmpty()) { return } diff --git a/sentry-compose/api/android/sentry-compose.api b/sentry-compose/api/android/sentry-compose.api index 3ae9af627b1..a25bb50532a 100644 --- a/sentry-compose/api/android/sentry-compose.api +++ b/sentry-compose/api/android/sentry-compose.api @@ -6,6 +6,13 @@ public final class io/sentry/compose/BuildConfig { public fun ()V } +public final class io/sentry/compose/ComposeHelper { + public static final field $stable I + public static final field INSTANCE Lio/sentry/compose/ComposeHelper; + public final fun getChildren (Landroidx/compose/ui/node/LayoutNode;)Ljava/util/List; + public final fun isTransparent (Landroidx/compose/ui/node/LayoutNode;)Z +} + public final class io/sentry/compose/SentryComposeHelperKt { public static final fun boundsInWindow (Landroidx/compose/ui/layout/LayoutCoordinates;Landroidx/compose/ui/layout/LayoutCoordinates;)Landroidx/compose/ui/geometry/Rect; } diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 3385d0328e2..b5e6523a3b2 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -2,6 +2,7 @@ import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.multiplatform) @@ -118,6 +119,80 @@ android { } } +// Compile Compose110Helper.kt against Compose 1.10 where internal LayoutNode accessors +// are mangled with module name "ui" (e.g. getChildren$ui()) instead of "ui_release" +val compose110Classpath by + configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true + attributes { + attribute(Attribute.of("artifactType", String::class.java), "android-classes-jar") + } + } + +val compose110KotlinCompiler by + configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true + } + +dependencies { + //noinspection UseTomlInstead + compose110Classpath("androidx.compose.ui:ui-android:1.10.0") + //noinspection UseTomlInstead + compose110KotlinCompiler("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0") +} + +val compileCompose110 by + tasks.registering(JavaExec::class) { + val sourceDir = file("src/compose110/kotlin") + val outputDir = layout.buildDirectory.dir("classes/kotlin/compose110") + val compileClasspathFiles = compose110Classpath.incoming.files + + inputs.dir(sourceDir) + inputs.files(compileClasspathFiles) + outputs.dir(outputDir) + + classpath = compose110KotlinCompiler + mainClass.set("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler") + + argumentProviders.add( + CommandLineArgumentProvider { + val cp = compileClasspathFiles.files.joinToString(File.pathSeparator) + outputDir.get().asFile.mkdirs() + listOf( + sourceDir.absolutePath, + "-classpath", + cp, + "-d", + outputDir.get().asFile.absolutePath, + "-jvm-target", + "1.8", + "-language-version", + "1.9", + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", + "-Xsuppress-version-warnings", + "-no-stdlib", + ) + } + ) + } + +// Make compose110 output available to the Android Kotlin compilation +val compose110Output = files(compileCompose110.map { it.outputs.files }) + +tasks.withType().configureEach { + if (name == "compileReleaseKotlinAndroid" || name == "compileDebugKotlinAndroid") { + dependsOn(compileCompose110) + libraries.from(compose110Output) + } +} + +// Include compose110 classes in the AAR +android.libraryVariants.all { + registerPreJavacGeneratedBytecode(project.files(compileCompose110.map { it.outputs.files })) +} + tasks.withType().configureEach { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryLayoutNodeHelper.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryLayoutNodeHelper.kt new file mode 100644 index 00000000000..715699fa094 --- /dev/null +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryLayoutNodeHelper.kt @@ -0,0 +1,58 @@ +@file:Suppress( + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "EXPOSED_PARAMETER_TYPE", + "EXPOSED_RETURN_TYPE", + "EXPOSED_FUNCTION_RETURN_TYPE", +) + +package io.sentry.compose + +import androidx.compose.ui.node.LayoutNode + +/** + * Provides access to internal LayoutNode members that are subject to Kotlin name-mangling. + * + * LayoutNode.children and LayoutNode.outerCoordinator are Kotlin `internal`, so their getters are + * mangled with the module name: getChildren$ui_release() in Compose < 1.10 vs getChildren$ui() in + * Compose >= 1.10. This class detects the version on first use and delegates to the correct + * accessor. + */ +public object SentryLayoutNodeHelper { + @Volatile private var compose110Helper: Compose110Helper? = null + @Volatile private var useCompose110: Boolean? = null + + public fun getChildren(node: LayoutNode): List { + return if (useCompose110 == true) { + compose110Helper!!.getChildren(node) + } else { + val helper = Compose110Helper() + try { + helper.getChildren(node).also { + compose110Helper = helper + useCompose110 = true + } + } catch (_: NoSuchMethodError) { + useCompose110 = false + node.children + } + } + } + + public fun isTransparent(node: LayoutNode): Boolean { + return if (useCompose110 == true) { + compose110Helper!!.getOuterCoordinator(node).isTransparent() + } else { + val helper = Compose110Helper() + try { + helper.getOuterCoordinator(node).isTransparent().also { + compose110Helper = helper + useCompose110 = true + } + } catch (_: NoSuchMethodError) { + useCompose110 = false + node.outerCoordinator.isTransparent() + } + } + } +} diff --git a/sentry-compose/src/compose110/kotlin/io/sentry/compose/Compose110Helper.kt b/sentry-compose/src/compose110/kotlin/io/sentry/compose/Compose110Helper.kt new file mode 100644 index 00000000000..c3ffde79ac3 --- /dev/null +++ b/sentry-compose/src/compose110/kotlin/io/sentry/compose/Compose110Helper.kt @@ -0,0 +1,23 @@ +@file:Suppress( + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "EXPOSED_PARAMETER_TYPE", + "EXPOSED_RETURN_TYPE", + "EXPOSED_FUNCTION_RETURN_TYPE", +) + +package io.sentry.compose + +import androidx.compose.ui.node.LayoutNode +import androidx.compose.ui.node.NodeCoordinator + +/** + * Compiled against Compose >= 1.10 where internal LayoutNode accessors are mangled with the module + * name "ui" (e.g. getChildren$ui(), getOuterCoordinator$ui()) instead of "ui_release" used in + * earlier versions. + */ +public class Compose110Helper { + public fun getChildren(node: LayoutNode): List = node.children + + public fun getOuterCoordinator(node: LayoutNode): NodeCoordinator = node.outerCoordinator +}