Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Android: Remove the dependency on protobuf-lite for tombstones ([#5157](https://github.com/getsentry/sentry-java/pull/5157))
- Support masking/unmasking and click/scroll detection for Jetpack Compose 1.10+ ([#5189](https://github.com/getsentry/sentry-java/pull/5189))

### Features

Expand Down
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 @@ -14,6 +14,13 @@ public final class io/sentry/compose/SentryComposeTracingKt {
public static final fun SentryTraced (Ljava/lang/String;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class io/sentry/compose/SentryLayoutNodeHelper {
public static final field $stable I
public static final field INSTANCE Lio/sentry/compose/SentryLayoutNodeHelper;
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/SentryModifier {
public static final field $stable I
public static final field INSTANCE Lio/sentry/compose/SentryModifier;
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,61 @@
@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 org.jetbrains.annotations.ApiStatus

/**
* 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.
*/
@ApiStatus.Internal
public object SentryLayoutNodeHelper {
@Volatile private var compose110Helper: Compose110Helper? = null
@Volatile private var useCompose110: Boolean? = null

private fun getHelper(): Compose110Helper {
compose110Helper?.let {
return it
}
val helper = Compose110Helper()
compose110Helper = helper
return helper
}

public fun getChildren(node: LayoutNode): List<LayoutNode> {
return if (useCompose110 == false) {
node.children
} else {
try {
getHelper().getChildren(node).also { useCompose110 = true }
} catch (_: NoSuchMethodError) {
useCompose110 = false
node.children
}
}
}

public fun isTransparent(node: LayoutNode): Boolean {
return if (useCompose110 == false) {
node.outerCoordinator.isTransparent()
} else {
try {
getHelper().getOuterCoordinator(node).isTransparent().also { useCompose110 = true }
} catch (_: NoSuchMethodError) {
useCompose110 = false
node.outerCoordinator.isTransparent()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ public class ComposeGestureTargetLocator(private val logger: ILogger) : GestureT

// the last known tag when iterating the node tree
var lastKnownTag: String? = null
var isClickable = false
var isScrollable = false
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

while (!queue.isEmpty()) {
val node = queue.poll() ?: continue
if (node.isPlaced && layoutNodeBoundsContain(rootLayoutNode, node, x, y)) {
var isClickable = false
var isScrollable = false

val modifiers = node.getModifierInfo()
for (index in modifiers.indices) {
Expand Down
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
}
Loading