diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-06-17 16:06:50 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-06-19 10:50:21 +0200 |
| commit | f31afd8ee3861d50d02a099afbbf181338072276 (patch) | |
| tree | 75ca652b057235d8af85e59a47f606f4799bcdb3 /android | |
| parent | 88c5d622d797faf99528f79a0907b101fa8f4cae (diff) | |
| download | mullvadvpn-f31afd8ee3861d50d02a099afbbf181338072276.tar.xz mullvadvpn-f31afd8ee3861d50d02a099afbbf181338072276.zip | |
Replace retry with exponential backoff with arrow schedule
Also clean up the code related to play purchase verification
Diffstat (limited to 'android')
15 files changed, 72 insertions, 98 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 798667ff3f..8e523ecb39 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -329,6 +329,7 @@ dependencies { implementation(Dependencies.AndroidX.lifecycleViewmodelKtx) implementation(Dependencies.AndroidX.lifecycleRuntimeCompose) implementation(Dependencies.Arrow.core) + implementation(Dependencies.Arrow.resilience) implementation(Dependencies.Compose.constrainLayout) implementation(Dependencies.Compose.foundation) implementation(Dependencies.Compose.material3) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt index 1cf1c32b2c..ba2413d4fc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt @@ -1,5 +1,7 @@ package net.mullvad.mullvadvpn.constant +import kotlin.time.Duration.Companion.seconds + const val VERIFICATION_MAX_ATTEMPTS = 4 -const val VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS = 3000L -const val VERIFICATION_BACK_OFF_FACTOR = 3L +val VERIFICATION_INITIAL_BACK_OFF_DURATION = 3.seconds +const val VERIFICATION_BACK_OFF_FACTOR = 3.toDouble() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt index fbd662f95e..23f6c797b0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt @@ -1,20 +1,24 @@ package net.mullvad.mullvadvpn.usecase import android.app.Activity +import arrow.core.Either +import arrow.core.right +import arrow.resilience.Schedule +import arrow.resilience.retryEither import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.transform import net.mullvad.mullvadvpn.constant.VERIFICATION_BACK_OFF_FACTOR -import net.mullvad.mullvadvpn.constant.VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS +import net.mullvad.mullvadvpn.constant.VERIFICATION_INITIAL_BACK_OFF_DURATION import net.mullvad.mullvadvpn.constant.VERIFICATION_MAX_ATTEMPTS import net.mullvad.mullvadvpn.lib.payment.PaymentRepository import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult +import net.mullvad.mullvadvpn.lib.payment.model.VerificationError import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult -import net.mullvad.mullvadvpn.util.retryWithExponentialBackOff interface PaymentUseCase { val paymentAvailability: Flow<PaymentAvailability?> @@ -26,7 +30,7 @@ interface PaymentUseCase { suspend fun resetPurchaseResult() - suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit = {}) + suspend fun verifyPurchases(): Either<VerificationError, VerificationResult> } class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : PaymentUseCase { @@ -60,24 +64,19 @@ class PlayPaymentUseCase(private val paymentRepository: PaymentRepository) : Pay } @Suppress("ensure every public functions method is named 'invoke' with operator modifier") - override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) { - paymentRepository - .verifyPurchases() - .retryWithExponentialBackOff( - maxAttempts = VERIFICATION_MAX_ATTEMPTS, - initialBackOffDelay = VERIFICATION_INITIAL_BACK_OFF_MILLISECONDS, - backOffDelayFactor = VERIFICATION_BACK_OFF_FACTOR - ) { - it is VerificationResult.Error - } - .collect { + override suspend fun verifyPurchases() = + Schedule.exponential<VerificationError>( + VERIFICATION_INITIAL_BACK_OFF_DURATION, + VERIFICATION_BACK_OFF_FACTOR + ) + .and(Schedule.recurs(VERIFICATION_MAX_ATTEMPTS.toLong())) + .retryEither { paymentRepository.verifyPurchases() } + .onRight { if (it == VerificationResult.Success) { // Update the payment availability after a successful verification. queryPaymentAvailability() - onSuccessfulVerification() } } - } private fun PurchaseResult?.shouldDelayLoading() = this is PurchaseResult.FetchingProducts || this is PurchaseResult.VerificationStarted @@ -107,7 +106,5 @@ class EmptyPaymentUseCase : PaymentUseCase { } @Suppress("ensure every public functions method is named 'invoke' with operator modifier") - override suspend fun verifyPurchases(onSuccessfulVerification: () -> Unit) { - // No op - } + override suspend fun verifyPurchases() = VerificationResult.NothingToVerify.right() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt index fbe44a5fea..13561aa7f8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt @@ -3,11 +3,7 @@ package net.mullvad.mullvadvpn.util import kotlinx.coroutines.Deferred -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.retryWhen inline fun <T1, T2, T3, T4, T5, T6, R> combine( flow: Flow<T1>, @@ -90,34 +86,3 @@ fun <T> Deferred<T>.getOrDefault(default: T) = } catch (e: IllegalStateException) { default } - -@Suppress("UNCHECKED_CAST") -suspend inline fun <T> Flow<T>.retryWithExponentialBackOff( - maxAttempts: Int, - initialBackOffDelay: Long, - backOffDelayFactor: Long, - crossinline predicate: (T) -> Boolean, -): Flow<T> = - map { - if (predicate(it)) { - throw ExceptionWrapper(it as Any) - } - it - } - .retryWhen { _, attempt -> - if (attempt >= maxAttempts) { - return@retryWhen false - } - val backOffDelay = initialBackOffDelay * backOffDelayFactor.pow(attempt.toInt()) - delay(backOffDelay) - true - } - .catch { - if (it is ExceptionWrapper) { - this.emit(it.item as T) - } else { - throw it - } - } - -class ExceptionWrapper(val item: Any) : Throwable() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/VerificationResultExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/VerificationResultExtensions.kt new file mode 100644 index 0000000000..d4484d50e6 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/VerificationResultExtensions.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.util + +import arrow.core.Either +import net.mullvad.mullvadvpn.lib.payment.model.VerificationError +import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult + +fun Either<VerificationError, VerificationResult>.isSuccess() = + getOrNull() == VerificationResult.Success diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt index a42003d6e2..7c2635b63a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt @@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.shared.AccountRepository import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.usecase.PaymentUseCase +import net.mullvad.mullvadvpn.util.isSuccess import net.mullvad.mullvadvpn.util.toPaymentState import org.joda.time.DateTime @@ -90,8 +91,9 @@ class AccountViewModel( private fun verifyPurchases() { viewModelScope.launch { - paymentUseCase.verifyPurchases() - updateAccountExpiry() + if (paymentUseCase.verifyPurchases().isSuccess()) { + updateAccountExpiry() + } } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt index b41a175dad..037c3243ab 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt @@ -32,6 +32,7 @@ import net.mullvad.mullvadvpn.usecase.PaymentUseCase import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase import net.mullvad.mullvadvpn.util.combine import net.mullvad.mullvadvpn.util.daysFromNow +import net.mullvad.mullvadvpn.util.isSuccess import net.mullvad.mullvadvpn.util.toInAddress import net.mullvad.mullvadvpn.util.toOutAddress @@ -114,8 +115,8 @@ class ConnectViewModel( init { viewModelScope.launch { - paymentUseCase.verifyPurchases { - viewModelScope.launch { accountRepository.getAccountData() } + if (paymentUseCase.verifyPurchases().isSuccess()) { + accountRepository.getAccountData() } } viewModelScope.launch { deviceRepository.updateDevice() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt index 7784181466..d279792853 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt @@ -20,6 +20,7 @@ import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase import net.mullvad.mullvadvpn.usecase.PaymentUseCase +import net.mullvad.mullvadvpn.util.isSuccess import net.mullvad.mullvadvpn.util.toPaymentState class OutOfTimeViewModel( @@ -76,8 +77,9 @@ class OutOfTimeViewModel( private fun verifyPurchases() { viewModelScope.launch { - paymentUseCase.verifyPurchases() - updateAccountExpiry() + if (paymentUseCase.verifyPurchases().isSuccess()) { + updateAccountExpiry() + } } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt index 4b61468f8e..9322ef9ce4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt @@ -21,6 +21,7 @@ import net.mullvad.mullvadvpn.lib.shared.AccountRepository import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.usecase.PaymentUseCase +import net.mullvad.mullvadvpn.util.isSuccess import net.mullvad.mullvadvpn.util.toPaymentState class WelcomeViewModel( @@ -79,8 +80,9 @@ class WelcomeViewModel( private fun verifyPurchases() { viewModelScope.launch { - paymentUseCase.verifyPurchases() - updateAccountExpiry() + if (paymentUseCase.verifyPurchases().isSuccess()) { + updateAccountExpiry() + } } } diff --git a/android/buildSrc/src/main/kotlin/Dependencies.kt b/android/buildSrc/src/main/kotlin/Dependencies.kt index a31859e5e5..26b1a03753 100644 --- a/android/buildSrc/src/main/kotlin/Dependencies.kt +++ b/android/buildSrc/src/main/kotlin/Dependencies.kt @@ -46,6 +46,7 @@ object Dependencies { const val core = "io.arrow-kt:arrow-core:${Versions.Arrow.base}" const val optics = "io.arrow-kt:arrow-optics:${Versions.Arrow.base}" const val opticsKsp = "io.arrow-kt:arrow-optics-ksp-plugin:${Versions.Arrow.base}" + const val resilience = "io.arrow-kt:arrow-resilience:${Versions.Arrow.base}" } object Compose { diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt index 8b3ad66171..b526a10032 100644 --- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt @@ -1,6 +1,9 @@ package net.mullvad.mullvadvpn.lib.billing import android.app.Activity +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.Purchase import kotlinx.coroutines.flow.Flow @@ -22,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.payment.ProductIds import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult +import net.mullvad.mullvadvpn.lib.payment.model.VerificationError import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult class BillingPaymentRepository( @@ -129,28 +133,19 @@ class BillingPaymentRepository( } } - override fun verifyPurchases(): Flow<VerificationResult> = flow { - emit(VerificationResult.FetchingUnfinishedPurchases) + override suspend fun verifyPurchases(): Either<VerificationError, VerificationResult> = either { val purchasesResult = billingRepository.queryPurchases() - when (purchasesResult.responseCode()) { - BillingResponseCode.OK -> { - val purchases = purchasesResult.nonPendingPurchases() - if (purchases.isNotEmpty()) { - emit(VerificationResult.VerificationStarted) - emit( - verifyPurchase(purchases.first()) - .fold( - { VerificationResult.Error.VerificationError(null) }, - { VerificationResult.Success } - ) - ) - } else { - emit(VerificationResult.NothingToVerify) - } - } - else -> - emit(VerificationResult.Error.BillingError(purchasesResult.toBillingException())) + ensure(purchasesResult.responseCode() == BillingResponseCode.OK) { + VerificationError.BillingError(purchasesResult.toBillingException()) + } + val purchases = purchasesResult.nonPendingPurchases() + if (purchases.isEmpty()) { + return@either VerificationResult.NothingToVerify } + verifyPurchase(purchases.first()) + .mapLeft { VerificationError.PlayVerificationError } + .map { VerificationResult.Success } + .bind() } private suspend fun initialisePurchase() = playPurchaseRepository.initializePlayPurchase() diff --git a/android/lib/payment/build.gradle.kts b/android/lib/payment/build.gradle.kts index 23f945b4f9..892ce21c75 100644 --- a/android/lib/payment/build.gradle.kts +++ b/android/lib/payment/build.gradle.kts @@ -39,6 +39,7 @@ android { } dependencies { + implementation(Dependencies.Arrow.core) implementation(Dependencies.Kotlin.stdlib) implementation(Dependencies.KotlinX.coroutinesAndroid) } diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt index 73fd0c061d..0f076eab74 100644 --- a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt +++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt @@ -1,10 +1,12 @@ package net.mullvad.mullvadvpn.lib.payment import android.app.Activity +import arrow.core.Either import kotlinx.coroutines.flow.Flow import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult +import net.mullvad.mullvadvpn.lib.payment.model.VerificationError import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult interface PaymentRepository { @@ -14,7 +16,7 @@ interface PaymentRepository { activityProvider: () -> Activity ): Flow<PurchaseResult> - fun verifyPurchases(): Flow<VerificationResult> + suspend fun verifyPurchases(): Either<VerificationError, VerificationResult> fun queryPaymentAvailability(): Flow<PaymentAvailability> } diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationError.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationError.kt new file mode 100644 index 0000000000..51cf8d1d28 --- /dev/null +++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationError.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.lib.payment.model + +sealed interface VerificationError { + data class BillingError(val exception: Throwable) : VerificationError + + data object PlayVerificationError : VerificationError +} diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt index 725ea0af68..8cf971194d 100644 --- a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt +++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt @@ -1,19 +1,7 @@ package net.mullvad.mullvadvpn.lib.payment.model -sealed interface VerificationResult { - data object FetchingUnfinishedPurchases : VerificationResult - - data object VerificationStarted : VerificationResult - - // No verification was needed as there is no purchases to verify +interface VerificationResult { data object NothingToVerify : VerificationResult data object Success : VerificationResult - - // Generic error, add more cases as needed - sealed interface Error : VerificationResult { - data class BillingError(val exception: Throwable?) : Error - - data class VerificationError(val exception: Throwable?) : Error - } } |
