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

RUM-4514 add support for progress bars #2047

Merged
merged 1 commit into from
May 30, 2024
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 @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -184,6 +186,16 @@ internal class DefaultRecorderProvider(
viewBoundsResolver,
drawableToColorMapper
)
),
MapperTypeWrapper(
ProgressBar::class.java,
ProgressBarWireframeMapper(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper,
true
)
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<P : ProgressBar>(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper,
val showProgressWhenMaskUserInput: Boolean
) : BaseAsyncBackgroundWireframeMapper<P>(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
) {

@UiThread
override fun map(
view: P,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): List<MobileSegment.Wireframe> {
val wireframes = mutableListOf<MobileSegment.Wireframe>()

// 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<MobileSegment.Wireframe>,
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
}
}
Loading