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] ANDROID: Support SR Image Recording #743

Merged
Merged
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
8 changes: 4 additions & 4 deletions packages/core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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.16.1"
implementation "com.datadoghq:dd-sdk-android-logs:2.16.1"
implementation "com.datadoghq:dd-sdk-android-trace:2.16.1"
implementation "com.datadoghq:dd-sdk-android-webview:2.16.1"
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"
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-session-replay/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ 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.16.1"
implementation "com.datadoghq:dd-sdk-android-internal:2.16.1"
implementation project(path: ':datadog_mobile-react-native')

testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MapperTypeWrapper<*>> {
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<OptionSelectorDetector> {
return listOf()
}

@VisibleForTesting
internal fun getUiManagerModule(): UIManagerModule? {
return try {
Expand All @@ -54,6 +57,14 @@ internal class ReactNativeSessionReplayExtensionSupport(
}
}

override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
return listOf()
}

override fun getCustomDrawableMapper(): List<DrawableToColorMapper> {
return emptyList()
}

internal companion object {
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import ReactViewBackgroundDrawableUtils
import android.view.Gravity
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.datadog.android.internal.utils.densityNormalized
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
Expand Down Expand Up @@ -134,7 +134,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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 android.graphics.Rect
import com.datadog.android.api.InternalLogger
import com.datadog.android.internal.utils.ImageViewUtils
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.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.getScaleTypeDrawable
import com.datadog.reactnative.sessionreplay.extensions.imageViewScaleType
import com.datadog.reactnative.sessionreplay.resources.ReactDrawableCopier
import com.facebook.drawee.drawable.FadeDrawable
import com.facebook.react.views.image.ReactImageView

internal class ReactNativeImageViewMapper: BaseAsyncBackgroundWireframeMapper<ReactImageView>(
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<MobileSegment.Wireframe> {
val wireframes = mutableListOf<MobileSegment.Wireframe>()
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?.toWireframeClip(),
shapeStyle = null,
border = null,
prefix = "drawable",
customResourceIdCacheKey = 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}"
}

private fun Rect.toWireframeClip(): MobileSegment.WireframeClip {
return MobileSegment.WireframeClip(
top = top.toLong(),
bottom = bottom.toLong(),
left = left.toLong(),
right = right.toLong()
)
}
}

Loading
Loading