diff --git a/detekt_custom.yml b/detekt_custom.yml index ab2327be2d..d4734a89ca 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -129,11 +129,11 @@ datadog: - "android.database.sqlite.SQLiteDatabase.setTransactionSuccessful():java.lang.IllegalStateException" - "android.graphics.Bitmap.compress(android.graphics.Bitmap.CompressFormat, kotlin.Int, java.io.OutputStream):java.lang.NullPointerException,java.lang.IllegalArgumentException" - "android.graphics.Bitmap.copy(android.graphics.Bitmap.Config, kotlin.Boolean):java.lang.IllegalArgumentException" - - "android.graphics.Bitmap.createBitmap(kotlin.Int, kotlin.Int, android.graphics.Bitmap.Config):java.lang.IllegalArgumentException" - "android.graphics.Bitmap.createBitmap(android.util.DisplayMetrics?, kotlin.Int, kotlin.Int, android.graphics.Bitmap.Config):java.lang.IllegalArgumentException" + - "android.graphics.Bitmap.createBitmap(kotlin.Int, kotlin.Int, android.graphics.Bitmap.Config):java.lang.IllegalArgumentException" - "android.graphics.Bitmap.createScaledBitmap(android.graphics.Bitmap, kotlin.Int, kotlin.Int, kotlin.Boolean):java.lang.IllegalArgumentException" - - "android.graphics.Color.parseColor(kotlin.String?):java.lang.IllegalArgumentException" - "android.graphics.Canvas.constructor(android.graphics.Bitmap):java.lang.IllegalStateException" + - "android.graphics.Color.parseColor(kotlin.String?):java.lang.IllegalArgumentException" - "android.graphics.drawable.LayerDrawable.getDrawable(kotlin.Int):java.lang.IndexOutOfBoundsException" - "android.net.ConnectivityManager.registerDefaultNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.IllegalArgumentException,java.lang.SecurityException" - "android.net.ConnectivityManager.unregisterNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.SecurityException" diff --git a/features/dd-sdk-android-session-replay-compose/consumer-rules.pro b/features/dd-sdk-android-session-replay-compose/consumer-rules.pro index 7178866211..a2b99b09de 100644 --- a/features/dd-sdk-android-session-replay-compose/consumer-rules.pro +++ b/features/dd-sdk-android-session-replay-compose/consumer-rules.pro @@ -14,6 +14,15 @@ -keepclassmembers class androidx.compose.foundation.text.modifiers.TextStringSimpleElement { ; } +-keepclassmembers class androidx.compose.material.CheckDrawingCache { + ; +} +-keepclassmembers class androidx.compose.material.CheckboxKt { + ; +} +-keepclassmembers class androidx.compose.ui.draw.DrawBehindElement { + ; +} -keepclassmembers class androidx.compose.foundation.BackgroundElement { ; } diff --git a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapper.kt b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapper.kt index 0d19c303c2..09ebd0a93c 100644 --- a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapper.kt +++ b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapper.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.state.ToggleableState import com.datadog.android.sessionreplay.ImagePrivacy +import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.compose.internal.data.SemanticsWireframe import com.datadog.android.sessionreplay.compose.internal.data.UiContext import com.datadog.android.sessionreplay.compose.internal.utils.PathUtils @@ -33,27 +34,70 @@ internal class CheckboxSemanticsNodeMapper( ): SemanticsWireframe { val globalBounds = resolveBounds(semanticsNode) - val wireframes = if (isCheckboxChecked(semanticsNode)) { - createCheckedWireframes( - parentContext = parentContext, - asyncJobStatusCallback = asyncJobStatusCallback, + val checkableWireframes = if (parentContext.textAndInputPrivacy != TextAndInputPrivacy.MASK_SENSITIVE_INPUTS) { + resolveMaskedCheckable( semanticsNode = semanticsNode, globalBounds = globalBounds ) } else { - createUncheckedWireframes( + // Resolves checkable view regardless the state + resolveCheckable( semanticsNode = semanticsNode, - globalBounds = globalBounds, - backgroundColor = DEFAULT_COLOR_WHITE + parentContext = parentContext, + asyncJobStatusCallback = asyncJobStatusCallback, + globalBounds = globalBounds ) } return SemanticsWireframe( uiContext = null, - wireframes = wireframes + wireframes = checkableWireframes ) } + private fun resolveMaskedCheckable( + semanticsNode: SemanticsNode, + globalBounds: GlobalBounds + ): List { + // TODO RUM-5118: Decide how to display masked checkbox, Currently use old unchecked shape wireframe, + return createUncheckedWireframes( + semanticsNode = semanticsNode, + globalBounds = globalBounds, + backgroundColor = DEFAULT_COLOR_WHITE, + borderColor = DEFAULT_COLOR_BLACK, + currentIndex = 0 + ) + } + + private fun resolveCheckable( + semanticsNode: SemanticsNode, + parentContext: UiContext, + asyncJobStatusCallback: AsyncJobStatusCallback, + globalBounds: GlobalBounds + ): List = + if (isCheckboxChecked(semanticsNode)) { + createCheckedWireframes( + parentContext = parentContext, + asyncJobStatusCallback = asyncJobStatusCallback, + semanticsNode = semanticsNode, + globalBounds = globalBounds + ) + } else { + val borderColor = + semanticsUtils.resolveBorderColor(semanticsNode) + ?.let { rawColor -> + convertColor(rawColor) + } ?: DEFAULT_COLOR_BLACK + + createUncheckedWireframes( + semanticsNode = semanticsNode, + globalBounds = globalBounds, + backgroundColor = DEFAULT_COLOR_WHITE, + borderColor = borderColor, + currentIndex = 0 + ) + } + private fun createCheckedWireframes( parentContext: UiContext, asyncJobStatusCallback: AsyncJobStatusCallback, @@ -119,18 +163,32 @@ internal class CheckboxSemanticsNodeMapper( ): List { val strokeColor = getFallbackCheckmarkColor(backgroundColor) - val background: MobileSegment.Wireframe = createUncheckedWireframes( + val wireframesList = mutableListOf() + var index = 0 + + val borderColor = + semanticsUtils.resolveBorderColor(semanticsNode) + ?.let { rawColor -> + convertColor(rawColor) + } ?: DEFAULT_COLOR_BLACK + + createUncheckedWireframes( semanticsNode = semanticsNode, globalBounds = globalBounds, - backgroundColor = backgroundColor - )[0] + backgroundColor = backgroundColor, + borderColor = borderColor, + currentIndex = 0 + ).firstOrNull()?.let { + wireframesList.add(it) + index++ + } val checkmarkWidth = globalBounds.width * CHECKMARK_SIZE_FACTOR val checkmarkHeight = globalBounds.height * CHECKMARK_SIZE_FACTOR val xPos = globalBounds.x + ((globalBounds.width / 2) - (checkmarkWidth / 2)) val yPos = globalBounds.y + ((globalBounds.height / 2) - (checkmarkHeight / 2)) - val foreground: MobileSegment.Wireframe = MobileSegment.Wireframe.ShapeWireframe( - id = resolveId(semanticsNode, 1), + val foreground = MobileSegment.Wireframe.ShapeWireframe( + id = resolveId(semanticsNode, index), x = xPos.toLong(), y = yPos.toLong(), width = checkmarkWidth.toLong(), @@ -145,23 +203,21 @@ internal class CheckboxSemanticsNodeMapper( width = BOX_BORDER_WIDTH_DP ) ) - return listOf(background, foreground) + + wireframesList.add(foreground) + return wireframesList } private fun createUncheckedWireframes( semanticsNode: SemanticsNode, globalBounds: GlobalBounds, - backgroundColor: String + backgroundColor: String, + borderColor: String, + currentIndex: Int ): List { - val borderColor = - semanticsUtils.resolveBorderColor(semanticsNode) - ?.let { rawColor -> - convertColor(rawColor) - } ?: DEFAULT_COLOR_BLACK - return listOf( MobileSegment.Wireframe.ShapeWireframe( - id = resolveId(semanticsNode, 0), + id = resolveId(semanticsNode, currentIndex), x = globalBounds.x, y = globalBounds.y, width = globalBounds.width, diff --git a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/PathUtils.kt b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/PathUtils.kt index 739a6996d4..c05143eb10 100644 --- a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/PathUtils.kt +++ b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/PathUtils.kt @@ -19,15 +19,16 @@ import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.Chec import com.datadog.android.sessionreplay.recorder.wrappers.BitmapWrapper import com.datadog.android.sessionreplay.recorder.wrappers.CanvasWrapper import java.util.Locale +import android.graphics.Path as AndroidPath internal class PathUtils( private val logger: InternalLogger = InternalLogger.UNBOUND, private val canvasWrapper: CanvasWrapper = CanvasWrapper(logger), private val bitmapWrapper: BitmapWrapper = BitmapWrapper() ) { - @Suppress("UnsafeThirdPartyFunctionCall") // handling IllegalArgumentException internal fun parseColorSafe(color: String): Int? { return try { + @Suppress("UnsafeThirdPartyFunctionCall") // handling IllegalArgumentException Color.parseColor(color) } catch (e: IllegalArgumentException) { logger.log( @@ -56,9 +57,9 @@ internal class PathUtils( return "#$alphaValue$rgbColor" } - @Suppress("UnsafeThirdPartyFunctionCall") // handling UnsupportedOperationException - internal fun asAndroidPathSafe(path: Path): android.graphics.Path? { + internal fun asAndroidPathSafe(path: Path): AndroidPath? { return try { + @Suppress("UnsafeThirdPartyFunctionCall") // handling UnsupportedOperationException path.asAndroidPath() } catch (e: UnsupportedOperationException) { logger.log( @@ -144,28 +145,18 @@ internal class PathUtils( checkmarkColor: Int ): Bitmap? { val canvas = canvasWrapper.createCanvas(bitmap) ?: return null - drawCanvasBackground(canvas, fillColor) - drawCanvasForeground(canvas, scaledPath, checkmarkColor) - return bitmap - } - private fun drawCanvasBackground( - canvas: Canvas, - fillColor: Int - ) { + // draw the background canvas.drawColor(fillColor) - } - private fun drawCanvasForeground( - canvas: Canvas, - path: Path, - checkmarkColor: Int - ) { - drawPathToBitmap(checkmarkColor, path, canvas) + // draw the checkmark + drawPathToBitmap(checkmarkColor, scaledPath, canvas) + + return bitmap } @Suppress("UnsafeThirdPartyFunctionCall") // handling IllegalArgumentException - private fun drawPathSafe(canvas: Canvas?, path: android.graphics.Path, paint: Paint) { + private fun drawPathSafe(canvas: Canvas?, path: AndroidPath, paint: Paint) { try { canvas?.drawPath(path, paint) } catch (e: IllegalArgumentException) { diff --git a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/ReflectionUtils.kt b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/ReflectionUtils.kt index a308acf436..fef00f96af 100644 --- a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/ReflectionUtils.kt +++ b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/ReflectionUtils.kt @@ -7,10 +7,12 @@ package com.datadog.android.sessionreplay.compose.internal.utils import android.view.View +import androidx.compose.animation.core.AnimationState import androidx.compose.runtime.Composition import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.Placeable import androidx.compose.ui.semantics.SemanticsNode @@ -48,6 +50,10 @@ internal class ReflectionUtils { return ComposeReflection.AndroidComposeViewClass?.isInstance(any) == true } + fun isDrawBehindElementClass(modifier: Modifier): Boolean { + return ComposeReflection.DrawBehindElementClass?.isInstance(modifier) == true + } + fun getOwner(composition: Composition): Any? { return ComposeReflection.OwnerField?.getSafe(composition) } @@ -97,4 +103,28 @@ internal class ReflectionUtils { fun getClipShape(modifier: Modifier): Shape? { return ComposeReflection.ClipShapeField?.getSafe(modifier) as? Shape } + + fun getOnDraw(modifier: Modifier): Any? { + return ComposeReflection.OnDrawField?.getSafe(modifier) + } + + fun getBoxColor(onDrawInstance: Any): AnimationState<*, *>? { + return ComposeReflection.BoxColorField?.getSafe(onDrawInstance) as? AnimationState<*, *> + } + + fun getCheckColor(onDrawInstance: Any): AnimationState<*, *>? { + return ComposeReflection.CheckColorField?.getSafe(onDrawInstance) as? AnimationState<*, *> + } + + fun getBorderColor(onDrawInstance: Any): AnimationState<*, *>? { + return ComposeReflection.BorderColorField?.getSafe(onDrawInstance) as? AnimationState<*, *> + } + + fun getCheckCache(onDrawInstance: Any): Any? { + return ComposeReflection.CheckCacheField?.getSafe(onDrawInstance) + } + + fun getCheckPath(checkCache: Any): Path? { + return ComposeReflection.CheckPathField?.getSafe(checkCache) as? Path + } } diff --git a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtils.kt b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtils.kt index 394f28edff..319f5d38ff 100644 --- a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtils.kt +++ b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtils.kt @@ -7,7 +7,6 @@ package com.datadog.android.sessionreplay.compose.internal.utils import android.view.View -import androidx.compose.animation.core.AnimationState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size @@ -21,7 +20,6 @@ import androidx.compose.ui.text.TextLayoutInput import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.unit.Density import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.TextLayoutInfo -import com.datadog.android.sessionreplay.compose.internal.reflection.ComposeReflection import com.datadog.android.sessionreplay.utils.GlobalBounds @Suppress("TooManyFunctions") @@ -108,8 +106,8 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref internal fun resolveCheckPath(semanticsNode: SemanticsNode): Path? = resolveOnDrawInstance(semanticsNode)?.let { onDraw -> - ComposeReflection.CheckCacheField?.getSafe(onDraw)?.let { checkCache -> - ComposeReflection.CheckPathField?.getSafe(checkCache) as? Path + reflectionUtils.getCheckCache(onDraw)?.let { checkCache -> + reflectionUtils.getCheckPath(checkCache) } } @@ -241,32 +239,33 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref private fun resolveOnDrawInstance(semanticsNode: SemanticsNode): Any? { val drawBehindElement = semanticsNode.layoutInfo.getModifierInfo().firstOrNull { modifierInfo -> - ComposeReflection.DrawBehindElementClass?.isInstance(modifierInfo.modifier) == true + reflectionUtils.isDrawBehindElementClass(modifierInfo.modifier) }?.modifier return drawBehindElement?.let { - ComposeReflection.OnDrawField?.getSafe(drawBehindElement) + reflectionUtils.getOnDraw(it) } } private fun resolveReflectedProperty(semanticsNode: SemanticsNode, fieldType: CheckmarkFieldType): Long? { val onDrawInstance = resolveOnDrawInstance(semanticsNode) - val checkmarkColor: AnimationState<*, *>? = onDrawInstance?.let { + val color = onDrawInstance?.let { when (fieldType) { CheckmarkFieldType.FILL_COLOR -> { - ComposeReflection.BoxColorField?.getSafe(onDrawInstance) as? AnimationState<*, *> + reflectionUtils.getBoxColor(onDrawInstance) } CheckmarkFieldType.CHECKMARK_COLOR -> { - ComposeReflection.CheckColorField?.getSafe(onDrawInstance) as? AnimationState<*, *> + reflectionUtils.getCheckColor(onDrawInstance) } CheckmarkFieldType.BORDER_COLOR -> { - ComposeReflection.BorderColorField?.getSafe(onDrawInstance) as? AnimationState<*, *> + reflectionUtils.getBorderColor(onDrawInstance) } } } - val result = (checkmarkColor?.value as? androidx.compose.ui.graphics.Color)?.value + val result = (color?.value as? Color) + ?.value return result?.toLong() } diff --git a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapperTest.kt b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapperTest.kt index a623bd56ba..224d63d577 100644 --- a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/mappers/semantics/CheckboxSemanticsNodeMapperTest.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.state.ToggleableState import com.datadog.android.sessionreplay.ImagePrivacy +import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.compose.internal.data.UiContext import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.BOX_BORDER_WIDTH_DP import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.CHECKBOX_CORNER_RADIUS @@ -42,6 +43,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -107,6 +109,9 @@ internal class CheckboxSemanticsNodeMapperTest : AbstractSemanticsNodeMapperTest whenever(mockUiContext.density) .thenReturn(fakeDensity) + whenever(mockUiContext.textAndInputPrivacy) + .thenReturn(TextAndInputPrivacy.MASK_SENSITIVE_INPUTS) + whenever(mockSemanticsUtils.resolveInnerBounds(mockSemanticsNode)) doReturn rectToBounds( fakeBounds, fakeDensity @@ -284,6 +289,7 @@ internal class CheckboxSemanticsNodeMapperTest : AbstractSemanticsNodeMapperTest parentContext = mockUiContext, asyncJobStatusCallback = mockAsyncJobStatusCallback ) + assertThat(semanticsWireframe.wireframes).hasSize(2) val foregroundWireframe = semanticsWireframe.wireframes[1] as? MobileSegment.Wireframe.ShapeWireframe val expectedShapeStyle = MobileSegment.ShapeStyle( @@ -472,4 +478,37 @@ internal class CheckboxSemanticsNodeMapperTest : AbstractSemanticsNodeMapperTest assertThat(wireframes.wireframes).hasSize(1) assertThat(wireframes.wireframes[0]).isInstanceOf(MobileSegment.Wireframe.ImageWireframe::class.java) } + + @Test + fun `M show unchecked wireframe W map() { masked }`() { + // Given + whenever(mockUiContext.textAndInputPrivacy) + .thenReturn(TextAndInputPrivacy.MASK_ALL_INPUTS) + + // When + val wireframes = testedMapper.map( + semanticsNode = mockSemanticsNode, + parentContext = mockUiContext, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + assertThat(wireframes.wireframes).hasSize(1) + val actualWireframe = wireframes.wireframes[0] as? MobileSegment.Wireframe.ShapeWireframe + assertThat(actualWireframe).isNotNull + + // Then + verify(mockUiContext.imageWireframeHelper, never()).createImageWireframeByBitmap( + id = any(), + globalBounds = any(), + bitmap = any(), + density = any(), + isContextualImage = any(), + imagePrivacy = any(), + asyncJobStatusCallback = any(), + clipping = anyOrNull(), + shapeStyle = anyOrNull(), + border = anyOrNull() + ) + assertThat(actualWireframe?.shapeStyle?.backgroundColor).isEqualTo(DEFAULT_COLOR_WHITE) + assertThat(actualWireframe?.border?.color).isEqualTo(DEFAULT_COLOR_BLACK) + } } diff --git a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtilsTest.kt b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtilsTest.kt index 2211a52074..869374f531 100644 --- a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtilsTest.kt +++ b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/internal/utils/SemanticsUtilsTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.sessionreplay.compose.internal.utils import android.view.View +import androidx.compose.animation.core.AnimationState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composition @@ -15,6 +16,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.LayoutInfo import androidx.compose.ui.layout.ModifierInfo @@ -84,6 +86,12 @@ class SemanticsUtilsTest { @Mock private lateinit var mockModifierInfo: ModifierInfo + @Mock + private lateinit var mockOnDraw: Any + + @Mock + private lateinit var mockCheckCache: Any + @Mock private lateinit var mockModifier: Modifier @@ -103,6 +111,9 @@ class SemanticsUtilsTest { whenever(mockModifierInfo.modifier) doReturn mockModifier whenever(mockLayoutInfo.density) doReturn Density(fakeDensity) whenever(mockSemanticsNode.config) doReturn mockConfig + whenever(mockReflectionUtils.isDrawBehindElementClass(mockModifier)) doReturn true + whenever(mockReflectionUtils.getOnDraw(mockModifier)) doReturn mockOnDraw + whenever(mockReflectionUtils.getCheckCache(mockOnDraw)) doReturn mockCheckCache fakeOffset = Offset(x = forge.aFloat(), y = forge.aFloat()) } @@ -140,6 +151,71 @@ class SemanticsUtilsTest { assertThat(result).isEqualTo(mockShape) } + @Test + fun `M return check path W resolveCheckPath`( + @Mock mockPath: Path + ) { + // Given + whenever(mockReflectionUtils.getCheckPath(mockCheckCache)) doReturn mockPath + + // When + val result = testedSemanticsUtils.resolveCheckPath(mockSemanticsNode) + + // Then + assertThat(result).isEqualTo(mockPath) + } + + @Test + fun `M return checkbox fill color W resolveCheckboxFillColor`( + @IntForgery fakeColorValue: Int + ) { + // Given + val fakeColor = Color(fakeColorValue) + val mockAnimationState = mock>() + whenever(mockReflectionUtils.getBoxColor(mockOnDraw)) doReturn mockAnimationState + whenever(mockAnimationState.value).thenReturn(fakeColor) + + // When + val result = testedSemanticsUtils.resolveCheckboxFillColor(mockSemanticsNode) + + // Then + assertThat(result).isEqualTo(fakeColor.value.toLong()) + } + + @Test + fun `M return checkmark color W resolveCheckmarkColor`( + @IntForgery fakeColorValue: Int + ) { + // Given + val fakeColor = Color(fakeColorValue) + val mockAnimationState = mock>() + whenever(mockReflectionUtils.getCheckColor(mockOnDraw)) doReturn mockAnimationState + whenever(mockAnimationState.value).thenReturn(fakeColor) + + // When + val result = testedSemanticsUtils.resolveCheckmarkColor(mockSemanticsNode) + + // Then + assertThat(result).isEqualTo(fakeColor.value.toLong()) + } + + @Test + fun `M return border color W resolveBorderColor`( + @IntForgery fakeColorValue: Int + ) { + // Given + val fakeColor = Color(fakeColorValue) + val mockAnimationState = mock>() + whenever(mockReflectionUtils.getBorderColor(mockOnDraw)) doReturn mockAnimationState + whenever(mockAnimationState.value).thenReturn(fakeColor) + + // When + val result = testedSemanticsUtils.resolveBorderColor(mockSemanticsNode) + + // Then + assertThat(result).isEqualTo(fakeColor.value.toLong()) + } + @Test fun `M return inner bounds W resolveInnerBounds`() { // Given diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/BitmapWrapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/BitmapWrapper.kt index 687c0bf716..0503935b22 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/BitmapWrapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/BitmapWrapper.kt @@ -10,10 +10,12 @@ import android.graphics.Bitmap import android.graphics.Bitmap.Config import android.util.DisplayMetrics import com.datadog.android.api.InternalLogger +import com.datadog.android.lint.InternalApi /** * Wraps the Bitmap class to catch potential crashes. */ +@InternalApi class BitmapWrapper( private val logger: InternalLogger = InternalLogger.UNBOUND ) { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/CanvasWrapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/CanvasWrapper.kt index 3bd929e619..bcde43faa1 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/CanvasWrapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/wrappers/CanvasWrapper.kt @@ -9,10 +9,12 @@ package com.datadog.android.sessionreplay.recorder.wrappers import android.graphics.Bitmap import android.graphics.Canvas import com.datadog.android.api.InternalLogger +import com.datadog.android.lint.InternalApi /** * Wraps the Canvas class to catch potential crashes. */ +@InternalApi class CanvasWrapper( private val logger: InternalLogger ) {