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

Feature/session-replay/compound-button-mappers #2120

Merged
merged 5 commits into from
Jul 8, 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
5 changes: 5 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,10 @@ datadog:
- "android.graphics.drawable.Drawable.getPadding(android.graphics.Rect)"
- "android.graphics.drawable.Drawable.resolveShapeStyleAndBorder(kotlin.Float)"
- "android.graphics.drawable.Drawable.setBounds(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
- "android.graphics.drawable.Drawable.setState(kotlin.IntArray)"
- "android.graphics.drawable.Drawable.setTintList(android.content.res.ColorStateList?)"
- "android.graphics.drawable.RippleDrawable.findIndexByLayerId(kotlin.Int)"
- "android.graphics.drawable.DrawableContainer.DrawableContainerState.getChild(kotlin.Int)"
- "android.graphics.Point.constructor()"
- "android.graphics.Rect.centerX()"
- "android.graphics.Rect.centerY()"
Expand Down Expand Up @@ -988,6 +991,8 @@ datadog:
- "kotlin.collections.listOf(kotlin.Array)"
- "kotlin.collections.listOf(kotlin.String)"
- "kotlin.collections.listOf(okhttp3.ConnectionSpec)"
- "kotlin.collections.listOfNotNull(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe?)"
- "kotlin.collections.listOfNotNull(kotlin.Array)"
- "kotlin.collections.mapOf(kotlin.Array)"
- "kotlin.collections.mapOf(kotlin.Pair)"
- "kotlin.collections.mutableListOf()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ internal class DefaultRecorderProvider(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
drawableToColorMapper,
internalLogger = sdkCore.internalLogger
)
),
MapperTypeWrapper(
Expand All @@ -126,7 +127,8 @@ internal class DefaultRecorderProvider(
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
drawableToColorMapper,
internalLogger = sdkCore.internalLogger
)
),
MapperTypeWrapper(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.android.sessionreplay.internal.recorder.mapper

import android.widget.CheckBox
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
Expand All @@ -18,11 +19,13 @@ internal open class CheckBoxMapper(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper
drawableToColorMapper: DrawableToColorMapper,
internalLogger: InternalLogger
) : CheckableCompoundButtonMapper<CheckBox>(
textWireframeMapper,
viewIdentifierResolver,
colorStringFormatter,
viewBoundsResolver,
drawableToColorMapper
drawableToColorMapper,
internalLogger
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

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

import android.graphics.drawable.Drawable
import android.graphics.drawable.DrawableContainer
import android.os.Build
import android.widget.CompoundButton
import androidx.annotation.UiThread
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.recorder.densityNormalized
import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
Expand All @@ -22,7 +25,8 @@ internal abstract class CheckableCompoundButtonMapper<T : CompoundButton>(
viewIdentifierResolver: ViewIdentifierResolver,
colorStringFormatter: ColorStringFormatter,
viewBoundsResolver: ViewBoundsResolver,
drawableToColorMapper: DrawableToColorMapper
drawableToColorMapper: DrawableToColorMapper,
private val internalLogger: InternalLogger
) : CheckableTextViewMapper<T>(
textWireframeMapper,
viewIdentifierResolver,
Expand All @@ -36,27 +40,94 @@ internal abstract class CheckableCompoundButtonMapper<T : CompoundButton>(
@UiThread
override fun resolveCheckableBounds(view: T, pixelsDensity: Float): GlobalBounds {
val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, pixelsDensity)
var checkBoxHeight = DEFAULT_CHECKABLE_HEIGHT_IN_PX
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.buttonDrawable?.let {
checkBoxHeight = it.intrinsicHeight.toLong()
}
val checkBoxHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.buttonDrawable?.intrinsicHeight?.toLong()?.densityNormalized(pixelsDensity)
?: DEFAULT_CHECKABLE_HEIGHT_IN_DP
} else {
DEFAULT_CHECKABLE_HEIGHT_IN_DP
}
// minus the padding
checkBoxHeight -= MIN_PADDING_IN_PX * 2
checkBoxHeight = checkBoxHeight.densityNormalized(pixelsDensity)
return GlobalBounds(
x = viewGlobalBounds.x + MIN_PADDING_IN_PX.densityNormalized(pixelsDensity),
x = viewGlobalBounds.x,
y = viewGlobalBounds.y + (viewGlobalBounds.height - checkBoxHeight) / 2,
width = checkBoxHeight,
height = checkBoxHeight
)
}

override fun getCheckableDrawable(view: T): Drawable? {
val originCheckableDrawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// drawable from [CompoundButton] can not be retrieved according to the state,
// so here two hardcoded indexes are used to retrieve "checked" and "not checked" drawables.
val checkableDrawableIndex = if (view.isChecked) {
CHECK_BOX_CHECKED_DRAWABLE_INDEX
} else {
CHECK_BOX_NOT_CHECKED_DRAWABLE_INDEX
}
(view.buttonDrawable?.constantState as? DrawableContainer.DrawableContainerState)?.getChild(
checkableDrawableIndex
)
} else {
// view.buttonDrawable is not available below API 23, so reflection is used to retrieve it.
try {
mButtonDrawableField?.get(view) as? Drawable
} catch (e: IllegalAccessException) {
internalLogger.log(
level = InternalLogger.Level.ERROR,
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
messageBuilder = { GET_DRAWABLE_FAIL_MESSAGE },
throwable = e
)
null
} catch (e: IllegalArgumentException) {
internalLogger.log(
level = InternalLogger.Level.ERROR,
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
messageBuilder = { GET_DRAWABLE_FAIL_MESSAGE },
throwable = e
)
null
}
}
return originCheckableDrawable?.let { cloneCheckableDrawable(view, it) } ?: run {
internalLogger.log(
level = InternalLogger.Level.ERROR,
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
messageBuilder = { GET_DRAWABLE_FAIL_MESSAGE }
)
null
}
}

private fun cloneCheckableDrawable(view: T, drawable: Drawable): Drawable? {
return drawable.constantState?.newDrawable(view.resources)?.apply {
// Set state to make the drawable have correct tint.
setState(view.drawableState)
// Set tint list to drawable if the button has declared `buttonTint` attribute.
view.buttonTintList?.let {
setTintList(it)
}
}
}

// endregion

companion object {
internal const val MIN_PADDING_IN_PX = 20L
internal const val DEFAULT_CHECKABLE_HEIGHT_IN_PX = 84L
internal const val DEFAULT_CHECKABLE_HEIGHT_IN_DP = 32L
internal const val GET_DRAWABLE_FAIL_MESSAGE =
"Failed to get buttonDrawable from the checkable compound button."

// Reflects the field at the initialization of the class instead of reflecting it for every wireframe generation
@Suppress("PrivateApi", "SwallowedException", "TooGenericExceptionCaught")
internal val mButtonDrawableField = try {
CompoundButton::class.java.getDeclaredField("mButtonDrawable").apply {
isAccessible = true
}
} catch (e: NoSuchFieldException) {
null
} catch (e: SecurityException) {
null
} catch (e: NullPointerException) {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

import android.graphics.drawable.Drawable
import android.widget.Checkable
import android.widget.TextView
import androidx.annotation.UiThread
Expand Down Expand Up @@ -47,39 +48,23 @@ internal abstract class CheckableTextViewMapper<T>(
}

@UiThread
override fun resolveCheckedCheckable(
override fun resolveCheckable(
view: T,
mappingContext: MappingContext
): List<MobileSegment.Wireframe>? {
val checkableId = viewIdentifierResolver.resolveChildUniqueIdentifier(
view,
CHECKABLE_KEY_NAME
) ?: return null
val checkBoxColor = resolveCheckableColor(view)
val checkBoxBounds = resolveCheckableBounds(
view,
mappingContext.systemInformation.screenDensity
)
val shapeStyle = resolveCheckedShapeStyle(view, checkBoxColor)
val shapeBorder = resolveCheckedShapeBorder(view, checkBoxColor)
return listOf(
MobileSegment.Wireframe.ShapeWireframe(
id = checkableId,
x = checkBoxBounds.x,
y = checkBoxBounds.y,
width = checkBoxBounds.width,
height = checkBoxBounds.height,
border = shapeBorder,
shapeStyle = shapeStyle
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
return listOfNotNull(
createCheckableDrawableWireFrames(
view,
mappingContext,
asyncJobStatusCallback
)
)
}

@UiThread
override fun resolveNotCheckedCheckable(
view: T,
mappingContext: MappingContext
): List<MobileSegment.Wireframe>? {
override fun resolveMaskedCheckable(view: T, mappingContext: MappingContext): List<MobileSegment.Wireframe>? {
// TODO RUM-5118: Decide how to display masked checkbox, Currently use old unchecked shape wireframe,
val checkableId = viewIdentifierResolver.resolveChildUniqueIdentifier(
view,
CHECKABLE_KEY_NAME
Expand All @@ -104,34 +89,48 @@ internal abstract class CheckableTextViewMapper<T>(
)
}

@UiThread
override fun resolveMaskedCheckable(view: T, mappingContext: MappingContext): List<MobileSegment.Wireframe>? {
return resolveNotCheckedCheckable(view, mappingContext)
}

// endregion

// region CheckableTextViewMapper

@UiThread
abstract fun resolveCheckableBounds(view: T, pixelsDensity: Float): GlobalBounds

protected open fun resolveCheckableColor(view: T): String {
return colorStringFormatter.formatColorAndAlphaAsHexString(view.currentTextColor, OPAQUE_ALPHA_VALUE)
@UiThread
private fun createCheckableDrawableWireFrames(
view: T,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): MobileSegment.Wireframe? {
// Checkbox drawable has animation transition which produces intermediate wireframe, to avoid that, the final
// state drawable "checked" or "unchecked" needs to be extracted to generate the correct wireframes.
return getCheckableDrawable(view)?.let {
val checkBoxBounds = resolveCheckableBounds(
view,
mappingContext.systemInformation.screenDensity
)
mappingContext.imageWireframeHelper.createImageWireframe(
view = view,
currentWireframeIndex = 0,
x = checkBoxBounds.x,
y = checkBoxBounds.y,
width = it.intrinsicWidth,
height = it.intrinsicHeight,
drawable = it,
shapeStyle = null,
border = null,
usePIIPlaceholder = true,
clipping = MobileSegment.WireframeClip(),
asyncJobStatusCallback = asyncJobStatusCallback
)
}
}

protected open fun resolveCheckedShapeStyle(view: T, checkBoxColor: String): MobileSegment.ShapeStyle? {
return MobileSegment.ShapeStyle(
backgroundColor = checkBoxColor,
view.alpha
)
}
@UiThread
abstract fun getCheckableDrawable(view: T): Drawable?

protected open fun resolveCheckedShapeBorder(view: T, checkBoxColor: String): MobileSegment.ShapeBorder? {
return MobileSegment.ShapeBorder(
color = checkBoxColor,
width = CHECKABLE_BORDER_WIDTH
)
protected open fun resolveCheckableColor(view: T): String {
return colorStringFormatter.formatColorAndAlphaAsHexString(view.currentTextColor, OPAQUE_ALPHA_VALUE)
}

protected open fun resolveNotCheckedShapeStyle(view: T, checkBoxColor: String): MobileSegment.ShapeStyle? {
Expand All @@ -150,5 +149,7 @@ internal abstract class CheckableTextViewMapper<T>(
companion object {
internal const val CHECKABLE_KEY_NAME = "checkable"
internal const val CHECKABLE_BORDER_WIDTH = 1L
internal const val CHECK_BOX_CHECKED_DRAWABLE_INDEX = 0
internal const val CHECK_BOX_NOT_CHECKED_DRAWABLE_INDEX = 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ internal abstract class CheckableWireframeMapper<T>(
val mainWireframes = resolveMainWireframes(view, mappingContext, asyncJobStatusCallback, internalLogger)
val checkableWireframes = if (mappingContext.privacy != SessionReplayPrivacy.ALLOW) {
resolveMaskedCheckable(view, mappingContext)
} else if (view.isChecked) {
resolveCheckedCheckable(view, mappingContext)
} else {
resolveNotCheckedCheckable(view, mappingContext)
// Resolves checkable view regardless the state
resolveCheckable(view, mappingContext, asyncJobStatusCallback)
}
checkableWireframes?.let { wireframes ->
return mainWireframes + wireframes
Expand All @@ -68,14 +67,9 @@ internal abstract class CheckableWireframeMapper<T>(
): List<MobileSegment.Wireframe>?

@UiThread
abstract fun resolveNotCheckedCheckable(
abstract fun resolveCheckable(
view: T,
mappingContext: MappingContext
): List<MobileSegment.Wireframe>?

@UiThread
abstract fun resolveCheckedCheckable(
view: T,
mappingContext: MappingContext
): List<MobileSegment.Wireframe>?
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

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

import android.graphics.drawable.Drawable
import android.graphics.drawable.DrawableContainer
import android.widget.CheckedTextView
import androidx.annotation.UiThread
import com.datadog.android.sessionreplay.internal.recorder.densityNormalized
Expand Down Expand Up @@ -62,5 +64,25 @@ internal open class CheckedTextViewMapper(
)
}

override fun getCheckableDrawable(view: CheckedTextView): Drawable? {
// drawable from [CheckedTextView] can not be retrieved according to the state,
// so here two hardcoded indexes are used to retrieve "checked" and "not checked" drawables.
val checkableDrawableIndex = if (view.isChecked) {
CHECK_BOX_CHECKED_DRAWABLE_INDEX
} else {
CHECK_BOX_NOT_CHECKED_DRAWABLE_INDEX
}

return (view.checkMarkDrawable?.constantState as? DrawableContainer.DrawableContainerState)?.getChild(
checkableDrawableIndex
)?.constantState?.newDrawable(view.resources)?.apply {
// Set state to make the drawable have correct tint according to the state.
setState(view.drawableState)
// Set tint list to drawable if the button has declared `checkMarkTint` attribute.
view.checkMarkTintList?.let {
setTintList(view.checkMarkTintList)
}
}
}
// endregion
}
Loading