Skip to content

Commit

Permalink
Refactor handleBitmap and fix detekt-custom issues
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Sep 10, 2023
1 parent 2bc4cfd commit 564979a
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 94 deletions.
8 changes: 8 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ datadog:
- "android.database.sqlite.SQLiteDatabase.setTransactionSuccessful():java.lang.IllegalStateException"
- "android.graphics.Bitmap.compress(android.graphics.Bitmap.CompressFormat, kotlin.Int, java.io.OutputStream):java.lang.NullPointerException,java.lang.IllegalArgumentException"
- "android.graphics.Bitmap.createBitmap(android.util.DisplayMetrics?, kotlin.Int, kotlin.Int, android.graphics.Bitmap.Config):java.lang.IllegalArgumentException"
- "android.graphics.Bitmap.createScaledBitmap(android.graphics.Bitmap, kotlin.Int, kotlin.Int, kotlin.Boolean):java.lang.IllegalArgumentException"
- "android.graphics.Canvas.constructor(android.graphics.Bitmap):java.lang.IllegalStateException"
- "android.graphics.drawable.LayerDrawable.getDrawable(kotlin.Int):java.lang.IndexOutOfBoundsException"
- "android.net.ConnectivityManager.registerDefaultNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.IllegalArgumentException,java.lang.SecurityException"
- "android.net.ConnectivityManager.unregisterNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.SecurityException"
- "android.util.Base64.encodeToString(kotlin.ByteArray, kotlin.Int):java.lang.AssertionError"
Expand Down Expand Up @@ -282,6 +284,7 @@ datadog:
- "android.app.FragmentManager.unregisterFragmentLifecycleCallbacks(android.app.FragmentManager.FragmentLifecycleCallbacks)"
- "android.content.Context.createDeviceProtectedStorageContext()"
- "android.content.Context.getSystemService(kotlin.String)"
- "android.content.Context.registerComponentCallbacks(android.content.ComponentCallbacks)"
- "android.content.Context.registerReceiver(android.content.BroadcastReceiver?, android.content.IntentFilter)"
- "android.content.Context.unregisterReceiver(android.content.BroadcastReceiver)"
- "android.content.Intent.getBooleanExtra(kotlin.String, kotlin.Boolean)"
Expand Down Expand Up @@ -377,12 +380,14 @@ datadog:
# endregion
# region Android Graphics
- "android.graphics.Bitmap.recycle()"
- "android.graphics.Canvas.drawColor(kotlin.Int, android.graphics.PorterDuff.Mode)"
- "android.graphics.Color.argb(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
- "android.graphics.Color.blue(kotlin.Int)"
- "android.graphics.Color.green(kotlin.Int)"
- "android.graphics.Color.red(kotlin.Int)"
- "android.graphics.Color.rgb(kotlin.Int, kotlin.Int, kotlin.Int)"
- "android.graphics.drawable.Drawable.draw(android.graphics.Canvas)"
- "android.graphics.drawable.Drawable.ConstantState.newDrawable(android.content.res.Resources?)"
- "android.graphics.drawable.Drawable.getDrawable(kotlin.Int)"
- "android.graphics.drawable.Drawable.getPadding(android.graphics.Rect)"
- "android.graphics.drawable.Drawable.setBounds(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
Expand Down Expand Up @@ -622,6 +627,7 @@ datadog:
- "java.security.SecureRandom.constructor()"
- "java.security.SecureRandom.nextFloat()"
- "java.security.SecureRandom.nextLong()"
- "java.util.HashSet.find(kotlin.Function1)"
- "java.util.Properties.constructor()"
- "java.util.Properties.setProperty(kotlin.String, kotlin.String)"
- "java.util.UUID.constructor(kotlin.Long, kotlin.Long)"
Expand Down Expand Up @@ -653,6 +659,7 @@ datadog:
- "kotlin.Array.first(kotlin.Function1)"
- "kotlin.Array.firstOrNull(kotlin.Function1)"
- "kotlin.Array.forEach(kotlin.Function1)"
- "kotlin.Array.forEachIndexed(kotlin.Function2)"
- "kotlin.Array.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
- "kotlin.Array.none(kotlin.Function1)"
- "kotlin.Array.orEmpty()"
Expand Down Expand Up @@ -856,6 +863,7 @@ datadog:
- "kotlin.Int.toFloat()"
- "kotlin.Int.toLong()"
- "kotlin.Int.and(kotlin.Int)"
- "kotlin.IntArray.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
- "kotlin.IntArray.constructor(kotlin.Int)"
- "kotlin.Long.asTime()"
- "kotlin.Long.coerceIn(kotlin.Long, kotlin.Long)"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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

import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import com.datadog.android.api.InternalLogger

