Skip to content

Commit ffb98cd

Browse files
committed
Fix RippleDrawables not rendering correctly
1 parent aa1c2e1 commit ffb98cd

File tree

16 files changed

+742
-99
lines changed

16 files changed

+742
-99
lines changed

detekt_custom.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ datadog:
9494
- "android.database.sqlite.SQLiteDatabase.setTransactionSuccessful():java.lang.IllegalStateException"
9595
- "android.graphics.Bitmap.compress(android.graphics.Bitmap.CompressFormat, kotlin.Int, java.io.OutputStream):java.lang.NullPointerException,java.lang.IllegalArgumentException"
9696
- "android.graphics.Bitmap.createBitmap(android.util.DisplayMetrics?, kotlin.Int, kotlin.Int, android.graphics.Bitmap.Config):java.lang.IllegalArgumentException"
97+
- "android.graphics.Bitmap.createScaledBitmap(android.graphics.Bitmap, kotlin.Int, kotlin.Int, kotlin.Boolean):java.lang.IllegalArgumentException"
9798
- "android.graphics.Canvas.constructor(android.graphics.Bitmap):java.lang.IllegalStateException"
99+
- "android.graphics.drawable.LayerDrawable.getDrawable(kotlin.Int):java.lang.IndexOutOfBoundsException"
98100
- "android.net.ConnectivityManager.registerDefaultNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.IllegalArgumentException,java.lang.SecurityException"
99101
- "android.net.ConnectivityManager.unregisterNetworkCallback(android.net.ConnectivityManager.NetworkCallback):java.lang.SecurityException"
100102
- "android.util.Base64.encodeToString(kotlin.ByteArray, kotlin.Int):java.lang.AssertionError"
@@ -282,6 +284,7 @@ datadog:
282284
- "android.app.FragmentManager.unregisterFragmentLifecycleCallbacks(android.app.FragmentManager.FragmentLifecycleCallbacks)"
283285
- "android.content.Context.createDeviceProtectedStorageContext()"
284286
- "android.content.Context.getSystemService(kotlin.String)"
287+
- "android.content.Context.registerComponentCallbacks(android.content.ComponentCallbacks)"
285288
- "android.content.Context.registerReceiver(android.content.BroadcastReceiver?, android.content.IntentFilter)"
286289
- "android.content.Context.unregisterReceiver(android.content.BroadcastReceiver)"
287290
- "android.content.Intent.getBooleanExtra(kotlin.String, kotlin.Boolean)"
@@ -377,12 +380,14 @@ datadog:
377380
# endregion
378381
# region Android Graphics
379382
- "android.graphics.Bitmap.recycle()"
383+
- "android.graphics.Canvas.drawColor(kotlin.Int, android.graphics.PorterDuff.Mode)"
380384
- "android.graphics.Color.argb(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
381385
- "android.graphics.Color.blue(kotlin.Int)"
382386
- "android.graphics.Color.green(kotlin.Int)"
383387
- "android.graphics.Color.red(kotlin.Int)"
384388
- "android.graphics.Color.rgb(kotlin.Int, kotlin.Int, kotlin.Int)"
385389
- "android.graphics.drawable.Drawable.draw(android.graphics.Canvas)"
390+
- "android.graphics.drawable.Drawable.ConstantState.newDrawable(android.content.res.Resources?)"
386391
- "android.graphics.drawable.Drawable.getDrawable(kotlin.Int)"
387392
- "android.graphics.drawable.Drawable.getPadding(android.graphics.Rect)"
388393
- "android.graphics.drawable.Drawable.setBounds(kotlin.Int, kotlin.Int, kotlin.Int, kotlin.Int)"
@@ -622,6 +627,7 @@ datadog:
622627
- "java.security.SecureRandom.constructor()"
623628
- "java.security.SecureRandom.nextFloat()"
624629
- "java.security.SecureRandom.nextLong()"
630+
- "java.util.HashSet.find(kotlin.Function1)"
625631
- "java.util.Properties.constructor()"
626632
- "java.util.Properties.setProperty(kotlin.String, kotlin.String)"
627633
- "java.util.UUID.constructor(kotlin.Long, kotlin.Long)"
@@ -653,6 +659,7 @@ datadog:
653659
- "kotlin.Array.first(kotlin.Function1)"
654660
- "kotlin.Array.firstOrNull(kotlin.Function1)"
655661
- "kotlin.Array.forEach(kotlin.Function1)"
662+
- "kotlin.Array.forEachIndexed(kotlin.Function2)"
656663
- "kotlin.Array.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
657664
- "kotlin.Array.none(kotlin.Function1)"
658665
- "kotlin.Array.orEmpty()"
@@ -856,6 +863,7 @@ datadog:
856863
- "kotlin.Int.toFloat()"
857864
- "kotlin.Int.toLong()"
858865
- "kotlin.Int.and(kotlin.Int)"
866+
- "kotlin.IntArray.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
859867
- "kotlin.IntArray.constructor(kotlin.Int)"
860868
- "kotlin.Long.asTime()"
861869
- "kotlin.Long.coerceIn(kotlin.Long, kotlin.Long)"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.sessionreplay.internal.recorder
8+
9+
import android.graphics.drawable.Drawable
10+
import android.graphics.drawable.LayerDrawable
11+
import com.datadog.android.api.InternalLogger
12+
13+
@Suppress("TooGenericExceptionCaught")
14+
internal fun LayerDrawable.safeGetDrawable(index: Int, logger: InternalLogger = InternalLogger.UNBOUND): Drawable? {
15+
return if (index < 0 || index >= this.numberOfLayers) {
16+
logger.log(
17+
level = InternalLogger.Level.ERROR,
18+
target = InternalLogger.Target.MAINTAINER,
19+
{ "Failed to get drawable from layer - invalid index passed: $index" }
20+
)
21+
null
22+
} else {
23+
this.getDrawable(index)
24+
}
25+
}

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64LRUCache.kt

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import android.graphics.drawable.DrawableContainer
1414
import android.graphics.drawable.LayerDrawable
1515
import androidx.annotation.VisibleForTesting
1616
import androidx.collection.LruCache
17+
import com.datadog.android.sessionreplay.internal.recorder.safeGetDrawable
1718
import com.datadog.android.sessionreplay.internal.utils.CacheUtils
1819
import com.datadog.android.sessionreplay.internal.utils.InvocationUtils
1920

