Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[RUM-7018] React Native Session Replay support #2448

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dd-sdk-android-internal/api/apiSurface
Original file line number Diff line number Diff line change
@@ -29,6 +29,12 @@ sealed class com.datadog.android.internal.telemetry.InternalTelemetryEvent
constructor(Boolean, Boolean, Boolean, MutableMap<String, Any?> = 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)
16 changes: 16 additions & 0 deletions dd-sdk-android-internal/api/dd-sdk-android-internal.api
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@ internal class ChipWireframeMapper(
height = view.chipDrawable.intrinsicHeight,
usePIIPlaceholder = false,
drawable = view.chipDrawable,
customResourceIdCacheKey = null,
asyncJobStatusCallback = asyncJobStatusCallback
)
backgroundWireframe?.let {
Original file line number Diff line number Diff line change
@@ -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()
)
}

18 changes: 11 additions & 7 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
@@ -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<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
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<T: android.view.View> : BaseWireframeMapper<T>
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<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
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<T: android.view.ViewGroup> : BaseAsyncBackgroundWireframeMapper<T>, TraverseAllChildrenMapper<T>
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<android.widget.ImageView>
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<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
open class com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper<T: android.widget.TextView> : BaseAsyncBackgroundWireframeMapper<T>
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<T: a
interface com.datadog.android.sessionreplay.recorder.mapper.TraverseAllChildrenMapper<T: android.view.ViewGroup> : WireframeMapper<T>
interface com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper<T: android.view.View>
fun map(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
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<DrawableToColorMapper> = 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<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.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<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
companion object
open class com.datadog.android.sessionreplay.utils.LegacyDrawableToColorMapper : DrawableToColorMapper
constructor(List<DrawableToColorMapper> = emptyList())
Original file line number Diff line number Diff line change
@@ -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 <init> ()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 <init> (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 <init> (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 <init> (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 <init> (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 <init> ()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 <init> ()V
public fun <init> (Ljava/util/List;)V
@@ -1597,17 +1601,17 @@ 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 {
}

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 {
Original file line number Diff line number Diff line change
@@ -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<TextView>(
viewIdentifierResolver,
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<T>(
border = null,
usePIIPlaceholder = true,
clipping = MobileSegment.WireframeClip(),
customResourceIdCacheKey = null,
asyncJobStatusCallback = asyncJobStatusCallback
)
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import androidx.annotation.MainThread
import com.datadog.android.api.InternalLogger

internal class BitmapCachesManager(
private val resourcesLRUCache: Cache<Drawable, ByteArray>,
private val resourcesLRUCache: Cache<String, ByteArray>,
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)
}
Original file line number Diff line number Diff line change
@@ -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 {
Original file line number Diff line number Diff line change
@@ -9,8 +9,8 @@ package com.datadog.android.sessionreplay.internal.recorder.resources
internal interface Cache<K : Any, V : Any> {

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()

Original file line number Diff line number Diff line change
@@ -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<MobileSegment.Wireframe> {
val result = mutableListOf<MobileSegment.Wireframe>()
@@ -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)
Original file line number Diff line number Diff line change
@@ -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 {
Original file line number Diff line number Diff line change
@@ -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,15 +325,22 @@ internal class ResourceResolver(
bitmap = scaledBitmap,
compressedBitmapBytes = compressedBitmapBytes,
shouldCacheBitmap = shouldCacheBitmap,
customResourceIdCacheKey = customResourceIdCacheKey,
resolveResourceCallback = resolveResourceCallback
)

return scaledBitmap
}

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 &&
Original file line number Diff line number Diff line change
@@ -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<Drawable, ByteArray>, ComponentCallbacks2 {
) : Cache<String, ByteArray>, 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) {
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()
)
Original file line number Diff line number Diff line change
@@ -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<in T : View> internal constructor(
abstract class BaseAsyncBackgroundWireframeMapper<in T : View> (
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
@@ -57,8 +57,16 @@ abstract class BaseAsyncBackgroundWireframeMapper<in T : View> 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<in T : View> 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<in T : View> internal construc
)
}

/**
* Function used to resolve the view background as a Image wireframe.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Function used to resolve the view background as a Image wireframe.
* Function used to resolve the view background as an [ImageWireframe].

*
* @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<in T : View> internal construc
clipping = MobileSegment.WireframeClip(),
shapeStyle = null,
border = null,
prefix = PREFIX_BACKGROUND_DRAWABLE
prefix = PREFIX_BACKGROUND_DRAWABLE,
customResourceIdCacheKey = null
)
}

Original file line number Diff line number Diff line change
@@ -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,34 +27,39 @@ import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver
*/
open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper<ImageView> {
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(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper,
imageViewUtils: ImageViewUtils
imageViewUtils: ImageViewUtils,
drawableCopier: DrawableCopier
) : super(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
) {
this.imageViewUtils = imageViewUtils
this.drawableCopier = drawableCopier
}

@UiThread
@@ -76,7 +83,7 @@ open class ImageViewMapper : BaseAsyncBackgroundWireframeMapper<ImageView> {
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<ImageView> {
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)
}
Original file line number Diff line number Diff line change
@@ -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<in T : TextView>(
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper
) : BaseAsyncBackgroundWireframeMapper<T>(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
viewIdentifierResolver = viewIdentifierResolver,
colorStringFormatter = colorStringFormatter,
viewBoundsResolver = viewBoundsResolver,
drawableToColorMapper = drawableToColorMapper
) {

@UiThread
@@ -54,24 +54,25 @@ open class TextViewMapper<in T : TextView>(

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
)
)

Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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 {

/**
Original file line number Diff line number Diff line change
@@ -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<MobileSegment.Wireframe>

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<T> :
clipping = eq(MobileSegment.WireframeClip()),
shapeStyle = isNull(),
border = isNull(),
prefix = anyString()
prefix = anyString(),
customResourceIdCacheKey = anyOrNull()
)
}

@@ -268,7 +270,8 @@ internal abstract class BaseCheckableTextViewMapperTest<T> :
clipping = eq(MobileSegment.WireframeClip()),
shapeStyle = isNull(),
border = isNull(),
prefix = anyString()
prefix = anyString(),
customResourceIdCacheKey = anyOrNull()
)
}

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
Original file line number Diff line number Diff line change
@@ -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<Drawable, ByteArray>,
resourcesLRUCache: Cache<String, ByteArray>,
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<Drawable, ByteArray> {
private class FakeNonComponentsCallbackCache : Cache<String, ByteArray> {

override fun size(): Int = 0

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -47,6 +47,9 @@ internal class ResourcesLRUCacheTest {
@Mock
lateinit var mockInvocationUtils: InvocationUtils

@StringForgery
lateinit var fakeResourceKey: String

val argumentCaptor = argumentCaptor<String>()

@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,36 +127,31 @@ 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)
}

@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)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val mockImageView: ImageView = mock {
val mockImageView = mock<ImageView> {

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
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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