Skip to content

Commit

Permalink
RUM-6566: Implement touch override
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Oct 21, 2024
1 parent 774c798 commit fb3e526
Show file tree
Hide file tree
Showing 29 changed files with 524 additions and 112 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
@@ -0,0 +1,24 @@
/*
* 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 com.datadog.android.api.InternalLogger

internal class PrivacyHelper(private val internalLogger: InternalLogger) {
internal fun logInvalidPrivacyLevelError(e: Exception) {
internalLogger.log(
InternalLogger.Level.ERROR,
listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY),
{ INVALID_PRIVACY_LEVEL_ERROR },
e
)
}

internal companion object {
internal const val INVALID_PRIVACY_LEVEL_ERROR = "Invalid privacy level"
}
}
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,65 @@
/*
* 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.UiThread
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>()

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

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

@UiThread
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 @@ -13,6 +13,7 @@ import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.R
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.internal.PrivacyHelper
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
Expand All @@ -25,7 +26,8 @@ internal class SnapshotProducer(
private val imageWireframeHelper: ImageWireframeHelper,
private val treeViewTraversal: TreeViewTraversal,
private val optionSelectorDetector: OptionSelectorDetector,
private val internalLogger: InternalLogger
private val internalLogger: InternalLogger,
private val privacyHelper: PrivacyHelper = PrivacyHelper(internalLogger)
) {

@UiThread
Expand Down Expand Up @@ -112,7 +114,7 @@ internal class SnapshotProducer(
ImagePrivacy.valueOf(privacy)
}
} catch (e: IllegalArgumentException) {
logInvalidPrivacyLevelError(e)
privacyHelper.logInvalidPrivacyLevelError(e)
mappingContext.imagePrivacy
}

Expand All @@ -125,7 +127,7 @@ internal class SnapshotProducer(
TextAndInputPrivacy.valueOf(privacy)
}
} catch (e: IllegalArgumentException) {
logInvalidPrivacyLevelError(e)
privacyHelper.logInvalidPrivacyLevelError(e)
mappingContext.textAndInputPrivacy
}

Expand All @@ -134,17 +136,4 @@ internal class SnapshotProducer(
textAndInputPrivacy = textAndInputPrivacy
)
}

private fun logInvalidPrivacyLevelError(e: Exception) {
internalLogger.log(
InternalLogger.Level.ERROR,
listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY),
{ INVALID_PRIVACY_LEVEL_ERROR },
e
)
}

internal companion object {
internal const val INVALID_PRIVACY_LEVEL_ERROR = "Invalid privacy level"
}
}
Loading

0 comments on commit fb3e526

Please sign in to comment.