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-5332] Fix text properties for android new architecture #795

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
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@

package com.datadog.reactnative.sessionreplay

import android.annotation.SuppressLint
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.TouchPrivacy
import com.datadog.reactnative.DatadogSDKWrapperStorage
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactContext
import java.util.Locale

/**
* The entry point to use Datadog's Session Replay feature.
Expand All @@ -33,6 +31,7 @@ class DdSessionReplayImplementation(
* @param customEndpoint Custom server url for sending replay data.
* @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled.
*/
@SuppressLint("VisibleForTests")
fun enable(
replaySampleRate: Double,
customEndpoint: String,
Expand All @@ -42,12 +41,13 @@ class DdSessionReplayImplementation(
) {
val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
val logger = sdkCore.internalLogger
val textViewUtils = TextViewUtils.create(reactContext, logger)
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
.startRecordingImmediately(startRecordingImmediately)
.setImagePrivacy(privacySettings.imagePrivacyLevel)
.setTouchPrivacy(privacySettings.touchPrivacyLevel)
.setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel)
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext, logger))
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(textViewUtils))

if (customEndpoint != "") {
configuration.useCustomEndpoint(customEndpoint)
Expand Down

This file was deleted.

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

package com.datadog.reactnative.sessionreplay

import androidx.annotation.VisibleForTesting
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
Expand All @@ -16,56 +14,34 @@ 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.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
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


internal class ReactNativeSessionReplayExtensionSupport(
private val reactContext: ReactContext,
private val logger: InternalLogger
private val textViewUtils: TextViewUtils
) : 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)),
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(textViewUtils)),
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(textViewUtils)),
)
}

@VisibleForTesting
internal fun getUiManagerModule(): UIManagerModule? {
return try {
reactContext.getNativeModule(UIManagerModule::class.java)
} catch (e: IllegalStateException) {
logger.log(
level = InternalLogger.Level.WARN,
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
throwable = e
)
return null
}
}

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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ internal class ShadowNodeWrapper(
internal companion object {
internal fun getShadowNodeWrapper(
reactContext: ReactContext,
uiManagerModule: UIManagerModule,
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)
val node = uiManagerModule?.let { resolveShadowNode(reflectionUtils, it, viewId) }
if (node != null) {
target = node
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,11 @@ import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
import com.datadog.android.sessionreplay.utils.GlobalBounds
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
import com.facebook.react.views.textinput.ReactEditText

internal class ReactEditTextMapper(
private val reactTextPropertiesResolver: TextPropertiesResolver =
NoopTextPropertiesResolver(),
private val textViewUtils: TextViewUtils = TextViewUtils(),
private val textViewUtils: TextViewUtils
) : BaseAsyncBackgroundWireframeMapper<ReactEditText>(
viewIdentifierResolver = DefaultViewIdentifierResolver,
colorStringFormatter = DefaultColorStringFormatter,
Expand All @@ -46,20 +39,6 @@ internal class ReactEditTextMapper(
drawableToColorMapper = drawableToColorMapper,
)

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

override fun map(
view: ReactEditText,
mappingContext: MappingContext,
Expand Down Expand Up @@ -88,7 +67,6 @@ internal class ReactEditTextMapper(
wireframes = backgroundWireframes,
view = view,
mappingContext = mappingContext,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}

Expand Down
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 com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
Expand All @@ -17,50 +16,29 @@ 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.NoopTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils

internal class ReactTextMapper(
private val reactTextPropertiesResolver: TextPropertiesResolver =
NoopTextPropertiesResolver(),
private val textViewUtils: TextViewUtils = TextViewUtils(),
private val textViewUtils: TextViewUtils
): TextViewMapper<TextView>(
viewIdentifierResolver = DefaultViewIdentifierResolver,
colorStringFormatter = DefaultColorStringFormatter,
viewBoundsResolver = DefaultViewBoundsResolver,
drawableToColorMapper = DrawableToColorMapper.getDefault()
) {

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

override fun map(
view: TextView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): List<MobileSegment.Wireframe> {
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)

return textViewUtils.mapTextViewToWireframes(
wireframes = wireframes,
view = view,
mappingContext = mappingContext,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.datadog.reactnative.sessionreplay.utils.text

import android.text.Spannable
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.TextView
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
import com.facebook.react.bridge.ReactContext
import java.util.Locale

internal class FabricTextViewUtils(private val reactContext: ReactContext, private val logger: InternalLogger, drawableUtils: DrawableUtils): TextViewUtils(reactContext, drawableUtils) {

override fun resolveTextStyle(
textWireframe: MobileSegment.Wireframe.TextWireframe,
pixelsDensity: Float,
view: TextView
): MobileSegment.TextStyle {

val fontColor = getTextColor(view, textWireframe)
val fontSize = getFontSize(view, pixelsDensity)
val fontFamily = getFontFamily(textWireframe)

return MobileSegment.TextStyle(
family = fontFamily,
size = fontSize,
color = fontColor
)
}

private fun getTextColor(view: TextView, textWireframe: MobileSegment.Wireframe.TextWireframe): String {
val spanned = getFieldFromView(view, SPANNED_FIELD_NAME) as? Spannable
val spans = spanned?.getSpans(0, spanned.length, ForegroundColorSpan::class.java)
val fontColor = spans?.firstOrNull()?.foregroundColor?.let { formatAsRgba(it) } ?: textWireframe.textStyle.color

return fontColor
}

private fun getFontSize(view: TextView, pixelsDensity: Float): Long {
val fontSize = (view.textSize / pixelsDensity).toLong()
return fontSize
}

private fun getFontFamily(textWireframe: MobileSegment.Wireframe.TextWireframe): String {
val fontFamily = textWireframe.textStyle.family
return resolveFontFamily(fontFamily.lowercase(Locale.US))
}

internal fun getFieldFromView(view: View, value: String): Any? {
try {
val field = view.javaClass.getDeclaredField(value)
field.isAccessible = true
return field.get(view)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
when (e) {
is NoSuchFieldException -> handleError(e, RESOLVE_FABRICFIELD_ERROR)
is NullPointerException -> handleError(e, NULL_FABRICFIELD_ERROR)
else -> handleError(e, RESOLVE_FABRICFIELD_ERROR)
}
return null
}
}

private fun handleError(e: Exception, message: String) {
logger.log(
level = InternalLogger.Level.WARN,
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
messageBuilder = { message },
throwable = e
)
}
}
Loading