Skip to content

Commit

Permalink
PR Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Dec 19, 2023
1 parent 8f2601e commit ebd8619
Show file tree
Hide file tree
Showing 20 changed files with 689 additions and 242 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

package com.datadog.reactnative.sessionreplay

import com.datadog.android.Datadog
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.facebook.react.bridge.Promise
Expand All @@ -27,9 +29,11 @@ class DdSessionReplayImplementation(
* @param defaultPrivacyLevel The privacy level used for replay.
*/
fun enable(replaySampleRate: Double, defaultPrivacyLevel: String, promise: Promise) {
val sdkCore = Datadog.getInstance() as FeatureSdkCore
val logger = sdkCore.internalLogger
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
.setPrivacy(buildPrivacy(defaultPrivacyLevel))
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext))
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(logger, reactContext))
.build()
sessionReplayProvider().enable(configuration)
promise.resolve(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
*
* * 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

import android.widget.TextView
import com.datadog.android.sessionreplay.model.MobileSegment

internal class NoopTextPropertiesResolver: TextPropertiesResolver {
override fun addReactNativeProperties(
originalWireframe: MobileSegment.Wireframe.TextWireframe,
view: TextView,
pixelDensity: Float
): MobileSegment.Wireframe.TextWireframe {
return originalWireframe
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
package com.datadog.reactnative.sessionreplay

import android.annotation.SuppressLint
import android.util.Log
import android.view.View
import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.ExtensionSupport
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector
Expand All @@ -23,6 +23,7 @@ import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup

internal class ReactNativeSessionReplayExtensionSupport(
private val logger: InternalLogger,
private val reactContext: ReactContext,
) : ExtensionSupport {

Expand All @@ -31,10 +32,12 @@ internal class ReactNativeSessionReplayExtensionSupport(

return mapOf(
SessionReplayPrivacy.ALLOW to mapOf(
ReactViewGroup::class.java to ReactViewGroupMapper() as WireframeMapper<View, *>,
ReactTextView::class.java to ReactTextMapper(reactContext, uiManagerModule) as WireframeMapper<View, *>,
ReactEditText::class.java to ReactTextMapper(reactContext, uiManagerModule) as WireframeMapper<View, *>
)
ReactViewGroup::class.java to ReactViewGroupMapper(logger),
ReactTextView::class.java to ReactTextMapper(logger, reactContext, uiManagerModule),
ReactEditText::class.java to ReactTextMapper(logger, reactContext, uiManagerModule)
).map{
it.key to it.value as WireframeMapper<View, *>
}.toMap(),
)
}

Expand All @@ -46,15 +49,19 @@ internal class ReactNativeSessionReplayExtensionSupport(
@VisibleForTesting
internal fun getUiManagerModule(): UIManagerModule? {
return try {
reactContext.getNativeModule(UIManagerModule::class.java) as UIManagerModule
reactContext.getNativeModule(UIManagerModule::class.java)
} catch (e: IllegalStateException) {
Log.e(TAG, RESOLVE_UIMANAGERMODULE_ERROR, e)
logger.log(
level = InternalLogger.Level.WARN,
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
throwable = e
)
return null
}
}

internal companion object {
internal const val TAG = "ReactNativeSessionReplayExtensionSupport"
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve uiManagerModule"
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,53 @@

package com.datadog.reactnative.sessionreplay

import android.view.Gravity
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
import com.datadog.reactnative.sessionreplay.utils.getReactBackgroundFromDrawable
import com.datadog.reactnative.sessionreplay.utils.resolveShapeAndBorder
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.views.text.TextAttributes
import com.facebook.react.views.view.ReactViewBackgroundDrawable
import java.util.Locale

internal class ReactTextPropertiesResolver(
private val reactContext: ReactContext,
private val uiManagerModule: UIManagerModule?,
private val reflectionUtils: ReflectionUtils = ReflectionUtils(reactContext, uiManagerModule)
) {
internal fun addReactNativeProperties(
logger: InternalLogger,
reactContext: ReactContext,
uiManagerModule: UIManagerModule?,
private val shadowNodeWrapper: ShadowNodeWrapper =
ShadowNodeWrapper(logger, reactContext, uiManagerModule),
private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils =
ReactViewBackgroundDrawableUtils(logger),
private val drawableUtils: DrawableUtils = DrawableUtils()
): TextPropertiesResolver {
override fun addReactNativeProperties(
originalWireframe: MobileSegment.Wireframe.TextWireframe,
view: TextView,
pixelDensity: Float,
): MobileSegment.Wireframe.TextWireframe {
val reflectionUtilsSuccessfullyInitialized = reflectionUtils.initialize(view.id)
if (!reflectionUtilsSuccessfullyInitialized) {
return originalWireframe
}
val textWireframe = resolveDrawableProperties(view, pixelDensity, originalWireframe)

val isInitializedForView = shadowNodeWrapper.initialize(view.id)
if (!isInitializedForView) return textWireframe

val textStyle = resolveTextStyle(textWireframe, pixelDensity)
val textPosition = textWireframe.textPosition
val padding = textPosition?.padding
val alignment = resolveTextAlignment(view, textWireframe)

var textWireframe: MobileSegment.Wireframe.TextWireframe =
resolveDrawableProperties(view, pixelDensity, originalWireframe)
textWireframe = getReactNativeTextProperties(textWireframe, pixelDensity)
return textWireframe
return textWireframe.copy(
textStyle = textStyle,
textPosition = MobileSegment.TextPosition(
alignment = alignment,
padding = padding
)
)
}

private fun resolveDrawableProperties(
Expand All @@ -47,15 +61,16 @@ internal class ReactTextPropertiesResolver(
textWireframe: MobileSegment.Wireframe.TextWireframe
): MobileSegment.Wireframe.TextWireframe {
val backgroundDrawable: ReactViewBackgroundDrawable =
getReactBackgroundFromDrawable(view.background) ?: return textWireframe
drawableUtils.getReactBackgroundFromDrawable(view.background) ?: return textWireframe

var resultWireframe = textWireframe

// view.alpha is the value of the opacity prop on the js side
val opacity = view.alpha

val (shapeStyle, border) =
resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity)
reactViewBackgroundDrawableUtils
.resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity)

if (shapeStyle != null || border != null) {
resultWireframe = resultWireframe.copy(
Expand All @@ -67,16 +82,24 @@ internal class ReactTextPropertiesResolver(
return resultWireframe
}

private fun getReactNativeTextProperties(
textWireframe: MobileSegment.Wireframe.TextWireframe,
pixelDensity: Float,
): MobileSegment.Wireframe.TextWireframe {
val textStyle = resolveTextStyle(textWireframe, pixelDensity)
val textPosition = textWireframe.textPosition

return textWireframe.copy(
textStyle = textStyle,
textPosition = textPosition
private fun resolveTextAlignment(
view: TextView,
textWireframe: MobileSegment.Wireframe.TextWireframe
): MobileSegment.Alignment {
val gravity = view.gravity
val horizontal = textWireframe.textPosition?.alignment?.horizontal
val vertical =
when (gravity.and(Gravity.VERTICAL_GRAVITY_MASK)) {
Gravity.TOP -> MobileSegment.Vertical.TOP
Gravity.CENTER_VERTICAL,
Gravity.CENTER -> MobileSegment.Vertical.CENTER
Gravity.BOTTOM -> MobileSegment.Vertical.BOTTOM
else -> MobileSegment.Vertical.TOP
}

return MobileSegment.Alignment(
horizontal = horizontal,
vertical = vertical
)
}

Expand All @@ -100,7 +123,8 @@ internal class ReactTextPropertiesResolver(
}

private fun getTextColor(): String? {
val resolvedColor = reflectionUtils.getReflectedFieldValue(COLOR_FIELD_NAME) as Int?
val resolvedColor = shadowNodeWrapper
.getDeclaredShadowNodeField(COLOR_FIELD_NAME) as Int?
if (resolvedColor != null) {
return formatAsRgba(resolvedColor)
}
Expand All @@ -109,7 +133,8 @@ internal class ReactTextPropertiesResolver(
}

private fun getFontSize(): Long? {
val textAttributes = reflectionUtils.getReflectedFieldValue(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
val textAttributes = shadowNodeWrapper
.getDeclaredShadowNodeField(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
if (textAttributes != null) {
return textAttributes.effectiveFontSize.toLong()
}
Expand All @@ -118,7 +143,8 @@ internal class ReactTextPropertiesResolver(
}

private fun getFontFamily(): String? {
val fontFamily = reflectionUtils.getReflectedFieldValue(FONT_FAMILY_FIELD_NAME) as? String
val fontFamily = shadowNodeWrapper
.getDeclaredShadowNodeField(FONT_FAMILY_FIELD_NAME) as? String

if (fontFamily != null) {
return resolveFontFamily(fontFamily.lowercase(Locale.US))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
*
* * 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

import com.datadog.android.api.InternalLogger
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.UIImplementation
import com.facebook.react.uimanager.UIManagerModule
import java.util.concurrent.CountDownLatch

internal class ShadowNodeWrapper(
private val logger: InternalLogger,
private val reactContext: ReactContext,
private val uiManagerModule: UIManagerModule?,
private val reflectionUtils: ReflectionUtils = ReflectionUtils(logger)
) {
private var shadowNode: ReactShadowNode<out ReactShadowNode<*>>? = null

internal fun initialize(viewId: Int): Boolean {
shadowNode = getShadowNode(viewId)
return shadowNode != null
}

internal fun getDeclaredShadowNodeField(fieldName: String): Any? {
return reflectionUtils.getDeclaredField(
shadowNode?.javaClass?.superclass as Class<*>,
shadowNode,
fieldName
)
}

private fun getShadowNode(viewId: Int): ReactShadowNode<out ReactShadowNode<*>>? {
val countDownLatch = CountDownLatch(1)
var target: ReactShadowNode<out ReactShadowNode<*>>? = null
val shadowNodeRunnable = Runnable {
val node = uiManagerModule?.resolveShadowNode(viewId)
if (node != null) {
target = node
countDownLatch.countDown()
}
}
synchronized(this) {
reactContext.runOnNativeModulesQueueThread(shadowNodeRunnable)
countDownLatch.await()
return target
}
}

private fun UIManagerModule.resolveShadowNode(tag: Int): ReactShadowNode<out ReactShadowNode<*>>? {
javaClass.getDeclaredField("mUIImplementation").let {
it.isAccessible = true
val value = it.get(this) as UIImplementation
return value.resolveShadowNode(tag)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
*
* * 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

import android.widget.TextView
import com.datadog.android.sessionreplay.model.MobileSegment

internal interface TextPropertiesResolver {
fun addReactNativeProperties(
originalWireframe: MobileSegment.Wireframe.TextWireframe,
view: TextView,
pixelDensity: Float,
): MobileSegment.Wireframe.TextWireframe
}

This file was deleted.

Loading

0 comments on commit ebd8619

Please sign in to comment.