Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry-android-replay/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -157,15 +158,15 @@ 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,
)
}

val isVisible =
!node.outerCoordinator.isTransparent() &&
!SentryLayoutNodeHelper.isTransparent(node) &&
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
visibleRect.height() > 0 &&
visibleRect.width() > 0
Expand Down Expand Up @@ -301,7 +302,7 @@ internal object ComposeViewHierarchyNode {
options: SentryMaskingOptions,
logger: ILogger,
) {
val children = this.children
val children = SentryLayoutNodeHelper.getChildren(this)
if (children.isEmpty()) {
return
}
Expand Down
7 changes: 7 additions & 0 deletions sentry-compose/api/android/sentry-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ public final class io/sentry/compose/BuildConfig {
public fun <init> ()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;
}
Expand Down
75 changes: 75 additions & 0 deletions sentry-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<KotlinCompile>().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<Detekt>().configureEach {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LayoutNode> {
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()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<LayoutNode> = node.children

public fun getOuterCoordinator(node: LayoutNode): NodeCoordinator = node.outerCoordinator
}