diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 3883de8f5..41466f332 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -192,10 +192,10 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compileOnly "com.squareup.okhttp3:okhttp:3.12.13" - implementation "com.datadoghq:dd-sdk-android-rum:2.14.0" - implementation "com.datadoghq:dd-sdk-android-logs:2.14.0" - implementation "com.datadoghq:dd-sdk-android-trace:2.14.0" - implementation "com.datadoghq:dd-sdk-android-webview:2.14.0" + implementation "com.datadoghq:dd-sdk-android-rum:2.17.0-SNAPSHOT" + implementation "com.datadoghq:dd-sdk-android-logs:2.17.0-SNAPSHOT" + implementation "com.datadoghq:dd-sdk-android-trace:2.17.0-SNAPSHOT" + implementation "com.datadoghq:dd-sdk-android-webview:2.17.0-SNAPSHOT" implementation "com.google.code.gson:gson:2.10.0" testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 8d833e098..f5ce66432 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -188,7 +188,7 @@ dependencies { api "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.datadoghq:dd-sdk-android-session-replay:2.14.0" + implementation "com.datadoghq:dd-sdk-android-session-replay:2.17.0-SNAPSHOT" implementation project(path: ':datadog_mobile-react-native') testImplementation "org.junit.platform:junit-platform-launcher:1.6.2" diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt index bce3bd6b8..8212bd990 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt @@ -11,11 +11,14 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.ExtensionSupport import com.datadog.android.sessionreplay.MapperTypeWrapper import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper +import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.views.image.ReactImageView import com.facebook.react.views.text.ReactTextView import com.facebook.react.views.textinput.ReactEditText import com.facebook.react.views.view.ReactViewGroup @@ -24,21 +27,21 @@ internal class ReactNativeSessionReplayExtensionSupport( private val reactContext: ReactContext, private val logger: InternalLogger ) : ExtensionSupport { + override fun name(): String { + return ReactNativeSessionReplayExtensionSupport::class.java.simpleName + } override fun getCustomViewMappers(): List> { val uiManagerModule = getUiManagerModule() return listOf( + MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()), MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()), MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(reactContext, uiManagerModule)), MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(reactContext, uiManagerModule)), ) } - override fun getOptionSelectorDetectors(): List { - return listOf() - } - @VisibleForTesting internal fun getUiManagerModule(): UIManagerModule? { return try { @@ -54,6 +57,14 @@ internal class ReactNativeSessionReplayExtensionSupport( } } + override fun getOptionSelectorDetectors(): List { + return listOf() + } + + override fun getCustomDrawableMapper(): List { + return emptyList() + } + internal companion object { internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule" } diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt index 7e39bac47..3898c14a4 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt @@ -10,7 +10,7 @@ import android.view.Gravity import android.widget.TextView import androidx.annotation.VisibleForTesting import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized +import com.datadog.reactnative.sessionreplay.extensions.densityNormalized import com.datadog.reactnative.sessionreplay.utils.DrawableUtils import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils @@ -136,7 +136,7 @@ internal class ReactTextPropertiesResolver( val fontFamily = getFontFamily(shadowNodeWrapper) ?: textWireframe.textStyle.family val fontSize = getFontSize(shadowNodeWrapper) - ?.convertToDensityNormalized(pixelsDensity) + ?.densityNormalized(pixelsDensity) ?: textWireframe.textStyle.size val fontColor = getTextColor(shadowNodeWrapper) ?: textWireframe.textStyle.color diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/IntExt.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/IntExt.kt new file mode 100644 index 000000000..2b0ee6948 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/IntExt.kt @@ -0,0 +1,14 @@ +/* + * 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.reactnative.sessionreplay.extensions + +internal fun Int.densityNormalized(density: Float): Int { + if (density == 0f) { + return this + } + return (this / density).toInt() +} \ No newline at end of file diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt index 7750479af..dbeb4e0b4 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt @@ -6,7 +6,7 @@ package com.datadog.reactnative.sessionreplay.extensions -internal fun Long.convertToDensityNormalized(density: Float): Long { +internal fun Long.densityNormalized(density: Float): Long { return if (density == 0f) { this } else { diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt new file mode 100644 index 000000000..6c6fb5e46 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt @@ -0,0 +1,154 @@ +/* + * 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.reactnative.sessionreplay.extensions + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.VectorDrawable +import android.widget.ImageView +import androidx.appcompat.graphics.drawable.DrawerArrowDrawable +import androidx.core.graphics.drawable.toBitmapOrNull +import com.facebook.drawee.drawable.ArrayDrawable +import com.facebook.drawee.drawable.ForwardingDrawable +import com.facebook.drawee.drawable.RoundedBitmapDrawable +import com.facebook.drawee.drawable.ScaleTypeDrawable +import com.facebook.drawee.drawable.ScalingUtils + +internal fun ScaleTypeDrawable.imageViewScaleType(): ImageView.ScaleType? { + return when (this.scaleType) { + ScalingUtils.ScaleType.CENTER -> ImageView.ScaleType.CENTER + ScalingUtils.ScaleType.CENTER_CROP -> ImageView.ScaleType.CENTER_CROP + ScalingUtils.ScaleType.CENTER_INSIDE -> ImageView.ScaleType.CENTER_INSIDE + ScalingUtils.ScaleType.FIT_CENTER -> ImageView.ScaleType.FIT_CENTER + ScalingUtils.ScaleType.FIT_START -> ImageView.ScaleType.FIT_START + ScalingUtils.ScaleType.FIT_END -> ImageView.ScaleType.FIT_END + ScalingUtils.ScaleType.FIT_XY -> ImageView.ScaleType.FIT_XY + else -> null + } +} + +internal fun ArrayDrawable.getScaleTypeDrawable(): ScaleTypeDrawable? { + for (i in 0 until this.numberOfLayers) { + try { + (this.getDrawable(i) as? ScaleTypeDrawable)?.let { + return it + } + } catch(_: IllegalArgumentException) { } + } + + return null +} + +internal fun ArrayDrawable.getDrawableOrNull(index: Int): Drawable? { + return try { + this.getDrawable(index) + } catch (_: IllegalArgumentException) { + null + } +} + +internal fun ForwardingDrawable.tryToExtractBitmap(resources: Resources): Bitmap? { + val forwardedDrawable = this.drawable + return if (forwardedDrawable != null) { + forwardedDrawable.tryToExtractBitmap(resources) + } else { + this.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } +} + +internal fun RoundedBitmapDrawable.tryToExtractBitmap(): Bitmap? { + val privateBitmap = try { + val field = RoundedBitmapDrawable::class.java.getDeclaredField("mBitmap") + field.isAccessible = true + field.get(this) as? Bitmap + } catch (_: NoSuchFieldException) { + null + } catch (_: IllegalAccessException) { + null + } catch (_: Exception) { + null + } + + return privateBitmap ?: this.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) +} + +internal fun BitmapDrawable.tryToExtractBitmap(resources: Resources): Bitmap? { + if (this.bitmap != null) { + return this.bitmap + } + + if (this.constantState != null) { + val copy = this.constantState?.newDrawable(resources) + return (copy as? BitmapDrawable)?.bitmap ?: copy?.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + + return null +} + +internal fun ArrayDrawable.tryToExtractBitmap(resources: Resources): Bitmap? { + var width = 0 + var height = 0 + for (index in 0 until this.numberOfLayers) { + val drawable = this.getDrawableOrNull(index) ?: continue + + if (drawable is ScaleTypeDrawable) { + return drawable.tryToExtractBitmap(resources) + } + + if (drawable.intrinsicWidth * drawable.intrinsicHeight > width * height) { + width = drawable.intrinsicWidth + height = drawable.intrinsicHeight + } + } + + return if (width > 0 && height > 0) + this.toBitmapOrNull(width, height, Bitmap.Config.ARGB_8888) + else + null +} + +internal fun Drawable.tryToExtractBitmap( + resources: Resources +): Bitmap? { + when (this) { + is ArrayDrawable -> { + return this.tryToExtractBitmap(resources) + } + is ForwardingDrawable -> { + return this.tryToExtractBitmap(resources) + } + is RoundedBitmapDrawable -> { + return this.tryToExtractBitmap() + } + is BitmapDrawable -> { + return this.tryToExtractBitmap(resources) + } + is VectorDrawable, is ShapeDrawable, is DrawerArrowDrawable -> { + return this.toBitmapOrNull( + this.intrinsicWidth, + this.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + else -> return null + } +} \ No newline at end of file diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt new file mode 100644 index 000000000..c0b00e103 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt @@ -0,0 +1,97 @@ +/* + * 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.reactnative.sessionreplay.mappers + +import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.recorder.MappingContext +import com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter +import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver +import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.reactnative.sessionreplay.extensions.densityNormalized +import com.datadog.reactnative.sessionreplay.extensions.getScaleTypeDrawable +import com.datadog.reactnative.sessionreplay.extensions.imageViewScaleType +import com.datadog.reactnative.sessionreplay.resources.ReactDrawableCopier +import com.datadog.reactnative.sessionreplay.utils.ImageViewUtils +import com.facebook.drawee.drawable.FadeDrawable +import com.facebook.react.views.image.ReactImageView + +internal class ReactNativeImageViewMapper: BaseAsyncBackgroundWireframeMapper( + viewIdentifierResolver = DefaultViewIdentifierResolver, + colorStringFormatter = DefaultColorStringFormatter, + viewBoundsResolver = DefaultViewBoundsResolver, + drawableToColorMapper = DrawableToColorMapper.getDefault() +) { + private val drawableCopier = ReactDrawableCopier() + + override fun map( + view: ReactImageView, + mappingContext: MappingContext, + asyncJobStatusCallback: AsyncJobStatusCallback, + internalLogger: InternalLogger + ): List { + val wireframes = mutableListOf() + wireframes.addAll(super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)) + + val drawable = view.drawable?.current ?: return wireframes + + val parentRect = ImageViewUtils.resolveParentRectAbsPosition(view) + val scaleType = (drawable as? FadeDrawable) + ?.getScaleTypeDrawable() + ?.imageViewScaleType() ?: view.scaleType + val contentRect = ImageViewUtils.resolveContentRectWithScaling(view, drawable, scaleType) + + val resources = view.resources + val density = resources.displayMetrics.density + + val clipping = if (view.cropToPadding) { + ImageViewUtils.calculateClipping(parentRect, contentRect, density) + } else { + null + } + + val contentXPosInDp = contentRect.left.densityNormalized(density).toLong() + val contentYPosInDp = contentRect.top.densityNormalized(density).toLong() + val contentWidthPx = contentRect.width() + val contentHeightPx = contentRect.height() + + // resolve foreground + mappingContext.imageWireframeHelper.createImageWireframeByDrawable( + view = view, + imagePrivacy = mappingContext.imagePrivacy, + currentWireframeIndex = wireframes.size, + x = contentXPosInDp, + y = contentYPosInDp, + width = contentWidthPx, + height = contentHeightPx, + usePIIPlaceholder = true, + drawable = drawable, + drawableCopier = drawableCopier, + asyncJobStatusCallback = asyncJobStatusCallback, + clipping = clipping, + shapeStyle = null, + border = null, + prefix = "drawable", + resourceIdCacheKey = generateUUID(view) + )?.let { + wireframes.add(it) + } + + return wireframes + } + + private fun generateUUID(reactImageView: ReactImageView): String { + val source = reactImageView.imageSource?.source ?: + System.identityHashCode(reactImageView).toString() + val drawableType = reactImageView.drawable.current::class.java.name + return "${drawableType}-${source}" + } +} + diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt new file mode 100644 index 000000000..ec95074c4 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt @@ -0,0 +1,34 @@ +/* + * 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.reactnative.sessionreplay.resources + +import android.content.res.Resources +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import com.datadog.android.sessionreplay.recorder.resources.DefaultDrawableCopier +import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier +import com.datadog.reactnative.sessionreplay.extensions.tryToExtractBitmap + +internal class ReactDrawableCopier : DrawableCopier { + private val defaultCopier = DefaultDrawableCopier() + + override fun copy( + originalDrawable: Drawable, + resources: Resources + ): Drawable? { + return if (originalDrawable.constantState != null) { + defaultCopier.copy(originalDrawable, resources) + } else { + originalDrawable.tryToExtractBitmap(resources)?.let { bitmap -> + BitmapDrawable(resources, bitmap).apply { + bounds = originalDrawable.bounds + alpha = originalDrawable.alpha + } + } + } + } +} diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ImageViewUtils.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ImageViewUtils.kt new file mode 100644 index 000000000..af16f511d --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ImageViewUtils.kt @@ -0,0 +1,225 @@ +/* + * 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.reactnative.sessionreplay.utils + +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.reactnative.sessionreplay.extensions.densityNormalized + +internal object ImageViewUtils { + internal fun resolveParentRectAbsPosition(view: View): Rect { + val coords = IntArray(2) + // this will always have size >= 2 + @Suppress("UnsafeThirdPartyFunctionCall") + view.getLocationOnScreen(coords) + val leftPadding = view.paddingLeft + val rightPadding = view.paddingRight + val topPadding = view.paddingTop + val bottomPadding = view.paddingBottom + return Rect( + coords[0] + leftPadding, + coords[1] + topPadding, + coords[0] + view.width - rightPadding, + coords[1] + view.height - bottomPadding + ) + } + + internal fun calculateClipping(parentRect: Rect, childRect: Rect, density: Float): MobileSegment.WireframeClip { + val left = if (childRect.left < parentRect.left) { + parentRect.left - childRect.left + } else { + 0 + } + val top = if (childRect.top < parentRect.top) { + parentRect.top - childRect.top + } else { + 0 + } + val right = if (childRect.right > parentRect.right) { + childRect.right - parentRect.right + } else { + 0 + } + val bottom = if (childRect.bottom > parentRect.bottom) { + childRect.bottom - parentRect.bottom + } 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() + ) + } + + internal fun resolveContentRectWithScaling( + imageView: ImageView, + drawable: Drawable, + scaleType: ImageView.ScaleType + ): Rect { + val drawableWidthPx = drawable.intrinsicWidth + val drawableHeightPx = drawable.intrinsicHeight + + val parentRect = resolveParentRectAbsPosition(imageView) + + val childRect = Rect( + 0, + 0, + drawableWidthPx, + drawableHeightPx + ) + + val resultRect: Rect + + when (scaleType) { + ImageView.ScaleType.FIT_START -> { + val contentRect = scaleRectToFitParent(parentRect, childRect) + resultRect = positionRectAtStart(parentRect, contentRect) + } + ImageView.ScaleType.FIT_END -> { + val contentRect = scaleRectToFitParent(parentRect, childRect) + resultRect = positionRectAtEnd(parentRect, contentRect) + } + ImageView.ScaleType.FIT_CENTER -> { + val contentRect = scaleRectToFitParent(parentRect, childRect) + resultRect = positionRectInCenter(parentRect, contentRect) + } + ImageView.ScaleType.CENTER_INSIDE -> { + val contentRect = scaleRectToCenterInsideParent(parentRect, childRect) + resultRect = positionRectInCenter(parentRect, contentRect) + } + ImageView.ScaleType.CENTER -> { + resultRect = positionRectInCenter(parentRect, childRect) + } + ImageView.ScaleType.CENTER_CROP -> { + val contentRect = scaleRectToCenterCrop(parentRect, childRect) + resultRect = positionRectInCenter(parentRect, contentRect) + } + ImageView.ScaleType.FIT_XY, + ImageView.ScaleType.MATRIX, + null -> { + resultRect = Rect( + parentRect.left, + parentRect.top, + parentRect.right, + parentRect.bottom + ) + } + } + + return resultRect + } + + private fun scaleRectToCenterInsideParent( + parentRect: Rect, + childRect: Rect + ): Rect { + // it already fits inside the parent + if (parentRect.width() > childRect.width() && parentRect.height() > childRect.height()) { + return childRect + } + + val scaleX: Float = parentRect.width().toFloat() / childRect.width().toFloat() + val scaleY: Float = parentRect.height().toFloat() / childRect.height().toFloat() + + var scaleFactor: Float = minOf(scaleX, scaleY) + + // center inside doesn't enlarge, it only reduces + if (scaleFactor >= 1F) scaleFactor = 1F + + val newWidth = childRect.width() * scaleFactor + val newHeight = childRect.height() * scaleFactor + + val resultRect = Rect() + resultRect.left = parentRect.left + resultRect.top = parentRect.top + resultRect.right = resultRect.left + newWidth.toInt() + resultRect.bottom = resultRect.top + newHeight.toInt() + return resultRect + } + + private fun scaleRectToCenterCrop( + parentRect: Rect, + childRect: Rect + ): Rect { + val scaleX: Float = parentRect.width().toFloat() / childRect.width().toFloat() + val scaleY: Float = parentRect.height().toFloat() / childRect.height().toFloat() + val scaleFactor = maxOf(scaleX, scaleY) + + val newWidth = childRect.width() * scaleFactor + val newHeight = childRect.height() * scaleFactor + + val resultRect = Rect() + resultRect.left = 0 + resultRect.top = 0 + resultRect.right = newWidth.toInt() + resultRect.bottom = newHeight.toInt() + return resultRect + } + + private fun scaleRectToFitParent( + parentRect: Rect, + childRect: Rect + ): Rect { + val scaleX: Float = parentRect.width().toFloat() / childRect.width().toFloat() + val scaleY: Float = parentRect.height().toFloat() / childRect.height().toFloat() + val scaleFactor = minOf(scaleX, scaleY) + + val newWidth = childRect.width() * scaleFactor + val newHeight = childRect.height() * scaleFactor + + val resultRect = Rect() + resultRect.left = 0 + resultRect.top = 0 + resultRect.right = newWidth.toInt() + resultRect.bottom = newHeight.toInt() + return resultRect + } + + private fun positionRectInCenter(parentRect: Rect, childRect: Rect): Rect { + val centerXParentPx = parentRect.centerX() + val centerYParentPx = parentRect.centerY() + val childRectWidthPx = childRect.width() + val childRectHeightPx = childRect.height() + + val resultRect = Rect() + resultRect.left = centerXParentPx - (childRectWidthPx / 2) + resultRect.top = centerYParentPx - (childRectHeightPx / 2) + resultRect.right = resultRect.left + childRectWidthPx + resultRect.bottom = resultRect.top + childRectHeightPx + return resultRect + } + + private fun positionRectAtStart(parentRect: Rect, childRect: Rect): Rect { + val childRectWidthPx = childRect.width() + val childRectHeightPx = childRect.height() + + val resultRect = Rect() + resultRect.left = parentRect.left + resultRect.top = parentRect.top + resultRect.right = resultRect.left + childRectWidthPx + resultRect.bottom = resultRect.top + childRectHeightPx + return resultRect + } + + private fun positionRectAtEnd(parentRect: Rect, childRect: Rect): Rect { + val childRectWidthPx = childRect.width() + val childRectHeightPx = childRect.height() + + val resultRect = Rect() + resultRect.right = parentRect.right + resultRect.bottom = parentRect.bottom + resultRect.left = parentRect.right - childRectWidthPx + resultRect.top = parentRect.bottom - childRectHeightPx + return resultRect + } +} \ No newline at end of file diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt index fabacf4ba..ee3e17b0b 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt @@ -7,7 +7,7 @@ package com.datadog.reactnative.sessionreplay.utils import com.datadog.android.sessionreplay.model.MobileSegment -import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized +import com.datadog.reactnative.sessionreplay.extensions.densityNormalized import com.facebook.react.uimanager.Spacing import com.facebook.react.views.view.ReactViewBackgroundDrawable @@ -28,7 +28,7 @@ internal class ReactViewBackgroundDrawableUtils( } val cornerRadius = - drawable.fullBorderRadius.toLong().convertToDensityNormalized(pixelDensity) + drawable.fullBorderRadius.toLong().densityNormalized(pixelDensity) return MobileSegment.ShapeStyle( colorHexString, @@ -51,7 +51,7 @@ internal class ReactViewBackgroundDrawableUtils( pixelDensity: Float ): MobileSegment.ShapeBorder { val borderWidth = - backgroundDrawable.fullBorderWidth.toLong().convertToDensityNormalized(pixelDensity) + backgroundDrawable.fullBorderWidth.toLong().densityNormalized(pixelDensity) val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL)) return MobileSegment.ShapeBorder( diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt index de64684de..6ebf23bbf 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt @@ -8,6 +8,7 @@ package com.datadog.reactnative.sessionreplay import com.datadog.android.api.InternalLogger import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper +import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper import com.facebook.react.bridge.NativeModule @@ -62,15 +63,18 @@ internal class ReactNativeSessionReplayExtensionSupportTest { val customViewMappers = testedExtensionSupport.getCustomViewMappers() // Then - assertThat(customViewMappers).hasSize(3) + assertThat(customViewMappers).hasSize(4) assertThat(customViewMappers[0].getUnsafeMapper()) - .isInstanceOf(ReactViewGroupMapper::class.java) + .isInstanceOf(ReactNativeImageViewMapper::class.java) assertThat(customViewMappers[1].getUnsafeMapper()) - .isInstanceOf(ReactTextMapper::class.java) + .isInstanceOf(ReactViewGroupMapper::class.java) assertThat(customViewMappers[2].getUnsafeMapper()) + .isInstanceOf(ReactTextMapper::class.java) + + assertThat(customViewMappers[3].getUnsafeMapper()) .isInstanceOf(ReactEditTextMapper::class.java) }