@Suppress("TooGenericExceptionCaught")
internal fun LayerDrawable.safeGetDrawable(index: Int, logger: InternalLogger = InternalLogger.UNBOUND): Drawable? {
return try {
this.getDrawable(index)
} catch (e: IndexOutOfBoundsException) {
// this should never happen
logger.log(
level = InternalLogger.Level.ERROR,
target = InternalLogger.Target.MAINTAINER,
{ "Failed to get drawable from layer" },
e
)
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.graphics.drawable.DrawableContainer
import android.graphics.drawable.LayerDrawable
import androidx.annotation.VisibleForTesting
import androidx.collection.LruCache
import com.datadog.android.sessionreplay.internal.recorder.safeGetDrawable
import com.datadog.android.sessionreplay.internal.utils.CacheUtils
import com.datadog.android.sessionreplay.internal.utils.InvocationUtils

Expand Down Expand Up @@ -99,18 +100,14 @@ internal class Base64LRUCache(
}

private fun getPrefixForLayerDrawable(drawable: LayerDrawable): String {
return if (drawable.numberOfLayers > 1) {
val sb = StringBuilder()
for (index in 0 until drawable.numberOfLayers) {
val layer = drawable.getDrawable(index)
val layerHash = System.identityHashCode(layer).toString()
sb.append(layerHash)
sb.append("-")
}
"$sb"
} else {
""
val sb = StringBuilder()
for (index in 0 until drawable.numberOfLayers) {
val layer = drawable.safeGetDrawable(index)
val layerHash = System.identityHashCode(layer).toString()
sb.append(layerHash)
sb.append("-")
}
return "$sb"
}

internal companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit

@Suppress("UndocumentedPublicClass")
@Suppress("TooManyFunctions")
internal class Base64Serializer private constructor(
private val threadPoolExecutor: ExecutorService,
private val drawableUtils: DrawableUtils,
Expand All @@ -54,54 +54,14 @@ internal class Base64Serializer private constructor(
drawableHeight: Int,
imageWireframe: MobileSegment.Wireframe.ImageWireframe
) {
registerCacheForCallbacks(applicationContext)
registerBitmapPoolForCallbacks(applicationContext)
registerCallbacks(applicationContext)

asyncImageProcessingCallback?.startProcessingImage()

val cachedBase64 = base64LRUCache?.get(drawable)
if (cachedBase64 != null) {
finalizeRecordedDataItem(cachedBase64, imageWireframe, asyncImageProcessingCallback)
return
}

var shouldCacheBitmap = false

val bitmap = if (
drawable is BitmapDrawable &&
drawable.bitmap != null &&
!drawable.bitmap.isRecycled
) {
val origBitmap = drawable.bitmap
drawableUtils.createScaledBitmap(
drawable.bitmap
)?.let { scaledBitmap ->
if (scaledBitmap != origBitmap) {
shouldCacheBitmap = true
}
scaledBitmap
}
} else {
drawableUtils.createBitmapOfApproxSizeFromDrawable(
drawable,
drawableWidth,
drawableHeight,
displayMetrics
)?.let {
shouldCacheBitmap = true
it
}
}

if (bitmap == null) {
asyncImageProcessingCallback?.finishProcessingImage()
return
}

Runnable {
@Suppress("ThreadSafety") // this runs inside an executor
serialiseBitmap(drawable, bitmap, shouldCacheBitmap, imageWireframe, asyncImageProcessingCallback)
}.let { executeRunnable(it) }
tryToGetBase64FromCache(drawable, imageWireframe)
?: tryToGetBitmapFromBitmapDrawable(drawable, imageWireframe)
?: tryToDrawNewBitmap(drawable, drawableWidth, drawableHeight, displayMetrics, imageWireframe)
?: asyncImageProcessingCallback?.finishProcessingImage()
}

internal fun registerAsyncLoadingCallback(
Expand Down Expand Up @@ -185,6 +145,85 @@ internal class Base64Serializer private constructor(
return base64Result
}

@MainThread
private fun tryToDrawNewBitmap(
drawable: Drawable,
drawableWidth: Int,
drawableHeight: Int,
displayMetrics: DisplayMetrics,
imageWireframe: MobileSegment.Wireframe.ImageWireframe
): Bitmap? {
drawableUtils.createBitmapOfApproxSizeFromDrawable(
drawable,
drawableWidth,
drawableHeight,
displayMetrics
)?.let { resizedBitmap ->
serializeBitmapAsynchronously(
drawable,
bitmap = resizedBitmap,
shouldCacheBitmap = true,
imageWireframe
)
return resizedBitmap
}

return null
}

@MainThread
private fun tryToGetBitmapFromBitmapDrawable(
drawable: Drawable,
imageWireframe: MobileSegment.Wireframe.ImageWireframe
): Bitmap? {
var result: Bitmap? = null
if (shouldUseDrawableBitmap(drawable)) {
drawableUtils.createScaledBitmap(
(drawable as BitmapDrawable).bitmap
)?.let { scaledBitmap ->
val shouldCacheBitmap = scaledBitmap != drawable.bitmap

serializeBitmapAsynchronously(
drawable,
scaledBitmap,
shouldCacheBitmap,
imageWireframe
)

result = scaledBitmap
}
}
return result
}

private fun tryToGetBase64FromCache(
drawable: Drawable,
imageWireframe: MobileSegment.Wireframe.ImageWireframe
): String? {
return base64LRUCache?.get(drawable)?.let { base64String ->
finalizeRecordedDataItem(base64String, imageWireframe, asyncImageProcessingCallback)
base64String
}
}

private fun serializeBitmapAsynchronously(
drawable: Drawable,
bitmap: Bitmap,
shouldCacheBitmap: Boolean,
imageWireframe: MobileSegment.Wireframe.ImageWireframe
) {
Runnable {
@Suppress("ThreadSafety") // this runs inside an executor
serialiseBitmap(
drawable,
bitmap,
shouldCacheBitmap,
imageWireframe,
asyncImageProcessingCallback
)
}.let { executeRunnable(it) }
}

private fun finalizeRecordedDataItem(
base64String: String,
wireframe: MobileSegment.Wireframe.ImageWireframe,
Expand All @@ -210,6 +249,20 @@ internal class Base64Serializer private constructor(
}
}

private fun shouldUseDrawableBitmap(drawable: Drawable): Boolean {
return drawable is BitmapDrawable &&
drawable.bitmap != null &&
!drawable.bitmap.isRecycled &&
drawable.bitmap.width > 0 &&
drawable.bitmap.height > 0
}

@MainThread
private fun registerCallbacks(applicationContext: Context) {
registerCacheForCallbacks(applicationContext)
registerBitmapPoolForCallbacks(applicationContext)
}

// endregion

// region builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ internal class BitmapPool(
val cacheIndex = bitmapIndex.incrementAndGet()
val cacheKey = "$key-$cacheIndex"

cache.put(cacheKey, bitmap)
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
bitmapPoolHelper.safeCall {
cache.put(cacheKey, bitmap)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
bitmapPoolHelper.safeCall {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.annotation.VisibleForTesting
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.internal.recorder.safeGetDrawable
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator

Expand Down Expand Up @@ -134,7 +135,7 @@ internal class ImageWireframeHelper(
return when (drawable) {
is LayerDrawable -> {
if (drawable.numberOfLayers > 0) {
resolveDrawableProperties(view, drawable.getDrawable(0))
resolveDrawableProperties(view, drawable.safeGetDrawable(0))
} else {
DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.base64.Base64Serializer
import com.datadog.android.sessionreplay.internal.recorder.base64.ImageWireframeHelper
import com.datadog.android.sessionreplay.internal.recorder.densityNormalized
import com.datadog.android.sessionreplay.internal.recorder.safeGetDrawable
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.StringUtils
import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator
Expand Down Expand Up @@ -95,7 +96,7 @@ abstract class BaseWireframeMapper<T : View, S : MobileSegment.Wireframe>(
val color = colorAndAlphaAsStringHexa(color, alpha)
MobileSegment.ShapeStyle(color, viewAlpha) to null
} else if (this is RippleDrawable && numberOfLayers >= 1) {
getDrawable(0).resolveShapeStyleAndBorder(viewAlpha)
this.safeGetDrawable(0)?.resolveShapeStyleAndBorder(viewAlpha)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && this is InsetDrawable) {
drawable?.resolveShapeStyleAndBorder(viewAlpha)
} else {
Expand Down Expand Up @@ -172,6 +173,8 @@ abstract class BaseWireframeMapper<T : View, S : MobileSegment.Wireframe>(
width: Long,
height: Long
): MobileSegment.Wireframe? {
val resources = view.resources

@Suppress("ThreadSafety") // TODO REPLAY-1861 caller thread of .map is unknown?
return imageWireframeHelper?.createImageWireframe(
view = view,
Expand All @@ -180,7 +183,7 @@ abstract class BaseWireframeMapper<T : View, S : MobileSegment.Wireframe>(
y = bounds.y,
width,
height,
view.background?.constantState?.newDrawable(),
view.background?.constantState?.newDrawable(resources),
shapeStyle = null,
border = null,
prefix = PREFIX_BACKGROUND_DRAWABLE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@
package com.datadog.android.sessionreplay.internal.recorder.wrappers

import android.util.Base64
import com.datadog.android.api.InternalLogger
import java.lang.AssertionError

internal class Base64Wrapper {
internal class Base64Wrapper(
private val logger: InternalLogger = InternalLogger.UNBOUND
) {
internal fun encodeToString(byteArray: ByteArray, flags: Int): String {
@Suppress("SwallowedException", "TooGenericExceptionCaught")
return try {
Base64.encodeToString(byteArray, flags)
} catch (e: AssertionError) {
// This should never happen since we are using the default encoding
// TODO: REPLAY-1364 Add logs here once the sdkLogger is added
logger.log(
level = InternalLogger.Level.ERROR,
target = InternalLogger.Target.MAINTAINER,
{ FAILED_TO_ENCODE_TO_STRING },
e
)
""
}
}

private companion object {
private const val FAILED_TO_ENCODE_TO_STRING = "Failed to encode to string"
}
}
Loading

0 comments on commit 564979a

Please sign in to comment.