mirror of
https://github.com/ProtonVPN/android-app.git
synced 2026-03-13 09:02:15 +08:00
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:
committed by
MargeBot
parent
911245fe79
commit
e41d93c997
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user