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-3180: Avoid crash when applicationContext is null #1864

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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