Initialize Glide lazily: [VPNAND-2507]

- use dagger.Lazy,
- don't prefetch images from "file:" scheme,
- use a listener instead of blocking Future to wait for prefetch - this avoids
  spawning additional threads on app star.
This commit is contained in:
Marcin Simonides
2026-02-06 10:31:57 +01:00
committed by MargeBot
parent 911245fe79
commit e41d93c997
2 changed files with 44 additions and 25 deletions

View File

@@ -20,8 +20,11 @@
package com.protonvpn.android.promooffers.data
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.VisibleForTesting
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.protonvpn.android.BuildConfig
import com.protonvpn.android.api.ProtonApiRetroFit
import com.protonvpn.android.appconfig.AppConfig
@@ -40,7 +43,7 @@ import com.protonvpn.android.promooffers.ui.PromoOfferImage
import com.protonvpn.android.promooffers.usecase.GenerateNotificationsForIntroductoryOffers
import com.protonvpn.android.promooffers.usecase.isIntroductoryPriceOffer
import com.protonvpn.android.utils.UserPlanManager
import com.protonvpn.android.utils.runCatchingCheckedExceptions
import com.protonvpn.android.utils.getValue
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -54,7 +57,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
@@ -63,35 +65,54 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import me.proton.core.network.domain.ApiResult
import me.proton.core.util.kotlin.DispatcherProvider
import me.proton.core.util.kotlin.deserialize
import me.proton.core.util.kotlin.mapAsync
import me.proton.core.util.kotlin.mapNotNullAsync
import me.proton.core.util.kotlin.serialize
import me.proton.core.util.kotlin.startsWith
import me.proton.core.util.kotlin.takeIfNotEmpty
import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
private val MIN_NOTIFICATION_REFRESH_INTERVAL_MS = TimeUnit.HOURS.toMillis(3)
fun interface ImagePrefetcher {
fun prefetch(url: String): Boolean
suspend fun prefetch(url: String): Boolean
}
@Singleton
class GlideImagePrefetcher @Inject constructor(
@ApplicationContext private val appContext: Context
) : ImagePrefetcher {
override fun prefetch(url: String): Boolean {
val future = Glide.with(appContext).download(url).submit()
return {
future.get()
true
}.runCatchingCheckedExceptions {
false
override suspend fun prefetch(url: String): Boolean = suspendCancellableCoroutine { continuation ->
val target = object : CustomTarget<File>() {
override fun onResourceReady(
resource: File,
transition: Transition<in File>?
) {
continuation.resume(true)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
continuation.resume(false)
}
override fun onDestroy() {
super.onDestroy()
continuation.cancel()
}
override fun onLoadCleared(placeholder: Drawable?) {
continuation.cancel()
}
}
Glide.with(appContext).download(url).into(target)
}
}
@@ -101,7 +122,6 @@ class GlideImagePrefetcher @Inject constructor(
class ApiNotificationManager @Inject constructor(
@ApplicationContext private val appContext: Context,
private val mainScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
@WallClock private val wallClockMs: () -> Long,
appConfig: AppConfig,
private val apiNotificationsStore: ApiNotificationsStore,
@@ -109,11 +129,12 @@ class ApiNotificationManager @Inject constructor(
private val currentUser: CurrentUser,
private val userPlanManager: UserPlanManager,
private val generateNotificationsForIntroductoryOffers: GenerateNotificationsForIntroductoryOffers,
private val imagePrefetcher: ImagePrefetcher,
lazyImagePrefetcher: dagger.Lazy<ImagePrefetcher>,
private val periodicUpdateManager: PeriodicUpdateManager,
@IsInForeground private val inForeground: Flow<Boolean>,
@IsLoggedIn private val isLoggedIn: Flow<Boolean>,
) {
private val imagePrefetcher by lazyImagePrefetcher
private val testNotifications = MutableStateFlow<List<ApiNotification>>(emptyList())
@@ -136,13 +157,12 @@ class ApiNotificationManager @Inject constructor(
.combine(prefetchTrigger) { notifications, _ -> notifications }
.mapLatest { notifications ->
notifications.mapNotNullAsync { notification ->
notification.takeIf { notification.allImageUrls().ensureAllPrefetched() }
notification.takeIf { notification.allRemoteImageUrls().ensureAllPrefetched() }
}.also {
logDebugRemovedNotifications(notifications, it, "can't load images")
}
}
.distinctUntilChanged()
.flowOn(dispatcherProvider.Io)
.shareIn(mainScope, SharingStarted.Eagerly, replay = 1)
// Active notifications are sorted by end time - the ones that end sooner are first.
@@ -245,7 +265,7 @@ class ApiNotificationManager @Inject constructor(
}
}
private fun ApiNotification.allImageUrls(): List<String> = buildList {
private fun ApiNotification.allRemoteImageUrls(): List<String> = buildList {
val width = PromoOfferImage.getFullScreenImageMaxSizePx(appContext).width
val fullScreenImages = listOfNotNull(
offer?.panel?.fullScreenImage,
@@ -259,7 +279,7 @@ class ApiNotificationManager @Inject constructor(
add(offer?.iconUrl)
}
.filterNotNull()
.filter(String::isNotBlank)
.filter { it.isNotBlank() && !it.startsWith("file:") }
private fun ApiNotification.allActionNames(): List<String> = buildList {
val buttons: List<ApiNotificationOfferButton> = listOfNotNull(

View File

@@ -131,7 +131,7 @@ class ApiNotificationManagerTests {
every { mockAppConfig.appConfigUpdateEvent } returns MutableSharedFlow()
every { mockAppConfig.appConfigFlow } returns appConfigFlow
every { mockImagePrefetcher.prefetch(any()) } returns true
coEvery { mockImagePrefetcher.prefetch(any()) } returns true
coEvery { mockGenerateNotificationsForIntroductoryOffers.invoke() } returns emptyList()
infoChangeFlow = MutableSharedFlow()
@@ -206,8 +206,8 @@ class ApiNotificationManagerTests {
mockOffer("failureLight", -1, 1, iconUrl = "urlSuccess", panel = mockFullScreenImagePanel("urlSuccess", "urlFailure"))
)
every { mockImagePrefetcher.prefetch(any()) } returns false
every { mockImagePrefetcher.prefetch("urlSuccess") } returns true
coEvery { mockImagePrefetcher.prefetch(any()) } returns false
coEvery { mockImagePrefetcher.prefetch("urlSuccess") } returns true
notificationManager.updateNotifications()
}
@@ -231,11 +231,11 @@ class ApiNotificationManagerTests {
mockOffer("id", -1, 1, iconUrl = "url")
)
notificationManager.updateNotifications()
verify(exactly = 1) { mockImagePrefetcher.prefetch("url") }
coVerify(exactly = 1) { mockImagePrefetcher.prefetch("url") }
notificationManager.activeListFlow.first()
verify(exactly = 2) { mockImagePrefetcher.prefetch("url") }
coVerify(exactly = 2) { mockImagePrefetcher.prefetch("url") }
notificationManager.activeListFlow.first()
verify(exactly = 3) { mockImagePrefetcher.prefetch("url") }
coVerify(exactly = 3) { mockImagePrefetcher.prefetch("url") }
}
@Test
@@ -315,7 +315,6 @@ class ApiNotificationManagerTests {
) = ApiNotificationManager(
appContext = mockContext,
mainScope = testScope.backgroundScope,
dispatcherProvider = TestDispatcherProvider(testDispatcher),
wallClockMs = { testScope.currentTime },
appConfig = mockAppConfig,
apiNotificationsStore = apiNotificationsStore,
@@ -323,7 +322,7 @@ class ApiNotificationManagerTests {
currentUser = currentUser,
userPlanManager = mockUserPlanManager,
generateNotificationsForIntroductoryOffers = mockGenerateNotificationsForIntroductoryOffers,
imagePrefetcher = mockImagePrefetcher,
lazyImagePrefetcher = dagger.Lazy { mockImagePrefetcher },
periodicUpdateManager = mockPeriodicUpdateManager,
inForeground = flowOf(true),
isLoggedIn = flowOf(true),