diff --git a/dd-sdk-android-internal/api/apiSurface b/dd-sdk-android-internal/api/apiSurface index f80629999a..c258e96a2e 100644 --- a/dd-sdk-android-internal/api/apiSurface +++ b/dd-sdk-android-internal/api/apiSurface @@ -29,6 +29,12 @@ sealed class com.datadog.android.internal.telemetry.InternalTelemetryEvent constructor(Boolean, Boolean, Boolean, MutableMap = mutableMapOf()) object InterceptorInstantiated : InternalTelemetryEvent fun ByteArray.toHexString(): String +object com.datadog.android.internal.utils.ImageViewUtils + fun resolveParentRectAbsPosition(android.view.View): android.graphics.Rect + fun calculateClipping(android.graphics.Rect, android.graphics.Rect, Float): android.graphics.Rect + fun resolveContentRectWithScaling(android.widget.ImageView, android.graphics.drawable.Drawable, android.widget.ImageView.ScaleType? = null): android.graphics.Rect +fun Int.densityNormalized(Float): Int +fun Long.densityNormalized(Float): Long fun Throwable.loggableStackTrace(): String annotation com.datadog.tools.annotation.NoOpImplementation constructor(Boolean = false) diff --git a/dd-sdk-android-internal/api/dd-sdk-android-internal.api b/dd-sdk-android-internal/api/dd-sdk-android-internal.api index a6ae5b9963..bb200c41a6 100644 --- a/dd-sdk-android-internal/api/dd-sdk-android-internal.api +++ b/dd-sdk-android-internal/api/dd-sdk-android-internal.api @@ -108,6 +108,22 @@ public final class com/datadog/android/internal/utils/ByteArrayExtKt { public static final fun toHexString ([B)Ljava/lang/String; } +public final class com/datadog/android/internal/utils/ImageViewUtils { + public static final field INSTANCE Lcom/datadog/android/internal/utils/ImageViewUtils; + public final fun calculateClipping (Landroid/graphics/Rect;Landroid/graphics/Rect;F)Landroid/graphics/Rect; + public final fun resolveContentRectWithScaling (Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;Landroid/widget/ImageView$ScaleType;)Landroid/graphics/Rect; + public static synthetic fun resolveContentRectWithScaling$default (Lcom/datadog/android/internal/utils/ImageViewUtils;Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;Landroid/widget/ImageView$ScaleType;ILjava/lang/Object;)Landroid/graphics/Rect; + public final fun resolveParentRectAbsPosition (Landroid/view/View;)Landroid/graphics/Rect; +} + +public final class com/datadog/android/internal/utils/IntExtKt { + public static final fun densityNormalized (IF)I +} + +public final class com/datadog/android/internal/utils/LongExtKt { + public static final fun densityNormalized (JF)J +} + public final class com/datadog/android/internal/utils/ThrowableExtKt { public static final fun loggableStackTrace (Ljava/lang/Throwable;)Ljava/lang/String; } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtils.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ImageViewUtils.kt similarity index 82% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtils.kt rename to dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ImageViewUtils.kt index d5ca9d6b26..cf3f148758 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtils.kt +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/ImageViewUtils.kt @@ -4,17 +4,25 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal.utils +package com.datadog.android.internal.utils import android.graphics.Rect import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.model.MobileSegment -internal object ImageViewUtils { - internal fun resolveParentRectAbsPosition(view: View): Rect { +/** + * A collection of view utility functions for resolving absolute + * positions, clipping bounds, and other useful data for + * image views operations. + */ +object ImageViewUtils { + /** + * Resolves the absolute position on the screen of the given [View]. + * @param view the [View]. + * @return the [Rect] representing the absolute position of the view. + */ + fun resolveParentRectAbsPosition(view: View): Rect { val coords = IntArray(2) // this will always have size >= 2 @Suppress("UnsafeThirdPartyFunctionCall") @@ -31,7 +39,15 @@ internal object ImageViewUtils { ) } - internal fun calculateClipping(parentRect: Rect, childRect: Rect, density: Float): MobileSegment.WireframeClip { + /** + * Calculates the clipping [Rect] of the given child [Rect] using its parent [Rect] and + * the screen density. + * @param parentRect the parent [Rect]. + * @param childRect the child [Rect]. + * @param density the screen density. + * @return the clipping [Rect]. + */ + fun calculateClipping(parentRect: Rect, childRect: Rect, density: Float): Rect { val left = if (childRect.left < parentRect.left) { parentRect.left - childRect.left } else { @@ -52,18 +68,25 @@ internal object ImageViewUtils { } else { 0 } - - return MobileSegment.WireframeClip( - left = left.densityNormalized(density).toLong(), - top = top.densityNormalized(density).toLong(), - right = right.densityNormalized(density).toLong(), - bottom = bottom.densityNormalized(density).toLong() + return Rect( + left.densityNormalized(density), + top.densityNormalized(density), + right.densityNormalized(density), + bottom.densityNormalized(density) ) } - internal fun resolveContentRectWithScaling( + /** + * Resolves the [Drawable] content [Rect] using the given [ImageView] scale type. + * @param imageView the [ImageView]. + * @param drawable the [Drawable]. + * @param customScaleType optional custom [ImageView.ScaleType]. + * @return the resolved content [Rect]. + */ + fun resolveContentRectWithScaling( imageView: ImageView, - drawable: Drawable + drawable: Drawable, + customScaleType: ImageView.ScaleType? = null ): Rect { val drawableWidthPx = drawable.intrinsicWidth val drawableHeightPx = drawable.intrinsicHeight @@ -79,7 +102,7 @@ internal object ImageViewUtils { val resultRect: Rect - when (imageView.scaleType) { + when (customScaleType ?: imageView.scaleType) { ImageView.ScaleType.FIT_START -> { val contentRect = scaleRectToFitParent(parentRect, childRect) resultRect = positionRectAtStart(parentRect, contentRect) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/IntExt.kt similarity index 83% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExt.kt rename to dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/IntExt.kt index 59d515a40e..04d5528f34 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExt.kt +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/IntExt.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal.recorder +package com.datadog.android.internal.utils /** * Normalizes an Int value (font size, view dimension, view position, etc.) according with the @@ -13,7 +13,7 @@ package com.datadog.android.sessionreplay.internal.recorder * view.height/2. * @param density */ -internal fun Int.densityNormalized(density: Float): Int { +fun Int.densityNormalized(density: Float): Int { if (density == 0f) { return this } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExt.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/LongExt.kt similarity index 83% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExt.kt rename to dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/LongExt.kt index 53e04d322e..b057e92220 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExt.kt +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/utils/LongExt.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal.recorder +package com.datadog.android.internal.utils /** * Normalizes a Long value (font size, view dimension, view position, etc.) according with the @@ -13,7 +13,7 @@ package com.datadog.android.sessionreplay.internal.recorder * view.height/2. * @param density */ -internal fun Long.densityNormalized(density: Float): Long { +fun Long.densityNormalized(density: Float): Long { if (density == 0f) { return this } diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/internal/ChipWireframeMapper.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/internal/ChipWireframeMapper.kt index ee7adb68c7..1846d17820 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/internal/ChipWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/internal/ChipWireframeMapper.kt @@ -55,6 +55,7 @@ internal class ChipWireframeMapper( height = view.chipDrawable.intrinsicHeight, usePIIPlaceholder = false, drawable = view.chipDrawable, + customResourceIdCacheKey = null, asyncJobStatusCallback = asyncJobStatusCallback ) backgroundWireframe?.let { diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/ChipWireframeMapperTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/ChipWireframeMapperTest.kt index b1f64285ed..ac1fa55523 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/ChipWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/ChipWireframeMapperTest.kt @@ -41,11 +41,11 @@ 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.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock -import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -176,7 +176,8 @@ class ChipWireframeMapperTest { clipping = isNull(), shapeStyle = isNull(), border = isNull(), - prefix = any() + prefix = any(), + customResourceIdCacheKey = anyOrNull() ) } diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index ff15ba03b4..be470a030f 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -62,10 +62,6 @@ interface com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringO fun obfuscate(String): String companion object fun getStringObfuscator(): StringObfuscator -class com.datadog.android.sessionreplay.internal.recorder.resources.DefaultDrawableCopier : DrawableCopier - override fun copy(android.graphics.drawable.Drawable, android.content.res.Resources): android.graphics.drawable.Drawable? -interface com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier - fun copy(android.graphics.drawable.Drawable, android.content.res.Resources): android.graphics.drawable.Drawable? interface com.datadog.android.sessionreplay.recorder.InteropViewCallback fun map(android.view.View, MappingContext): List data class com.datadog.android.sessionreplay.recorder.MappingContext @@ -75,7 +71,11 @@ interface com.datadog.android.sessionreplay.recorder.OptionSelectorDetector data class com.datadog.android.sessionreplay.recorder.SystemInformation constructor(com.datadog.android.sessionreplay.utils.GlobalBounds, Int = Configuration.ORIENTATION_UNDEFINED, Float, String? = null) abstract class com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper : BaseWireframeMapper + constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) override fun map(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List + protected open fun resolveViewBackground(android.view.View, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? + protected open fun resolveBackgroundAsShapeWireframe(android.view.View, com.datadog.android.sessionreplay.utils.GlobalBounds, Int, Int, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle?): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ShapeWireframe? + protected open fun resolveBackgroundAsImageWireframe(android.view.View, com.datadog.android.sessionreplay.utils.GlobalBounds, Int, Int, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? companion object open class com.datadog.android.sessionreplay.recorder.mapper.BaseViewGroupMapper : BaseAsyncBackgroundWireframeMapper, TraverseAllChildrenMapper constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) @@ -88,7 +88,7 @@ class com.datadog.android.sessionreplay.recorder.mapper.EditTextMapper : TextVie override fun resolveCapturedText(android.widget.EditText, com.datadog.android.sessionreplay.TextAndInputPrivacy, Boolean): String companion object open class com.datadog.android.sessionreplay.recorder.mapper.ImageViewMapper : BaseAsyncBackgroundWireframeMapper - constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) + constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper, com.datadog.android.sessionreplay.recorder.resources.DrawableCopier) override fun map(android.widget.ImageView, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List open class com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper : BaseAsyncBackgroundWireframeMapper constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) @@ -98,6 +98,10 @@ open class com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper : WireframeMapper interface com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper fun map(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List +class com.datadog.android.sessionreplay.recorder.resources.DefaultDrawableCopier : DrawableCopier + override fun copy(android.graphics.drawable.Drawable, android.content.res.Resources): android.graphics.drawable.Drawable? +interface com.datadog.android.sessionreplay.recorder.resources.DrawableCopier + fun copy(android.graphics.drawable.Drawable, android.content.res.Resources): android.graphics.drawable.Drawable? open class com.datadog.android.sessionreplay.utils.AndroidMDrawableToColorMapper : LegacyDrawableToColorMapper constructor(List = emptyList()) override fun resolveRippleDrawable(android.graphics.drawable.RippleDrawable, com.datadog.android.api.InternalLogger): Int? @@ -131,8 +135,8 @@ data class com.datadog.android.sessionreplay.utils.GlobalBounds constructor(Long, Long, Long, Long) interface com.datadog.android.sessionreplay.utils.ImageWireframeHelper fun createImageWireframeByBitmap(Long, GlobalBounds, android.graphics.Bitmap, Float, Boolean, com.datadog.android.sessionreplay.ImagePrivacy, AsyncJobStatusCallback, com.datadog.android.sessionreplay.model.MobileSegment.WireframeClip? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeBorder? = null): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? - fun createImageWireframeByDrawable(android.view.View, com.datadog.android.sessionreplay.ImagePrivacy, Int, Long, Long, Int, Int, Boolean, android.graphics.drawable.Drawable, com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier = DefaultDrawableCopier(), AsyncJobStatusCallback, com.datadog.android.sessionreplay.model.MobileSegment.WireframeClip? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeBorder? = null, String? = DRAWABLE_CHILD_NAME): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? - fun createCompoundDrawableWireframes(android.widget.TextView, com.datadog.android.sessionreplay.recorder.MappingContext, Int, AsyncJobStatusCallback): MutableList + fun createImageWireframeByDrawable(android.view.View, com.datadog.android.sessionreplay.ImagePrivacy, Int, Long, Long, Int, Int, Boolean, android.graphics.drawable.Drawable, com.datadog.android.sessionreplay.recorder.resources.DrawableCopier = DefaultDrawableCopier(), AsyncJobStatusCallback, com.datadog.android.sessionreplay.model.MobileSegment.WireframeClip? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle? = null, com.datadog.android.sessionreplay.model.MobileSegment.ShapeBorder? = null, String? = DRAWABLE_CHILD_NAME, String?): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? + fun createCompoundDrawableWireframes(android.widget.TextView, com.datadog.android.sessionreplay.recorder.MappingContext, Int, String?, AsyncJobStatusCallback): MutableList companion object open class com.datadog.android.sessionreplay.utils.LegacyDrawableToColorMapper : DrawableToColorMapper constructor(List = emptyList()) diff --git a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api index f213c3f1ed..4884b056b9 100644 --- a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api +++ b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api @@ -122,15 +122,6 @@ public final class com/datadog/android/sessionreplay/internal/recorder/obfuscato public final fun getStringObfuscator ()Lcom/datadog/android/sessionreplay/internal/recorder/obfuscator/StringObfuscator; } -public final class com/datadog/android/sessionreplay/internal/recorder/resources/DefaultDrawableCopier : com/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier { - public fun ()V - public fun copy (Landroid/graphics/drawable/Drawable;Landroid/content/res/Resources;)Landroid/graphics/drawable/Drawable; -} - -public abstract interface class com/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier { - public abstract fun copy (Landroid/graphics/drawable/Drawable;Landroid/content/res/Resources;)Landroid/graphics/drawable/Drawable; -} - public final class com/datadog/android/sessionreplay/model/MobileSegment { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Companion; public fun (Lcom/datadog/android/sessionreplay/model/MobileSegment$Application;Lcom/datadog/android/sessionreplay/model/MobileSegment$Session;Lcom/datadog/android/sessionreplay/model/MobileSegment$View;JJJLjava/lang/Long;Ljava/lang/Boolean;Lcom/datadog/android/sessionreplay/model/MobileSegment$Source;Ljava/util/List;)V @@ -1466,7 +1457,11 @@ public final class com/datadog/android/sessionreplay/recorder/SystemInformation public abstract class com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper : com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper { public static final field Companion Lcom/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper$Companion; + public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V public fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List; + protected fun resolveBackgroundAsImageWireframe (Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; + protected fun resolveBackgroundAsShapeWireframe (Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; + protected fun resolveViewBackground (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; } public final class com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper$Companion { @@ -1496,7 +1491,7 @@ public final class com/datadog/android/sessionreplay/recorder/mapper/EditTextMap } public class com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapper : com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper { - public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V + public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;Lcom/datadog/android/sessionreplay/recorder/resources/DrawableCopier;)V public synthetic fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List; public fun map (Landroid/widget/ImageView;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List; } @@ -1516,6 +1511,15 @@ public abstract interface class com/datadog/android/sessionreplay/recorder/mappe public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List; } +public final class com/datadog/android/sessionreplay/recorder/resources/DefaultDrawableCopier : com/datadog/android/sessionreplay/recorder/resources/DrawableCopier { + public fun ()V + public fun copy (Landroid/graphics/drawable/Drawable;Landroid/content/res/Resources;)Landroid/graphics/drawable/Drawable; +} + +public abstract interface class com/datadog/android/sessionreplay/recorder/resources/DrawableCopier { + public abstract fun copy (Landroid/graphics/drawable/Drawable;Landroid/content/res/Resources;)Landroid/graphics/drawable/Drawable; +} + public class com/datadog/android/sessionreplay/utils/AndroidMDrawableToColorMapper : com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper { public fun ()V public fun (Ljava/util/List;)V @@ -1597,9 +1601,9 @@ public final class com/datadog/android/sessionreplay/utils/GlobalBounds { public abstract interface class com/datadog/android/sessionreplay/utils/ImageWireframeHelper { public static final field Companion Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper$Companion; - public abstract fun createCompoundDrawableWireframes (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/recorder/MappingContext;ILcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; + public abstract fun createCompoundDrawableWireframes (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/recorder/MappingContext;ILjava/lang/String;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Ljava/util/List; public abstract fun createImageWireframeByBitmap (JLcom/datadog/android/sessionreplay/utils/GlobalBounds;Landroid/graphics/Bitmap;FZLcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; - public abstract fun createImageWireframeByDrawable (Landroid/view/View;Lcom/datadog/android/sessionreplay/ImagePrivacy;IJJIIZLandroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; + public abstract fun createImageWireframeByDrawable (Landroid/view/View;Lcom/datadog/android/sessionreplay/ImagePrivacy;IJJIIZLandroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/recorder/resources/DrawableCopier;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; } public final class com/datadog/android/sessionreplay/utils/ImageWireframeHelper$Companion { @@ -1607,7 +1611,7 @@ public final class com/datadog/android/sessionreplay/utils/ImageWireframeHelper$ public final class com/datadog/android/sessionreplay/utils/ImageWireframeHelper$DefaultImpls { public static synthetic fun createImageWireframeByBitmap$default (Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;JLcom/datadog/android/sessionreplay/utils/GlobalBounds;Landroid/graphics/Bitmap;FZLcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; - public static synthetic fun createImageWireframeByDrawable$default (Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Landroid/view/View;Lcom/datadog/android/sessionreplay/ImagePrivacy;IJJIIZLandroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; + public static synthetic fun createImageWireframeByDrawable$default (Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Landroid/view/View;Lcom/datadog/android/sessionreplay/ImagePrivacy;IJJIIZLandroid/graphics/drawable/Drawable;Lcom/datadog/android/sessionreplay/recorder/resources/DrawableCopier;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; } public class com/datadog/android/sessionreplay/utils/LegacyDrawableToColorMapper : com/datadog/android/sessionreplay/utils/DrawableToColorMapper { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt index bbe80d462c..d73d4e15b3 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt @@ -22,6 +22,7 @@ import android.widget.TextView import androidx.appcompat.widget.ActionBarContainer import androidx.appcompat.widget.SwitchCompat import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.MapperTypeWrapper import com.datadog.android.sessionreplay.TextAndInputPrivacy @@ -41,12 +42,12 @@ import com.datadog.android.sessionreplay.internal.resources.ResourceDataStoreMan import com.datadog.android.sessionreplay.internal.storage.RecordWriter import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter import com.datadog.android.sessionreplay.internal.time.SessionReplayTimeProvider -import com.datadog.android.sessionreplay.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector import com.datadog.android.sessionreplay.recorder.mapper.EditTextMapper import com.datadog.android.sessionreplay.recorder.mapper.ImageViewMapper import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper +import com.datadog.android.sessionreplay.recorder.resources.DefaultDrawableCopier import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver @@ -101,7 +102,8 @@ internal class DefaultRecorderProvider( colorStringFormatter = colorStringFormatter, viewBoundsResolver = viewBoundsResolver, drawableToColorMapper = drawableToColorMapper, - imageViewUtils = ImageViewUtils + imageViewUtils = ImageViewUtils, + drawableCopier = DefaultDrawableCopier() ) val textViewMapper = TextViewMapper( viewIdentifierResolver, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt index 41bca26faa..dd9f119ab0 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewUtilsInternal.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.ViewStub import androidx.appcompat.widget.ActionBarContextView import androidx.appcompat.widget.Toolbar +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultImageWireframeHelper import com.datadog.android.sessionreplay.utils.GlobalBounds diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/callback/RecorderWindowCallback.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/callback/RecorderWindowCallback.kt index 40293a5ae4..0c60391473 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/callback/RecorderWindowCallback.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/callback/RecorderWindowCallback.kt @@ -12,13 +12,13 @@ import android.view.MotionEvent import android.view.Window import androidx.annotation.MainThread import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.internal.TouchPrivacyManager import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler import com.datadog.android.sessionreplay.internal.recorder.ViewOnDrawInterceptor import com.datadog.android.sessionreplay.internal.recorder.WindowInspector -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.utils.TimeProvider import com.datadog.android.sessionreplay.model.MobileSegment import java.util.LinkedList diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt index 712a2b6dfd..7031d33ea7 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BasePickerMapper.kt @@ -9,7 +9,7 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.os.Build import android.widget.NumberPicker import androidx.annotation.RequiresApi -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMapper import com.datadog.android.sessionreplay.utils.ColorStringFormatter diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt index 1d057ff9a9..8cc7d1f449 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableCompoundButtonMapper.kt @@ -12,7 +12,7 @@ import android.os.Build import android.widget.CompoundButton import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt index fcf3f26349..c4fb1f894b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt @@ -11,10 +11,10 @@ import android.widget.Checkable import android.widget.TextView import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper @@ -125,6 +125,7 @@ internal abstract class CheckableTextViewMapper( border = null, usePIIPlaceholder = true, clipping = MobileSegment.WireframeClip(), + customResourceIdCacheKey = null, asyncJobStatusCallback = asyncJobStatusCallback ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt index 05222776c7..9b788120bc 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckedTextViewMapper.kt @@ -10,7 +10,7 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.DrawableContainer import android.widget.CheckedTextView import androidx.annotation.UiThread -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt index 621cc5e6fb..f5ba0fdfc8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt @@ -14,8 +14,8 @@ import android.widget.ProgressBar import androidx.annotation.RequiresApi import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.TextAndInputPrivacy -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt index c2cd516288..e3cce77f8e 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt @@ -8,8 +8,8 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.SeekBar import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.TextAndInputPrivacy -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt index 03b9dc9b93..c25c7ce16d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt @@ -9,11 +9,11 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import androidx.annotation.UiThread import androidx.appcompat.widget.SwitchCompat import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper @@ -97,6 +97,7 @@ internal open class SwitchCompatMapper( shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = asyncJobStatusCallback ) } @@ -141,6 +142,7 @@ internal open class SwitchCompatMapper( border = null, usePIIPlaceholder = true, clipping = null, + customResourceIdCacheKey = null, asyncJobStatusCallback = asyncJobStatusCallback ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt index 2efede60d2..e60d934c67 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManager.kt @@ -14,7 +14,7 @@ import androidx.annotation.MainThread import com.datadog.android.api.InternalLogger internal class BitmapCachesManager( - private val resourcesLRUCache: Cache, + private val resourcesLRUCache: Cache, private val bitmapPool: BitmapPool, private val logger: InternalLogger ) { @@ -51,15 +51,20 @@ internal class BitmapCachesManager( isBitmapPoolRegisteredForCallbacks = true } - internal fun putInResourceCache(drawable: Drawable, resourceId: String) { - resourcesLRUCache.put(drawable, resourceId.toByteArray(Charsets.UTF_8)) + internal fun putInResourceCache(key: String, resourceId: String) { + resourcesLRUCache.put(key, resourceId.toByteArray(Charsets.UTF_8)) } - internal fun getFromResourceCache(drawable: Drawable): String? { - val resourceId = resourcesLRUCache.get(drawable) ?: return null + internal fun getFromResourceCache(key: String): String? { + val resourceId = resourcesLRUCache.get(key) ?: return null return String(resourceId, Charsets.UTF_8) } + internal fun generateResourceKeyFromDrawable(drawable: Drawable): String? { + // TODO RUM-7740 - Handle unsafe cast + return (resourcesLRUCache as? ResourcesLRUCache)?.generateKeyFromDrawable(drawable) + } + internal fun putInBitmapPool(bitmap: Bitmap) { bitmapPool.put(bitmap) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapPool.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapPool.kt index b071489414..cf7a4e0c16 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapPool.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapPool.kt @@ -91,8 +91,8 @@ internal class BitmapPool( } @Synchronized - override fun get(element: String): Bitmap? { - val bitmapsWithReqDimensions = bitmapsBySize[element] ?: return null + override fun get(key: String): Bitmap? { + val bitmapsWithReqDimensions = bitmapsBySize[key] ?: return null // find the first unused bitmap, mark it as used and return it return bitmapsWithReqDimensions.find { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/Cache.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/Cache.kt index e73c9975ec..5711f10c5a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/Cache.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/Cache.kt @@ -9,8 +9,8 @@ package com.datadog.android.sessionreplay.internal.recorder.resources internal interface Cache { fun put(value: V) {} - fun put(element: K, value: V) {} - fun get(element: K): V? = null + fun put(key: K, value: V) {} + fun get(key: K): V? = null fun size(): Int fun clear() diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt index 18aede3556..d9bae6304e 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt @@ -15,11 +15,12 @@ import android.widget.TextView import androidx.annotation.UiThread import androidx.annotation.VisibleForTesting import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.android.sessionreplay.utils.ImageWireframeHelper @@ -115,7 +116,8 @@ internal class DefaultImageWireframeHelper( clipping: MobileSegment.WireframeClip?, shapeStyle: MobileSegment.ShapeStyle?, border: MobileSegment.ShapeBorder?, - prefix: String? + prefix: String?, + customResourceIdCacheKey: String? ): MobileSegment.Wireframe? { val id = viewIdentifierResolver.resolveChildUniqueIdentifier(view, prefix + currentWireframeIndex) val drawableProperties = resolveDrawableProperties( @@ -125,7 +127,9 @@ internal class DefaultImageWireframeHelper( height = height ) - if (id == null || !drawableProperties.isValid()) return null + if (id == null || !drawableProperties.isValid()) { + return null + } val resources = view.resources @@ -204,6 +208,7 @@ internal class DefaultImageWireframeHelper( drawableCopier = drawableCopier, drawableWidth = width, drawableHeight = height, + customResourceIdCacheKey = customResourceIdCacheKey, resourceResolverCallback = object : ResourceResolverCallback { override fun onSuccess(resourceId: String) { populateResourceIdInWireframe(resourceId, imageWireframe) @@ -225,6 +230,7 @@ internal class DefaultImageWireframeHelper( textView: TextView, mappingContext: MappingContext, prevWireframeIndex: Int, + customResourceIdCacheKey: String?, asyncJobStatusCallback: AsyncJobStatusCallback ): MutableList { val result = mutableListOf() @@ -252,6 +258,12 @@ internal class DefaultImageWireframeHelper( position = compoundDrawablePosition ) + val resourceCacheKey = if (customResourceIdCacheKey != null) { + "$customResourceIdCacheKey" + "_$compoundDrawableIndex" + } else { + null + } + createImageWireframeByDrawable( view = textView, imagePrivacy = mappingContext.imagePrivacy, @@ -265,6 +277,7 @@ internal class DefaultImageWireframeHelper( border = null, usePIIPlaceholder = true, clipping = MobileSegment.WireframeClip(), + customResourceIdCacheKey = resourceCacheKey, asyncJobStatusCallback = asyncJobStatusCallback )?.let { resultWireframe -> result.add(resultWireframe) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageTypeResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageTypeResolver.kt index a414a0849b..4417eb3927 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageTypeResolver.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ImageTypeResolver.kt @@ -9,7 +9,7 @@ package com.datadog.android.sessionreplay.internal.recorder.resources import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import androidx.annotation.VisibleForTesting -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized internal class ImageTypeResolver { fun isDrawablePII(drawable: Drawable, density: Float): Boolean { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt index fe0d18f1c6..0ff79eaeb5 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolver.kt @@ -18,6 +18,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.utils.executeSafe import com.datadog.android.sessionreplay.internal.async.DataQueueHandler import com.datadog.android.sessionreplay.internal.utils.DrawableUtils +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import java.util.concurrent.ExecutorService import java.util.concurrent.LinkedBlockingDeque import java.util.concurrent.ThreadPoolExecutor @@ -80,11 +81,12 @@ internal class ResourceResolver( drawableCopier: DrawableCopier, drawableWidth: Int, drawableHeight: Int, + customResourceIdCacheKey: String?, resourceResolverCallback: ResourceResolverCallback ) { bitmapCachesManager.registerCallbacks(applicationContext) - val resourceId = tryToGetResourceFromCache(drawable = originalDrawable) + val resourceId = tryToGetResourceFromCache(drawable = originalDrawable, key = customResourceIdCacheKey) if (resourceId != null) { // if we got here it means we saw the bitmap before, @@ -109,12 +111,13 @@ internal class ResourceResolver( // do in the background threadPoolExecutor.executeSafe("resolveResourceId", logger) { createBitmap( - resources = resources, drawable = originalDrawable, + copiedDrawable = copiedDrawable, drawableWidth = drawableWidth, drawableHeight = drawableHeight, displayMetrics = displayMetrics, bitmapFromDrawable = bitmapFromDrawable, + customResourceIdCacheKey = customResourceIdCacheKey, resolveResourceCallback = object : ResolveResourceCallback { override fun onResolved(resourceId: String, resourceData: ByteArray) { resourceItemCreationHandler.queueItem(resourceId, resourceData) @@ -135,18 +138,20 @@ internal class ResourceResolver( @WorkerThread private fun createBitmap( - resources: Resources, drawable: Drawable, + copiedDrawable: Drawable, drawableWidth: Int, drawableHeight: Int, displayMetrics: DisplayMetrics, bitmapFromDrawable: Bitmap?, + customResourceIdCacheKey: String?, resolveResourceCallback: ResolveResourceCallback ) { val handledBitmap = if (bitmapFromDrawable != null) { tryToGetBitmapFromBitmapDrawable( - drawable = drawable as BitmapDrawable, + drawable = drawable, bitmapFromDrawable = bitmapFromDrawable, + customResourceIdCacheKey = customResourceIdCacheKey, resolveResourceCallback = resolveResourceCallback ) } else { @@ -155,11 +160,11 @@ internal class ResourceResolver( if (handledBitmap == null) { tryToDrawNewBitmap( - resources = resources, - drawable = drawable, + drawable = copiedDrawable, drawableWidth = drawableWidth, drawableHeight = drawableHeight, displayMetrics = displayMetrics, + customResourceIdCacheKey = customResourceIdCacheKey, resolveResourceCallback = resolveResourceCallback ) } @@ -195,6 +200,7 @@ internal class ResourceResolver( bitmap: Bitmap, compressedBitmapBytes: ByteArray, shouldCacheBitmap: Boolean, + customResourceIdCacheKey: String?, resolveResourceCallback: ResolveResourceCallback ) { // failed to get image data @@ -217,6 +223,7 @@ internal class ResourceResolver( shouldCacheBitmap = shouldCacheBitmap, bitmap = bitmap, resourceId = resourceId, + customResourceIdCacheKey = customResourceIdCacheKey, drawable = drawable ) @@ -227,26 +234,29 @@ internal class ResourceResolver( shouldCacheBitmap: Boolean, bitmap: Bitmap, resourceId: String, + customResourceIdCacheKey: String?, drawable: Drawable ) { if (shouldCacheBitmap) { bitmapCachesManager.putInBitmapPool(bitmap) } - bitmapCachesManager.putInResourceCache(drawable, resourceId) + val key = customResourceIdCacheKey + ?: bitmapCachesManager.generateResourceKeyFromDrawable(drawable) + ?: return + bitmapCachesManager.putInResourceCache(key, resourceId) } @WorkerThread private fun tryToDrawNewBitmap( - resources: Resources, drawable: Drawable, drawableWidth: Int, drawableHeight: Int, displayMetrics: DisplayMetrics, + customResourceIdCacheKey: String?, resolveResourceCallback: ResolveResourceCallback ) { drawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = resources, drawable = drawable, drawableWidth = drawableWidth, drawableHeight = drawableHeight, @@ -267,6 +277,7 @@ internal class ResourceResolver( bitmap = bitmap, compressedBitmapBytes = compressedBitmapBytes, shouldCacheBitmap = true, + customResourceIdCacheKey = customResourceIdCacheKey, resolveResourceCallback = resolveResourceCallback ) } @@ -282,8 +293,9 @@ internal class ResourceResolver( @WorkerThread @Suppress("ReturnCount") private fun tryToGetBitmapFromBitmapDrawable( - drawable: BitmapDrawable, + drawable: Drawable, bitmapFromDrawable: Bitmap, + customResourceIdCacheKey: String?, resolveResourceCallback: ResolveResourceCallback ): Bitmap? { val scaledBitmap = drawableUtils.createScaledBitmap(bitmapFromDrawable) @@ -313,6 +325,7 @@ internal class ResourceResolver( bitmap = scaledBitmap, compressedBitmapBytes = compressedBitmapBytes, shouldCacheBitmap = shouldCacheBitmap, + customResourceIdCacheKey = customResourceIdCacheKey, resolveResourceCallback = resolveResourceCallback ) @@ -320,8 +333,14 @@ internal class ResourceResolver( } private fun tryToGetResourceFromCache( - drawable: Drawable - ): String? = bitmapCachesManager.getFromResourceCache(drawable) + drawable: Drawable, + key: String? + ): String? { + val cacheKey = key + ?: bitmapCachesManager.generateResourceKeyFromDrawable(drawable) + ?: return null + return bitmapCachesManager.getFromResourceCache(cacheKey) + } private fun shouldUseDrawableBitmap(drawable: BitmapDrawable): Boolean { return drawable.bitmap != null && diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCache.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCache.kt index 1694832c84..104c9a9e78 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCache.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCache.kt @@ -12,7 +12,6 @@ import android.graphics.drawable.AnimationDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.DrawableContainer import android.graphics.drawable.LayerDrawable -import androidx.annotation.VisibleForTesting import androidx.collection.LruCache import com.datadog.android.sessionreplay.internal.recorder.safeGetDrawable import com.datadog.android.sessionreplay.internal.utils.CacheUtils @@ -28,7 +27,7 @@ internal class ResourcesLRUCache( return value.size } } -) : Cache, ComponentCallbacks2 { +) : Cache, ComponentCallbacks2 { override fun onTrimMemory(level: Int) { cacheUtils.handleTrimMemory(level, cache) @@ -45,9 +44,7 @@ internal class ResourcesLRUCache( } @Synchronized - override fun put(element: Drawable, value: ByteArray) { - val key = generateKey(element) - + override fun put(key: String, value: ByteArray) { @Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block invocationUtils.safeCallWithErrorLogging( call = { cache.put(key, value) }, @@ -56,11 +53,11 @@ internal class ResourcesLRUCache( } @Synchronized - override fun get(element: Drawable): ByteArray? = + override fun get(key: String): ByteArray? = @Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block invocationUtils.safeCallWithErrorLogging( call = { - cache.get(generateKey(element)) + cache.get(key) }, failureMessage = FAILURE_MSG_GET_CACHE ) @@ -76,9 +73,8 @@ internal class ResourcesLRUCache( ) } - @VisibleForTesting - internal fun generateKey(drawable: Drawable): String = - generatePrefix(drawable) + System.identityHashCode(drawable) + internal fun generateKeyFromDrawable(element: Drawable): String = + generatePrefix(element) + System.identityHashCode(element) private fun generatePrefix(drawable: Drawable): String { return when (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 49eebf59ea..1e6dadf165 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 @@ -6,7 +6,6 @@ package com.datadog.android.sessionreplay.internal.utils -import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Bitmap.Config import android.graphics.Color @@ -39,7 +38,6 @@ internal class DrawableUtils( */ @WorkerThread internal fun createBitmapOfApproxSizeFromDrawable( - resources: Resources, drawable: Drawable, drawableWidth: Int, drawableHeight: Int, @@ -59,7 +57,6 @@ internal class DrawableUtils( override fun onSuccess(bitmap: Bitmap) { executorService.submitSafe("drawOnCanvas", internalLogger) { drawOnCanvas( - resources, bitmap, drawable, bitmapCreationCallback @@ -103,27 +100,21 @@ internal class DrawableUtils( @WorkerThread private fun drawOnCanvas( - resources: Resources, bitmap: Bitmap, drawable: Drawable, bitmapCreationCallback: ResourceResolver.BitmapCreationCallback ) { - // don't use the original drawable - it will affect the view hierarchy - val newDrawable = drawable.constantState?.newDrawable(resources)?.apply { - // `constantState` contains only immutable properties of drawable,the state needs to be set manually - setState(drawable.current.state) - } val canvas = canvasWrapper.createCanvas(bitmap) - if (canvas == null || newDrawable == null) { + if (canvas == null) { bitmapCreationCallback.onFailure() } else { // erase the canvas // needed because overdrawing an already used bitmap causes unusual visual artifacts canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY) - newDrawable.setBounds(0, 0, canvas.width, canvas.height) - newDrawable.draw(canvas) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) bitmapCreationCallback.onReady(bitmap) } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt index 96a7859abb..324d7db542 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtils.kt @@ -13,7 +13,7 @@ import android.os.Build import android.util.TypedValue import android.view.WindowManager import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.recorder.SystemInformation import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter import com.datadog.android.sessionreplay.utils.GlobalBounds diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/RectExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/RectExt.kt new file mode 100644 index 0000000000..e10ec9cc2d --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/RectExt.kt @@ -0,0 +1,17 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.sessionreplay.internal.utils + +import android.graphics.Rect +import com.datadog.android.sessionreplay.model.MobileSegment + +internal fun Rect.toWireframeClip() = MobileSegment.WireframeClip( + this.top.toLong(), + this.bottom.toLong(), + this.left.toLong(), + this.right.toLong() +) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt index 5bc27d4bcb..300b3f0494 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt @@ -9,7 +9,7 @@ package com.datadog.android.sessionreplay.recorder.mapper import android.view.View import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback @@ -31,7 +31,7 @@ import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver * @param viewBoundsResolver the [ViewBoundsResolver] to get a view boundaries in density independent units * @param drawableToColorMapper the [DrawableToColorMapper] to convert a background drawable into a solid color */ -abstract class BaseAsyncBackgroundWireframeMapper internal constructor( +abstract class BaseAsyncBackgroundWireframeMapper ( viewIdentifierResolver: ViewIdentifierResolver, colorStringFormatter: ColorStringFormatter, viewBoundsResolver: ViewBoundsResolver, @@ -57,8 +57,16 @@ abstract class BaseAsyncBackgroundWireframeMapper internal construc return backgroundWireframe?.let { listOf(it) } ?: emptyList() } + /** + * Function used to resolve the [MobileSegment.Wireframe] to represent the view background, based on its type. + * + * @param view the [View] to map + * @param mappingContext the [MappingContext] which contains contextual data, useful for mapping. + * @param asyncJobStatusCallback the [AsyncJobStatusCallback] callback used for internal async operations. + * @param internalLogger the [InternalLogger], used for internal logging. + */ @UiThread - private fun resolveViewBackground( + protected open fun resolveViewBackground( view: View, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback, @@ -90,7 +98,16 @@ abstract class BaseAsyncBackgroundWireframeMapper internal construc } } - private fun resolveBackgroundAsShapeWireframe( + /** + * Function used to resolve the view background as a [MobileSegment.Wireframe.ShapeWireframe] wireframe. + * + * @param view the [View] to map + * @param bounds the [GlobalBounds] of the view. + * @param width the view width. + * @param height the view height. + * @param shapeStyle the optional [MobileSegment.ShapeStyle] to use. + */ + protected open fun resolveBackgroundAsShapeWireframe( view: View, bounds: GlobalBounds, width: Int, @@ -115,8 +132,18 @@ abstract class BaseAsyncBackgroundWireframeMapper internal construc ) } + /** + * Function used to resolve the view background as a Image wireframe. + * + * @param view the [View] to map + * @param bounds the [GlobalBounds] of the view. + * @param width the view width. + * @param height the view height. + * @param mappingContext the [MappingContext] which contains contextual data, useful for mapping. + * @param asyncJobStatusCallback the [AsyncJobStatusCallback] callback used for internal async operations. + */ @UiThread - private fun resolveBackgroundAsImageWireframe( + protected open fun resolveBackgroundAsImageWireframe( view: View, bounds: GlobalBounds, width: Int, @@ -139,7 +166,8 @@ abstract class BaseAsyncBackgroundWireframeMapper internal construc clipping = MobileSegment.WireframeClip(), shapeStyle = null, border = null, - prefix = PREFIX_BACKGROUND_DRAWABLE + prefix = PREFIX_BACKGROUND_DRAWABLE, + customResourceIdCacheKey = null ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapper.kt index 9af09cede5..b080a898df 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapper.kt @@ -9,10 +9,12 @@ package com.datadog.android.sessionreplay.recorder.mapper import android.widget.ImageView import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized -import com.datadog.android.sessionreplay.internal.utils.ImageViewUtils +import com.datadog.android.internal.utils.ImageViewUtils +import com.datadog.android.internal.utils.densityNormalized +import com.datadog.android.sessionreplay.internal.utils.toWireframeClip import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper @@ -25,19 +27,22 @@ import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver */ open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper { private val imageViewUtils: ImageViewUtils + private val drawableCopier: DrawableCopier @Suppress("Unused") // used by external mappers constructor( viewIdentifierResolver: ViewIdentifierResolver, colorStringFormatter: ColorStringFormatter, viewBoundsResolver: ViewBoundsResolver, - drawableToColorMapper: DrawableToColorMapper + drawableToColorMapper: DrawableToColorMapper, + drawableCopier: DrawableCopier ) : this( viewIdentifierResolver, colorStringFormatter, viewBoundsResolver, drawableToColorMapper, - ImageViewUtils + ImageViewUtils, + drawableCopier ) internal constructor( @@ -45,7 +50,8 @@ open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper { colorStringFormatter: ColorStringFormatter, viewBoundsResolver: ViewBoundsResolver, drawableToColorMapper: DrawableToColorMapper, - imageViewUtils: ImageViewUtils + imageViewUtils: ImageViewUtils, + drawableCopier: DrawableCopier ) : super( viewIdentifierResolver, colorStringFormatter, @@ -53,6 +59,7 @@ open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper { drawableToColorMapper ) { this.imageViewUtils = imageViewUtils + this.drawableCopier = drawableCopier } @UiThread @@ -76,7 +83,7 @@ open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper { val density = resources.displayMetrics.density val clipping = if (view.cropToPadding) { - imageViewUtils.calculateClipping(parentRect, contentRect, density) + imageViewUtils.calculateClipping(parentRect, contentRect, density).toWireframeClip() } else { null } @@ -97,11 +104,13 @@ open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper { height = contentHeightPx, usePIIPlaceholder = true, drawable = drawable, + drawableCopier = drawableCopier, asyncJobStatusCallback = asyncJobStatusCallback, clipping = clipping, shapeStyle = null, border = null, - prefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME + prefix = ImageWireframeHelper.DRAWABLE_CHILD_NAME, + customResourceIdCacheKey = null )?.let { wireframes.add(it) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt index 285da3e292..289e66e44d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt @@ -11,8 +11,8 @@ import android.view.Gravity import android.widget.TextView import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.TextAndInputPrivacy -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringObfuscator import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext @@ -34,10 +34,10 @@ open class TextViewMapper( viewBoundsResolver: ViewBoundsResolver, drawableToColorMapper: DrawableToColorMapper ) : BaseAsyncBackgroundWireframeMapper( - viewIdentifierResolver, - colorStringFormatter, - viewBoundsResolver, - drawableToColorMapper + viewIdentifierResolver = viewIdentifierResolver, + colorStringFormatter = colorStringFormatter, + viewBoundsResolver = viewBoundsResolver, + drawableToColorMapper = drawableToColorMapper ) { @UiThread @@ -54,24 +54,25 @@ open class TextViewMapper( val density = mappingContext.systemInformation.screenDensity val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds( - view, - density + view = view, + screenDensity = density ) wireframes.add( createTextWireframe( - view, - mappingContext, - viewGlobalBounds + textView = view, + mappingContext = mappingContext, + viewGlobalBounds = viewGlobalBounds ) ) wireframes.addAll( mappingContext.imageWireframeHelper.createCompoundDrawableWireframes( - view, - mappingContext, - wireframes.size, - asyncJobStatusCallback + textView = view, + mappingContext = mappingContext, + prevWireframeIndex = wireframes.size, + customResourceIdCacheKey = null, + asyncJobStatusCallback = asyncJobStatusCallback ) ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultDrawableCopier.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/resources/DefaultDrawableCopier.kt similarity index 83% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultDrawableCopier.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/resources/DefaultDrawableCopier.kt index 8bd17fdba1..92f527d404 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultDrawableCopier.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/resources/DefaultDrawableCopier.kt @@ -4,16 +4,14 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal.recorder.resources +package com.datadog.android.sessionreplay.recorder.resources import android.content.res.Resources import android.graphics.drawable.Drawable -import com.datadog.android.lint.InternalApi /** * Default implementation of [DrawableCopier] interface, it copies the drawable from constant state. */ -@InternalApi class DefaultDrawableCopier : DrawableCopier { override fun copy(originalDrawable: Drawable, resources: Resources): Drawable? { return originalDrawable.constantState?.newDrawable(resources) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/resources/DrawableCopier.kt similarity index 84% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/resources/DrawableCopier.kt index f732b1b5a4..a01e0bc09f 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DrawableCopier.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/resources/DrawableCopier.kt @@ -4,16 +4,14 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.sessionreplay.internal.recorder.resources +package com.datadog.android.sessionreplay.recorder.resources import android.content.res.Resources import android.graphics.drawable.Drawable -import com.datadog.android.lint.InternalApi /** * Interface of copying drawable to a new one. */ -@InternalApi fun interface DrawableCopier { /** diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt index 34f0a2909d..d627d49fe0 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/utils/ImageWireframeHelper.kt @@ -12,10 +12,10 @@ import android.view.View import android.widget.TextView import androidx.annotation.UiThread import com.datadog.android.sessionreplay.ImagePrivacy -import com.datadog.android.sessionreplay.internal.recorder.resources.DefaultDrawableCopier -import com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext +import com.datadog.android.sessionreplay.recorder.resources.DefaultDrawableCopier +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier /** * A Helper to handle capturing images in Session replay wireframes. @@ -65,6 +65,8 @@ interface ImageWireframeHelper { * @param shapeStyle provides a custom shape (e.g. rounded corners) to the image wireframe * @param border provides a custom border to the image wireframe * @param prefix a prefix identifying the drawable in the parent view's context + * @param customResourceIdCacheKey an optional key with which to cache or retrieve from the resource cache. + * If this key is not provided then one will be generated from the drawable. */ // TODO RUM-3666 limit the number of params to this function fun createImageWireframeByDrawable( @@ -82,18 +84,25 @@ interface ImageWireframeHelper { clipping: MobileSegment.WireframeClip? = null, shapeStyle: MobileSegment.ShapeStyle? = null, border: MobileSegment.ShapeBorder? = null, - prefix: String? = DRAWABLE_CHILD_NAME + prefix: String? = DRAWABLE_CHILD_NAME, + customResourceIdCacheKey: String? ): MobileSegment.Wireframe? /** * Creates the wireframes for the compound drawables in a [TextView]. - * @param + * @param textView the [TextView] to capture the compound drawables from. + * @param mappingContext the [MappingContext] for the [TextView]. + * @param prevWireframeIndex the index of the previous wireframe in the list of wireframes for the [TextView]. + * @param customResourceIdCacheKey an optional key with which to cache or retrieve from the resource cache. + * If this key is not provided then one will be generated from the drawable. + * @param asyncJobStatusCallback the callback for the async capture process. */ @UiThread fun createCompoundDrawableWireframes( textView: TextView, mappingContext: MappingContext, prevWireframeIndex: Int, + customResourceIdCacheKey: String?, asyncJobStatusCallback: AsyncJobStatusCallback ): MutableList diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExtTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExtTest.kt index 68a9b90fc4..cc4da5de0e 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExtTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/IntExtTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.sessionreplay.internal.recorder +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.forge.ForgeConfigurator import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.IntForgery diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExtTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExtTest.kt index 0715d2f8b1..be1842864d 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExtTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/LongExtTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.sessionreplay.internal.recorder +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.forge.ForgeConfigurator import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.LongForgery diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckableTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckableTextViewMapperTest.kt index 08e855542c..15558911ff 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckableTextViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseCheckableTextViewMapperTest.kt @@ -17,9 +17,9 @@ import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckableTextViewMapper.Companion.CHECK_BOX_CHECKED_DRAWABLE_INDEX import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckableTextViewMapper.Companion.CHECK_BOX_NOT_CHECKED_DRAWABLE_INDEX -import com.datadog.android.sessionreplay.internal.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.android.sessionreplay.utils.OPAQUE_ALPHA_VALUE import com.datadog.tools.unit.annotations.TestTargetApi @@ -43,6 +43,7 @@ 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.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.isNull @@ -224,7 +225,8 @@ internal abstract class BaseCheckableTextViewMapperTest : clipping = eq(MobileSegment.WireframeClip()), shapeStyle = isNull(), border = isNull(), - prefix = anyString() + prefix = anyString(), + customResourceIdCacheKey = anyOrNull() ) } @@ -268,7 +270,8 @@ internal abstract class BaseCheckableTextViewMapperTest : clipping = eq(MobileSegment.WireframeClip()), shapeStyle = isNull(), border = isNull(), - prefix = anyString() + prefix = anyString(), + customResourceIdCacheKey = anyOrNull() ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt index 67b0596bb0..e74dcfbd27 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseNumberPickerMapperTest.kt @@ -7,7 +7,7 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.widget.NumberPicker -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.android.sessionreplay.utils.OPAQUE_ALPHA_VALUE diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt index c48948df15..e3a7e09327 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseSwitchCompatMapperTest.kt @@ -11,9 +11,9 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable.ConstantState import androidx.appcompat.widget.SwitchCompat +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.utils.GlobalBounds diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt index ed4e4dfcb4..31da2a5dad 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt @@ -4,9 +4,9 @@ import android.content.res.ColorStateList import android.graphics.Rect import android.os.Build import android.widget.ProgressBar +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.recorder.mapper.SeekBarWireframeMapper.Companion.TRACK_HEIGHT_IN_PX import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.mapper.AbstractWireframeMapperTest diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt index a282d78e9f..638ba063a8 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt @@ -5,9 +5,9 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.os.Build import android.widget.SeekBar +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.recorder.mapper.SeekBarWireframeMapper.Companion.TRACK_HEIGHT_IN_PX import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.mapper.AbstractWireframeMapperTest diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt index d10e8b37ec..b93ef068a9 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt @@ -7,10 +7,10 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper import android.graphics.drawable.Drawable +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -24,6 +24,7 @@ import org.mockito.ArgumentMatchers import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.never @@ -102,7 +103,8 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { clipping = eq(null), shapeStyle = eq(null), border = eq(null), - prefix = ArgumentMatchers.anyString() + prefix = ArgumentMatchers.anyString(), + customResourceIdCacheKey = anyOrNull() ) assertThat(xCaptor.allValues).containsOnly(expectedThumbLeft, expectedTrackLeft) @@ -201,7 +203,8 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { clipping = any(), shapeStyle = any(), border = any(), - prefix = any() + prefix = any(), + customResourceIdCacheKey = anyOrNull() ) } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt index 3f20197199..ddedcc5850 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/BitmapCachesManagerTest.kt @@ -8,7 +8,6 @@ package com.datadog.android.sessionreplay.internal.recorder.resources import android.content.Context import android.graphics.Bitmap -import android.graphics.drawable.Drawable import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.utils.verifyLog @@ -24,6 +23,7 @@ import org.junit.jupiter.api.extension.Extensions 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.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -49,17 +49,19 @@ internal class BitmapCachesManagerTest { @Mock lateinit var mockApplicationContext: Context - @Mock - lateinit var mockDrawable: Drawable - @Mock lateinit var mockBitmap: Bitmap @StringForgery lateinit var fakeResourceId: String + @StringForgery + lateinit var fakeResourceKey: String + @BeforeEach fun `set up`() { + whenever(mockResourcesCache.generateKeyFromDrawable(any())).thenReturn(fakeResourceId) + testedCachesManager = createBitmapCachesManager( bitmapPool = mockBitmapPool, resourcesLRUCache = mockResourcesCache, @@ -103,20 +105,20 @@ internal class BitmapCachesManagerTest { @Test fun `M put in resource cache W putInResourceCache`() { // When - testedCachesManager.putInResourceCache(mockDrawable, fakeResourceId) + testedCachesManager.putInResourceCache(fakeResourceKey, fakeResourceId) // Then - verify(mockResourcesCache).put(mockDrawable, fakeResourceId.toByteArray(Charsets.UTF_8)) + verify(mockResourcesCache).put(fakeResourceKey, fakeResourceId.toByteArray(Charsets.UTF_8)) } @Test fun `M get resource from resource cache W getFromResourceCache { resource exists in cache }`() { // Given val fakeCacheData = fakeResourceId.toByteArray(Charsets.UTF_8) - whenever(mockResourcesCache.get(mockDrawable)).thenReturn(fakeCacheData) + whenever(mockResourcesCache.get(fakeResourceKey)).thenReturn(fakeCacheData) // When - val result = testedCachesManager.getFromResourceCache(mockDrawable) + val result = testedCachesManager.getFromResourceCache(fakeResourceKey) // Then assertThat(result).isEqualTo(fakeResourceId) @@ -125,10 +127,10 @@ internal class BitmapCachesManagerTest { @Test fun `M get null from resource cache W getFromResourceCache { resource not in cache }`() { // When - val result = testedCachesManager.getFromResourceCache(mockDrawable) + val result = testedCachesManager.getFromResourceCache(fakeResourceKey) // Then - verify(mockResourcesCache).get(mockDrawable) + verify(mockResourcesCache).get(fakeResourceKey) assertThat(result).isNull() } @@ -187,7 +189,7 @@ internal class BitmapCachesManagerTest { private fun createBitmapCachesManager( bitmapPool: BitmapPool, - resourcesLRUCache: Cache, + resourcesLRUCache: Cache, logger: InternalLogger ): BitmapCachesManager = BitmapCachesManager( @@ -198,7 +200,7 @@ internal class BitmapCachesManagerTest { // this is in order to test having a class that implements // Cache, but does NOT implement ComponentCallbacks2 - private class FakeNonComponentsCallbackCache : Cache { + private class FakeNonComponentsCallbackCache : Cache { override fun size(): Int = 0 diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt index 65d1488c6a..aa57de1575 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt @@ -12,6 +12,7 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.util.DisplayMetrics import android.view.View @@ -49,7 +50,9 @@ 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.anyOrNull import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -179,10 +182,229 @@ internal class DefaultImageWireframeHelperTest { ) } - // region createImageWireframe + @Test + fun `M return wireframe W createImageWireframeByBitmap`( + @Mock mockShapeStyle: MobileSegment.ShapeStyle, + @Mock mockBorder: MobileSegment.ShapeBorder, + @Mock stubWireframeClip: MobileSegment.WireframeClip + ) { + // Given + whenever( + mockResourceResolver.resolveResourceId( + bitmap = any(), + resourceResolverCallback = any() + ) + ).thenAnswer { + val callback = it.getArgument(1) + callback.onSuccess(fakeResourceId) + } + + val expectedWireframe = MobileSegment.Wireframe.ImageWireframe( + id = fakeGeneratedIdentifier, + x = fakeBounds.x, + y = fakeBounds.y, + width = fakeBounds.width, + height = fakeBounds.height, + shapeStyle = mockShapeStyle, + border = mockBorder, + resourceId = fakeResourceId, + clip = stubWireframeClip, + isEmpty = false + ) + + // When + val wireframe = testedHelper.createImageWireframeByBitmap( + id = fakeGeneratedIdentifier, + imagePrivacy = ImagePrivacy.MASK_LARGE_ONLY, + density = fakeDensity, + globalBounds = fakeBounds, + bitmap = mockBitmap, + shapeStyle = mockShapeStyle, + border = mockBorder, + asyncJobStatusCallback = mockAsyncJobStatusCallback, + isContextualImage = false, + clipping = stubWireframeClip + ) + + // Then + verify(mockResourceResolver).resolveResourceId( + bitmap = any(), + resourceResolverCallback = any() + ) + verify(mockAsyncJobStatusCallback).jobStarted() + verify(mockAsyncJobStatusCallback).jobFinished() + verifyNoMoreInteractions(mockAsyncJobStatusCallback) + assertThat(wireframe).isEqualTo(expectedWireframe) + } + + // region createImageWireframeByBitmap + + @Test + fun `M return content placeholder W createImageWireframeByBitmap { ImagePrivacy MASK_ALL }`() { + // When + val wireframe = testedHelper.createImageWireframeByBitmap( + id = fakeViewId, + bitmap = mockBitmap, + density = fakeDensity, + imagePrivacy = ImagePrivacy.MASK_ALL, + isContextualImage = false, + globalBounds = fakeBounds, + shapeStyle = null, + border = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + assertThat(wireframe).isInstanceOf(MobileSegment.Wireframe.PlaceholderWireframe::class.java) + } @Test - fun `M return content placeholder W createImageWireframeByDrawable() { ImagePrivacy MASK_ALL }`() { + fun `M return placeholder W createImageWireframeByBitmap { MASK_LARGE_ONLY & isContextual }`() { + // When + val wireframe = testedHelper.createImageWireframeByBitmap( + id = fakeViewId, + bitmap = mockBitmap, + density = fakeDensity, + imagePrivacy = ImagePrivacy.MASK_LARGE_ONLY, + isContextualImage = true, + globalBounds = fakeBounds, + shapeStyle = null, + border = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + assertThat(wireframe).isInstanceOf(MobileSegment.Wireframe.PlaceholderWireframe::class.java) + } + + @Test + fun `M call jobFinished W createImageWireframeByBitmap { failure }`() { + // Given + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) + .thenReturn(fakeGeneratedIdentifier) + + whenever( + mockResourceResolver.resolveResourceId( + bitmap = any(), + resourceResolverCallback = any() + ) + ).thenAnswer { + val callback = it.getArgument(1) + callback.onFailure() + } + + // When + testedHelper.createImageWireframeByBitmap( + id = fakeViewId, + bitmap = mockBitmap, + density = fakeDensity, + imagePrivacy = ImagePrivacy.MASK_LARGE_ONLY, + isContextualImage = false, + globalBounds = fakeBounds, + shapeStyle = null, + border = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + verify(mockAsyncJobStatusCallback).jobStarted() + verify(mockAsyncJobStatusCallback).jobFinished() + verifyNoMoreInteractions(mockAsyncJobStatusCallback) + } + + // endregion + + // region createImageWireframeByDrawable + + @Test + fun `M call jobFinished W createImageWireframeByDrawable { failure }`() { + // Given + whenever(mockViewIdentifierResolver.resolveChildUniqueIdentifier(any(), any())) + .thenReturn(fakeGeneratedIdentifier) + whenever( + mockResourceResolver.resolveResourceId( + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), + resourceResolverCallback = any() + ) + ).thenAnswer { + val callback = it.getArgument(8) + callback.onFailure() + } + + // When + testedHelper.createImageWireframeByDrawable( + view = mockView, + imagePrivacy = ImagePrivacy.MASK_LARGE_ONLY, + currentWireframeIndex = 0, + x = 0, + y = 0, + width = 100, + height = 100, + drawable = mockDrawable, + shapeStyle = null, + border = null, + usePIIPlaceholder = true, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + verify(mockAsyncJobStatusCallback).jobStarted() + verify(mockAsyncJobStatusCallback).jobFinished() + verifyNoMoreInteractions(mockAsyncJobStatusCallback) + } + + @Test + fun `M use customResourceIdCacheKey W createImageWireframeByDrawable { key provided }`( + forge: Forge, + @StringForgery fakeResourceIdCacheKey: String + ) { + // Given + val fakeXPosition = forge.aPositiveLong() + val fakeYPosition = forge.aPositiveLong() + val fakeWidth = forge.aPositiveInt() + val fakeHeight = forge.aPositiveInt() + + // When + testedHelper.createImageWireframeByDrawable( + view = mockView, + imagePrivacy = ImagePrivacy.MASK_NONE, + currentWireframeIndex = 0, + x = fakeXPosition, + y = fakeYPosition, + width = fakeWidth, + height = fakeHeight, + drawable = mockDrawable, + shapeStyle = null, + border = null, + usePIIPlaceholder = false, + customResourceIdCacheKey = fakeResourceIdCacheKey, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + verify(mockResourceResolver).resolveResourceId( + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = eq(fakeResourceIdCacheKey), + resourceResolverCallback = any() + ) + } + + @Test + fun `M return content placeholder W createImageWireframeByDrawable { ImagePrivacy MASK_ALL }`() { // When val wireframe = testedHelper.createImageWireframeByDrawable( view = mockView, @@ -196,6 +418,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -223,7 +446,7 @@ internal class DefaultImageWireframeHelperTest { } @Test - fun `M not return image wireframe W createImageWireframe(usePIIPlaceholder = true) { ImagePrivacy MASK_NONE }`() { + fun `M not return image wireframe W createImageWireframeByDrawable(usePIIPlaceholder = true) { MASK_NONE }`() { // When val wireframe = testedHelper.createImageWireframeByDrawable( view = mockView, @@ -237,6 +460,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -245,7 +469,7 @@ internal class DefaultImageWireframeHelperTest { } @Test - fun `M not return image wireframe W createImageWireframe { ImagePrivacy MASK_LARGE_ONLY & isContextual image}`() { + fun `M not return image wireframe W createImageWireframeByBitmap { MASK_LARGE_ONLY & isContextual image}`() { // When val wireframe = testedHelper.createImageWireframeByBitmap( id = fakeViewId, @@ -264,7 +488,7 @@ internal class DefaultImageWireframeHelperTest { } @Test - fun `M return null W createImageWireframe() { application context is null }`() { + fun `M return null W createImageWireframeByDrawable() { application context is null }`() { // Given whenever(mockView.context.applicationContext).thenReturn(null) @@ -281,6 +505,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -306,6 +531,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -335,6 +561,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -365,6 +592,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -388,6 +616,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -410,6 +639,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -428,17 +658,18 @@ internal class DefaultImageWireframeHelperTest { .thenReturn(fakeGeneratedIdentifier) whenever( mockResourceResolver.resolveResourceId( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any() + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), + resourceResolverCallback = any() ) ).thenAnswer { - val callback = it.arguments[7] as ResourceResolverCallback + val callback = it.getArgument(8) callback.onSuccess(fakeResourceId) } @@ -469,7 +700,8 @@ internal class DefaultImageWireframeHelperTest { border = mockBorder, asyncJobStatusCallback = mockAsyncJobStatusCallback, usePIIPlaceholder = true, - clipping = stubWireframeClip + clipping = stubWireframeClip, + customResourceIdCacheKey = null ) // Then @@ -481,6 +713,7 @@ internal class DefaultImageWireframeHelperTest { drawableCopier = any(), drawableWidth = any(), drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), resourceResolverCallback = any() ) verify(mockAsyncJobStatusCallback).jobStarted() @@ -502,7 +735,7 @@ internal class DefaultImageWireframeHelperTest { resourceResolverCallback = any() ) ).thenAnswer { - val callback = it.arguments[1] as ResourceResolverCallback + val callback = it.getArgument(1) callback.onSuccess(fakeResourceId) } @@ -544,6 +777,73 @@ internal class DefaultImageWireframeHelperTest { assertThat(wireframe).isEqualTo(expectedWireframe) } + @Test + fun `M use intrinsic dimensions W createImageWireframeByBitmap { inset drawable }`( + @Mock mockInsetDrawable: InsetDrawable, + forge: Forge + ) { + // Given + val fakeWidth = forge.aPositiveInt() + val fakeHeight = forge.aPositiveInt() + whenever(mockInsetDrawable.intrinsicWidth).thenReturn(fakeWidth) + whenever(mockInsetDrawable.intrinsicHeight).thenReturn(fakeHeight) + + // When + val placeholderWireframe = testedHelper.createImageWireframeByDrawable( + view = mockView, + imagePrivacy = ImagePrivacy.MASK_ALL, + currentWireframeIndex = 0, + x = 0, + y = 0, + width = fakeWidth, + height = fakeHeight, + drawable = mockInsetDrawable, + shapeStyle = null, + border = null, + usePIIPlaceholder = true, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) as MobileSegment.Wireframe.PlaceholderWireframe + + // Then + assertThat(placeholderWireframe.width.toInt()).isEqualTo(fakeWidth) + assertThat(placeholderWireframe.height.toInt()).isEqualTo(fakeHeight) + } + + @Test + fun `M use intrinsic dimensions W createImageWireframeByBitmap { layerDrawable no layers }`( + @Mock mockLayerDrawable: LayerDrawable, + forge: Forge + ) { + // Given + val fakeWidth = forge.aPositiveInt() + val fakeHeight = forge.aPositiveInt() + whenever(mockLayerDrawable.intrinsicWidth).thenReturn(fakeWidth) + whenever(mockLayerDrawable.intrinsicHeight).thenReturn(fakeHeight) + whenever(mockLayerDrawable.numberOfLayers).thenReturn(0) + + // When + val placeholderWireframe = testedHelper.createImageWireframeByDrawable( + view = mockView, + imagePrivacy = ImagePrivacy.MASK_ALL, + currentWireframeIndex = 0, + x = 0, + y = 0, + width = fakeWidth, + height = fakeHeight, + drawable = mockLayerDrawable, + shapeStyle = null, + border = null, + usePIIPlaceholder = true, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) as MobileSegment.Wireframe.PlaceholderWireframe + + // Then + assertThat(placeholderWireframe.width.toInt()).isEqualTo(fakeWidth) + assertThat(placeholderWireframe.height.toInt()).isEqualTo(fakeHeight) + } + // endregion // region createCompoundDrawableWireframes @@ -556,9 +856,10 @@ internal class DefaultImageWireframeHelperTest { // When val wireframes = testedHelper.createCompoundDrawableWireframes( - mockTextView, - mockMappingContext, - 0, + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -572,10 +873,10 @@ internal class DefaultImageWireframeHelperTest { // Given whenever( mockViewUtilsInternal.resolveCompoundDrawableBounds( - any(), - any(), - any(), - any() + view = any(), + drawable = any(), + pixelsDensity = any(), + position = any() ) ).thenReturn(fakeBounds) val fakeDrawables = arrayOf(null, mockDrawable, null, null) @@ -584,12 +885,14 @@ internal class DefaultImageWireframeHelperTest { // When val wireframes = testedHelper.createCompoundDrawableWireframes( - mockTextView, - mockMappingContext, - 0, + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) - wireframes[0] as MobileSegment.Wireframe.ImageWireframe + + assertThat(wireframes[0]).isInstanceOf(MobileSegment.Wireframe.ImageWireframe::class.java) // Then val argumentCaptor = argumentCaptor() @@ -602,6 +905,7 @@ internal class DefaultImageWireframeHelperTest { drawableCopier = any(), drawableWidth = any(), drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), resourceResolverCallback = argumentCaptor.capture() ) argumentCaptor.allValues.forEach { @@ -609,7 +913,7 @@ internal class DefaultImageWireframeHelperTest { } verify(mockAsyncJobStatusCallback).jobStarted() verify(mockAsyncJobStatusCallback).jobFinished() - assertThat(wireframes.size).isEqualTo(1) + assertThat(wireframes).hasSize(1) } @Test @@ -617,10 +921,10 @@ internal class DefaultImageWireframeHelperTest { // Given whenever( mockViewUtilsInternal.resolveCompoundDrawableBounds( - any(), - any(), - any(), - any() + view = any(), + drawable = any(), + pixelsDensity = any(), + position = any() ) ) .thenReturn(fakeBounds) @@ -630,31 +934,34 @@ internal class DefaultImageWireframeHelperTest { // When val wireframes = testedHelper.createCompoundDrawableWireframes( - mockTextView, - mockMappingContext, - 0, + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) - wireframes[0] as MobileSegment.Wireframe.ImageWireframe + + assertThat(wireframes[0]).isInstanceOf(MobileSegment.Wireframe.ImageWireframe::class.java) // Then val argumentCaptor = argumentCaptor() verify(mockResourceResolver, times(2)).resolveResourceId( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - argumentCaptor.capture() + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), + resourceResolverCallback = argumentCaptor.capture() ) argumentCaptor.allValues.forEach { it.onSuccess(fakeResourceId) } verify(mockAsyncJobStatusCallback, times(2)).jobStarted() verify(mockAsyncJobStatusCallback, times(2)).jobFinished() - assertThat(wireframes.size).isEqualTo(2) + assertThat(wireframes).hasSize(2) } @Test @@ -665,9 +972,10 @@ internal class DefaultImageWireframeHelperTest { // When val wireframes = testedHelper.createCompoundDrawableWireframes( - mockTextView, - mockMappingContext, - 0, + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -704,6 +1012,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -717,6 +1026,7 @@ internal class DefaultImageWireframeHelperTest { drawableCopier = any(), drawableWidth = captor.capture(), drawableHeight = captor.capture(), + customResourceIdCacheKey = anyOrNull(), resourceResolverCallback = any() ) assertThat(captor.allValues).containsExactly(fakeViewWidth, fakeViewHeight) @@ -737,6 +1047,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -750,6 +1061,7 @@ internal class DefaultImageWireframeHelperTest { drawableCopier = any(), drawableWidth = captor.capture(), drawableHeight = captor.capture(), + customResourceIdCacheKey = anyOrNull(), resourceResolverCallback = any() ) @@ -757,14 +1069,19 @@ internal class DefaultImageWireframeHelperTest { } @Test - fun `M not try to resolve bitmap W createImageWireframe() { PII image }`( + fun `M not try to resolve bitmap W createImageWireframeByDrawable() { PII image }`( forge: Forge, @Mock mockResources: Resources, @Mock mockDisplayMetrics: DisplayMetrics, @Mock mockContext: Context ) { // Given - whenever(mockImageTypeResolver.isDrawablePII(any(), any())).thenReturn(true) + whenever( + mockImageTypeResolver.isDrawablePII( + drawable = any(), + density = any() + ) + ).thenReturn(true) val fakeGlobalX = forge.aPositiveInt() val fakeGlobalY = forge.aPositiveInt() @@ -789,6 +1106,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) as MobileSegment.Wireframe.PlaceholderWireframe @@ -799,7 +1117,7 @@ internal class DefaultImageWireframeHelperTest { } @Test - fun `M try to resolve bitmap W createImageWireframe() { non-PII image }`() { + fun `M try to resolve bitmap W createImageWireframeByDrawable() { non-PII image }`() { // Given whenever(mockImageTypeResolver.isDrawablePII(any(), any())).thenReturn(false) @@ -816,6 +1134,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -828,12 +1147,13 @@ internal class DefaultImageWireframeHelperTest { drawableCopier = any(), drawableWidth = any(), drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), resourceResolverCallback = any() ) } @Test - fun `M return content placeholder W createImageWireframe() { PII image }`() { + fun `M return content placeholder W createImageWireframeByDrawable() { PII image }`() { // Given whenever(mockImageTypeResolver.isDrawablePII(any(), any())).thenReturn(true) @@ -850,6 +1170,7 @@ internal class DefaultImageWireframeHelperTest { shapeStyle = null, border = null, usePIIPlaceholder = true, + customResourceIdCacheKey = null, asyncJobStatusCallback = mockAsyncJobStatusCallback ) @@ -860,4 +1181,189 @@ internal class DefaultImageWireframeHelperTest { } // endregion + + // region createCompoundDrawableWireframes + + @Test + fun `M use correct customResourceIdCacheKeys W createCompoundDrawableWireframes { key provided }`( + @StringForgery fakeResourceIdCacheKey: String + ) { + // Given + whenever(mockTextView.compoundDrawables) + .thenReturn(arrayOf(mockDrawable, mockDrawable, mockDrawable, mockDrawable)) + + whenever( + mockViewUtilsInternal.resolveCompoundDrawableBounds( + view = any(), + drawable = any(), + pixelsDensity = any(), + position = any() + ) + ).thenReturn(fakeBounds) + + // When + testedHelper.createCompoundDrawableWireframes( + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = fakeResourceIdCacheKey, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + mockTextView.compoundDrawables.forEachIndexed { index, _ -> + val expectedKey = fakeResourceIdCacheKey + "_$index" + + // Then + verify(mockResourceResolver).resolveResourceId( + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = eq(expectedKey), + resourceResolverCallback = any() + ) + } + } + + @Test + fun `M return empty list W createCompoundDrawableWireframes { no compound drawables }`() { + // Given + whenever(mockTextView.compoundDrawables) + .thenReturn(arrayOf(null, null, null, null)) + + // When + val wireframes = testedHelper.createCompoundDrawableWireframes( + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + verifyNoInteractions(mockAsyncJobStatusCallback) + assertThat(wireframes).isEmpty() + } + + @Test + fun `M return wireframe W createCompoundDrawableWireframes`() { + // Given + val fakeDrawables = arrayOf(null, mockDrawable, null, null) + whenever(mockTextView.compoundDrawables) + .thenReturn(fakeDrawables) + + whenever( + mockViewUtilsInternal.resolveCompoundDrawableBounds( + view = any(), + drawable = any(), + pixelsDensity = any(), + position = any() + ) + ).thenReturn(fakeBounds) + + // When + val wireframes = testedHelper.createCompoundDrawableWireframes( + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + assertThat(wireframes[0]).isInstanceOf(MobileSegment.Wireframe.ImageWireframe::class.java) + + // Then + val argumentCaptor = argumentCaptor() + + verify(mockResourceResolver).resolveResourceId( + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), + resourceResolverCallback = argumentCaptor.capture() + ) + argumentCaptor.allValues.forEach { + it.onSuccess(fakeResourceId) + } + verify(mockAsyncJobStatusCallback).jobStarted() + verify(mockAsyncJobStatusCallback).jobFinished() + assertThat(wireframes).hasSize(1) + } + + @Test + fun `M return multiple wireframes W createCompoundDrawableWireframes { multiple drawables }`() { + // Given + whenever( + mockViewUtilsInternal.resolveCompoundDrawableBounds( + view = any(), + drawable = any(), + pixelsDensity = any(), + position = any() + ) + ) + .thenReturn(fakeBounds) + val fakeDrawables = arrayOf(null, mockDrawable, null, mockDrawable) + whenever(mockTextView.compoundDrawables) + .thenReturn(fakeDrawables) + + // When + val wireframes = testedHelper.createCompoundDrawableWireframes( + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + assertThat(wireframes[0]).isInstanceOf(MobileSegment.Wireframe.ImageWireframe::class.java) + + // Then + val argumentCaptor = argumentCaptor() + verify(mockResourceResolver, times(2)).resolveResourceId( + resources = any(), + applicationContext = any(), + displayMetrics = any(), + originalDrawable = any(), + drawableCopier = any(), + drawableWidth = any(), + drawableHeight = any(), + customResourceIdCacheKey = anyOrNull(), + resourceResolverCallback = argumentCaptor.capture() + ) + argumentCaptor.allValues.forEach { + it.onSuccess(fakeResourceId) + } + verify(mockAsyncJobStatusCallback, times(2)).jobStarted() + verify(mockAsyncJobStatusCallback, times(2)).jobFinished() + assertThat(wireframes).hasSize(2) + } + + @Test + fun `M skip invalid elements W createCompoundDrawableWireframes { invalid indices }`() { + // Given + whenever(mockTextView.compoundDrawables) + .thenReturn(arrayOf(null, null, null, null, null, null)) + + // When + val wireframes = testedHelper.createCompoundDrawableWireframes( + textView = mockTextView, + mappingContext = mockMappingContext, + prevWireframeIndex = 0, + customResourceIdCacheKey = null, + asyncJobStatusCallback = mockAsyncJobStatusCallback + ) + + // Then + verifyNoInteractions(mockAsyncJobStatusCallback) + assertThat(wireframes).isEmpty() + } + + // endregion } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt index a51395680e..cee6006dac 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt @@ -11,7 +11,6 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import android.graphics.drawable.Drawable.ConstantState import android.graphics.drawable.LayerDrawable import android.graphics.drawable.StateListDrawable import android.util.DisplayMetrics @@ -19,6 +18,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler import com.datadog.android.sessionreplay.internal.utils.DrawableUtils +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.StringForgery @@ -108,9 +108,6 @@ internal class ResourceResolverTest { @Mock lateinit var mockResources: Resources - @Mock - lateinit var mockBitmapConstantState: ConstantState - private var fakeBitmapWidth: Int = 1 private var fakeBitmapHeight: Int = 1 @@ -118,6 +115,9 @@ internal class ResourceResolverTest { @Forgery lateinit var fakeApplicationid: UUID + @StringForgery + lateinit var fakeResourceKey: String + @StringForgery lateinit var fakeResourceId: String @@ -128,6 +128,7 @@ internal class ResourceResolverTest { whenever(mockDrawableCopier.copy(eq(mockBitmapDrawable), any())).thenReturn( mockBitmapDrawable ) + whenever(mockBitmapCachesManager.generateResourceKeyFromDrawable(mockDrawable)).thenReturn(fakeResourceKey) whenever(mockDrawableCopier.copy(eq(mockDrawable), any())).thenReturn(mockDrawable) fakeImageCompressionByteArray = forge.aString().toByteArray() @@ -141,7 +142,6 @@ internal class ResourceResolverTest { whenever( mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -151,7 +151,7 @@ internal class ResourceResolverTest { bitmapCreationCallback = any() ) ).then { - (it.arguments[7] as ResourceResolver.BitmapCreationCallback).onReady(mockBitmap) + (it.getArgument(6)).onReady(mockBitmap) } // executeSafe is an extension so we have to mock the internal execute function @@ -175,7 +175,7 @@ internal class ResourceResolverTest { @Test fun `M get data from cache W resolveResourceId() { cache hit with resourceId }`() { // Given - whenever(mockBitmapCachesManager.getFromResourceCache(mockDrawable)).thenReturn(fakeResourceId) + whenever(mockBitmapCachesManager.getFromResourceCache(fakeResourceKey)).thenReturn(fakeResourceId) whenever(mockWebPImageCompression.compressBitmap(any())) .thenReturn(fakeImageCompressionByteArray) @@ -189,6 +189,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -210,7 +211,7 @@ internal class ResourceResolverTest { val emptyByteArray = ByteArray(0) - whenever(mockBitmapCachesManager.getFromResourceCache(mockBitmapDrawable)) + whenever(mockBitmapCachesManager.getFromResourceCache(fakeResourceKey)) .thenReturn(null) whenever(mockWebPImageCompression.compressBitmap(any())) @@ -226,12 +227,12 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) // Then verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -263,6 +264,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -289,6 +291,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -321,7 +324,7 @@ internal class ResourceResolverTest { @Test fun `M calculate resourceId W resolveResourceId() { cache miss }`() { // Given - whenever(mockResourcesLRUCache.get(mockDrawable)).thenReturn(null) + whenever(mockResourcesLRUCache.get(fakeResourceKey)).thenReturn(null) // When testedResourceResolver.resolveResourceId( @@ -332,12 +335,12 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) // Then verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -351,10 +354,9 @@ internal class ResourceResolverTest { @Test fun `M return failure W resolveResourceId { createBitmapOfApproxSizeFromDrawable failed }`() { // Given - whenever(mockResourcesLRUCache.get(mockDrawable)).thenReturn(null) + whenever(mockResourcesLRUCache.get(fakeResourceKey)).thenReturn(null) whenever( mockDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -364,7 +366,7 @@ internal class ResourceResolverTest { bitmapCreationCallback = any() ) ).then { - (it.arguments[7] as ResourceResolver.BitmapCreationCallback).onFailure() + (it.getArgument(6)).onFailure() } // When @@ -376,6 +378,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -425,6 +428,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -446,12 +450,12 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) // Then verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -476,12 +480,12 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) // Then verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -503,6 +507,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -527,6 +532,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -536,7 +542,6 @@ internal class ResourceResolverTest { anyOrNull() ) verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -561,6 +566,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -570,7 +576,6 @@ internal class ResourceResolverTest { anyOrNull() ) verify(mockDrawableUtils, times(1)).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -596,6 +601,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -628,6 +634,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -660,6 +667,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -670,7 +678,7 @@ internal class ResourceResolverTest { @Test fun `M cache bitmap W resolveResourceId() { from BitmapDrawable with null bitmap }`() { // Given - whenever(mockBitmapCachesManager.getFromResourceCache(mockBitmapDrawable)) + whenever(mockBitmapCachesManager.getFromResourceCache(fakeResourceKey)) .thenReturn(null) whenever(mockBitmapDrawable.bitmap).thenReturn(null) @@ -683,6 +691,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -704,6 +713,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -718,12 +728,16 @@ internal class ResourceResolverTest { @Mock mockFirstDrawable: Drawable, @Mock mockSecondDrawable: Drawable, @StringForgery fakeFirstResourceId: String, - @StringForgery fakeSecondResourceId: String + @StringForgery fakeSecondResourceId: String, + @StringForgery fakeFirstKey: String, + @StringForgery fakeSecondKey: String ) { // Given - whenever(mockBitmapCachesManager.getFromResourceCache(mockFirstDrawable)) + whenever(mockBitmapCachesManager.generateResourceKeyFromDrawable(mockFirstDrawable)).thenReturn(fakeFirstKey) + whenever(mockBitmapCachesManager.generateResourceKeyFromDrawable(mockSecondDrawable)).thenReturn(fakeSecondKey) + whenever(mockBitmapCachesManager.getFromResourceCache(fakeFirstKey)) .thenReturn(fakeFirstResourceId) - whenever(mockBitmapCachesManager.getFromResourceCache(mockSecondDrawable)) + whenever(mockBitmapCachesManager.getFromResourceCache(fakeSecondKey)) .thenReturn(fakeSecondResourceId) val countDownLatch = CountDownLatch(2) @@ -736,6 +750,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockFirstCallback ) Thread.sleep(1500) @@ -750,6 +765,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSecondCallback ) Thread.sleep(500) @@ -844,12 +860,12 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) // Then verify(mockDrawableUtils).createBitmapOfApproxSizeFromDrawable( - resources = any(), drawable = any(), drawableWidth = any(), drawableHeight = any(), @@ -896,6 +912,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -910,6 +927,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -928,6 +946,7 @@ internal class ResourceResolverTest { drawableCopier = mockDrawableCopier, drawableWidth = fakeBitmapWidth, drawableHeight = fakeBitmapHeight, + customResourceIdCacheKey = null, resourceResolverCallback = mockSerializerCallback ) @@ -938,6 +957,76 @@ internal class ResourceResolverTest { ) } + @Test + fun `M return cache miss W resolveResourceId() { failed to generate resource key }`() { + // Given + whenever(mockBitmapCachesManager.generateResourceKeyFromDrawable(mockDrawable)).thenReturn(null) + + // When + testedResourceResolver.resolveResourceId( + resources = mockResources, + applicationContext = mockApplicationContext, + displayMetrics = mockDisplayMetrics, + originalDrawable = mockDrawable, + drawableCopier = mockDrawableCopier, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = null, + resourceResolverCallback = mockSerializerCallback + ) + + // Then + verify(mockDrawableCopier).copy(mockDrawable, mockResources) + } + + @Test + fun `M use cache key W resolveResourceId() { cache hit, key provided } `( + @StringForgery fakeCacheKey: String + ) { + // Given + whenever(mockBitmapCachesManager.getFromResourceCache(fakeCacheKey)).thenReturn(fakeResourceId) + + // When + testedResourceResolver.resolveResourceId( + resources = mockResources, + applicationContext = mockApplicationContext, + displayMetrics = mockDisplayMetrics, + originalDrawable = mockDrawable, + drawableCopier = mockDrawableCopier, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = fakeCacheKey, + resourceResolverCallback = mockSerializerCallback + ) + + // Then + verify(mockSerializerCallback).onSuccess(fakeResourceId) + } + + @Test + fun `M use cache key W resolveResourceId() { cache miss, key provided } `( + @StringForgery fakeCacheKey: String + ) { + // Given + whenever(mockBitmapCachesManager.getFromResourceCache(fakeCacheKey)).thenReturn(null) + + // When + testedResourceResolver.resolveResourceId( + resources = mockResources, + applicationContext = mockApplicationContext, + displayMetrics = mockDisplayMetrics, + originalDrawable = mockDrawable, + drawableCopier = mockDrawableCopier, + drawableWidth = mockDrawable.intrinsicWidth, + drawableHeight = mockDrawable.intrinsicHeight, + customResourceIdCacheKey = fakeCacheKey, + resourceResolverCallback = mockSerializerCallback + ) + + // Then + verify(mockBitmapCachesManager).putInResourceCache(fakeCacheKey, fakeResourceId) + } + private fun createResourceResolver(): ResourceResolver = ResourceResolver( logger = mockLogger, threadPoolExecutor = mockExecutorService, diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCacheTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCacheTest.kt index cf8288198d..54c9def650 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCacheTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourcesLRUCacheTest.kt @@ -47,6 +47,9 @@ internal class ResourcesLRUCacheTest { @Mock lateinit var mockInvocationUtils: InvocationUtils + @StringForgery + lateinit var fakeResourceKey: String + val argumentCaptor = argumentCaptor() @BeforeEach @@ -61,7 +64,7 @@ internal class ResourcesLRUCacheTest { @Test fun `M return null W get() { item not in cache }`() { // When - val cacheItem = testedCache.get(mockDrawable) + val cacheItem = testedCache.get(fakeResourceKey) // Then assertThat(cacheItem).isNull() @@ -73,57 +76,47 @@ internal class ResourcesLRUCacheTest { ) { // Given val fakeResourceIdByteArray = fakeResourceId.toByteArray(Charsets.UTF_8) - testedCache.put(mockDrawable, fakeResourceIdByteArray) + testedCache.put(fakeResourceKey, fakeResourceIdByteArray) // When - val cacheItem = testedCache.get(mockDrawable) + val cacheItem = testedCache.get(fakeResourceKey) // Then assertThat(cacheItem).isEqualTo(fakeResourceIdByteArray) } @Test - fun `M not generate prefix W put() { animationDrawable }`( - @StringForgery fakeResourceId: String - ) { + fun `M not generate prefix W put() { animationDrawable }`() { // Given - val fakeResourceIdByteArray = fakeResourceId.toByteArray(Charsets.UTF_8) val mockAnimationDrawable: AnimationDrawable = mock() // When - testedCache.put(mockAnimationDrawable, fakeResourceIdByteArray) + val key = testedCache.generateKeyFromDrawable(mockAnimationDrawable) // Then - val key = testedCache.generateKey(mockAnimationDrawable) assertThat(key).doesNotContain("-") } @Test fun `M generate key prefix with state W put() { drawableContainer }`( - @StringForgery fakeResourceId: String, forge: Forge ) { // Given - val fakeResourceIdByteArray = fakeResourceId.toByteArray(Charsets.UTF_8) val mockStatelistDrawable: StateListDrawable = mock() val fakeStateArray = intArrayOf(forge.aPositiveInt()) val expectedPrefix = fakeStateArray[0].toString() + "-" whenever(mockStatelistDrawable.state).thenReturn(fakeStateArray) // When - testedCache.put(mockStatelistDrawable, fakeResourceIdByteArray) + val key = testedCache.generateKeyFromDrawable(mockStatelistDrawable) // Then - val key = testedCache.generateKey(mockStatelistDrawable) assertThat(key).startsWith(expectedPrefix) } @Test - fun `M generate key prefix with layer hash W put() { layerDrawable }`( - @StringForgery fakeResourceId: String - ) { + fun `M generate key prefix with layer hash W put() { layerDrawable }`() { // Given - val fakeResourceIdByteArray = fakeResourceId.toByteArray(Charsets.UTF_8) val mockRippleDrawable: RippleDrawable = mock() val mockBackgroundLayer: Drawable = mock() val mockForegroundLayer: Drawable = mock() @@ -134,14 +127,12 @@ internal class ResourcesLRUCacheTest { .thenReturn(mockForegroundLayer) whenever(mockRippleDrawable.numberOfLayers).thenReturn(2) - testedCache.put(mockRippleDrawable, fakeResourceIdByteArray) - val expectedPrefix = System.identityHashCode(mockBackgroundLayer).toString() + "-" + System.identityHashCode(mockForegroundLayer).toString() + "-" val expectedHash = System.identityHashCode(mockRippleDrawable).toString() // When - val key = testedCache.generateKey(mockRippleDrawable) + val key = testedCache.generateKeyFromDrawable(mockRippleDrawable) // Then assertThat(key).isEqualTo(expectedPrefix + expectedHash) @@ -149,21 +140,18 @@ internal class ResourcesLRUCacheTest { @Test fun `M not generate key prefix W put() { layerDrawable with only one layer }`( - @StringForgery fakeResourceId: String, @Mock mockRippleDrawable: RippleDrawable, @Mock mockBackgroundLayer: Drawable ) { // Given - val fakeResourceIdByteArray = fakeResourceId.toByteArray(Charsets.UTF_8) whenever(mockRippleDrawable.numberOfLayers).thenReturn(1) whenever(mockRippleDrawable.safeGetDrawable(0)).thenReturn(mockBackgroundLayer) - testedCache.put(mockRippleDrawable, fakeResourceIdByteArray) val expectedPrefix = System.identityHashCode(mockBackgroundLayer).toString() + "-" val drawableHash = System.identityHashCode(mockRippleDrawable).toString() // When - val key = testedCache.generateKey(mockRippleDrawable) + val key = testedCache.generateKeyFromDrawable(mockRippleDrawable) // Then assertThat(key).isEqualTo(expectedPrefix + drawableHash) 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 e46f22922d..60b419f7da 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 @@ -33,7 +33,6 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -141,7 +140,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -179,7 +177,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -214,7 +211,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -254,7 +250,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -279,7 +274,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -297,12 +291,13 @@ internal class DrawableUtilsTest { // Given whenever(mockDrawable.intrinsicWidth).thenReturn(1) whenever(mockDrawable.intrinsicHeight).thenReturn(1) - whenever(mockConstantState.newDrawable(mockResources)) - .thenReturn(null) + whenever(mockBitmap.isRecycled).thenReturn(true) + whenever(mockBitmapWrapper.createBitmap(any(), any(), any(), any())).thenReturn( + null + ) // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -325,7 +320,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -353,7 +347,6 @@ internal class DrawableUtilsTest { // When testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, drawable = mockDrawable, drawableWidth = mockDrawable.intrinsicWidth, drawableHeight = mockDrawable.intrinsicHeight, @@ -378,30 +371,6 @@ internal class DrawableUtilsTest { assertThat(displayMetricsCaptor.firstValue).isEqualTo(mockDisplayMetrics) } - @Test - fun `M not use original drawable W createBitmapOfApproxSizeFromDrawable`() { - // Given - whenever(mockDrawable.intrinsicWidth).thenReturn(1) - whenever(mockDrawable.intrinsicHeight).thenReturn(1) - - // When - testedDrawableUtils.createBitmapOfApproxSizeFromDrawable( - resources = mockResources, - drawable = mockDrawable, - drawableWidth = mockDrawable.intrinsicWidth, - drawableHeight = mockDrawable.intrinsicHeight, - displayMetrics = mockDisplayMetrics, - config = mockConfig, - bitmapCreationCallback = mockBitmapCreationCallback - ) - - // Then - verify(mockDrawable, never()).setBounds(any(), any(), any(), any()) - verify(mockDrawable, never()).draw(any()) - verify(mockSecondDrawable).setBounds(any(), any(), any(), any()) - verify(mockSecondDrawable).draw(any()) - } - @Test fun `M return scaled bitmap W createScaledBitmap()`( @Mock mockScaledBitmap: Bitmap diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtilsTest.kt index 45bf0adcdd..6e753edb14 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/ImageViewUtilsTest.kt @@ -10,6 +10,7 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView +import com.datadog.android.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.utils.isCloseToOrGreaterThan @@ -72,7 +73,7 @@ internal class ImageViewUtilsTest { ) // Then - assertThat(result).isEqualTo(expectedClipping) + assertThat(result.toWireframeClip()).isEqualTo(expectedClipping) } @Test @@ -104,7 +105,7 @@ internal class ImageViewUtilsTest { ) // Then - assertThat(result).isEqualTo(expectedClipping) + assertThat(result.toWireframeClip()).isEqualTo(expectedClipping) } @Test @@ -136,7 +137,7 @@ internal class ImageViewUtilsTest { ) // Then - assertThat(result).isEqualTo(expectedClipping) + assertThat(result.toWireframeClip()).isEqualTo(expectedClipping) } @Test @@ -168,7 +169,7 @@ internal class ImageViewUtilsTest { ) // Then - assertThat(result).isEqualTo(expectedClipping) + assertThat(result.toWireframeClip()).isEqualTo(expectedClipping) } @Test @@ -200,7 +201,7 @@ internal class ImageViewUtilsTest { ) // Then - assertThat(result).isEqualTo(expectedClipping) + assertThat(result.toWireframeClip()).isEqualTo(expectedClipping) } // endregion @@ -725,4 +726,51 @@ internal class ImageViewUtilsTest { // Then assertThat(result).isEqualTo(parentRect) } + + @Test + fun `M returns content rect W resolveContentRectWithScaling() { custom scale type }`( + @Mock mockDrawable: Drawable, + forge: Forge + ) { + // Given + val fakeGlobalX = forge.aPositiveInt() + val fakeGlobalY = forge.aPositiveInt() + val fakeWidth = forge.aPositiveInt() + val fakeHeight = forge.aPositiveInt() + val fakeDrawableWidth = forge.aPositiveInt() + val fakeDrawableHeight = forge.aPositiveInt() + val fakeScaleType = ImageView.ScaleType.FIT_END + whenever(mockDrawable.intrinsicWidth).thenReturn(fakeDrawableWidth) + whenever(mockDrawable.intrinsicHeight).thenReturn(fakeDrawableHeight) + + val mockImageView: ImageView = mock { + whenever(it.getLocationOnScreen(any())).thenAnswer { + val coords = it.arguments[0] as IntArray + coords[0] = fakeGlobalX + coords[1] = fakeGlobalY + null + } + whenever(it.width).thenReturn(fakeWidth) + whenever(it.height).thenReturn(fakeHeight) + whenever(it.scaleType).thenReturn(fakeScaleType) + } + + val parentRect = Rect( + fakeGlobalX, + fakeGlobalY, + fakeGlobalX + fakeWidth, + fakeGlobalY + fakeHeight + ) + + // When + val result = testedImageViewUtils.resolveContentRectWithScaling( + imageView = mockImageView, + drawable = mockDrawable, + customScaleType = ImageView.ScaleType.CENTER_CROP + ) + + // Then (expect CENTER_CROP behavior) + assertThat(result.width().isCloseToOrGreaterThan(parentRect.width())).isTrue + assertThat(result.height().isCloseToOrGreaterThan(parentRect.height())).isTrue + } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt index 12187927b2..1d2c73c720 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/MiscUtilsTest.kt @@ -19,8 +19,8 @@ import android.view.Display import android.view.WindowManager import android.view.WindowMetrics import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.internal.utils.MiscUtils.DESERIALIZE_JSON_ERROR import com.datadog.android.sessionreplay.recorder.SystemInformation import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapperTest.kt index 1e26cfe75d..462f8c0b21 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/ImageViewMapperTest.kt @@ -16,12 +16,13 @@ import android.util.DisplayMetrics import android.view.View import android.widget.ImageView import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.utils.ImageViewUtils import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.SystemInformation +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper @@ -100,6 +101,9 @@ internal class ImageViewMapperTest { @Mock lateinit var mockDrawableToColorMapper: DrawableToColorMapper + @Mock + lateinit var mockDrawableCopier: DrawableCopier + @Mock lateinit var mockGlobalBounds: GlobalBounds @@ -113,7 +117,7 @@ internal class ImageViewMapperTest { lateinit var mockBackgroundConstantState: ConstantState @Mock - lateinit var stubClipping: MobileSegment.WireframeClip + lateinit var stubClipping: Rect @Mock lateinit var stubParentRect: Rect @@ -168,7 +172,8 @@ internal class ImageViewMapperTest { whenever(mockDrawableToColorMapper.mapDrawableToColor(any(), eq(mockInternalLogger))) doReturn null whenever(stubImageViewUtils.resolveParentRectAbsPosition(any())).thenReturn(stubParentRect) - whenever(stubImageViewUtils.resolveContentRectWithScaling(any(), any())).thenReturn(stubContentRect) + whenever(stubImageViewUtils.resolveContentRectWithScaling(any(), any(), anyOrNull())) + .thenReturn(stubContentRect) whenever(stubImageViewUtils.calculateClipping(any(), any(), any())).thenReturn(stubClipping) stubContentRect.left = forge.aPositiveInt() stubContentRect.top = forge.aPositiveInt() @@ -197,7 +202,8 @@ internal class ImageViewMapperTest { colorStringFormatter = mockColorStringFormatter, viewBoundsResolver = mockViewBoundsResolver, drawableToColorMapper = mockDrawableToColorMapper, - imageViewUtils = stubImageViewUtils + imageViewUtils = stubImageViewUtils, + drawableCopier = mockDrawableCopier ) } @@ -298,7 +304,8 @@ internal class ImageViewMapperTest { clipping = anyOrNull(), shapeStyle = anyOrNull(), border = anyOrNull(), - prefix = anyOrNull() + prefix = anyOrNull(), + customResourceIdCacheKey = anyOrNull() ) ).thenReturn(expectedImageWireframe) @@ -330,7 +337,8 @@ internal class ImageViewMapperTest { clipping = anyOrNull(), shapeStyle = anyOrNull(), border = anyOrNull(), - prefix = anyOrNull() + prefix = anyOrNull(), + customResourceIdCacheKey = anyOrNull() ) } @@ -460,7 +468,8 @@ internal class ImageViewMapperTest { clipping = anyOrNull(), shapeStyle = anyOrNull(), border = anyOrNull(), - prefix = eq(expectedPrefix) + prefix = eq(expectedPrefix), + customResourceIdCacheKey = anyOrNull() ) ) .thenReturn(returnedWireframe) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt index 4242325e1b..69eab2fcaf 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/utils/DefaultViewBoundsResolverTest.kt @@ -7,8 +7,8 @@ package com.datadog.android.sessionreplay.utils import android.view.View +import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.forge.ForgeConfigurator -import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension