Skip to content

Commit

Permalink
RUM-6296: Implement touch overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Oct 20, 2024
1 parent 774c798 commit 940f01c
Show file tree
Hide file tree
Showing 26 changed files with 483 additions and 95 deletions.
2 changes: 2 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ datadog:
- "android.graphics.drawable.RippleDrawable.findIndexByLayerId(kotlin.Int)"
- "android.graphics.drawable.DrawableContainer.DrawableContainerState.getChild(kotlin.Int)"
- "android.graphics.Point.constructor()"
- "android.graphics.Point.constructor(kotlin.Int, kotlin.Int)"
- "android.graphics.Rect.centerX()"
- "android.graphics.Rect.centerY()"
- "android.graphics.Rect.constructor()"
Expand Down Expand Up @@ -729,6 +730,7 @@ datadog:
- "java.security.SecureRandom.nextFloat()"
- "java.security.SecureRandom.nextInt()"
- "java.security.SecureRandom.nextLong()"
- "java.util.HashMap.clear()"
- "java.util.HashSet.addAll(kotlin.collections.Collection)"
- "java.util.HashSet.find(kotlin.Function1)"
- "java.util.LinkedList.addFirst(com.datadog.android.webview.internal.rum.domain.WebViewNativeRumViewsCache.ViewEntry?)"
Expand Down
1 change: 1 addition & 0 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface com.datadog.android.sessionreplay.PrivacyLevel
fun android.view.View.setSessionReplayHidden(Boolean)
fun android.view.View.setSessionReplayImagePrivacy(ImagePrivacy?)
fun android.view.View.setSessionReplayTextAndInputPrivacy(TextAndInputPrivacy?)
fun android.view.View.setSessionReplayTouchPrivacy(TouchPrivacy?)
object com.datadog.android.sessionreplay.SessionReplay
fun enable(SessionReplayConfiguration, com.datadog.android.api.SdkCore = Datadog.getInstance())
fun startRecording(com.datadog.android.api.SdkCore = Datadog.getInstance())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class com/datadog/android/sessionreplay/PrivacyOverrideExtensionsKt
public static final fun setSessionReplayHidden (Landroid/view/View;Z)V
public static final fun setSessionReplayImagePrivacy (Landroid/view/View;Lcom/datadog/android/sessionreplay/ImagePrivacy;)V
public static final fun setSessionReplayTextAndInputPrivacy (Landroid/view/View;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;)V
public static final fun setSessionReplayTouchPrivacy (Landroid/view/View;Lcom/datadog/android/sessionreplay/TouchPrivacy;)V
}

public final class com/datadog/android/sessionreplay/SessionReplay {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@ fun View.setSessionReplayTextAndInputPrivacy(privacy: TextAndInputPrivacy?) {
this.setTag(R.id.datadog_text_and_input_privacy, privacy.toString())
}
}

/**
* Allows overriding the touch privacy for a view in Session Replay.
*
* @param privacy the new privacy level to use for the view
* or null to remove the override.
*/
fun View.setSessionReplayTouchPrivacy(privacy: TouchPrivacy?) {
if (privacy == null) {
this.setTag(R.id.datadog_touch_privacy, null)
} else {
this.setTag(R.id.datadog_touch_privacy, privacy.toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.datadog.android.api.SdkCore
import com.datadog.android.api.feature.Feature.Companion.SESSION_REPLAY_FEATURE_NAME
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.sessionreplay.internal.SessionReplayFeature
import com.datadog.android.sessionreplay.internal.TouchPrivacyManager

/**
* An entry point to Datadog Session Replay feature.
Expand All @@ -35,12 +36,14 @@ object SessionReplay {
val featureSdkCore = sdkCore as FeatureSdkCore
sessionReplayConfiguration.systemRequirementsConfiguration
.runIfRequirementsMet(featureSdkCore.internalLogger) {
val touchPrivacyManager = TouchPrivacyManager(sessionReplayConfiguration.touchPrivacy)
val sessionReplayFeature = SessionReplayFeature(
sdkCore = featureSdkCore,
customEndpointUrl = sessionReplayConfiguration.customEndpointUrl,
privacy = sessionReplayConfiguration.privacy,
imagePrivacy = sessionReplayConfiguration.imagePrivacy,
touchPrivacy = sessionReplayConfiguration.touchPrivacy,
touchPrivacyManager = touchPrivacyManager,
textAndInputPrivacy = sessionReplayConfiguration.textAndInputPrivacy,
customMappers = sessionReplayConfiguration.customMappers,
customOptionSelectorDetectors = sessionReplayConfiguration.customOptionSelectorDetectors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.MapperTypeWrapper
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.TouchPrivacy
import com.datadog.android.sessionreplay.internal.recorder.Recorder
import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder
import com.datadog.android.sessionreplay.internal.recorder.mapper.ActionBarContainerMapper
Expand Down Expand Up @@ -60,7 +59,7 @@ internal class DefaultRecorderProvider(
private val sdkCore: FeatureSdkCore,
private val textAndInputPrivacy: TextAndInputPrivacy,
private val imagePrivacy: ImagePrivacy,
private val touchPrivacy: TouchPrivacy,
private val touchPrivacyManager: TouchPrivacyManager,
private val customMappers: List<MapperTypeWrapper<*>>,
private val customOptionSelectorDetectors: List<OptionSelectorDetector>,
private val dynamicOptimizationEnabled: Boolean
Expand All @@ -78,7 +77,7 @@ internal class DefaultRecorderProvider(
resourcesWriter = resourceWriter,
rumContextProvider = SessionReplayRumContextProvider(sdkCore),
imagePrivacy = imagePrivacy,
touchPrivacy = touchPrivacy,
touchPrivacyManager = touchPrivacyManager,
textAndInputPrivacy = textAndInputPrivacy,
recordWriter = recordWriter,
timeProvider = SessionReplayTimeProvider(sdkCore),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ internal class SessionReplayFeature(

private val currentRumSessionId = AtomicReference<String>()

@Suppress("LongParameterList")
internal constructor(
sdkCore: FeatureSdkCore,
customEndpointUrl: String?,
privacy: SessionReplayPrivacy,
textAndInputPrivacy: TextAndInputPrivacy,
touchPrivacy: TouchPrivacy,
touchPrivacyManager: TouchPrivacyManager,
imagePrivacy: ImagePrivacy,
customMappers: List<MapperTypeWrapper<*>>,
customOptionSelectorDetectors: List<OptionSelectorDetector>,
Expand All @@ -81,7 +83,7 @@ internal class SessionReplayFeature(
sdkCore,
textAndInputPrivacy,
imagePrivacy,
touchPrivacy,
touchPrivacyManager,
customMappers,
customOptionSelectorDetectors,
dynamicOptimizationEnabled
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.android.sessionreplay.internal

import android.graphics.Point
import android.graphics.Rect
import androidx.annotation.VisibleForTesting
import com.datadog.android.sessionreplay.TouchPrivacy

internal class TouchPrivacyManager(
private val globalTouchPrivacy: TouchPrivacy
) {
// areas on screen where overrides are applied
@VisibleForTesting internal val currentOverrideSnapshot = HashMap<Rect, TouchPrivacy>()

// built during the view traversal and copied to currentOverrideSnapshot at the end
@VisibleForTesting internal val nextOverrideSnapshot = HashMap<Rect, TouchPrivacy>()

internal fun addTouchOverrideArea(bounds: Rect, touchPrivacy: TouchPrivacy) {
nextOverrideSnapshot[bounds] = touchPrivacy
}

internal fun copyNextSnapshotToCurrentSnapshot() {
currentOverrideSnapshot.clear()
@Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here
currentOverrideSnapshot.putAll(nextOverrideSnapshot)
nextOverrideSnapshot.clear()
}

internal fun resolveTouchPrivacy(touchLocation: Point): TouchPrivacy {
var showOverrideSet = false

// avoid having the collection change while we iterate through it
val overrideAreas = HashMap<Rect, TouchPrivacy>()
@Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here
overrideAreas.putAll(currentOverrideSnapshot)

@Suppress("UnsafeThirdPartyFunctionCall") // ConcurrentModification cannot happen here
overrideAreas.forEach { entry ->
val area = entry.key
val overrideValue = entry.value

if (isWithinOverrideArea(touchLocation, area)) {
when (overrideValue) {
TouchPrivacy.HIDE -> return TouchPrivacy.HIDE
TouchPrivacy.SHOW -> showOverrideSet = true
}
}
}

return if (showOverrideSet) TouchPrivacy.SHOW else globalTouchPrivacy
}

private fun isWithinOverrideArea(touchPoint: Point, overrideArea: Rect): Boolean =
touchPoint.x in overrideArea.left..overrideArea.right &&
touchPoint.y in overrideArea.top..overrideArea.bottom
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.core.metrics.MethodCallSamplingRate
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.internal.TouchPrivacyManager
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.recorder.listener.WindowsOnDrawListener

Expand All @@ -25,7 +26,8 @@ internal class DefaultOnDrawListenerProducer(
override fun create(
decorViews: List<View>,
textAndInputPrivacy: TextAndInputPrivacy,
imagePrivacy: ImagePrivacy
imagePrivacy: ImagePrivacy,
touchPrivacyManager: TouchPrivacyManager
): ViewTreeObserver.OnDrawListener {
return WindowsOnDrawListener(
zOrderedDecorViews = decorViews,
Expand All @@ -35,7 +37,8 @@ internal class DefaultOnDrawListenerProducer(
imagePrivacy = imagePrivacy,
sdkCore = sdkCore,
methodCallSamplingRate = MethodCallSamplingRate.LOW.rate,
dynamicOptimizationEnabled = dynamicOptimizationEnabled
dynamicOptimizationEnabled = dynamicOptimizationEnabled,
touchPrivacyManager = touchPrivacyManager
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import android.view.View
import android.view.ViewTreeObserver
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.internal.TouchPrivacyManager

internal fun interface OnDrawListenerProducer {
fun create(
decorViews: List<View>,
textAndInputPrivacy: TextAndInputPrivacy,
imagePrivacy: ImagePrivacy
imagePrivacy: ImagePrivacy,
touchPrivacyManager: TouchPrivacyManager
): ViewTreeObserver.OnDrawListener
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.MapperTypeWrapper
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.TouchPrivacy
import com.datadog.android.sessionreplay.internal.LifecycleCallback
import com.datadog.android.sessionreplay.internal.SessionReplayLifecycleCallback
import com.datadog.android.sessionreplay.internal.TouchPrivacyManager
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.processor.MutationResolver
import com.datadog.android.sessionreplay.internal.processor.RecordedDataProcessor
Expand Down Expand Up @@ -58,7 +58,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
private val rumContextProvider: RumContextProvider
private val textAndInputPrivacy: TextAndInputPrivacy
private val imagePrivacy: ImagePrivacy
private val touchPrivacy: TouchPrivacy
private val touchPrivacyManager: TouchPrivacyManager
private val recordWriter: RecordWriter
private val timeProvider: TimeProvider
private val mappers: List<MapperTypeWrapper<*>>
Expand All @@ -80,7 +80,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
rumContextProvider: RumContextProvider,
textAndInputPrivacy: TextAndInputPrivacy,
imagePrivacy: ImagePrivacy,
touchPrivacy: TouchPrivacy,
touchPrivacyManager: TouchPrivacyManager,
recordWriter: RecordWriter,
timeProvider: TimeProvider,
mappers: List<MapperTypeWrapper<*>> = emptyList(),
Expand Down Expand Up @@ -110,7 +110,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
this.rumContextProvider = rumContextProvider
this.textAndInputPrivacy = textAndInputPrivacy
this.imagePrivacy = imagePrivacy
this.touchPrivacy = touchPrivacy
this.touchPrivacyManager = touchPrivacyManager
this.recordWriter = recordWriter
this.timeProvider = timeProvider
this.mappers = mappers
Expand Down Expand Up @@ -179,7 +179,8 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
viewIdentifierResolver = viewIdentifierResolver
),
viewUtilsInternal = ViewUtilsInternal(),
internalLogger = internalLogger
internalLogger = internalLogger,
touchPrivacyManager = touchPrivacyManager
),
ComposedOptionSelectorDetector(
customOptionSelectorDetectors + DefaultOptionSelectorDetector()
Expand All @@ -189,16 +190,17 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
recordedDataQueueHandler = recordedDataQueueHandler,
sdkCore = sdkCore,
dynamicOptimizationEnabled = dynamicOptimizationEnabled
)
),
touchPrivacyManager = touchPrivacyManager
)
this.windowCallbackInterceptor = WindowCallbackInterceptor(
recordedDataQueueHandler,
viewOnDrawInterceptor,
timeProvider,
internalLogger,
imagePrivacy,
touchPrivacy,
textAndInputPrivacy
textAndInputPrivacy,
touchPrivacyManager
)
this.sessionReplayLifecycleCallback = SessionReplayLifecycleCallback(this)
this.uiHandler = Handler(Looper.getMainLooper())
Expand All @@ -212,7 +214,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
rumContextProvider: RumContextProvider,
textAndInputPrivacy: TextAndInputPrivacy,
imagePrivacy: ImagePrivacy,
touchPrivacy: TouchPrivacy,
touchPrivacyManager: TouchPrivacyManager,
recordWriter: RecordWriter,
timeProvider: TimeProvider,
mappers: List<MapperTypeWrapper<*>> = emptyList(),
Expand All @@ -230,7 +232,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
this.rumContextProvider = rumContextProvider
this.textAndInputPrivacy = textAndInputPrivacy
this.imagePrivacy = imagePrivacy
this.touchPrivacy = touchPrivacy
this.touchPrivacyManager = touchPrivacyManager
this.recordWriter = recordWriter
this.timeProvider = timeProvider
this.mappers = mappers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.datadog.android.sessionreplay.internal.recorder

import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
Expand All @@ -14,6 +15,8 @@ import com.datadog.android.api.feature.measureMethodCallPerf
import com.datadog.android.core.metrics.MethodCallSamplingRate
import com.datadog.android.sessionreplay.MapperTypeWrapper
import com.datadog.android.sessionreplay.R
import com.datadog.android.sessionreplay.TouchPrivacy
import com.datadog.android.sessionreplay.internal.TouchPrivacyManager
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.internal.recorder.mapper.HiddenViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.QueueStatusCallback
Expand All @@ -23,14 +26,16 @@ import com.datadog.android.sessionreplay.recorder.mapper.TraverseAllChildrenMapp
import com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.NoOpAsyncJobStatusCallback
import java.util.Locale

internal class TreeViewTraversal(
private val mappers: List<MapperTypeWrapper<*>>,
private val defaultViewMapper: WireframeMapper<View>,
private val hiddenViewMapper: HiddenViewMapper,
private val decorViewMapper: WireframeMapper<View>,
private val viewUtilsInternal: ViewUtilsInternal,
private val internalLogger: InternalLogger
private val internalLogger: InternalLogger,
private val touchPrivacyManager: TouchPrivacyManager
) {

@Suppress("ReturnCount")
Expand All @@ -53,6 +58,7 @@ internal class TreeViewTraversal(

// try to resolve from the exhaustive type mappers
var mapper = findMapperForView(view)
updateTouchOverrideAreas(view)

if (isHidden(view)) {
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
Expand Down Expand Up @@ -115,6 +121,30 @@ internal class TreeViewTraversal(
private fun isHidden(view: View): Boolean =
view.getTag(R.id.datadog_hidden) == true

private fun updateTouchOverrideAreas(view: View) {
val touchPrivacy = view.getTag(R.id.datadog_touch_privacy)

if (touchPrivacy != null) {
val coords = IntArray(2)

// this will always have size >= 2
@Suppress("UnsafeThirdPartyFunctionCall")
view.getLocationOnScreen(coords)

val x = coords[0]
val y = coords[1]
val rect = Rect(
x - view.paddingLeft,
y - view.paddingTop,
x + view.width + view.paddingRight,
y + view.height + view.paddingBottom
)

val privacyLevel = TouchPrivacy.valueOf(touchPrivacy.toString().uppercase(Locale.US))
touchPrivacyManager.addTouchOverrideArea(rect, privacyLevel)
}
}

data class TraversedTreeView(
val mappedWireframes: List<MobileSegment.Wireframe>,
val nextActionStrategy: TraversalStrategy
Expand Down
Loading

0 comments on commit 940f01c

Please sign in to comment.