diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt index 5d9429eede..1855de50bb 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt @@ -15,6 +15,7 @@ import android.widget.CheckedTextView import android.widget.EditText import android.widget.ImageView import android.widget.NumberPicker +import android.widget.ProgressBar import android.widget.RadioButton import android.widget.SeekBar import android.widget.TextView @@ -31,6 +32,7 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckedTextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.ImageViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.NumberPickerMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ProgressBarWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.RadioButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.SeekBarWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.SwitchCompatMapper @@ -184,6 +186,16 @@ internal class DefaultRecorderProvider( viewBoundsResolver, drawableToColorMapper ) + ), + MapperTypeWrapper( + ProgressBar::class.java, + ProgressBarWireframeMapper( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper, + true + ) ) ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt new file mode 100644 index 0000000000..8698aa168c --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt @@ -0,0 +1,204 @@ +/* + * 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.recorder.mapper + +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.os.Build +import android.os.Build.VERSION +import android.widget.ProgressBar +import androidx.annotation.RequiresApi +import androidx.annotation.UiThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.SessionReplayPrivacy +import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.recorder.MappingContext +import com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper +import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.utils.ColorStringFormatter +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.GlobalBounds +import com.datadog.android.sessionreplay.utils.OPAQUE_ALPHA_VALUE +import com.datadog.android.sessionreplay.utils.PARTIALLY_OPAQUE_ALPHA_VALUE +import com.datadog.android.sessionreplay.utils.ViewBoundsResolver +import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver + +internal open class ProgressBarWireframeMapper

( + viewIdentifierResolver: ViewIdentifierResolver, + colorStringFormatter: ColorStringFormatter, + viewBoundsResolver: ViewBoundsResolver, + drawableToColorMapper: DrawableToColorMapper, + val showProgressWhenMaskUserInput: Boolean +) : BaseAsyncBackgroundWireframeMapper

( + viewIdentifierResolver, + colorStringFormatter, + viewBoundsResolver, + drawableToColorMapper +) { + + @UiThread + override fun map( + view: P, + mappingContext: MappingContext, + asyncJobStatusCallback: AsyncJobStatusCallback, + internalLogger: InternalLogger + ): List { + val wireframes = mutableListOf() + + // add background if needed + wireframes.addAll(super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)) + + val screenDensity = mappingContext.systemInformation.screenDensity + val viewPaddedBounds = viewBoundsResolver.resolveViewPaddedBounds(view, screenDensity) + val trackHeight = TRACK_HEIGHT_IN_PX.densityNormalized(screenDensity) + val trackBounds = GlobalBounds( + x = viewPaddedBounds.x, + y = viewPaddedBounds.y + (viewPaddedBounds.height - trackHeight) / 2, + width = viewPaddedBounds.width, + height = trackHeight + ) + + val defaultColor = getDefaultColor(view) + val trackColor = getColor(view.progressTintList, view.drawableState) ?: defaultColor + + buildNonActiveTrackWireframe(view, trackBounds, trackColor)?.let(wireframes::add) + + val hasProgress = !view.isIndeterminate + val showProgress = (mappingContext.privacy == SessionReplayPrivacy.ALLOW) || + (mappingContext.privacy == SessionReplayPrivacy.MASK_USER_INPUT && showProgressWhenMaskUserInput) + + if (hasProgress && showProgress) { + val normalizedProgress = normalizedProgress(view) + mapDeterminate( + wireframes = wireframes, + view = view, + mappingContext = mappingContext, + asyncJobStatusCallback = asyncJobStatusCallback, + internalLogger = internalLogger, + trackBounds = trackBounds, + trackColor = trackColor, + normalizedProgress = normalizedProgress + ) + } + + return wireframes + } + + protected open fun mapDeterminate( + wireframes: MutableList, + view: P, + mappingContext: MappingContext, + asyncJobStatusCallback: AsyncJobStatusCallback, + internalLogger: InternalLogger, + trackBounds: GlobalBounds, + trackColor: Int, + normalizedProgress: Float + ) { + buildActiveTrackWireframe(view, trackBounds, normalizedProgress, trackColor)?.let(wireframes::add) + } + + private fun buildNonActiveTrackWireframe( + view: P, + trackBounds: GlobalBounds, + trackColor: Int + ): MobileSegment.Wireframe? { + val nonActiveTrackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, NON_ACTIVE_TRACK_KEY_NAME) + ?: return null + val backgroundColor = colorStringFormatter.formatColorAndAlphaAsHexString( + trackColor, + PARTIALLY_OPAQUE_ALPHA_VALUE + ) + return MobileSegment.Wireframe.ShapeWireframe( + id = nonActiveTrackId, + x = trackBounds.x, + y = trackBounds.y, + width = trackBounds.width, + height = trackBounds.height, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = backgroundColor, + opacity = view.alpha + ) + ) + } + + private fun buildActiveTrackWireframe( + view: P, + trackBounds: GlobalBounds, + normalizedProgress: Float, + trackColor: Int + ): MobileSegment.Wireframe? { + val activeTrackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, ACTIVE_TRACK_KEY_NAME) + ?: return null + val backgroundColor = colorStringFormatter.formatColorAndAlphaAsHexString( + trackColor, + OPAQUE_ALPHA_VALUE + ) + return MobileSegment.Wireframe.ShapeWireframe( + id = activeTrackId, + x = trackBounds.x, + y = trackBounds.y, + width = (trackBounds.width * normalizedProgress).toLong(), + height = trackBounds.height, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = backgroundColor, + opacity = view.alpha + ) + ) + } + + private fun normalizedProgress(view: P): Float { + return if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { + normalizedProgressAndroidO(view) + } else { + normalizedProgressLegacy(view) + } + } + + private fun normalizedProgressLegacy(view: P): Float { + val range = view.max.toFloat() + return if (view.max == 0) { + 0f + } else { + view.progress.toFloat() / range + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun normalizedProgressAndroidO(view: P): Float { + val range = view.max.toFloat() - view.min.toFloat() + return if (range == 0f) { + 0f + } else { + (view.progress - view.min) / range + } + } + + protected fun getColor(colorStateList: ColorStateList?, state: IntArray): Int? { + return colorStateList?.getColorForState(state, colorStateList.defaultColor) + } + + protected fun getDefaultColor(view: P): Int { + val uiModeFlags = view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return if (uiModeFlags == Configuration.UI_MODE_NIGHT_YES) { + NIGHT_MODE_COLOR + } else { + DAY_MODE_COLOR + } + } + + companion object { + internal const val NIGHT_MODE_COLOR = 0xffffff // White + internal const val DAY_MODE_COLOR = 0 // Black + internal const val ACTIVE_TRACK_KEY_NAME = "seekbar_active_track" + internal const val NON_ACTIVE_TRACK_KEY_NAME = "seekbar_non_active_track" + internal const val THUMB_KEY_NAME = "seekbar_thumb" + + internal const val THUMB_SHAPE_CORNER_RADIUS = 10 + internal const val TRACK_HEIGHT_IN_PX = 8L + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt index 43d7314c99..fd4d6c21c8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapper.kt @@ -6,25 +6,17 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.os.Build -import android.os.Build.VERSION import android.widget.SeekBar -import androidx.annotation.RequiresApi -import androidx.annotation.UiThread import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.SessionReplayPrivacy import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext -import com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.android.sessionreplay.utils.OPAQUE_ALPHA_VALUE -import com.datadog.android.sessionreplay.utils.PARTIALLY_OPAQUE_ALPHA_VALUE import com.datadog.android.sessionreplay.utils.ViewBoundsResolver import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver import kotlin.math.max @@ -34,48 +26,40 @@ internal open class SeekBarWireframeMapper( colorStringFormatter: ColorStringFormatter, viewBoundsResolver: ViewBoundsResolver, drawableToColorMapper: DrawableToColorMapper -) : BaseAsyncBackgroundWireframeMapper( - viewIdentifierResolver, - colorStringFormatter, - viewBoundsResolver, - drawableToColorMapper +) : ProgressBarWireframeMapper( + viewIdentifierResolver = viewIdentifierResolver, + colorStringFormatter = colorStringFormatter, + viewBoundsResolver = viewBoundsResolver, + drawableToColorMapper = drawableToColorMapper, + showProgressWhenMaskUserInput = false ) { - @UiThread - override fun map( + + override fun mapDeterminate( + wireframes: MutableList, view: SeekBar, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback, - internalLogger: InternalLogger - ): List { - val wireframes = mutableListOf() - - // add background if needed - wireframes.addAll(super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)) - - val screenDensity = mappingContext.systemInformation.screenDensity - val viewPaddedBounds = viewBoundsResolver.resolveViewPaddedBounds(view, screenDensity) - - val trackHeight = TRACK_HEIGHT_IN_PX.densityNormalized(screenDensity) - val trackBounds = GlobalBounds( - x = viewPaddedBounds.x, - y = viewPaddedBounds.y + (viewPaddedBounds.height - trackHeight) / 2, - width = viewPaddedBounds.width, - height = trackHeight + internalLogger: InternalLogger, + trackBounds: GlobalBounds, + trackColor: Int, + normalizedProgress: Float + ) { + super.mapDeterminate( + wireframes, + view, + mappingContext, + asyncJobStatusCallback, + internalLogger, + trackBounds, + trackColor, + normalizedProgress ) - val defaultColor = getDefaultColor(view) - val trackColor = getColor(view.progressTintList, view.drawableState) ?: defaultColor - val thumbColor = getColor(view.thumbTintList, view.drawableState) ?: defaultColor - - buildNonActiveTrackWireframe(view, trackBounds, trackColor)?.let(wireframes::add) - if (mappingContext.privacy == SessionReplayPrivacy.ALLOW) { - val normalizedProgress = if (VERSION.SDK_INT >= Build.VERSION_CODES.O) { - normalizedProgressAndroidO(view) - } else { - normalizedProgressLegacy(view) - } - buildActiveTrackWireframe(view, trackBounds, normalizedProgress, trackColor)?.let(wireframes::add) + val screenDensity = mappingContext.systemInformation.screenDensity + val trackHeight = ProgressBarWireframeMapper.TRACK_HEIGHT_IN_PX.densityNormalized(screenDensity) + val thumbColor = getColor(view.thumbTintList, view.drawableState) ?: getDefaultColor(view) + buildThumbWireframe( view = view, trackBounds = trackBounds, @@ -85,57 +69,6 @@ internal open class SeekBarWireframeMapper( thumbColor = thumbColor )?.let(wireframes::add) } - - return wireframes - } - - private fun buildNonActiveTrackWireframe( - view: SeekBar, - trackBounds: GlobalBounds, - trackColor: Int - ): MobileSegment.Wireframe? { - val nonActiveTrackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, NON_ACTIVE_TRACK_KEY_NAME) - ?: return null - val backgroundColor = colorStringFormatter.formatColorAndAlphaAsHexString( - trackColor, - PARTIALLY_OPAQUE_ALPHA_VALUE - ) - return MobileSegment.Wireframe.ShapeWireframe( - id = nonActiveTrackId, - x = trackBounds.x, - y = trackBounds.y, - width = trackBounds.width, - height = trackBounds.height, - shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = backgroundColor, - opacity = view.alpha - ) - ) - } - - private fun buildActiveTrackWireframe( - view: SeekBar, - trackBounds: GlobalBounds, - normalizedProgress: Float, - trackColor: Int - ): MobileSegment.Wireframe? { - val activeTrackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, ACTIVE_TRACK_KEY_NAME) - ?: return null - val backgroundColor = colorStringFormatter.formatColorAndAlphaAsHexString( - trackColor, - OPAQUE_ALPHA_VALUE - ) - return MobileSegment.Wireframe.ShapeWireframe( - id = activeTrackId, - x = trackBounds.x, - y = trackBounds.y, - width = (trackBounds.width * normalizedProgress).toLong(), - height = trackBounds.height, - shapeStyle = MobileSegment.ShapeStyle( - backgroundColor = backgroundColor, - opacity = view.alpha - ) - ) } private fun buildThumbWireframe( @@ -166,38 +99,6 @@ internal open class SeekBarWireframeMapper( ) } - private fun normalizedProgressLegacy(view: SeekBar): Float { - val range = view.max.toFloat() - return if (view.max == 0) { - 0f - } else { - view.progress.toFloat() / range - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun normalizedProgressAndroidO(view: SeekBar): Float { - val range = view.max.toFloat() - view.min.toFloat() - return if (range == 0f) { - 0f - } else { - (view.progress - view.min) / range - } - } - - private fun getColor(colorStateList: ColorStateList?, state: IntArray): Int? { - return colorStateList?.getColorForState(state, colorStateList.defaultColor) - } - - private fun getDefaultColor(view: SeekBar): Int { - val uiModeFlags = view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return if (uiModeFlags == Configuration.UI_MODE_NIGHT_YES) { - NIGHT_MODE_COLOR - } else { - DAY_MODE_COLOR - } - } - companion object { internal const val NIGHT_MODE_COLOR = 0xffffff // White internal const val DAY_MODE_COLOR = 0 // Black diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt new file mode 100644 index 0000000000..362cf543bd --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapperTest.kt @@ -0,0 +1,388 @@ +package com.datadog.android.sessionreplay.internal.recorder.mapper + +import android.content.res.ColorStateList +import android.graphics.Rect +import android.os.Build +import android.widget.ProgressBar +import com.datadog.android.sessionreplay.SessionReplayPrivacy +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.android.sessionreplay.internal.recorder.densityNormalized +import com.datadog.android.sessionreplay.internal.recorder.mapper.SeekBarWireframeMapper.Companion.TRACK_HEIGHT_IN_PX +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.recorder.mapper.AbstractWireframeMapperTest +import com.datadog.android.sessionreplay.utils.OPAQUE_ALPHA_VALUE +import com.datadog.android.sessionreplay.utils.PARTIALLY_OPAQUE_ALPHA_VALUE +import com.datadog.tools.unit.annotations.TestTargetApi +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class ProgressBarWireframeMapperTest : + AbstractWireframeMapperTest>() { + + @LongForgery + var fakeActiveTrackId: Long = 0L + + @LongForgery + var fakeNonActiveTrackId: Long = 0L + + @IntForgery(min = 0, max = 512) + var fakeMinValue: Int = 0 + + @IntForgery(min = 512, max = 65536) + var fakeMaxValue: Int = 0 + + @FloatForgery(min = 0f, max = 1f) + var fakeProgress: Float = 0f + + @IntForgery + lateinit var fakeDrawableState: List + + @IntForgery + var fakeTrackColor: Int = 0 + + @StringForgery(regex = "#[0-9A-Fa-f]{8}") + lateinit var fakeNonActiveTrackHtmlColor: String + + @StringForgery(regex = "#[0-9A-Fa-f]{8}") + lateinit var fakeActiveTrackHtmlColor: String + + @Mock + lateinit var mockProgressTintList: ColorStateList + + @Mock + lateinit var mockThumbBounds: Rect + + lateinit var expectedActiveTrackWireframe: MobileSegment.Wireframe + lateinit var expectedNonActiveTrackWireframe: MobileSegment.Wireframe + + @BeforeEach + fun `set up`() { + testedWireframeMapper = ProgressBarWireframeMapper( + mockViewIdentifierResolver, + mockColorStringFormatter, + mockViewBoundsResolver, + mockDrawableToColorMapper, + showProgressWhenMaskUserInput = true + ) + } + + // region Indeterminate + + @Test + fun `M return generic wireframes W map {indeterminate}`() { + // Given + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = true) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + } + + // endregion + + // region Android O+, determinate + + @Test + @TestTargetApi(Build.VERSION_CODES.O) + fun `M return partial wireframes W map {determinate, invalid track id, Android 0+}`() { + // Given + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = false) + mockChildUniqueIdentifier(SeekBarWireframeMapper.ACTIVE_TRACK_KEY_NAME, null) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.O) + fun `M return partial wireframes W map {determinate, invalid non active track id, Android 0+}`() { + // Given + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = false) + mockChildUniqueIdentifier(SeekBarWireframeMapper.NON_ACTIVE_TRACK_KEY_NAME, null) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedActiveTrackWireframe) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.O) + fun `M return wireframes W map {determinate, privacy=ALLOW, Android 0+}`() { + // Given + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = false) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(2) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + assertThatBoundsAreCloseEnough(wireframes[1], expectedActiveTrackWireframe) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.O) + fun `M return wireframes W map {determinate, privacy=MASK, Android 0+}`() { + // Given + withPrivacy(SessionReplayPrivacy.MASK) + prepareMockProgressBar(isIndeterminate = false) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + } + + @Test + @TestTargetApi(Build.VERSION_CODES.O) + fun `M return wireframes W map {determinate, privacy=MASK_USER_INPUT, Android 0+}`() { + // Given + withPrivacy(SessionReplayPrivacy.MASK_USER_INPUT) + prepareMockProgressBar(isIndeterminate = false) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(2) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + assertThatBoundsAreCloseEnough(wireframes[1], expectedActiveTrackWireframe) + } + + // endregion + + // region API < Android O, determinate + + @Test + fun `M return partial wireframes W map {determinate, invalid track id}`() { + // Given + fakeMinValue = 0 + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = false) + mockChildUniqueIdentifier(SeekBarWireframeMapper.ACTIVE_TRACK_KEY_NAME, null) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + } + + @Test + fun `M return partial wireframes W map {determinate, invalid non active track id}`() { + // Given + fakeMinValue = 0 + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = false) + mockChildUniqueIdentifier(SeekBarWireframeMapper.NON_ACTIVE_TRACK_KEY_NAME, null) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedActiveTrackWireframe) + } + + @Test + fun `M return wireframes W map {determinate, privacy=ALLOW}`() { + // Given + fakeMinValue = 0 + withPrivacy(SessionReplayPrivacy.ALLOW) + prepareMockProgressBar(isIndeterminate = false) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(2) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + assertThatBoundsAreCloseEnough(wireframes[1], expectedActiveTrackWireframe) + } + + @Test + fun `M return wireframes W map {determinate, privacy=MASK}`() { + // Given + fakeMinValue = 0 + withPrivacy(SessionReplayPrivacy.MASK) + prepareMockProgressBar(isIndeterminate = false) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(1) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + } + + @Test + fun `M return wireframes W map {determinate, privacy=MASK_USER_INPUT}`() { + // Given + fakeMinValue = 0 + withPrivacy(SessionReplayPrivacy.MASK_USER_INPUT) + prepareMockProgressBar(isIndeterminate = false) + + // When + val wireframes = testedWireframeMapper.map( + mockMappedView, + fakeMappingContext, + mockAsyncJobStatusCallback, + mockInternalLogger + ) + + // Then + assertThat(wireframes).hasSize(2) + assertThatBoundsAreCloseEnough(wireframes[0], expectedNonActiveTrackWireframe) + assertThatBoundsAreCloseEnough(wireframes[1], expectedActiveTrackWireframe) + } + + // endregion + + // region Internal + + private fun prepareMockProgressBar(isIndeterminate: Boolean) { + val fakeStateIntArray = fakeDrawableState.toIntArray() + + withUiMode(fakeUiTypeMode) + + prepareMockView { mockView -> + whenever(mockView.drawableState) doReturn fakeStateIntArray + + whenever(mockView.progressTintList) doReturn mockProgressTintList + + whenever(mockView.min) doReturn fakeMinValue + whenever(mockView.max) doReturn fakeMaxValue + whenever(mockView.progress) doReturn ((fakeMaxValue - fakeMinValue) * fakeProgress).toInt() + fakeMinValue + whenever(mockView.isIndeterminate) doReturn isIndeterminate + } + + whenever(mockProgressTintList.getColorForState(eq(fakeStateIntArray), any())) doReturn fakeTrackColor + + mockChildUniqueIdentifier(SeekBarWireframeMapper.ACTIVE_TRACK_KEY_NAME, fakeActiveTrackId) + mockChildUniqueIdentifier(SeekBarWireframeMapper.NON_ACTIVE_TRACK_KEY_NAME, fakeNonActiveTrackId) + + mockColorAndAlphaAsHexString(fakeTrackColor, OPAQUE_ALPHA_VALUE, fakeActiveTrackHtmlColor) + mockColorAndAlphaAsHexString(fakeTrackColor, PARTIALLY_OPAQUE_ALPHA_VALUE, fakeNonActiveTrackHtmlColor) + + val screenDensity = fakeMappingContext.systemInformation.screenDensity + val fakeTrackHeight = TRACK_HEIGHT_IN_PX.densityNormalized(screenDensity) + val fakeTrackY = fakeViewPaddedBounds.y + ((fakeViewPaddedBounds.height - fakeTrackHeight) / 2) + + expectedNonActiveTrackWireframe = MobileSegment.Wireframe.ShapeWireframe( + id = fakeNonActiveTrackId, + x = fakeViewPaddedBounds.x, + y = fakeTrackY, + width = fakeViewPaddedBounds.width, + height = fakeTrackHeight, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = fakeNonActiveTrackHtmlColor, + opacity = fakeViewAlpha + ) + ) + expectedActiveTrackWireframe = MobileSegment.Wireframe.ShapeWireframe( + id = fakeActiveTrackId, + x = fakeViewPaddedBounds.x, + y = fakeTrackY, + width = (fakeViewPaddedBounds.width * fakeProgress).toLong(), + height = fakeTrackHeight, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = fakeActiveTrackHtmlColor, + opacity = fakeViewAlpha + ) + ) + } + + // endregion +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt index c480ac0200..4b20d8c97b 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SeekBarWireframeMapperTest.kt @@ -245,7 +245,7 @@ internal class SeekBarWireframeMapperTest : AbstractWireframeMapperTest