summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-06-17 16:06:50 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-06-19 10:50:21 +0200
commitf31afd8ee3861d50d02a099afbbf181338072276 (patch)
tree75ca652b057235d8af85e59a47f606f4799bcdb3 /android
parent88c5d622d797faf99528f79a0907b101fa8f4cae (diff)
downloadmullvadvpn-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')
-rw-r--r--android/app/build.gradle.kts1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PaymentConstant.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PaymentUseCase.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/VerificationResultExtensions.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt6
-rw-r--r--android/buildSrc/src/main/kotlin/Dependencies.kt1
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt35
-rw-r--r--android/lib/payment/build.gradle.kts1
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt4
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationError.kt7
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt14
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
- }
}