Skip to content

Commit e83eac5

Browse files
committed
MOBILE-52: Add timeout for in-app image loading
1 parent 4ce839d commit e83eac5

4 files changed

Lines changed: 376 additions & 100 deletions

File tree

sdk/src/main/java/cloud/mindbox/mobile_sdk/Extensions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ internal val Int.dp: Int
149149
internal val Int.px: Int
150150
get() = (this * Resources.getSystem().displayMetrics.density).roundToInt()
151151

152+
internal fun Context.maxScreenDimension(): Int {
153+
val displayMetrics = resources.displayMetrics
154+
return maxOf(displayMetrics.widthPixels, displayMetrics.heightPixels)
155+
}
156+
152157
internal fun Animation.setOnAnimationEnd(runnable: Runnable) {
153158
setAnimationListener(object : AnimationListener {
154159
override fun onAnimationStart(animation: Animation?) {

sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppGlideImageLoaderImpl.kt

Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import cloud.mindbox.mobile_sdk.R
77
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppImageLoader
88
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppImageSizeStorage
99
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppContentFetchingError
10-
import cloud.mindbox.mobile_sdk.logger.mindboxLogD
1110
import cloud.mindbox.mobile_sdk.logger.mindboxLogE
11+
import cloud.mindbox.mobile_sdk.logger.mindboxLogI
12+
import cloud.mindbox.mobile_sdk.maxScreenDimension
1213
import com.bumptech.glide.Glide
1314
import com.bumptech.glide.load.DataSource
15+
import com.bumptech.glide.load.engine.DiskCacheStrategy
1416
import com.bumptech.glide.load.engine.GlideException
1517
import com.bumptech.glide.request.RequestListener
1618
import com.bumptech.glide.request.target.Target
19+
import kotlinx.coroutines.CancellableContinuation
20+
import kotlinx.coroutines.TimeoutCancellationException
1721
import kotlinx.coroutines.suspendCancellableCoroutine
22+
import kotlinx.coroutines.withTimeout
1823
import kotlin.coroutines.resume
1924
import kotlin.coroutines.resumeWithException
2025

@@ -26,53 +31,73 @@ internal class InAppGlideImageLoaderImpl(
2631
private val requests = HashMap<String, Target<Drawable>>()
2732

2833
override suspend fun loadImage(inAppId: String, url: String): Boolean {
29-
mindboxLogD("loading image for inapp with id $inAppId started")
30-
return suspendCancellableCoroutine { cancellableContinuation ->
31-
val target = Glide.with(context).load(url)
32-
.timeout(context.getString(R.string.mindbox_inapp_fetching_timeout).toInt())
33-
.listener(object :
34-
RequestListener<Drawable> {
35-
override fun onLoadFailed(
36-
e: GlideException?,
37-
model: Any?,
38-
target: Target<Drawable>?,
39-
isFirstResource: Boolean
40-
): Boolean {
41-
return runCatching {
42-
mindboxLogD("loading image with url = $url for inapp with id $inAppId failed")
43-
cancellableContinuation.resumeWithException(InAppContentFetchingError(e))
44-
true
45-
}.getOrElse {
46-
mindboxLogE(
47-
"Unknown error when loading image from network failed",
48-
exception = it
49-
)
50-
true
51-
}
52-
}
34+
mindboxLogI("Loading image for inapp with id $inAppId started")
35+
val timeoutMs = context.getString(R.string.mindbox_inapp_fetching_timeout).toLong()
36+
val maxDim = context.maxScreenDimension()
37+
return try {
38+
withTimeout(timeoutMs) {
39+
suspendCancellableCoroutine { continuation ->
40+
requests[inAppId] = startPreload(inAppId, url, maxDim, timeoutMs.toInt(), continuation)
41+
continuation.invokeOnCancellation { cancelLoading(inAppId) }
42+
}
43+
}
44+
} catch (e: TimeoutCancellationException) {
45+
mindboxLogE("Image loading timed out after ${timeoutMs}ms for inapp $inAppId", e)
46+
throw InAppContentFetchingError(null)
47+
}
48+
}
49+
50+
private fun startPreload(
51+
inAppId: String,
52+
url: String,
53+
maxDim: Int,
54+
timeoutMs: Int,
55+
continuation: CancellableContinuation<Boolean>,
56+
): Target<Drawable> = Glide.with(context)
57+
.load(url)
58+
.timeout(timeoutMs)
59+
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
60+
.override(maxDim, maxDim)
61+
.centerInside()
62+
.listener(buildRequestListener(inAppId, url, continuation))
63+
.preload(maxDim, maxDim)
64+
65+
private fun buildRequestListener(
66+
inAppId: String,
67+
url: String,
68+
continuation: CancellableContinuation<Boolean>,
69+
): RequestListener<Drawable> = object : RequestListener<Drawable> {
70+
71+
override fun onLoadFailed(
72+
e: GlideException?,
73+
model: Any?,
74+
target: Target<Drawable>?,
75+
isFirstResource: Boolean,
76+
): Boolean {
77+
mindboxLogI("Image loading failed for inapp $inAppId, url = $url")
78+
if (continuation.isActive) {
79+
continuation.resumeWithException(InAppContentFetchingError(e))
80+
}
81+
return true
82+
}
5383

54-
override fun onResourceReady(
55-
resource: Drawable,
56-
model: Any?,
57-
target: Target<Drawable>?,
58-
dataSource: DataSource?,
59-
isFirstResource: Boolean
60-
): Boolean {
61-
return runCatching {
62-
mindboxLogD("loading image with url = $url for inapp with id $inAppId succeeded")
63-
inAppImageSizeStorage.addSize(inAppId, url, resource.toBitmap().width, resource.toBitmap().height)
64-
cancellableContinuation.resume(true)
65-
true
66-
}.getOrElse {
67-
mindboxLogE(
68-
"Unknown error when loading image from network failed",
69-
exception = it
70-
)
71-
true
72-
}
73-
}
74-
}).preload()
75-
requests[inAppId] = target
84+
override fun onResourceReady(
85+
resource: Drawable,
86+
model: Any?,
87+
target: Target<Drawable>?,
88+
dataSource: DataSource?,
89+
isFirstResource: Boolean,
90+
): Boolean {
91+
mindboxLogI("Image loading succeeded for inapp $inAppId, url = $url")
92+
if (!continuation.isActive) return true
93+
return runCatching {
94+
val bitmap = resource.toBitmap()
95+
inAppImageSizeStorage.addSize(inAppId, url, bitmap.width, bitmap.height)
96+
continuation.resume(true)
97+
}.onFailure { e ->
98+
mindboxLogE("Failed to process loaded image for inapp $inAppId", e)
99+
continuation.resumeWithException(InAppContentFetchingError(null))
100+
}.isSuccess
76101
}
77102
}
78103

sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/AbstractInAppViewHolder.kt

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.widget.FrameLayout
1010
import android.widget.ImageView
1111
import androidx.core.view.ViewCompat
1212
import androidx.core.view.WindowInsetsCompat
13+
import androidx.core.view.isVisible
1314
import cloud.mindbox.mobile_sdk.R
1415
import cloud.mindbox.mobile_sdk.di.mindboxInject
1516
import cloud.mindbox.mobile_sdk.inapp.domain.extensions.sendPresentationFailure
@@ -22,6 +23,7 @@ import cloud.mindbox.mobile_sdk.inapp.presentation.InAppMessageViewDisplayerImpl
2223
import cloud.mindbox.mobile_sdk.inapp.presentation.MindboxView
2324
import cloud.mindbox.mobile_sdk.inapp.presentation.actions.InAppActionHandler
2425
import cloud.mindbox.mobile_sdk.logger.mindboxLogI
26+
import cloud.mindbox.mobile_sdk.maxScreenDimension
2527
import cloud.mindbox.mobile_sdk.removeChildById
2628
import cloud.mindbox.mobile_sdk.safeAs
2729
import cloud.mindbox.mobile_sdk.setSingleClickListener
@@ -122,64 +124,71 @@ internal abstract class AbstractInAppViewHolder<T : InAppType>(
122124
}
123125

124126
protected fun getImageFromCache(url: String, imageView: InAppImageView) {
127+
val maxDim = currentDialog.context.maxScreenDimension()
128+
val timeout = currentDialog.context.getString(R.string.mindbox_inapp_fetching_timeout).toInt()
125129
Glide
126130
.with(currentDialog.context)
127131
.load(url)
128-
.diskCacheStrategy(DiskCacheStrategy.ALL)
129-
.listener(object : RequestListener<Drawable> {
130-
override fun onLoadFailed(
131-
e: GlideException?,
132-
model: Any?,
133-
target: Target<Drawable>?,
134-
isFirstResource: Boolean
135-
): Boolean {
136-
return runCatching {
137-
inAppFailureTracker.sendPresentationFailure(
138-
inAppId = wrapper.inAppType.inAppId,
139-
errorDescription = "Failed to load in-app image with url = $url",
140-
throwable = e
141-
)
142-
inAppController.close()
143-
false
144-
}.getOrElse { throwable ->
145-
inAppFailureTracker.sendPresentationFailure(
146-
inAppId = wrapper.inAppType.inAppId,
147-
errorDescription = "Unknown error after loading image from cache succeeded",
148-
throwable = throwable
149-
)
150-
false
151-
}
152-
}
132+
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
133+
.override(maxDim, maxDim)
134+
.timeout(timeout)
135+
.centerInside()
136+
.listener(buildCacheRequestListener(url, imageView))
137+
.into(imageView)
138+
}
153139

154-
override fun onResourceReady(
155-
resource: Drawable?,
156-
model: Any?,
157-
target: Target<Drawable>?,
158-
dataSource: DataSource?,
159-
isFirstResource: Boolean
160-
): Boolean {
161-
return runCatching {
162-
bind()
163-
preparedImages[imageView] = true
164-
if (!preparedImages.values.contains(false)) {
165-
this@AbstractInAppViewHolder.mindboxLogI("In-app shown")
166-
wrapper.inAppActionCallbacks.onInAppShown.onShown()
167-
for (image in preparedImages.keys) {
168-
image.visibility = View.VISIBLE
169-
}
170-
}
171-
false
172-
}.getOrElse { throwable ->
173-
inAppFailureTracker.sendPresentationFailure(
174-
inAppId = wrapper.inAppType.inAppId,
175-
errorDescription = "Unknown error in onResourceReady callback",
176-
throwable = throwable
177-
)
178-
false
179-
}
140+
private fun buildCacheRequestListener(
141+
url: String,
142+
imageView: InAppImageView,
143+
): RequestListener<Drawable> = object : RequestListener<Drawable> {
144+
145+
override fun onLoadFailed(
146+
e: GlideException?,
147+
model: Any?,
148+
target: Target<Drawable>?,
149+
isFirstResource: Boolean,
150+
): Boolean {
151+
runCatching {
152+
inAppFailureTracker.sendPresentationFailure(
153+
inAppId = wrapper.inAppType.inAppId,
154+
errorDescription = "Failed to load in-app image with url = $url",
155+
throwable = e
156+
)
157+
inAppController.close()
158+
}.onFailure { throwable ->
159+
inAppFailureTracker.sendPresentationFailure(
160+
inAppId = wrapper.inAppType.inAppId,
161+
errorDescription = "Unknown error in onLoadFailed callback for url = $url",
162+
throwable = throwable
163+
)
164+
}
165+
return false
166+
}
167+
168+
override fun onResourceReady(
169+
resource: Drawable?,
170+
model: Any?,
171+
target: Target<Drawable>?,
172+
dataSource: DataSource?,
173+
isFirstResource: Boolean,
174+
): Boolean {
175+
runCatching {
176+
bind()
177+
preparedImages[imageView] = true
178+
if (!preparedImages.values.contains(false)) {
179+
mindboxLogI("In-app ${wrapper.inAppType.inAppId} shown")
180+
wrapper.inAppActionCallbacks.onInAppShown.onShown()
181+
preparedImages.keys.forEach { it.isVisible = true }
180182
}
181-
})
182-
.into(imageView)
183+
}.onFailure { throwable ->
184+
inAppFailureTracker.sendPresentationFailure(
185+
inAppId = wrapper.inAppType.inAppId,
186+
errorDescription = "Unknown error in onResourceReady callback for url = $url",
187+
throwable = throwable
188+
)
189+
}
190+
return false
191+
}
183192
}
184193

185194
protected open fun initView(currentRoot: ViewGroup) {

0 commit comments

Comments
 (0)