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

RUMM-2777 SR add dialogs recording support #1206

Merged
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 @@ -67,6 +67,7 @@ import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.quality.Strictness
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference

/**
Expand Down Expand Up @@ -159,6 +160,7 @@ internal class DatadogCoreTest {
) {
// Given
testedCore.coreFeature = mock()
whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean())
0xnm marked this conversation as resolved.
Show resolved Hide resolved
val mockUserInfoProvider = mock<MutableUserInfoProvider>()
whenever(testedCore.coreFeature.userInfoProvider) doReturn mockUserInfoProvider

Expand Down Expand Up @@ -360,6 +362,7 @@ internal class DatadogCoreTest {
) {
// Given
testedCore.coreFeature = mock()
whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean())
val mockTimeProvider = mock<TimeProvider>()
whenever(testedCore.coreFeature.timeProvider) doReturn mockTimeProvider
whenever(mockTimeProvider.getServerOffsetNanos()) doReturn TimeUnit.MILLISECONDS.toNanos(
Expand Down Expand Up @@ -391,6 +394,7 @@ internal class DatadogCoreTest {
fun `𝕄 provide time info without correction 𝕎 time() {NoOpTimeProvider}`() {
// Given
testedCore.coreFeature = mock()
whenever(testedCore.coreFeature.initialized).thenReturn(AtomicBoolean())
whenever(testedCore.coreFeature.timeProvider) doReturn NoOpTimeProvider()

// When
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ package com.datadog.android.sessionreplay
import android.app.Activity
import android.app.Application
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.datadog.android.sessionreplay.processor.RecordedDataProcessor
import com.datadog.android.sessionreplay.recorder.Recorder
import com.datadog.android.sessionreplay.recorder.ScreenRecorder
import com.datadog.android.sessionreplay.recorder.SnapshotProducer
import com.datadog.android.sessionreplay.recorder.callback.RecorderFragmentLifecycleCallback
import com.datadog.android.sessionreplay.utils.RumContextProvider
import com.datadog.android.sessionreplay.utils.TimeProvider
import java.util.WeakHashMap
import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
Expand All @@ -29,7 +30,7 @@ class SessionReplayLifecycleCallback(
privacy: SessionReplayPrivacy,
recordWriter: RecordWriter,
timeProvider: TimeProvider,
private val recordCallback: RecordCallback = NoOpRecordCallback()
recordCallback: RecordCallback = NoOpRecordCallback()
) : LifecycleCallback {

@Suppress("UnsafeThirdPartyFunctionCall") // workQueue can't be null
Expand All @@ -48,30 +49,36 @@ class SessionReplayLifecycleCallback(
recordWriter
),
SnapshotProducer(privacy.mapper()),
timeProvider
timeProvider,
recordCallback
)
internal val resumedActivities: WeakHashMap<Activity, Any?> = WeakHashMap()

// region callback

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
// No Op
if (activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
RecorderFragmentLifecycleCallback(recorder),
true
)
}
}

override fun onActivityStarted(activity: Activity) {
// No Op
}

override fun onActivityResumed(activity: Activity) {
recorder.startRecording(activity)
recordCallback.onStartRecording()
resumedActivities[activity] = null
activity.window?.let {
recorder.startRecording(listOf(it), activity)
}
}

override fun onActivityPaused(activity: Activity) {
recorder.stopRecording(activity)
recordCallback.onStopRecording()
resumedActivities.remove(activity)
activity.window?.let {
recorder.stopRecording(listOf(it))
}
}

override fun onActivityStopped(activity: Activity) {
Expand All @@ -96,12 +103,7 @@ class SessionReplayLifecycleCallback(

override fun unregisterAndStopRecorders(appContext: Application) {
appContext.unregisterActivityLifecycleCallbacks(this)
resumedActivities.keys.forEach {
it?.let { activity ->
recorder.stopRecording(activity)
}
}
resumedActivities.clear()
recorder.stopRecording()
}

// endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import com.datadog.android.sessionreplay.recorder.Node
import com.datadog.android.sessionreplay.recorder.OrientationChanged

internal interface Processor {
fun processScreenSnapshot(node: Node, orientationChanged: OrientationChanged? = null)

fun processScreenSnapshots(nodes: List<Node>, orientationChanged: OrientationChanged? = null)

fun processTouchEventsRecords(touchEventsRecords: List<MobileSegment.MobileRecord>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ internal class RecordedDataProcessor(
private var lastSnapshotTimestamp = 0L

@MainThread
override fun processScreenSnapshot(node: Node, orientationChanged: OrientationChanged?) {
override fun processScreenSnapshots(
nodes: List<Node>,
orientationChanged: OrientationChanged?
) {
buildRunnable { timestamp, newContext, currentContext ->
Runnable {
@Suppress("ThreadSafety") // this runs inside an executor
handleSnapshot(newContext, currentContext, timestamp, node, orientationChanged)
handleSnapshots(
newContext,
currentContext,
timestamp,
nodes,
orientationChanged
)
}
}?.let { executeRunnable(it) }
}
Expand All @@ -67,14 +76,14 @@ internal class RecordedDataProcessor(
}

@WorkerThread
private fun handleSnapshot(
private fun handleSnapshots(
newRumContext: SessionReplayRumContext,
prevRumContext: SessionReplayRumContext,
timestamp: Long,
snapshot: Node,
snapshots: List<Node>,
orientationChanged: OrientationChanged?
) {
val wireframes = nodeFlattener.flattenNode(snapshot)
val wireframes = snapshots.flatMap { nodeFlattener.flattenNode(it) }

if (wireframes.isEmpty()) {
// TODO: RUMM-2397 Add the proper logs here once the sdkLogger will be added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
package com.datadog.android.sessionreplay.recorder

import android.app.Activity
import android.view.Window

internal interface Recorder {

fun startRecording(activity: Activity)
fun startRecording(windows: List<Window>, ownerActivity: Activity)

fun stopRecording(activity: Activity)
fun stopRecording(windows: List<Window>)

fun stopRecording()
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,64 @@ package com.datadog.android.sessionreplay.recorder
import android.app.Activity
import android.view.ViewTreeObserver
import android.view.Window
import com.datadog.android.sessionreplay.NoOpRecordCallback
import com.datadog.android.sessionreplay.RecordCallback
import com.datadog.android.sessionreplay.processor.Processor
import com.datadog.android.sessionreplay.recorder.callback.NoOpWindowCallback
import com.datadog.android.sessionreplay.recorder.callback.RecorderWindowCallback
import com.datadog.android.sessionreplay.recorder.listener.WindowsOnDrawListener
import com.datadog.android.sessionreplay.utils.TimeProvider
import java.util.WeakHashMap

internal class ScreenRecorder(
private val processor: Processor,
private val snapshotProducer: SnapshotProducer,
private val timeProvider: TimeProvider
private val timeProvider: TimeProvider,
private val recordCallback: RecordCallback = NoOpRecordCallback()
) : Recorder {
internal val drawListeners: MutableMap<Int, ViewTreeObserver.OnDrawListener> = HashMap()
internal val windowsListeners: WeakHashMap<Window, ViewTreeObserver.OnDrawListener> =
WeakHashMap()

override fun startRecording(activity: Activity) {
if (activity.window != null) {
with(
RecorderOnDrawListener(
activity,
activity.resources.displayMetrics.density,
processor,
snapshotProducer
)
) {
drawListeners[activity.hashCode()] = this
activity.window.decorView.viewTreeObserver?.addOnDrawListener(this)
}

wrapWindowCallback(
activity.window,
activity.resources.displayMetrics.density
)
override fun startRecording(windows: List<Window>, ownerActivity: Activity) {
// first we make sure we don't record a window multiple times
stopRecordingAndRemove(windows)
val screenDensity = ownerActivity.resources.displayMetrics.density
val onDrawListener = WindowsOnDrawListener(
ownerActivity,
windows,
screenDensity,
processor,
snapshotProducer
)
windows.forEach { window ->
val decorView = window.decorView
windowsListeners[window] = onDrawListener
decorView.viewTreeObserver?.addOnDrawListener(onDrawListener)
wrapWindowCallback(window, screenDensity)
}
recordCallback.onStartRecording()
}

override fun stopRecording(activity: Activity) {
activity.hashCode().let { windowHashCode ->
drawListeners.remove(windowHashCode)?.let {
activity.window.decorView.viewTreeObserver.removeOnDrawListener(it)
}
override fun stopRecording(windows: List<Window>) {
stopRecordingAndRemove(windows)
recordCallback.onStopRecording()
}

override fun stopRecording() {
windowsListeners.entries.forEach {
it.key.decorView.viewTreeObserver.removeOnDrawListener(it.value)
unwrapWindowCallback(it.key)
}
windowsListeners.clear()
recordCallback.onStopRecording()
}

if (activity.window != null) {
unwrapWindowCallback(activity.window)
private fun stopRecordingAndRemove(windows: List<Window>) {
windows.forEach { window ->
windowsListeners.remove(window)?.let {
window.decorView.viewTreeObserver.removeOnDrawListener(it)
}
unwrapWindowCallback(window)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.recorder.callback

import android.os.Build
import android.view.MotionEvent

internal object MotionEventUtils {

// For Android SDK < 29 we will have to know the target View on which this event happened
// and then to compute the absolute as target.absoluteX,Y + event.getX,Y(pointerIndex)
// This will not be handled now as it is too complex and not very optimised. For now we will
// not support multi finger gestures in the player for version < 29.
fun getPointerAbsoluteX(event: MotionEvent, pointerIndex: Int): Float {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
event.getRawX(pointerIndex)
} else {
return event.rawX
}
}

fun getPointerAbsoluteY(event: MotionEvent, pointerIndex: Int): Float {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
event.getRawY(pointerIndex)
} else {
return event.rawY
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.recorder
package com.datadog.android.sessionreplay.recorder.callback

import android.view.ActionMode
import android.view.KeyEvent
Expand Down
Loading