diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c0b8bab5..d80037414d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Session Replay: Fix `VerifyError` in Compose masking under DexGuard/R8 obfuscation ([#5507](https://github.com/getsentry/sentry-java/pull/5507)) - Session Replay: Fix Compose view masking not working on obfuscated/minified builds ([#5503](https://github.com/getsentry/sentry-java/pull/5503)) ## 8.43.1 diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt index 2882b2113b..028f681d96 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt @@ -2,8 +2,8 @@ package io.sentry.android.replay.util -import android.graphics.Rect import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorProducer import androidx.compose.ui.graphics.painter.Painter @@ -11,6 +11,8 @@ import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.text.TextLayoutResult +import kotlin.math.ceil +import kotlin.math.floor import kotlin.math.roundToInt internal class ComposeTextLayout(internal val layout: TextLayoutResult) : TextLayout { @@ -176,7 +178,7 @@ internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight) if (boundsLeft == boundsRight || boundsTop == boundsBottom) { - return Rect() + return Rect(0.0f, 0.0f, 0.0f, 0.0f) } val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop)) @@ -200,5 +202,18 @@ internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY) val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY) - return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) + return Rect(left, top, right, bottom) +} + +internal fun Rect.toRect(): android.graphics.Rect { + // Round outward (floor min edges, ceil max edges) so that a sub-pixel but non-empty Rect doesn't + // collapse to a zero-width/height android.graphics.Rect. Otherwise a node could be marked visible + // and maskable based on the float bounds, while the integer rect the MaskRenderer draws has zero + // area, leaving sensitive content unmasked. Rounding outward also biases toward over-masking. + return android.graphics.Rect( + floor(left).toInt(), + floor(top).toInt(), + ceil(right).toInt(), + ceil(bottom).toInt(), + ) } 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 2b6bc3fc08..2e40144e2d 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 @@ -28,6 +28,7 @@ import io.sentry.android.replay.util.findPainter import io.sentry.android.replay.util.findTextColor import io.sentry.android.replay.util.isMaskable import io.sentry.android.replay.util.toOpaque +import io.sentry.android.replay.util.toRect import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode @@ -157,8 +158,8 @@ internal object ComposeViewHierarchyNode { // If we're unable to retrieve the semantics configuration // we should play safe and mask the whole node. return GenericViewHierarchyNode( - x = visibleRect.left.toFloat(), - y = visibleRect.top.toFloat(), + x = visibleRect.left, + y = visibleRect.top, width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), @@ -168,17 +169,17 @@ internal object ComposeViewHierarchyNode { isImportantForContentCapture = false, // will be set by children isVisible = !SentryLayoutNodeHelper.isTransparent(node) && - visibleRect.height() > 0 && - visibleRect.width() > 0, - visibleRect = visibleRect, + visibleRect.height > 0 && + visibleRect.width > 0, + visibleRect = visibleRect.toRect(), ) } val isVisible = !SentryLayoutNodeHelper.isTransparent(node) && (semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) && - visibleRect.height() > 0 && - visibleRect.width() > 0 + visibleRect.height > 0 && + visibleRect.width > 0 val isEditable = semantics?.contains(SemanticsActions.SetText) == true || semantics?.contains(SemanticsProperties.EditableText) == true @@ -213,8 +214,8 @@ internal object ComposeViewHierarchyNode { null }, dominantColor = textColor?.toArgb()?.toOpaque(), - x = visibleRect.left.toFloat(), - y = visibleRect.top.toFloat(), + x = visibleRect.left, + y = visibleRect.top, width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), @@ -223,7 +224,7 @@ internal object ComposeViewHierarchyNode { shouldMask = shouldMask, isImportantForContentCapture = true, isVisible = isVisible, - visibleRect = visibleRect, + visibleRect = visibleRect.toRect(), ) } else -> { @@ -233,8 +234,8 @@ internal object ComposeViewHierarchyNode { parent?.setImportantForCaptureToAncestors(true) ImageViewHierarchyNode( - x = visibleRect.left.toFloat(), - y = visibleRect.top.toFloat(), + x = visibleRect.left, + y = visibleRect.top, width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), @@ -243,7 +244,7 @@ internal object ComposeViewHierarchyNode { isVisible = isVisible, isImportantForContentCapture = true, shouldMask = shouldMask && painter.isMaskable(), - visibleRect = visibleRect, + visibleRect = visibleRect.toRect(), ) } else { val shouldMask = isVisible && semantics.shouldMask(isImage = false, options) @@ -252,8 +253,8 @@ internal object ComposeViewHierarchyNode { // TODO: traverse the ViewHierarchyNode here again. For now we can recommend // TODO: using custom modifiers to obscure the entire node if it's sensitive GenericViewHierarchyNode( - x = visibleRect.left.toFloat(), - y = visibleRect.top.toFloat(), + x = visibleRect.left, + y = visibleRect.top, width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), @@ -262,7 +263,7 @@ internal object ComposeViewHierarchyNode { shouldMask = shouldMask, isImportantForContentCapture = false, // will be set by children isVisible = isVisible, - visibleRect = visibleRect, + visibleRect = visibleRect.toRect(), ) } }