Skip to content

Commit ac09909

Browse files
authored
Merge ceb7654 into b936425
2 parents b936425 + ceb7654 commit ac09909

3 files changed

Lines changed: 41 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Session Replay: Fix `VerifyError` in Compose masking under DexGuard/R8 obfuscation ([#5507](https://github.com/getsentry/sentry-java/pull/5507))
8+
39
## 8.43.1
410

511
### Fixes

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
package io.sentry.android.replay.util
44

5-
import android.graphics.Rect
65
import androidx.compose.ui.geometry.Offset
6+
import androidx.compose.ui.geometry.Rect
77
import androidx.compose.ui.graphics.Color
88
import androidx.compose.ui.graphics.ColorProducer
99
import androidx.compose.ui.graphics.painter.Painter
1010
import androidx.compose.ui.layout.LayoutCoordinates
1111
import androidx.compose.ui.layout.findRootCoordinates
1212
import androidx.compose.ui.node.LayoutNode
1313
import androidx.compose.ui.text.TextLayoutResult
14+
import kotlin.math.ceil
15+
import kotlin.math.floor
1416
import kotlin.math.roundToInt
1517

1618
internal class ComposeTextLayout(internal val layout: TextLayoutResult) : TextLayout {
@@ -176,7 +178,7 @@ internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates
176178
val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)
177179

178180
if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
179-
return Rect()
181+
return Rect(0.0f, 0.0f, 0.0f, 0.0f)
180182
}
181183

182184
val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
@@ -200,5 +202,18 @@ internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates
200202
val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
201203
val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
202204

203-
return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
205+
return Rect(left, top, right, bottom)
206+
}
207+
208+
internal fun Rect.toRect(): android.graphics.Rect {
209+
// Round outward (floor min edges, ceil max edges) so that a sub-pixel but non-empty Rect doesn't
210+
// collapse to a zero-width/height android.graphics.Rect. Otherwise a node could be marked visible
211+
// and maskable based on the float bounds, while the integer rect the MaskRenderer draws has zero
212+
// area, leaving sensitive content unmasked. Rounding outward also biases toward over-masking.
213+
return android.graphics.Rect(
214+
floor(left).toInt(),
215+
floor(top).toInt(),
216+
ceil(right).toInt(),
217+
ceil(bottom).toInt(),
218+
)
204219
}

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

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.sentry.android.replay.util.findPainter
2727
import io.sentry.android.replay.util.findTextColor
2828
import io.sentry.android.replay.util.isMaskable
2929
import io.sentry.android.replay.util.toOpaque
30+
import io.sentry.android.replay.util.toRect
3031
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
3132
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
3233
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
@@ -150,8 +151,8 @@ internal object ComposeViewHierarchyNode {
150151
// If we're unable to retrieve the semantics configuration
151152
// we should play safe and mask the whole node.
152153
return GenericViewHierarchyNode(
153-
x = visibleRect.left.toFloat(),
154-
y = visibleRect.top.toFloat(),
154+
x = visibleRect.left,
155+
y = visibleRect.top,
155156
width = node.width,
156157
height = node.height,
157158
elevation = (parent?.elevation ?: 0f),
@@ -161,17 +162,17 @@ internal object ComposeViewHierarchyNode {
161162
isImportantForContentCapture = false, // will be set by children
162163
isVisible =
163164
!SentryLayoutNodeHelper.isTransparent(node) &&
164-
visibleRect.height() > 0 &&
165-
visibleRect.width() > 0,
166-
visibleRect = visibleRect,
165+
visibleRect.height > 0 &&
166+
visibleRect.width > 0,
167+
visibleRect = visibleRect.toRect(),
167168
)
168169
}
169170

170171
val isVisible =
171172
!SentryLayoutNodeHelper.isTransparent(node) &&
172173
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
173-
visibleRect.height() > 0 &&
174-
visibleRect.width() > 0
174+
visibleRect.height > 0 &&
175+
visibleRect.width > 0
175176
val isEditable =
176177
semantics?.contains(SemanticsActions.SetText) == true ||
177178
semantics?.contains(SemanticsProperties.EditableText) == true
@@ -206,8 +207,8 @@ internal object ComposeViewHierarchyNode {
206207
null
207208
},
208209
dominantColor = textColor?.toArgb()?.toOpaque(),
209-
x = visibleRect.left.toFloat(),
210-
y = visibleRect.top.toFloat(),
210+
x = visibleRect.left,
211+
y = visibleRect.top,
211212
width = node.width,
212213
height = node.height,
213214
elevation = (parent?.elevation ?: 0f),
@@ -216,7 +217,7 @@ internal object ComposeViewHierarchyNode {
216217
shouldMask = shouldMask,
217218
isImportantForContentCapture = true,
218219
isVisible = isVisible,
219-
visibleRect = visibleRect,
220+
visibleRect = visibleRect.toRect(),
220221
)
221222
}
222223
else -> {
@@ -226,8 +227,8 @@ internal object ComposeViewHierarchyNode {
226227

227228
parent?.setImportantForCaptureToAncestors(true)
228229
ImageViewHierarchyNode(
229-
x = visibleRect.left.toFloat(),
230-
y = visibleRect.top.toFloat(),
230+
x = visibleRect.left,
231+
y = visibleRect.top,
231232
width = node.width,
232233
height = node.height,
233234
elevation = (parent?.elevation ?: 0f),
@@ -236,7 +237,7 @@ internal object ComposeViewHierarchyNode {
236237
isVisible = isVisible,
237238
isImportantForContentCapture = true,
238239
shouldMask = shouldMask && painter.isMaskable(),
239-
visibleRect = visibleRect,
240+
visibleRect = visibleRect.toRect(),
240241
)
241242
} else {
242243
val shouldMask = isVisible && semantics.shouldMask(isImage = false, options)
@@ -245,8 +246,8 @@ internal object ComposeViewHierarchyNode {
245246
// TODO: traverse the ViewHierarchyNode here again. For now we can recommend
246247
// TODO: using custom modifiers to obscure the entire node if it's sensitive
247248
GenericViewHierarchyNode(
248-
x = visibleRect.left.toFloat(),
249-
y = visibleRect.top.toFloat(),
249+
x = visibleRect.left,
250+
y = visibleRect.top,
250251
width = node.width,
251252
height = node.height,
252253
elevation = (parent?.elevation ?: 0f),
@@ -255,7 +256,7 @@ internal object ComposeViewHierarchyNode {
255256
shouldMask = shouldMask,
256257
isImportantForContentCapture = false, // will be set by children
257258
isVisible = isVisible,
258-
visibleRect = visibleRect,
259+
visibleRect = visibleRect.toRect(),
259260
)
260261
}
261262
}

0 commit comments

Comments
 (0)