Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ abstract class com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgr
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 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.ShapeBorder? = null): 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>
Expand All @@ -104,6 +104,7 @@ abstract class com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMa
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)
protected fun resolveViewId(android.view.View): Long
protected fun resolveShapeStyle(android.graphics.drawable.Drawable, Float, com.datadog.android.api.InternalLogger): com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle?
protected fun resolveBackgroundStyleInfo(android.graphics.drawable.Drawable, Float, Float, com.datadog.android.api.InternalLogger): Pair<com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle?, com.datadog.android.sessionreplay.model.MobileSegment.ShapeBorder?>
class com.datadog.android.sessionreplay.recorder.mapper.EditTextMapper : TextViewMapper<android.widget.EditText>
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 resolveCapturedText(android.widget.EditText, com.datadog.android.sessionreplay.TextAndInputPrivacy, Boolean): String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1518,7 +1518,8 @@ public abstract class com/datadog/android/sessionreplay/recorder/mapper/BaseAsyn
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 resolveBackgroundAsShapeWireframe (Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe;
public static synthetic fun resolveBackgroundAsShapeWireframe$default (Lcom/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper;Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;ILjava/lang/Object;)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;
}

Expand All @@ -1535,6 +1536,7 @@ public abstract class com/datadog/android/sessionreplay/recorder/mapper/BaseWire
protected final fun getDrawableToColorMapper ()Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;
protected final fun getViewBoundsResolver ()Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;
protected final fun getViewIdentifierResolver ()Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;
protected final fun resolveBackgroundStyleInfo (Landroid/graphics/drawable/Drawable;FFLcom/datadog/android/api/InternalLogger;)Lkotlin/Pair;
protected final fun resolveShapeStyle (Landroid/graphics/drawable/Drawable;FLcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;
protected final fun resolveViewId (Landroid/view/View;)J
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,30 @@ internal class ActionBarContainerMapper(
// Fortunately, the ActionBarContainer.mBackground field we're interested in is package private,
// which allows us to access it via the DatadogActionBarContainerAccessor.
val background = DatadogActionBarContainerAccessor(view).getBackgroundDrawable()
val shapeStyle = background?.let { resolveShapeStyle(it, view.alpha, internalLogger) }
val id = viewIdentifierResolver.resolveChildUniqueIdentifier(view, PREFIX_BACKGROUND_DRAWABLE)

if ((shapeStyle != null) && (id != null)) {
return if (background != null && id != null) {
val density = mappingContext.systemInformation.screenDensity
val bounds = viewBoundsResolver.resolveViewGlobalBounds(view, density)
val (shapeStyle, shapeBorder) = resolveBackgroundStyleInfo(background, view.alpha, density, internalLogger)

return listOf(
MobileSegment.Wireframe.ShapeWireframe(
id,
x = bounds.x,
y = bounds.y,
width = bounds.width,
height = bounds.height,
shapeStyle = shapeStyle,
border = null
if (shapeStyle != null) {
val bounds = viewBoundsResolver.resolveViewGlobalBounds(view, density)
listOf(
MobileSegment.Wireframe.ShapeWireframe(
id,
x = bounds.x,
y = bounds.y,
width = bounds.width,
height = bounds.height,
shapeStyle = shapeStyle,
border = shapeBorder
)
)
)
} else {
emptyList()
}
} else {
return emptyList()
emptyList()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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.recorder.mapper

import android.annotation.SuppressLint
import android.graphics.Paint
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.graphics.drawable.StateListDrawable
import android.os.Build
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper

/**
* Extracted style information from a drawable, including fill color, border, and corner radius.
*/
internal data class DrawableStyleInfo(
val color: Int?,
val cornerRadius: Float,
val borderColor: Int?,
val borderWidth: Float
)

/**
* Extracts full style information (fill color, border, corner radius) from drawables.
* Complements [DrawableToColorMapper] by also extracting border and corner radius from
* [GradientDrawable] backgrounds via reflection.
*/
internal class DrawableStyleExtractor(
private val drawableToColorMapper: DrawableToColorMapper
) {

fun extractStyleInfo(drawable: Drawable, internalLogger: InternalLogger): DrawableStyleInfo {
val color = drawableToColorMapper.mapDrawableToColor(drawable, internalLogger)
val gradientDrawable = findGradientDrawable(drawable)

val cornerRadius = gradientDrawable?.let { extractCornerRadius(it) } ?: 0f
val borderColor: Int?
val borderWidth: Float

if (gradientDrawable != null) {
val strokeInfo = extractStrokeInfo(gradientDrawable)
borderColor = strokeInfo.first
borderWidth = strokeInfo.second
} else {
borderColor = null
borderWidth = 0f
}

return DrawableStyleInfo(
color = color,
cornerRadius = cornerRadius,
borderColor = borderColor,
borderWidth = borderWidth
)
}

/**
* Recursively unwraps wrapper drawables to find the inner [GradientDrawable].
*/
private fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
return when (drawable) {
is GradientDrawable -> drawable
is StateListDrawable -> {
@Suppress("UNNECESSARY_SAFE_CALL")
drawable.current?.let { findGradientDrawable(it) }
}
is RippleDrawable -> findGradientDrawableInLayers(drawable)
is LayerDrawable -> findGradientDrawableInLayers(drawable)
is InsetDrawable -> drawable.drawable?.let { findGradientDrawable(it) }
else -> null
}
}

private fun findGradientDrawableInLayers(drawable: LayerDrawable): GradientDrawable? {
for (i in 0 until drawable.numberOfLayers) {
@Suppress("UnsafeThirdPartyFunctionCall")
val layer = drawable.getDrawable(i)
if (layer != null) {
val result = findGradientDrawable(layer)
if (result != null) return result
}
}
return null
}

@Suppress("SwallowedException")
private fun extractCornerRadius(drawable: GradientDrawable): Float {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
drawable.cornerRadius
} else {
try {
@Suppress("UnsafeThirdPartyFunctionCall")
val state = gradientStateField?.get(drawable) ?: return 0f
@Suppress("UnsafeThirdPartyFunctionCall")
(radiusField?.get(state) as? Float) ?: 0f
} catch (e: IllegalAccessException) {
0f
} catch (e: IllegalArgumentException) {
0f
}
}
}

@Suppress("SwallowedException")
private fun extractStrokeInfo(drawable: GradientDrawable): Pair<Int?, Float> {
val strokePaint = try {
@Suppress("UnsafeThirdPartyFunctionCall")
strokePaintField?.get(drawable) as? Paint
} catch (e: IllegalAccessException) {
null
} catch (e: IllegalArgumentException) {
null
} catch (e: ExceptionInInitializerError) {
null
}

@Suppress("UnsafeThirdPartyFunctionCall")
return if (strokePaint != null && strokePaint.strokeWidth > 0f) {
Pair(strokePaint.color, strokePaint.strokeWidth)
} else {
Pair(null, 0f)
}
}

companion object {
@SuppressLint("DiscouragedPrivateApi")
@Suppress("PrivateAPI", "SwallowedException", "TooGenericExceptionCaught")
internal val strokePaintField = try {
GradientDrawable::class.java.getDeclaredField("mStrokePaint").apply {
this.isAccessible = true
}
} catch (e: NoSuchFieldException) {
null
} catch (e: SecurityException) {
null
} catch (e: NullPointerException) {
null
}

@SuppressLint("DiscouragedPrivateApi")
@Suppress("PrivateAPI", "SwallowedException", "TooGenericExceptionCaught")
private val gradientStateField = try {
GradientDrawable::class.java.getDeclaredField("mGradientState").apply {
this.isAccessible = true
}
} catch (e: NoSuchFieldException) {
null
} catch (e: SecurityException) {
null
} catch (e: NullPointerException) {
null
}

@SuppressLint("DiscouragedPrivateApi")
@Suppress("PrivateAPI", "SwallowedException", "TooGenericExceptionCaught")
private val radiusField = try {
gradientStateField?.type?.getDeclaredField("mRadius")?.apply {
this.isAccessible = true
}
} catch (e: NoSuchFieldException) {
null
} catch (e: SecurityException) {
null
} catch (e: NullPointerException) {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,25 @@ internal class ViewWireframeMapper(
view,
mappingContext.systemInformation.screenDensity
)
val shapeStyle = view.background?.let { resolveShapeStyle(it, view.alpha, internalLogger) }
val background = view.background ?: return emptyList()

if (shapeStyle != null) {
return listOf(
val density = mappingContext.systemInformation.screenDensity
val (shapeStyle, shapeBorder) = resolveBackgroundStyleInfo(background, view.alpha, density, internalLogger)

return if (shapeStyle != null) {
listOf(
MobileSegment.Wireframe.ShapeWireframe(
resolveViewId(view),
viewGlobalBounds.x,
viewGlobalBounds.y,
viewGlobalBounds.width,
viewGlobalBounds.height,
shapeStyle = shapeStyle,
border = null
border = shapeBorder
)
)
} else {
return emptyList()
emptyList()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ abstract class BaseAsyncBackgroundWireframeMapper<in T : View> (
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): MobileSegment.Wireframe? {
val shapeStyle = view.background?.let { resolveShapeStyle(it, view.alpha, internalLogger) }
val background = view.background ?: return null
val density = mappingContext.systemInformation.screenDensity
val (shapeStyle, shapeBorder) = resolveBackgroundStyleInfo(background, view.alpha, density, internalLogger)

val bounds = viewBoundsResolver.resolveViewGlobalBounds(view, mappingContext.systemInformation.screenDensity)
val bounds = viewBoundsResolver.resolveViewGlobalBounds(view, density)
val width = view.width
val height = view.height

Expand All @@ -93,7 +95,8 @@ abstract class BaseAsyncBackgroundWireframeMapper<in T : View> (
bounds = bounds,
width = width,
height = height,
shapeStyle = shapeStyle
shapeStyle = shapeStyle,
border = shapeBorder
)
}
}
Expand All @@ -106,13 +109,15 @@ abstract class BaseAsyncBackgroundWireframeMapper<in T : View> (
* @param width the view width.
* @param height the view height.
* @param shapeStyle the optional [MobileSegment.ShapeStyle] to use.
* @param border the optional [MobileSegment.ShapeBorder] to use.
*/
protected open fun resolveBackgroundAsShapeWireframe(
view: View,
bounds: GlobalBounds,
width: Int,
height: Int,
shapeStyle: MobileSegment.ShapeStyle?
shapeStyle: MobileSegment.ShapeStyle?,
border: MobileSegment.ShapeBorder? = null
): MobileSegment.Wireframe.ShapeWireframe? {
val id = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(
view,
Expand All @@ -128,7 +133,7 @@ abstract class BaseAsyncBackgroundWireframeMapper<in T : View> (
width = width.densityNormalized(density).toLong(),
height = height.densityNormalized(density).toLong(),
shapeStyle = shapeStyle,
border = null
border = border
)
}

Expand Down
Loading
Loading