Skip to content

Commit 0eb04f5

Browse files
committed
fix(replay): Mask read-only TextField Composables (#4362)
Newer versions of Compose seem to skip adding a SetText semantic entirely when a TextField is readOnly = true or enabled = false, so we fallback to the EditableText semantic which seems to be always present.
1 parent 5546e1e commit 0eb04f5

File tree

3 files changed

+28
-2
lines changed

3 files changed

+28
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Session Replay: Do not capture current replay for cached events from the past ([#4474](https://github.com/getsentry/sentry-java/pull/4474))
1515
- Session Replay: Fix crash on devices with the Unisoc/Spreadtrum T606 chipset ([#4477](https://github.com/getsentry/sentry-java/pull/4477))
1616
- Session Replay: Fix masking of non-styled `Text` Composables ([#4361](https://github.com/getsentry/sentry-java/pull/4361))
17+
- Session Replay: Fix masking read-only `TextField` Composables ([#4362](https://github.com/getsentry/sentry-java/pull/4362))
1718

1819
## 7.22.5
1920

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ internal object ComposeViewHierarchyNode {
4141
return when {
4242
isImage -> SentryReplayOptions.IMAGE_VIEW_CLASS_NAME
4343
collapsedSemantics?.contains(SemanticsProperties.Text) == true ||
44-
collapsedSemantics?.contains(SemanticsActions.SetText) == true -> SentryReplayOptions.TEXT_VIEW_CLASS_NAME
44+
collapsedSemantics?.contains(SemanticsActions.SetText) == true ||
45+
collapsedSemantics?.contains(SemanticsProperties.EditableText) == true -> SentryReplayOptions.TEXT_VIEW_CLASS_NAME
4546
else -> "android.view.View"
4647
}
4748
}
@@ -87,7 +88,8 @@ internal object ComposeViewHierarchyNode {
8788
val isVisible = !node.outerCoordinator.isTransparent() &&
8889
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
8990
visibleRect.height() > 0 && visibleRect.width() > 0
90-
val isEditable = semantics?.contains(SemanticsActions.SetText) == true
91+
val isEditable = semantics?.contains(SemanticsActions.SetText) == true ||
92+
semantics?.contains(SemanticsProperties.EditableText) == true
9193
return when {
9294
semantics?.contains(SemanticsProperties.Text) == true || isEditable -> {
9395
val shouldMask = isVisible && node.shouldMask(isImage = false, options)

sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import androidx.compose.material3.TextField
1515
import androidx.compose.ui.Alignment
1616
import androidx.compose.ui.Modifier
1717
import androidx.compose.ui.platform.testTag
18+
import androidx.compose.ui.semantics.clearAndSetSemantics
19+
import androidx.compose.ui.semantics.editableText
1820
import androidx.compose.ui.semantics.invisibleToUser
1921
import androidx.compose.ui.semantics.semantics
22+
import androidx.compose.ui.text.AnnotatedString
2023
import androidx.compose.ui.text.input.TextFieldValue
2124
import androidx.compose.ui.unit.TextUnit
2225
import androidx.compose.ui.unit.dp
@@ -52,6 +55,7 @@ class ComposeMaskingOptionsTest {
5255
fun setup() {
5356
System.setProperty("robolectric.areWindowsMarkedVisible", "true")
5457
ComposeMaskingOptionsActivity.textModifierApplier = null
58+
ComposeMaskingOptionsActivity.textFieldModifierApplier = null
5559
ComposeMaskingOptionsActivity.containerModifierApplier = null
5660
ComposeMaskingOptionsActivity.fontSizeApplier = null
5761
}
@@ -86,6 +90,23 @@ class ComposeMaskingOptionsTest {
8690
assertEquals("Random repo", (textNodes.first().layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text)
8791
}
8892

93+
@Test
94+
fun `when text input field is readOnly still masks it`() {
95+
ComposeMaskingOptionsActivity.textFieldModifierApplier = {
96+
// newer versions of compose basically do this when a TextField is readOnly
97+
Modifier.clearAndSetSemantics { editableText = AnnotatedString("Placeholder") }
98+
}
99+
val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup()
100+
shadowOf(Looper.getMainLooper()).idle()
101+
102+
val options = SentryOptions().apply {
103+
sessionReplay.maskAllText = true
104+
}
105+
106+
val textNodes = activity.get().collectNodesOfType<TextViewHierarchyNode>(options)
107+
assertTrue(textNodes[1].shouldMask)
108+
}
109+
89110
@Test
90111
fun `when maskAllText is set to false all Text nodes are unmasked`() {
91112
val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup()
@@ -220,6 +241,7 @@ private class ComposeMaskingOptionsActivity : ComponentActivity() {
220241

221242
companion object {
222243
var textModifierApplier: (() -> Modifier)? = null
244+
var textFieldModifierApplier: (() -> Modifier)? = null
223245
var containerModifierApplier: (() -> Modifier)? = null
224246
var fontSizeApplier: (() -> TextUnit)? = null
225247
}
@@ -243,6 +265,7 @@ private class ComposeMaskingOptionsActivity : ComponentActivity() {
243265
)
244266
Text("Random repo", fontSize = fontSizeApplier?.invoke() ?: TextUnit.Unspecified)
245267
TextField(
268+
modifier = textFieldModifierApplier?.invoke() ?: Modifier,
246269
value = TextFieldValue("Placeholder"),
247270
onValueChange = { _ -> }
248271
)

0 commit comments

Comments
 (0)