Skip to content

Commit

Permalink
Implement Android SR for ReactTextView and ReactEditText
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Dec 10, 2023
1 parent ffc1015 commit b27e59a
Show file tree
Hide file tree
Showing 14 changed files with 979 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ package com.datadog.reactnative.sessionreplay
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.SessionReplayPrivacy
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.
*/
class DdSessionReplayImplementation(
private val reactContext: ReactContext,
private val sessionReplayProvider: () -> SessionReplayWrapper = {
SessionReplaySDKWrapper()
}
Expand All @@ -27,7 +29,7 @@ class DdSessionReplayImplementation(
fun enable(replaySampleRate: Double, defaultPrivacyLevel: String, promise: Promise) {
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
.setPrivacy(buildPrivacy(defaultPrivacyLevel))
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport())
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext))
.build()
sessionReplayProvider().enable(configuration)
promise.resolve(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.datadog.reactnative.sessionreplay

import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
import com.facebook.react.views.view.ReactViewBackgroundDrawable

internal fun getReactBackgroundFromDrawable(drawable: Drawable?): ReactViewBackgroundDrawable? {
if (drawable is ReactViewBackgroundDrawable) {
return drawable
}

if (drawable is InsetDrawable) {
return getReactBackgroundFromDrawable(drawable.drawable)
}

if (drawable is LayerDrawable) {
for (layerNumber in 0 until drawable.numberOfLayers) {
val layer = drawable.getDrawable(layerNumber)
if (layer is ReactViewBackgroundDrawable) {
return layer
}
}
}

return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@ 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.views.text.ReactTextView
import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup

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

override fun getCustomViewMappers(): Map<SessionReplayPrivacy, Map<Class<*>, WireframeMapper<View, *>>> {
return mapOf(SessionReplayPrivacy.ALLOW to mapOf(
ReactViewGroup::class.java to ReactViewGroupMapper() as WireframeMapper<View, *>
))
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, *>
)
)
}

override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.datadog.reactnative.sessionreplay

import android.util.Log
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 com.facebook.react.views.text.ReactTextShadowNode
import com.facebook.react.views.text.TextAttributes
import okhttp3.internal.notify
import okhttp3.internal.wait

internal class ReactTextShadowNodeUtils(
private val reactContext: ReactContext
) {
internal fun getFontSize(shadowNode: ReactTextShadowNode): Int {
shadowNode.javaClass.superclass?.getDeclaredField("mTextAttributes").let {
it?.isAccessible = true
val textAttributes = it?.get(shadowNode) as TextAttributes
return textAttributes.effectiveFontSize
}
}

internal fun getFontFamily(shadowNode: ReactTextShadowNode): String? {
shadowNode.javaClass.superclass.getDeclaredField("mFontFamily").let {
it.isAccessible = true
return it.get(shadowNode)?.toString()
}
}

internal fun getGravity(viewId: Int): Int? {
val shadowNode = getShadowNode(viewId) as? ReactTextShadowNode ?: return null
shadowNode.javaClass.superclass.getDeclaredField("mTextAlign").let {
it.isAccessible = true
return it.getInt(shadowNode)
}
}

internal fun getColor(shadowNode: ReactTextShadowNode): Int {
shadowNode.javaClass.superclass.getDeclaredField("mColor").let {
it.isAccessible = true
return it.getInt(shadowNode)
}
}

internal fun getShadowNode(viewId: Int): ReactShadowNode<out ReactShadowNode<*>>? {
val uiManagerModule = try {
reactContext.getNativeModule(UIManagerModule::class.java) as UIManagerModule
} catch (e: IllegalStateException) {
Log.e(
ReactTextShadowNodeUtils::class.java.canonicalName,
"Unable to resolve uiManagerModule",
e
)
return null
}
var target: ReactShadowNode<out ReactShadowNode<*>>? = null
val shadowNodeRunnable = Runnable {
val node = uiManagerModule.resolveShadowNode(viewId)
if (node != null) {
target = node
}
synchronized(this) {
this.notify()
}
}
synchronized(this) {
reactContext.runOnNativeModulesQueueThread(shadowNodeRunnable)
this.wait()
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,15 @@
/*
* 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

internal fun Int.densityNormalized(density: Float): Int {
return if (density == 0f) {
this
} else {
(this / density).toInt()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.reactnative.sessionreplay
package com.datadog.reactnative.sessionreplay.extensions

internal fun Long.convertToDensityNormalized(density: Float): Long {
return if (density == 0f) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.datadog.reactnative.sessionreplay.extensions

import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.formatAsRgba
import com.facebook.react.uimanager.Spacing
import com.facebook.react.views.view.ReactViewBackgroundDrawable

internal fun ReactViewBackgroundDrawable.resolveShapeAndBorder(opacity: Float, pixelDensity: Float): Pair<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?> {
val borderProps = resolveBorder(this, pixelDensity)
val colorHexString = formatAsRgba(this.getBackgroundColor())
val cornerRadius = this.fullBorderRadius.toLong().convertToDensityNormalized(pixelDensity)

return MobileSegment.ShapeStyle(
colorHexString,
opacity,
cornerRadius
) to borderProps
}

private fun ReactViewBackgroundDrawable.getBackgroundColor(): Int {
javaClass.getDeclaredField("mColor").let {
it.isAccessible = true
return it.getInt(this)
}
}

private fun resolveBorder(backgroundDrawable: ReactViewBackgroundDrawable?, pixelDensity: Float): MobileSegment.ShapeBorder? {
if (backgroundDrawable == null) {
return null
}

val borderWidth = backgroundDrawable.fullBorderWidth.toLong().convertToDensityNormalized(pixelDensity)
val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL))

return MobileSegment.ShapeBorder(
color = borderColor,
width = borderWidth
)
}
Loading

0 comments on commit b27e59a

Please sign in to comment.