@@ -99,18 +100,14 @@ internal class Base64LRUCache(
99100
}
100101

101102
private fun getPrefixForLayerDrawable(drawable: LayerDrawable): String {
102-
return if (drawable.numberOfLayers > 1) {
103-
val sb = StringBuilder()
104-
for (index in 0 until drawable.numberOfLayers) {
105-
val layer = drawable.getDrawable(index)
106-
val layerHash = System.identityHashCode(layer).toString()
107-
sb.append(layerHash)
108-
sb.append("-")
109-
}
110-
"$sb"
111-
} else {
112-
""
103+
val sb = StringBuilder()
104+
for (index in 0 until drawable.numberOfLayers) {
105+
val layer = drawable.safeGetDrawable(index)
106+
val layerHash = System.identityHashCode(layer).toString()
107+
sb.append(layerHash)
108+
sb.append("-")
113109
}
110+
return "$sb"
114111
}
115112

116113
internal companion object {

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/Base64Serializer.kt

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import java.util.concurrent.RejectedExecutionException
2929
import java.util.concurrent.ThreadPoolExecutor
3030
import java.util.concurrent.TimeUnit
3131

32-
@Suppress("UndocumentedPublicClass")
32+
@Suppress("TooManyFunctions")
3333
internal class Base64Serializer private constructor(
3434
private val threadPoolExecutor: ExecutorService,
3535
private val drawableUtils: DrawableUtils,
@@ -50,45 +50,18 @@ internal class Base64Serializer private constructor(
5050
applicationContext: Context,
5151
displayMetrics: DisplayMetrics,
5252
drawable: Drawable,
53+
drawableWidth: Int,
54+
drawableHeight: Int,
5355
imageWireframe: MobileSegment.Wireframe.ImageWireframe
5456
) {
55-
registerCacheForCallbacks(applicationContext)
56-
registerBitmapPoolForCallbacks(applicationContext)
57+
registerCallbacks(applicationContext)
5758

5859
asyncImageProcessingCallback?.startProcessingImage()
5960

60-
var shouldCacheBitmap = false
61-
val cachedBase64 = base64LRUCache?.get(drawable)
62-
if (cachedBase64 != null) {
63-
finalizeRecordedDataItem(cachedBase64, imageWireframe, asyncImageProcessingCallback)
64-
return
65-
}
66-
67-
val bitmap = if (
68-
drawable is BitmapDrawable &&
69-
drawable.bitmap != null &&
70-
!drawable.bitmap.isRecycled
71-
) {
72-
drawable.bitmap
73-
} else {
74-
drawableUtils.createBitmapOfApproxSizeFromDrawable(
75-
drawable,
76-
displayMetrics
77-
)?.let {
78-
shouldCacheBitmap = true
79-
it
80-
}
81-
}
82-
83-
if (bitmap == null) {
84-
asyncImageProcessingCallback?.finishProcessingImage()
85-
return
86-
}
87-
88-
Runnable {
89-
@Suppress("ThreadSafety") // this runs inside an executor
90-
serialiseBitmap(drawable, bitmap, shouldCacheBitmap, imageWireframe, asyncImageProcessingCallback)
91-
}.let { executeRunnable(it) }
61+
tryToGetBase64FromCache(drawable, imageWireframe)
62+
?: tryToGetBitmapFromBitmapDrawable(drawable, imageWireframe)
63+
?: tryToDrawNewBitmap(drawable, drawableWidth, drawableHeight, displayMetrics, imageWireframe)
64+
?: asyncImageProcessingCallback?.finishProcessingImage()
9265
}
9366

9467
internal fun registerAsyncLoadingCallback(
@@ -172,6 +145,85 @@ internal class Base64Serializer private constructor(
172145
return base64Result
173146
}
174147

148+
@MainThread
149+
private fun tryToDrawNewBitmap(
150+
drawable: Drawable,
151+
drawableWidth: Int,
152+
drawableHeight: Int,
153+
displayMetrics: DisplayMetrics,
154+
imageWireframe: MobileSegment.Wireframe.ImageWireframe
155+
): Bitmap? {
156+
drawableUtils.createBitmapOfApproxSizeFromDrawable(
157+
drawable,
158+
drawableWidth,
159+
drawableHeight,
160+
displayMetrics
161+
)?.let { resizedBitmap ->
162+
serializeBitmapAsynchronously(
163+
drawable,
164+
bitmap = resizedBitmap,
165+
shouldCacheBitmap = true,
166+
imageWireframe
167+
)
168+
return resizedBitmap
169+
}
170+
171+
return null
172+
}
173+
174+
@MainThread
175+
private fun tryToGetBitmapFromBitmapDrawable(
176+
drawable: Drawable,
177+
imageWireframe: MobileSegment.Wireframe.ImageWireframe
178+
): Bitmap? {
179+
var result: Bitmap? = null
180+
if (shouldUseDrawableBitmap(drawable)) {
181+
drawableUtils.createScaledBitmap(
182+
(drawable as BitmapDrawable).bitmap
183+
)?.let { scaledBitmap ->
184+
val shouldCacheBitmap = scaledBitmap != drawable.bitmap
185+
186+
serializeBitmapAsynchronously(
187+
drawable,
188+
scaledBitmap,
189+
shouldCacheBitmap,
190+
imageWireframe
191+
)
192+
193+
result = scaledBitmap
194+
}
195+
}
196+
return result
197+
}
198+
199+
private fun tryToGetBase64FromCache(
200+
drawable: Drawable,
201+
imageWireframe: MobileSegment.Wireframe.ImageWireframe
202+
): String? {
203+
return base64LRUCache?.get(drawable)?.let { base64String ->
204+
finalizeRecordedDataItem(base64String, imageWireframe, asyncImageProcessingCallback)
205+
base64String
206+
}
207+
}
208+
209+
private fun serializeBitmapAsynchronously(
210+
drawable: Drawable,
211+
bitmap: Bitmap,
212+
shouldCacheBitmap: Boolean,
213+
imageWireframe: MobileSegment.Wireframe.ImageWireframe
214+
) {
215+
Runnable {
216+
@Suppress("ThreadSafety") // this runs inside an executor
217+
serialiseBitmap(
218+
drawable,
219+
bitmap,
220+
shouldCacheBitmap,
221+
imageWireframe,
222+
asyncImageProcessingCallback
223+
)
224+
}.let { executeRunnable(it) }
225+
}
226+
175227
private fun finalizeRecordedDataItem(
176228
base64String: String,
177229
wireframe: MobileSegment.Wireframe.ImageWireframe,
@@ -197,6 +249,20 @@ internal class Base64Serializer private constructor(
197249
}
198250
}
199251

252+
private fun shouldUseDrawableBitmap(drawable: Drawable): Boolean {
253+
return drawable is BitmapDrawable &&
254+
drawable.bitmap != null &&
255+
!drawable.bitmap.isRecycled &&
256+
drawable.bitmap.width > 0 &&
257+
drawable.bitmap.height > 0
258+
}
259+
260+
@MainThread
261+
private fun registerCallbacks(applicationContext: Context) {
262+
registerCacheForCallbacks(applicationContext)
263+
registerBitmapPoolForCallbacks(applicationContext)
264+
}
265+
200266
// endregion
201267

202268
// region builder

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/BitmapPool.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ internal class BitmapPool(
126126
val cacheIndex = bitmapIndex.incrementAndGet()
127127
val cacheKey = "$key-$cacheIndex"
128128

129-
cache.put(cacheKey, bitmap)
129+
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
130+
bitmapPoolHelper.safeCall {
131+
cache.put(cacheKey, bitmap)
132+
}
130133

131134
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
132135
bitmapPoolHelper.safeCall {

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/base64/ImageWireframeHelper.kt

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
package com.datadog.android.sessionreplay.internal.recorder.base64
88

99
import android.graphics.drawable.Drawable
10+
import android.graphics.drawable.GradientDrawable
11+
import android.graphics.drawable.InsetDrawable
12+
import android.graphics.drawable.LayerDrawable
1013
import android.view.View
1114
import android.widget.TextView
1215
import androidx.annotation.MainThread
1316
import androidx.annotation.VisibleForTesting
1417
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
1518
import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal
1619
import com.datadog.android.sessionreplay.internal.recorder.densityNormalized
20+
import com.datadog.android.sessionreplay.internal.recorder.safeGetDrawable
1721
import com.datadog.android.sessionreplay.model.MobileSegment
1822
import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator
1923

@@ -37,16 +41,9 @@ internal class ImageWireframeHelper(
3741
prefix: String = DRAWABLE_CHILD_NAME
3842
): MobileSegment.Wireframe.ImageWireframe? {
3943
val id = uniqueIdentifierGenerator.resolveChildUniqueIdentifier(view, prefix + currentWireframeIndex)
44+
val drawableProperties = resolveDrawableProperties(view, drawable)
4045

41-
@Suppress("ComplexCondition")
42-
if (
43-
drawable == null ||
44-
id == null ||
45-
drawable.intrinsicWidth <= 0 ||
46-
drawable.intrinsicHeight <= 0
47-
) {
48-
return null
49-
}
46+
if (id == null || !drawableProperties.isValid()) return null
5047

5148
val displayMetrics = view.resources.displayMetrics
5249
val applicationContext = view.context.applicationContext
@@ -66,10 +63,13 @@ internal class ImageWireframeHelper(
6663
isEmpty = true
6764
)
6865

66+
@Suppress("UnsafeCallOnNullableType") // drawable already checked for null in isValid
6967
base64Serializer.handleBitmap(
7068
applicationContext = applicationContext,
7169
displayMetrics = displayMetrics,
72-
drawable = drawable,
70+
drawable = drawableProperties.drawable!!,
71+
drawableWidth = drawableProperties.drawableWidth,
72+
drawableHeight = drawableProperties.drawableHeight,
7373
imageWireframe = imageWireframe
7474
)
7575

@@ -129,6 +129,23 @@ internal class ImageWireframeHelper(
129129
return result
130130
}
131131

132+
private fun resolveDrawableProperties(view: View, drawable: Drawable?): DrawableProperties {
133+
if (drawable == null) return DrawableProperties(null, 0, 0)
134+
135+
return when (drawable) {
136+
is LayerDrawable -> {
137+
if (drawable.numberOfLayers > 0) {
138+
resolveDrawableProperties(view, drawable.safeGetDrawable(0))
139+
} else {
140+
DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight)
141+
}
142+
}
143+
is InsetDrawable -> resolveDrawableProperties(view, drawable.drawable)
144+
is GradientDrawable -> DrawableProperties(drawable, view.width, view.height)
145+
else -> DrawableProperties(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight)
146+
}
147+
}
148+
132149
@Suppress("MagicNumber")
133150
private fun convertIndexToCompoundDrawablePosition(compoundDrawableIndex: Int): CompoundDrawablePositions? {
134151
return when (compoundDrawableIndex) {
@@ -147,6 +164,16 @@ internal class ImageWireframeHelper(
147164
BOTTOM
148165
}
149166

167+
private data class DrawableProperties(
168+
val drawable: Drawable?,
169+
val drawableWidth: Int,
170+
val drawableHeight: Int
171+
) {
172+
fun isValid(): Boolean {
173+
return drawable != null && drawableWidth > 0 && drawableHeight > 0
174+
}
175+
}
176+
150177
internal companion object {
151178
@VisibleForTesting internal const val DRAWABLE_CHILD_NAME = "drawable"
152179
}

0 commit comments

Comments
 (0)