Skip to content

Commit

Permalink
Refactor text mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Dec 18, 2023
1 parent aef3116 commit 8f2601e
Show file tree
Hide file tree
Showing 16 changed files with 617 additions and 383 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@

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.sessionreplay.ExtensionSupport
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
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.text.ReactTextView
import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup
Expand All @@ -23,16 +27,34 @@ internal class ReactNativeSessionReplayExtensionSupport(
) : ExtensionSupport {

override fun getCustomViewMappers(): Map<SessionReplayPrivacy, Map<Class<*>, WireframeMapper<View, *>>> {
val uiManagerModule = getUiManagerModule()

return mapOf(
SessionReplayPrivacy.ALLOW to mapOf(
ReactViewGroup::class.java to ReactViewGroupMapper() as WireframeMapper<View, *>,
ReactTextView::class.java to ReactTextMapper(reactContext) as WireframeMapper<View, *>,
ReactEditText::class.java to ReactTextMapper(reactContext) as WireframeMapper<View, *>
ReactTextView::class.java to ReactTextMapper(reactContext, uiManagerModule) as WireframeMapper<View, *>,
ReactEditText::class.java to ReactTextMapper(reactContext, uiManagerModule) as WireframeMapper<View, *>
)
)
}

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

@SuppressLint("LongLogTag")
@VisibleForTesting
internal fun getUiManagerModule(): UIManagerModule? {
return try {
reactContext.getNativeModule(UIManagerModule::class.java) as UIManagerModule
} catch (e: IllegalStateException) {
Log.e(TAG, RESOLVE_UIMANAGERMODULE_ERROR, e)
return null
}
}

internal companion object {
internal const val TAG = "ReactNativeSessionReplayExtensionSupport"
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve uiManagerModule"
}
}
Original file line number Diff line number Diff line change
@@ -1,133 +1,149 @@
/*
* 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.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.facebook.react.views.text.ReactTextShadowNode
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
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 reactTextShadowNodeUtils: ReactTextShadowNodeUtils
private val reactContext: ReactContext,
private val uiManagerModule: UIManagerModule?,
private val reflectionUtils: ReflectionUtils = ReflectionUtils(reactContext, uiManagerModule)
) {
internal fun resolveShadowProperties(
internal fun addReactNativeProperties(
originalWireframe: MobileSegment.Wireframe.TextWireframe,
view: TextView,
pixelDensity: Float,
): MobileSegment.Wireframe.TextWireframe {
val reflectionUtilsSuccessfullyInitialized = reflectionUtils.initialize(view.id)
if (!reflectionUtilsSuccessfullyInitialized) {
return originalWireframe
}

var textWireframe: MobileSegment.Wireframe.TextWireframe =
resolveDrawableProperties(view, pixelDensity, originalWireframe)
textWireframe = getReactNativeTextProperties(textWireframe, pixelDensity)
return textWireframe
}

private fun resolveDrawableProperties(
view: TextView,
pixelDensity: Float,
shapeStyle: MobileSegment.ShapeStyle?,
border: MobileSegment.ShapeBorder?,
textWireframe: MobileSegment.Wireframe.TextWireframe
): MobileSegment.Wireframe {
): MobileSegment.Wireframe.TextWireframe {
val backgroundDrawable: ReactViewBackgroundDrawable =
getReactBackgroundFromDrawable(view.background) ?: return textWireframe

val shadowNode = reactTextShadowNodeUtils.getShadowNode(view.id) as? ReactTextShadowNode
var resultWireframe = textWireframe

val textStyle = resolveTextStyle(view, pixelDensity, shadowNode)
val textPosition = resolveTextPosition(view, pixelDensity, shadowNode)
// view.alpha is the value of the opacity prop on the js side
val opacity = view.alpha

return textWireframe.copy(
val (shapeStyle, border) =
resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity)

if (shapeStyle != null || border != null) {
resultWireframe = resultWireframe.copy(
shapeStyle = shapeStyle,
border = border,
textStyle = textStyle,
textPosition = textPosition
border = border
)
}

return resultWireframe
}

private fun resolveTextPosition(
view: TextView,
pixelsDensity: Float,
shadowNode: ReactTextShadowNode?
): MobileSegment.TextPosition {
return MobileSegment.TextPosition(
resolvePadding(view, pixelsDensity),
resolveAlignment(view, shadowNode)
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 resolveTextStyle(
view: TextView,
textWireframe: MobileSegment.Wireframe.TextWireframe,
pixelsDensity: Float,
shadowNode: ReactTextShadowNode?
): MobileSegment.TextStyle {
return MobileSegment.TextStyle(
family = reactTextShadowNodeUtils.getFontFamily(view, shadowNode),
size = reactTextShadowNodeUtils.getFontSize(view, shadowNode).convertToDensityNormalized(pixelsDensity),
color = resolveTextColor(view, shadowNode)
)
}
val fontFamily = getFontFamily()
?: textWireframe.textStyle.family
val fontSize = getFontSize()
?.convertToDensityNormalized(pixelsDensity)
?: textWireframe.textStyle.size
val fontColor = getTextColor()
?: textWireframe.textStyle.color

private fun resolvePadding(textView: TextView, pixelsDensity: Float): MobileSegment.Padding {
return MobileSegment.Padding(
top = textView.totalPaddingTop.densityNormalized(pixelsDensity).toLong(),
bottom = textView.totalPaddingBottom.densityNormalized(pixelsDensity).toLong(),
left = textView.totalPaddingStart.densityNormalized(pixelsDensity).toLong(),
right = textView.totalPaddingEnd.densityNormalized(pixelsDensity).toLong()
return MobileSegment.TextStyle(
family = fontFamily,
size = fontSize,
color = fontColor
)
}

private fun resolveAlignment(
textView: TextView,
shadowNode: ReactTextShadowNode?
): MobileSegment.Alignment {
return when (textView.textAlignment) {
TextView.TEXT_ALIGNMENT_GRAVITY -> resolveShadowAlignment(textView, shadowNode = shadowNode)
TextView.TEXT_ALIGNMENT_CENTER -> MobileSegment.Alignment(
horizontal = MobileSegment.Horizontal.CENTER,
vertical = MobileSegment.Vertical.CENTER
)

TextView.TEXT_ALIGNMENT_TEXT_END,
TextView.TEXT_ALIGNMENT_VIEW_END -> MobileSegment.Alignment(
horizontal = MobileSegment.Horizontal.RIGHT,
vertical = MobileSegment.Vertical.CENTER
)

TextView.TEXT_ALIGNMENT_TEXT_START,
TextView.TEXT_ALIGNMENT_VIEW_START -> MobileSegment.Alignment(
horizontal = MobileSegment.Horizontal.LEFT,
vertical = MobileSegment.Vertical.CENTER
)

else -> MobileSegment.Alignment(
horizontal = MobileSegment.Horizontal.LEFT,
vertical = MobileSegment.Vertical.CENTER
)
private fun getTextColor(): String? {
val resolvedColor = reflectionUtils.getReflectedFieldValue(COLOR_FIELD_NAME) as Int?
if (resolvedColor != null) {
return formatAsRgba(resolvedColor)
}
}

private fun resolveShadowAlignment(
view: TextView,
shadowNode: ReactTextShadowNode?
): MobileSegment.Alignment {

val shadowGravity = reactTextShadowNodeUtils.getGravity(view, shadowNode)

return null
}

val horizontalAlignment = when (shadowGravity.and(Gravity.HORIZONTAL_GRAVITY_MASK)) {
Gravity.START,
Gravity.LEFT -> MobileSegment.Horizontal.LEFT
private fun getFontSize(): Long? {
val textAttributes = reflectionUtils.getReflectedFieldValue(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
if (textAttributes != null) {
return textAttributes.effectiveFontSize.toLong()
}

Gravity.END,
Gravity.RIGHT -> MobileSegment.Horizontal.RIGHT
return null
}

Gravity.CENTER,
Gravity.CENTER_HORIZONTAL -> MobileSegment.Horizontal.CENTER
private fun getFontFamily(): String? {
val fontFamily = reflectionUtils.getReflectedFieldValue(FONT_FAMILY_FIELD_NAME) as? String

else -> MobileSegment.Horizontal.LEFT
if (fontFamily != null) {
return resolveFontFamily(fontFamily.lowercase(Locale.US))
}
val verticalAlignment = when (shadowGravity.and(Gravity.VERTICAL_GRAVITY_MASK)) {
Gravity.TOP -> MobileSegment.Vertical.TOP
Gravity.BOTTOM -> MobileSegment.Vertical.BOTTOM

Gravity.CENTER_VERTICAL,
Gravity.CENTER -> MobileSegment.Vertical.CENTER
return null
}

else -> MobileSegment.Vertical.CENTER
private fun resolveFontFamily(typefaceName: String): String =
when (typefaceName) {
ROBOTO_TYPEFACE_NAME -> SANS_SERIF_FAMILY_NAME
MONOSPACE_FAMILY_NAME -> MONOSPACE_FAMILY_NAME
SERIF_FAMILY_NAME -> SERIF_FAMILY_NAME
else -> SANS_SERIF_FAMILY_NAME
}

return MobileSegment.Alignment(horizontalAlignment, verticalAlignment)
}
@VisibleForTesting
internal companion object {
internal const val TEXT_ATTRIBUTES_FIELD_NAME = "mTextAttributes"
internal const val FONT_FAMILY_FIELD_NAME = "mFontFamily"
internal const val COLOR_FIELD_NAME = "mColor"

private fun resolveTextColor(view: TextView, shadowNode: ReactTextShadowNode?): String {
return formatAsRgba(reactTextShadowNodeUtils.getColor(view, shadowNode))
private const val ROBOTO_TYPEFACE_NAME = "roboto"
private const val SERIF_FAMILY_NAME = "serif"
private const val SANS_SERIF_FAMILY_NAME = "roboto, sans-serif"
internal const val MONOSPACE_FAMILY_NAME = "monospace"
}
}

Loading

0 comments on commit 8f2601e

Please sign in to comment.