Skip to content

Commit

Permalink
Merge pull request #1864 from DataDog/jmoskovich/rum-3180/handle-null…
Browse files Browse the repository at this point in the history
…-applicationcontext

RUM-3180: Avoid crash when applicationContext is null
  • Loading branch information
jonathanmos committed Feb 20, 2024
2 parents 19f37ac + cf58dc5 commit 595d19e
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toolbar
import androidx.appcompat.widget.SwitchCompat
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.recorder.base64.Base64LRUCache
import com.datadog.android.sessionreplay.internal.recorder.base64.Base64Serializer
import com.datadog.android.sessionreplay.internal.recorder.base64.BitmapPool
Expand Down Expand Up @@ -76,9 +77,9 @@ enum class SessionReplayPrivacy {
MASK_USER_INPUT;

@Suppress("LongMethod")
internal fun mappers(): List<MapperTypeWrapper> {
internal fun mappers(internalLogger: InternalLogger): List<MapperTypeWrapper> {
val base64Serializer = buildBase64Serializer()
val imageWireframeHelper = ImageWireframeHelper(base64Serializer = base64Serializer)
val imageWireframeHelper = ImageWireframeHelper(logger = internalLogger, base64Serializer = base64Serializer)
val uniqueIdentifierGenerator = UniqueIdentifierGenerator

val unsupportedViewMapper = UnsupportedViewMapper()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
this.viewOnDrawInterceptor = ViewOnDrawInterceptor(
recordedDataQueueHandler = recordedDataQueueHandler,
SnapshotProducer(
TreeViewTraversal(customMappers + privacy.mappers()),
TreeViewTraversal(customMappers + privacy.mappers(internalLogger)),
ComposedOptionSelectorDetector(
customOptionSelectorDetectors + DefaultOptionSelectorDetector()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ import android.view.View
import android.widget.TextView
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal
import com.datadog.android.sessionreplay.internal.recorder.densityNormalized
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator
import java.util.Locale

// This should not have a callback but it should just create a placeholder for base64Serializer
// The base64Serializer dependency should be removed from here
// TODO: RUM-0000 Remove the base64Serializer dependency from here
internal class ImageWireframeHelper(
private val logger: InternalLogger,
private val base64Serializer: Base64Serializer,
private val imageCompression: ImageCompression = WebPImageCompression(),
private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator,
private val base64Serializer: Base64Serializer,
private val viewUtilsInternal: ViewUtilsInternal = ViewUtilsInternal(),
private val imageTypeResolver: ImageTypeResolver = ImageTypeResolver()
) {
Expand Down Expand Up @@ -58,6 +61,17 @@ internal class ImageWireframeHelper(

val displayMetrics = view.resources.displayMetrics
val applicationContext = view.context.applicationContext

if (applicationContext == null) {
logger.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.TELEMETRY,
{ APPLICATION_CONTEXT_NULL_ERROR.format(Locale.US, view.javaClass.canonicalName) }
)

return null
}

val mimeType = imageCompression.getMimeType()
val density = displayMetrics.density

Expand Down Expand Up @@ -234,5 +248,8 @@ internal class ImageWireframeHelper(
internal const val DRAWABLE_CHILD_NAME = "drawable"

@VisibleForTesting internal const val PLACEHOLDER_CONTENT_LABEL = "Content Image"

@VisibleForTesting internal const val APPLICATION_CONTEXT_NULL_ERROR =
"Application context is null for view %s"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toolbar
import androidx.appcompat.widget.SwitchCompat
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckedTextViewMapper
Expand All @@ -39,16 +41,30 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.UnsupportedViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
import com.datadog.tools.unit.setStaticValue
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.runners.Parameterized
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.kotlin.mock
import org.mockito.quality.Strictness
import java.util.stream.Stream
import androidx.appcompat.widget.Toolbar as AppCompatToolbar

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(ForgeConfigurator::class)
internal class SessionReplayPrivacyTest {

/*
Expand All @@ -58,6 +74,9 @@ internal class SessionReplayPrivacyTest {
*/
private val origApiLevel = Build.VERSION.SDK_INT

@Mock
lateinit var mockLogger: InternalLogger

@AfterEach
fun teardown() {
setApiLevel(origApiLevel)
Expand All @@ -75,9 +94,9 @@ internal class SessionReplayPrivacyTest {

// When
val actualMappers = when (maskLevel) {
SessionReplayPrivacy.ALLOW.toString() -> SessionReplayPrivacy.ALLOW.mappers()
SessionReplayPrivacy.MASK.toString() -> SessionReplayPrivacy.MASK.mappers()
SessionReplayPrivacy.MASK_USER_INPUT.toString() -> SessionReplayPrivacy.MASK_USER_INPUT.mappers()
SessionReplayPrivacy.ALLOW.toString() -> SessionReplayPrivacy.ALLOW.mappers(mockLogger)
SessionReplayPrivacy.MASK.toString() -> SessionReplayPrivacy.MASK.mappers(mockLogger)
SessionReplayPrivacy.MASK_USER_INPUT.toString() -> SessionReplayPrivacy.MASK_USER_INPUT.mappers(mockLogger)
else -> throw IllegalArgumentException("Unknown masking level")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ import android.graphics.drawable.RippleDrawable
import android.util.DisplayMetrics
import android.view.View
import android.widget.TextView
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.SystemInformation
import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal
import com.datadog.android.sessionreplay.internal.recorder.base64.ImageWireframeHelper.Companion.APPLICATION_CONTEXT_NULL_ERROR
import com.datadog.android.sessionreplay.internal.recorder.base64.ImageWireframeHelper.Companion.DRAWABLE_CHILD_NAME
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator
import com.datadog.android.utils.isCloseTo
import com.datadog.android.utils.verifyLog
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.annotation.IntForgery
import fr.xgouchet.elmyr.annotation.LongForgery
Expand All @@ -49,6 +52,7 @@ import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
import java.util.Locale

@Extensions(
ExtendWith(MockitoExtension::class),
Expand All @@ -62,6 +66,9 @@ internal class ImageWireframeHelperTest {
@Mock
lateinit var mockBase64Serializer: Base64Serializer

@Mock
lateinit var mockLogger: InternalLogger

@Mock
lateinit var mockUniqueIdentifierGenerator: UniqueIdentifierGenerator

Expand Down Expand Up @@ -162,16 +169,69 @@ internal class ImageWireframeHelperTest {
whenever(mockBounds.y).thenReturn(0L)

testedHelper = ImageWireframeHelper(
logger = mockLogger,
base64Serializer = mockBase64Serializer,
imageCompression = mockImageCompression,
uniqueIdentifierGenerator = mockUniqueIdentifierGenerator,
base64Serializer = mockBase64Serializer,
viewUtilsInternal = mockViewUtilsInternal,
imageTypeResolver = mockImageTypeResolver
)
}

// region createImageWireframe

@Test
fun `M return null W createImageWireframe() { application context is null }`() {
// Given
whenever(mockView.context.applicationContext).thenReturn(null)

// When
val wireframe = testedHelper.createImageWireframe(
view = mockView,
currentWireframeIndex = 0,
x = 0,
y = 0,
width = 0,
height = 0,
drawable = mockDrawable,
shapeStyle = null,
border = null,
usePIIPlaceholder = true,
imageWireframeHelperCallback = mockImageWireframeHelperCallback
)

// Then
assertThat(wireframe).isNull()
}

@Test
fun `M send telemetry W createImageWireframe() { application context is null }`() {
// Given
whenever(mockView.context.applicationContext).thenReturn(null)

// When
testedHelper.createImageWireframe(
view = mockView,
currentWireframeIndex = 0,
x = 0,
y = 0,
width = 0,
height = 0,
drawable = mockDrawable,
shapeStyle = null,
border = null,
usePIIPlaceholder = true,
imageWireframeHelperCallback = mockImageWireframeHelperCallback
)

// Then
mockLogger.verifyLog(
InternalLogger.Level.ERROR,
InternalLogger.Target.TELEMETRY,
APPLICATION_CONTEXT_NULL_ERROR.format(Locale.US, "android.view.View")
)
}

@Test
fun `M return null W createImageWireframe() { drawable is null }`() {
// When
Expand Down

0 comments on commit 595d19e

Please sign in to comment.