diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer.kt index c396dde6ce..ac5b1dbab7 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer.kt @@ -50,6 +50,8 @@ internal class Base64Serializer private constructor( applicationContext: Context, displayMetrics: DisplayMetrics, drawable: Drawable, + drawableWidth: Int, + drawableHeight: Int, imageWireframe: MobileSegment.Wireframe.ImageWireframe ) { registerCacheForCallbacks(applicationContext) @@ -73,6 +75,8 @@ internal class Base64Serializer private constructor( } else { drawableUtils.createBitmapOfApproxSizeFromDrawable( drawable, + drawableWidth, + drawableHeight, displayMetrics )?.let { shouldCacheBitmap = true diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelper.kt index 5e3a691e96..f46ee27406 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelper.kt @@ -7,6 +7,9 @@ package com.datadog.android.sessionreplay.internal.recorder.base64 import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable import android.view.View import android.widget.TextView import androidx.annotation.MainThread @@ -37,13 +40,14 @@ internal class ImageWireframeHelper( prefix: String = DRAWABLE_CHILD_NAME ): MobileSegment.Wireframe.ImageWireframe? { val id = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, prefix + currentWireframeIndex) + val (currentDrawable, drawableWidth, drawableHeight) = resolveDrawableProperties(view, drawable) @Suppress("ComplexCondition") if ( - drawable == null || + currentDrawable == null || id == null || - drawable.intrinsicWidth <= 0 || - drawable.intrinsicHeight <= 0 + drawableWidth <= 0 || + drawableHeight <= 0 ) { return null } @@ -69,7 +73,9 @@ internal class ImageWireframeHelper( base64Serializer.handleBitmap( applicationContext = applicationContext, displayMetrics = displayMetrics, - drawable = drawable, + drawable = currentDrawable, + drawableWidth, + drawableHeight, imageWireframe = imageWireframe ) @@ -129,6 +135,17 @@ internal class ImageWireframeHelper( return result } + private fun resolveDrawableProperties(view: View, drawable: Drawable?): DrawableProperties { + if (drawable == null) return DrawableProperties(null, 0, 0) + + return when (drawable) { + is LayerDrawable -> resolveDrawableProperties(view, drawable.getDrawable(0)) + is InsetDrawable -> resolveDrawableProperties(view, drawable.drawable) + is GradientDrawable -> DrawableProperties(drawable, view.width, view.height) + else -> DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight) + } + } + @Suppress("MagicNumber") private fun convertIndexToCompoundDrawablePosition(compoundDrawableIndex: Int): CompoundDrawablePositions? { return when (compoundDrawableIndex) { @@ -147,6 +164,12 @@ internal class ImageWireframeHelper( BOTTOM } + private data class DrawableProperties( + val drawable: Drawable?, + val drawableWidth: Int, + val drawableHeight: Int + ) + internal companion object { @VisibleForTesting internal const val DRAWABLE_CHILD_NAME = "drawable" } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt index 0f7b94da37..1758751fc6 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseWireframeMapper.kt @@ -180,7 +180,7 @@ abstract class BaseWireframeMapper( y = bounds.y, width, height, - view.background, + view.background?.constantState?.newDrawable(), shapeStyle = null, border = null, prefix = PREFIX_BACKGROUND_DRAWABLE diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt index 64aefb51be..90f3e7d805 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtils.kt @@ -36,11 +36,13 @@ internal class DrawableUtils( @Suppress("ReturnCount") internal fun createBitmapOfApproxSizeFromDrawable( drawable: Drawable, + drawableWidth: Int, + drawableHeight: Int, displayMetrics: DisplayMetrics, requestedSizeInBytes: Int = MAX_BITMAP_SIZE_IN_BYTES, config: Config = Config.ARGB_8888 ): Bitmap? { - val (width, height) = getScaledWidthAndHeight(drawable, requestedSizeInBytes) + val (width, height) = getScaledWidthAndHeight(drawableWidth, drawableHeight, requestedSizeInBytes) val bitmap = getBitmapBySize(displayMetrics, width, height, config) ?: return null val canvas = canvasWrapper.createCanvas(bitmap) ?: return null @@ -102,11 +104,12 @@ internal class DrawableUtils( } private fun getScaledWidthAndHeight( - drawable: Drawable, + drawableWidth: Int, + drawableHeight: Int, requestedSizeInBytes: Int ): Pair { - var width = drawable.intrinsicWidth - var height = drawable.intrinsicHeight + var width = drawableWidth + var height = drawableHeight val sizeAfterCreation = width * height * ARGB_8888_PIXEL_SIZE_BYTES if (sizeAfterCreation > requestedSizeInBytes) { diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64SerializerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64SerializerTest.kt index 21534a23ea..cb5ba62cab 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64SerializerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64SerializerTest.kt @@ -120,6 +120,8 @@ internal class Base64SerializerTest { whenever( mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -143,6 +145,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -156,6 +160,8 @@ internal class Base64SerializerTest { whenever( mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -167,6 +173,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -180,6 +188,8 @@ internal class Base64SerializerTest { whenever( mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -191,6 +201,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -209,6 +221,8 @@ internal class Base64SerializerTest { whenever( mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -223,6 +237,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -238,6 +254,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) } @@ -254,6 +272,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) } @@ -272,12 +292,16 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) // Then verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -303,6 +327,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockStateListDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -320,6 +346,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockStateListDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -337,12 +365,16 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) // Then verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -361,12 +393,16 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) // Then verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( drawable = any(), + drawableWidth = any(), + drawableHeight = any(), displayMetrics = any(), requestedSizeInBytes = anyOrNull(), config = anyOrNull() @@ -385,6 +421,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -404,6 +442,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -423,6 +463,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) @@ -440,6 +482,8 @@ internal class Base64SerializerTest { applicationContext = mockApplicationContext, displayMetrics = mockDisplayMetrics, drawable = mockBitmapDrawable, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, imageWireframe = fakeImageWireframe ) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelperTest.kt index 4651b17aab..55ac9b7a7c 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelperTest.kt @@ -9,6 +9,9 @@ package com.datadog.android.sessionreplay.internal.recorder.base64 import android.content.Context import android.content.res.Resources import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.RippleDrawable import android.util.DisplayMetrics import android.view.View import android.widget.TextView @@ -35,6 +38,8 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -371,5 +376,73 @@ internal class ImageWireframeHelperTest { assertThat(wireframes).isEmpty() } + @Test + fun `M resolve view width and height W createImageWireframe() { RippleDrawable }`( + @Mock mockDrawable: RippleDrawable, + @Mock mockInsetDrawable: InsetDrawable, + @Mock mockGradientDrawable: GradientDrawable, + @IntForgery(min = 1) fakeViewWidth: Int, + @IntForgery(min = 1) fakeViewHeight: Int + ) { + // Given + whenever(mockView.width).thenReturn(fakeViewWidth) + whenever(mockView.height).thenReturn(fakeViewHeight) + whenever(mockDrawable.getDrawable(0)).thenReturn(mockInsetDrawable) + whenever(mockInsetDrawable.drawable).thenReturn(mockGradientDrawable) + + // When + testedHelper.createImageWireframe( + view = mockView, + currentWireframeIndex = 0, + x = 0, + y = 0, + width = 0, + height = 0, + drawable = mockDrawable, + shapeStyle = null, + border = null + ) + + // Then + val captor = argumentCaptor() + verify(mockBase64Serializer).handleBitmap( + applicationContext = any(), + displayMetrics = any(), + drawable = any(), + drawableWidth = captor.capture(), + drawableHeight = captor.capture(), + imageWireframe = any() + ) + assertThat(captor.allValues).containsExactly(fakeViewWidth, fakeViewHeight) + } + + @Test + fun `M resolve drawable width and height W createImageWireframe() { TextView }`() { + // When + testedHelper.createImageWireframe( + view = mockView, + currentWireframeIndex = 0, + x = 0, + y = 0, + width = 0, + height = 0, + drawable = mockDrawable, + shapeStyle = null, + border = null + ) + + // Then + val captor = argumentCaptor() + verify(mockBase64Serializer).handleBitmap( + applicationContext = any(), + displayMetrics = any(), + drawable = any(), + drawableWidth = captor.capture(), + drawableHeight = captor.capture(), + imageWireframe = any() + ) + assertThat(captor.allValues).containsExactly(fakeDrawableWidth.toInt(), fakeDrawableHeight.toInt()) + } + // endregion } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt index 0b54aaabb3..8b70338ecc 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/DrawableUtilsTest.kt @@ -100,6 +100,8 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( mockDrawable, + mockDrawable.intrinsicWidth, + mockDrawable.intrinsicHeight, mockDisplayMetrics, requestedSize, mockConfig @@ -134,6 +136,8 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( mockDrawable, + mockDrawable.intrinsicWidth, + mockDrawable.intrinsicHeight, mockDisplayMetrics ) @@ -165,6 +169,8 @@ internal class DrawableUtilsTest { // When val result = testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( mockDrawable, + mockDrawable.intrinsicWidth, + mockDrawable.intrinsicHeight, mockDisplayMetrics, config = mockConfig ) @@ -198,6 +204,8 @@ internal class DrawableUtilsTest { // When val result = testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( mockDrawable, + mockDrawable.intrinsicWidth, + mockDrawable.intrinsicHeight, mockDisplayMetrics, config = mockConfig ) @@ -218,6 +226,8 @@ internal class DrawableUtilsTest { // When val result = testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( mockDrawable, + mockDrawable.intrinsicWidth, + mockDrawable.intrinsicHeight, mockDisplayMetrics, config = mockConfig ) @@ -242,6 +252,8 @@ internal class DrawableUtilsTest { // When val actualBitmap = testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( mockDrawable, + mockDrawable.intrinsicWidth, + mockDrawable.intrinsicHeight, mockDisplayMetrics, config = mockConfig )