Skip to content

Commit

Permalink
Make shadowNodeWrapper nonmutable
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Dec 19, 2023
1 parent ebd8619 commit e4e04b4
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ internal class ReactNativeSessionReplayExtensionSupport(

return mapOf(
SessionReplayPrivacy.ALLOW to mapOf(
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(),
ReactViewGroup::class.java to ReactViewGroupMapper(),
ReactTextView::class.java to ReactTextMapper(reactContext, uiManagerModule),
ReactEditText::class.java to ReactTextMapper(reactContext, uiManagerModule)
).mapValues{
it.value as WireframeMapper<View, *>
}
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ 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.DrawableUtils
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule
Expand All @@ -22,13 +22,11 @@ import com.facebook.react.views.view.ReactViewBackgroundDrawable
import java.util.Locale

internal class ReactTextPropertiesResolver(
logger: InternalLogger,
reactContext: ReactContext,
uiManagerModule: UIManagerModule?,
private val shadowNodeWrapper: ShadowNodeWrapper =
ShadowNodeWrapper(logger, reactContext, uiManagerModule),
private val reactContext: ReactContext,
private val uiManagerModule: UIManagerModule,
private val reflectionUtils: ReflectionUtils = ReflectionUtils(),
private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils =
ReactViewBackgroundDrawableUtils(logger),
ReactViewBackgroundDrawableUtils(),
private val drawableUtils: DrawableUtils = DrawableUtils()
): TextPropertiesResolver {
override fun addReactNativeProperties(
Expand All @@ -38,10 +36,14 @@ internal class ReactTextPropertiesResolver(
): MobileSegment.Wireframe.TextWireframe {
val textWireframe = resolveDrawableProperties(view, pixelDensity, originalWireframe)

val isInitializedForView = shadowNodeWrapper.initialize(view.id)
if (!isInitializedForView) return textWireframe
val shadowNodeWrapper: ShadowNodeWrapper =
ShadowNodeWrapper.getShadowNodeWrapper(
reactContext = reactContext,
uiManagerModule = uiManagerModule,
reflectionUtils = reflectionUtils,
viewId = view.id) ?: return textWireframe

val textStyle = resolveTextStyle(textWireframe, pixelDensity)
val textStyle = resolveTextStyle(textWireframe, pixelDensity, shadowNodeWrapper)
val textPosition = textWireframe.textPosition
val padding = textPosition?.padding
val alignment = resolveTextAlignment(view, textWireframe)
Expand Down Expand Up @@ -106,13 +108,14 @@ internal class ReactTextPropertiesResolver(
private fun resolveTextStyle(
textWireframe: MobileSegment.Wireframe.TextWireframe,
pixelsDensity: Float,
shadowNodeWrapper: ShadowNodeWrapper
): MobileSegment.TextStyle {
val fontFamily = getFontFamily()
val fontFamily = getFontFamily(shadowNodeWrapper)
?: textWireframe.textStyle.family
val fontSize = getFontSize()
val fontSize = getFontSize(shadowNodeWrapper)
?.convertToDensityNormalized(pixelsDensity)
?: textWireframe.textStyle.size
val fontColor = getTextColor()
val fontColor = getTextColor(shadowNodeWrapper)
?: textWireframe.textStyle.color

return MobileSegment.TextStyle(
Expand All @@ -122,7 +125,7 @@ internal class ReactTextPropertiesResolver(
)
}

private fun getTextColor(): String? {
private fun getTextColor(shadowNodeWrapper: ShadowNodeWrapper): String? {
val resolvedColor = shadowNodeWrapper
.getDeclaredShadowNodeField(COLOR_FIELD_NAME) as Int?
if (resolvedColor != null) {
Expand All @@ -132,7 +135,7 @@ internal class ReactTextPropertiesResolver(
return null
}

private fun getFontSize(): Long? {
private fun getFontSize(shadowNodeWrapper: ShadowNodeWrapper): Long? {
val textAttributes = shadowNodeWrapper
.getDeclaredShadowNodeField(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
if (textAttributes != null) {
Expand All @@ -142,7 +145,7 @@ internal class ReactTextPropertiesResolver(
return null
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

package com.datadog.reactnative.sessionreplay

import com.datadog.android.api.InternalLogger
import androidx.annotation.VisibleForTesting
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.ReactShadowNode
Expand All @@ -17,48 +17,55 @@ 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 val shadowNode: ReactShadowNode<out ReactShadowNode<*>>?,
private val reflectionUtils: ReflectionUtils = ReflectionUtils()
) {
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
)
return shadowNode?.let {
reflectionUtils.getDeclaredField(
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
internal companion object {
internal fun getShadowNodeWrapper(
reactContext: ReactContext,
uiManagerModule: UIManagerModule,
reflectionUtils: ReflectionUtils,
viewId: Int
): ShadowNodeWrapper? {
val countDownLatch = CountDownLatch(1)
var target: ReactShadowNode<out ReactShadowNode<*>>? = null

val shadowNodeRunnable = Runnable {
val node = resolveShadowNode(reflectionUtils, uiManagerModule, viewId)
if (node != null) {
target = node
}

countDownLatch.countDown()
}
}
synchronized(this) {

reactContext.runOnNativeModulesQueueThread(shadowNodeRunnable)
countDownLatch.await()
return target

if (target == null) {
return null
}

return ShadowNodeWrapper(reflectionUtils = reflectionUtils, shadowNode = 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)
private fun resolveShadowNode(reflectionUtils: ReflectionUtils, uiManagerModule: UIManagerModule, tag: Int): ReactShadowNode<out ReactShadowNode<*>>? {
val uiManagerImplementation = reflectionUtils.getDeclaredField(uiManagerModule, UI_IMPLEMENTATION_FIELD_NAME) as UIImplementation?
?: return null

return uiManagerImplementation.resolveShadowNode(tag)
}

@VisibleForTesting
internal const val UI_IMPLEMENTATION_FIELD_NAME = "mUIImplementation"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package com.datadog.reactnative.sessionreplay.mappers

import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper
Expand All @@ -19,32 +18,24 @@ import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule

internal class ReactTextMapper(): TextViewMapper() {
private var reactTextPropertiesResolver: TextPropertiesResolver =
internal class ReactTextMapper(
private val reactTextPropertiesResolver: TextPropertiesResolver =
NoopTextPropertiesResolver()
): TextViewMapper() {

internal constructor(
logger: InternalLogger,
reactContext: ReactContext,
uiManagerModule: UIManagerModule?
): this() {
if (uiManagerModule == null) {
this.reactTextPropertiesResolver = NoopTextPropertiesResolver()
): this(
reactTextPropertiesResolver = if (uiManagerModule == null) {
NoopTextPropertiesResolver()
} else {
this.reactTextPropertiesResolver = ReactTextPropertiesResolver(
logger = logger,
ReactTextPropertiesResolver(
reactContext = reactContext,
uiManagerModule = uiManagerModule
)
}
}

@VisibleForTesting
internal constructor(
reactTextPropertiesResolver: TextPropertiesResolver
): this() {
this.reactTextPropertiesResolver = reactTextPropertiesResolver
}
)

override fun map(
view: TextView,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

package com.datadog.reactnative.sessionreplay.mappers

import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.BaseWireframeMapper
Expand All @@ -17,9 +16,8 @@ import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUt
import com.facebook.react.views.view.ReactViewGroup

internal class ReactViewGroupMapper(
logger: InternalLogger,
private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils =
ReactViewBackgroundDrawableUtils(logger),
ReactViewBackgroundDrawableUtils(),
private val drawableUtils: DrawableUtils = DrawableUtils()
) :
BaseWireframeMapper<ReactViewGroup, MobileSegment.Wireframe>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@

package com.datadog.reactnative.sessionreplay.utils

import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized
import com.facebook.react.uimanager.Spacing
import com.facebook.react.views.view.ReactViewBackgroundDrawable

internal class ReactViewBackgroundDrawableUtils(
logger: InternalLogger,
private val reflectionUtils: ReflectionUtils = ReflectionUtils(logger)
private val reflectionUtils: ReflectionUtils = ReflectionUtils()
) {
internal fun resolveShapeAndBorder(
drawable: ReactViewBackgroundDrawable,
Expand Down Expand Up @@ -43,7 +41,6 @@ internal class ReactViewBackgroundDrawableUtils(
backgroundDrawable: ReactViewBackgroundDrawable,
): Int? {
return reflectionUtils.getDeclaredField(
backgroundDrawable.javaClass,
backgroundDrawable,
COLOR_FIELD_NAME
) as Int?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

package com.datadog.reactnative.sessionreplay.utils

import com.datadog.android.api.InternalLogger
import java.lang.reflect.Field

internal class ReflectionUtils(
private val logger: InternalLogger
) {
internal fun getDeclaredField(className: Class<*>, instance: Any?, fieldName: String): Any? {
internal class ReflectionUtils {
internal fun getDeclaredField(instance: Any, fieldName: String): Any? {
val className = instance.javaClass
val declaredField = searchForField(className, fieldName) ?: return null

declaredField.let {
Expand All @@ -22,25 +20,17 @@ internal class ReflectionUtils(
}

private fun searchForField(className: Class<*>, fieldName: String): Field? {
try {
val isFieldDefinedForClass = className.declaredFields.firstOrNull{ it.name == fieldName } != null
val hasSuperclass = className.superclass != null

if (isFieldDefinedForClass) {
return className.getDeclaredField(fieldName)
} catch (e: NoSuchFieldException) {
if (className.superclass != null) {
searchForField(className.superclass, fieldName)
} else {
logger.log(
level = InternalLogger.Level.WARN,
target = InternalLogger.Target.MAINTAINER,
messageBuilder = { RESOLVE_FIELD_ERROR },
throwable = e
)
}
}

return null
}
if (hasSuperclass) {
return searchForField(className.superclass, fieldName)
}

private companion object {
private const val RESOLVE_FIELD_ERROR = "Failed to resolve the declared field via reflection."
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
}

@Test
fun `M return null W getCustomViewMappers() { cannot get uiManagerModule }`() {
fun `M return null W getUiManagerModule() { cannot get uiManagerModule }`() {
// Given
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
.thenThrow(IllegalStateException())
Expand Down
Loading

0 comments on commit e4e04b4

Please sign in to comment.