Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUM-7631: Display captured text when the text has Ellipsis overflow #2446

Merged
merged 1 commit into from
Dec 11, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,12 @@
-keep class androidx.compose.ui.graphics.GraphicsLayerElement{
<fields>;
}

-keep class androidx.compose.ui.text.ParagraphInfo{
<fields>;
}
-keep class androidx.compose.ui.text.AndroidParagraph{
<fields>;
}
-keep class androidx.compose.ui.text.android.TextLayout{
<fields>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ internal open class TextSemanticsNodeMapper(
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): SemanticsWireframe {
val wireframes = mutableListOf<MobileSegment.Wireframe>()
val textAndInputPrivacy = semanticsUtils.getTextAndInputPrivacyOverride(semanticsNode)
?: parentContext.textAndInputPrivacy
val textWireframe = resolveTextWireFrame(parentContext, semanticsNode, textAndInputPrivacy)
val backgroundWireframes = resolveModifierWireframes(semanticsNode)
wireframes.addAll(backgroundWireframes)
textWireframe?.let {
wireframes.add(it)
}
return SemanticsWireframe(
wireframes = listOfNotNull(textWireframe),
wireframes = wireframes.toList(),
parentContext.copy(textAndInputPrivacy = textAndInputPrivacy)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.datadog.android.sessionreplay.compose.internal.reflection

import androidx.compose.ui.text.MultiParagraph
import com.datadog.android.Datadog
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.feature.FeatureSdkCore
Expand Down Expand Up @@ -75,6 +76,16 @@ internal object ComposeReflection {

val AsyncImagePainterClass = getClassSafe("coil.compose.AsyncImagePainter")
val PainterFieldOfAsyncImagePainter = AsyncImagePainterClass?.getDeclaredFieldSafe("_painter")

// Region of MultiParagraph text
val ParagraphInfoListField =
MultiParagraph::class.java.getDeclaredFieldSafe("paragraphInfoList")
val ParagraphInfoClass = getClassSafe("androidx.compose.ui.text.ParagraphInfo")
val ParagraphField = ParagraphInfoClass?.getDeclaredFieldSafe("paragraph")
val AndroidParagraphClass = getClassSafe("androidx.compose.ui.text.AndroidParagraph")
val LayoutField = AndroidParagraphClass?.getDeclaredFieldSafe("layout")
val TextLayoutClass = getClassSafe("androidx.compose.ui.text.android.TextLayout")
val StaticLayoutField = TextLayoutClass?.getDeclaredFieldSafe("layout")
}

internal fun Field.accessible(): Field {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.android.sessionreplay.compose.internal.utils

import android.graphics.Bitmap
import android.text.StaticLayout
import android.view.View
import androidx.compose.runtime.Composition
import androidx.compose.ui.Modifier
Expand All @@ -19,19 +20,22 @@ import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.MultiParagraph
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.BitmapField
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.CompositionField
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ContentPainterElementClass
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ContentPainterModifierClass
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.GetInnerLayerCoordinatorMethod
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.ImageField
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.LayoutField
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.LayoutNodeField
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterElementClass
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterField
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfAsyncImagePainter
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfContentPainterElement
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.PainterFieldOfContentPainterModifier
import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection.StaticLayoutField
import com.datadog.android.sessionreplay.compose.internal.reflection.getSafe

@Suppress("TooManyFunctions")
Expand Down Expand Up @@ -153,4 +157,13 @@ internal class ReflectionUtils {
fun getNestedPainter(painter: Painter): Painter? {
return PainterFieldOfAsyncImagePainter?.getSafe(painter) as? Painter
}

fun getMultiParagraphCapturedText(multiParagraph: MultiParagraph): String? {
val infoList = ComposeReflection.ParagraphInfoListField?.getSafe(multiParagraph) as? List<*>
val paragraphInfo = infoList?.firstOrNull()
val paragraph = ComposeReflection.ParagraphField?.getSafe(paragraphInfo)
val layout = LayoutField?.getSafe(paragraph)
val staticLayout = StaticLayoutField?.getSafe(layout) as? StaticLayout
return staticLayout?.text?.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,15 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref
semanticsNode.config.getOrNull(SemanticsActions.GetTextLayoutResult)?.action?.invoke(
textLayoutResults
)
val layoutInput = textLayoutResults.firstOrNull()?.layoutInput
val modifierColor = resolveModifierColor(semanticsNode)
return layoutInput?.let {
convertTextLayoutInfo(it, modifierColor)
return textLayoutResults.firstOrNull()?.let { textLayoutResult ->
val layoutInput = textLayoutResult.layoutInput
val multiParagraphCapturedText = if (textLayoutResult.didOverflowHeight) {
reflectionUtils.getMultiParagraphCapturedText(textLayoutResult.multiParagraph)
} else {
null
}
val modifierColor = resolveModifierColor(semanticsNode)
convertTextLayoutInfo(layoutInput, multiParagraphCapturedText, modifierColor)
}
}

Expand Down Expand Up @@ -240,10 +245,11 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref

private fun convertTextLayoutInfo(
layoutInput: TextLayoutInput,
multiParagraphCapturedText: String?,
modifierColor: Color?
): TextLayoutInfo {
return TextLayoutInfo(
text = resolveAnnotatedString(layoutInput.text),
text = multiParagraphCapturedText ?: resolveAnnotatedString(layoutInput.text),
color = modifierColor?.value ?: layoutInput.style.color.value,
textAlign = layoutInput.style.textAlign,
fontSize = layoutInput.style.fontSize.value.toLong(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.MultiParagraph
import androidx.compose.ui.text.TextLayoutInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
Expand Down Expand Up @@ -280,8 +281,7 @@ class SemanticsUtilsTest {
whenever(mockResult.action) doReturn mockAction
whenever(textLayoutResult.layoutInput) doReturn mockTextLayoutInput
doAnswer { invocation ->
@Suppress("UNCHECKED_CAST")
(invocation.arguments[0] as MutableList<TextLayoutResult>).add(textLayoutResult)
invocation.getArgument<MutableList<TextLayoutResult>>(0).add(textLayoutResult)
true
}.whenever(mockAction).invoke(textLayoutResults)
whenever(mockTextLayoutInput.style) doReturn mockTextStyle
Expand Down Expand Up @@ -309,6 +309,64 @@ class SemanticsUtilsTest {
assertThat(result).isEqualTo(expected)
}

@Test
fun `M return TextLayoutInfo W resolveTextLayoutInfo with text overflow`(forge: Forge) {
// Given
val fakeText = AnnotatedString(forge.aString())
val fakeCapturedText = forge.aString()
val fakeColorValue = forge.aLong().toULong()
val fakeModifierColorValue = forge.aLong().toULong()
val fakeFontSize = forge.aFloat()
val fakeFontFamily = forge.anElementFrom(
FontFamily.Serif,
FontFamily.SansSerif,
FontFamily.Cursive,
FontFamily.Monospace,
FontFamily.Default
)
val fakeTextAlign = forge.anElementFrom(TextAlign.values())
val mockResult = mock<AccessibilityAction<(MutableList<TextLayoutResult>) -> Boolean>>()
val mockAction = mock<(MutableList<TextLayoutResult>) -> Boolean>()
val textLayoutResult = mock<TextLayoutResult>()
val textLayoutResults = mutableListOf<TextLayoutResult>()
val mockTextLayoutInput = mock<TextLayoutInput>()
val mockTextStyle = mock<TextStyle>()
val mockMultiParagraph = mock<MultiParagraph>()
whenever(mockConfig.getOrNull(SemanticsActions.GetTextLayoutResult)) doReturn mockResult
whenever(mockResult.action) doReturn mockAction
whenever(textLayoutResult.layoutInput) doReturn mockTextLayoutInput
whenever(textLayoutResult.didOverflowHeight) doReturn true
whenever(textLayoutResult.multiParagraph) doReturn mockMultiParagraph
doAnswer { invocation ->
invocation.getArgument<MutableList<TextLayoutResult>>(0).add(textLayoutResult)
true
}.whenever(mockAction).invoke(textLayoutResults)
whenever(mockTextLayoutInput.style) doReturn mockTextStyle
whenever(mockTextLayoutInput.text) doReturn fakeText
whenever(mockTextStyle.color) doReturn Color(fakeColorValue)
whenever(mockTextStyle.textAlign) doReturn fakeTextAlign
whenever(mockTextStyle.fontSize) doReturn TextUnit(fakeFontSize, TextUnitType.Sp)
whenever(mockTextStyle.fontFamily) doReturn fakeFontFamily
whenever(mockReflectionUtils.isTextStringSimpleElement(mockModifier)) doReturn true
whenever(mockReflectionUtils.getColorProducerColor(mockModifier)) doReturn Color(
fakeModifierColorValue
)
whenever(mockReflectionUtils.getMultiParagraphCapturedText(mockMultiParagraph)) doReturn fakeCapturedText

// When
val result = testedSemanticsUtils.resolveTextLayoutInfo(mockSemanticsNode)

// Then
val expected = TextLayoutInfo(
text = fakeCapturedText,
color = fakeModifierColorValue,
textAlign = fakeTextAlign,
fontSize = fakeFontSize.toLong(),
fontFamily = fakeFontFamily
)
assertThat(result).isEqualTo(expected)
}

@Test
fun `M return backgroundInfo W resolveBackgroundInfo`(
forge: Forge,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@

package com.datadog.android.sample.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Suppress("LongMethod")
@Composable
internal fun TypographySample() {
Column {
Expand Down Expand Up @@ -56,12 +62,58 @@ internal fun TypographySample() {
),
modifier = Modifier.padding(16.dp)
)
Text(
text = FAKE_LONG_TEXT,
style = MaterialTheme.typography.caption.copy(
color = Color.Black
),
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.height(60.dp)
.fillMaxWidth()
.background(color = Color.Yellow)
.padding(16.dp)
)
Row {
Text(
text = FAKE_LONG_TEXT,
style = MaterialTheme.typography.caption.copy(
color = Color.Black
),
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.weight(1f)
.background(Color.Red)
.padding(16.dp),
maxLines = 1
)
Text(
text = FAKE_LONG_TEXT,
style = MaterialTheme.typography.caption.copy(
color = Color.Black
),
overflow = TextOverflow.Clip,
modifier = Modifier
.weight(1f)
.background(Color.Green)
.padding(16.dp),
maxLines = 1
)
}
}
}

@Preview
@Preview(showBackground = true)
@Composable
@Suppress("UnusedPrivateMember")
private fun PreviewTypographySample() {
TypographySample()
}

private const val FAKE_LONG_TEXT =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque posuere arcu eget est " +
"interdum, ac eleifend ex laoreet. Integer fringilla eros sed velit dapibus, sit" +
" amet egestas urna faucibus. Suspendisse potenti. Nulla facilisi. Proin sagittis " +
"eros eu nulla fringilla, quis consequat sapien tempus. Sed vel feugiat leo. Etiam " +
"id ultricies odio. Donec luctus sem vel magna consequat auctor. Fusce bibendum mi" +
" sed sapien faucibus, id scelerisque ligula hendrerit."