Skip to content

Commit 7e893a9

Browse files
authored
Merge pull request #507 from code-payments/chore/account-login-retry
chore: add a retry mechanism to retrieving account from AccountManager
2 parents 553b69b + 554c264 commit 7e893a9

11 files changed

Lines changed: 139 additions & 92 deletions

File tree

api/src/main/java/com/getcode/network/client/Client_Chat.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ suspend fun Client.fetchChats(owner: KeyPair): Result<List<Chat>> {
3030
.onSuccess {
3131
Timber.d("v2 chats fetched=${it.count()}")
3232
}.onFailure {
33-
trace("Failed fetching chats from V2", error = it, type = TraceType.Error)
33+
trace("Failed fetching chats from V2", type = TraceType.Error)
3434
}
3535

3636
val v1Chats = chatServiceV1.fetchChats(owner)
3737
.onSuccess {
3838
Timber.d("v1 chats fetched=${it.count()}")
3939
}.onFailure {
40-
trace("Failed fetching chats from V1", error = it, type = TraceType.Error)
40+
trace("Failed fetching chats from V1", type = TraceType.Error)
4141
}
4242

4343
if (v2Chats.isSuccess || v1Chats.isSuccess) {

api/src/main/java/com/getcode/network/client/Client_Transaction.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.getcode.model.intents.IntentPublicTransfer
2424
import com.getcode.model.intents.IntentRemoteSend
2525
import com.getcode.model.intents.SwapIntent
2626
import com.getcode.network.repository.TransactionRepository
27+
import com.getcode.network.repository.WithdrawException
2728
import com.getcode.network.repository.initiateSwap
2829
import com.getcode.solana.keys.PublicKey
2930
import com.getcode.solana.keys.base58
@@ -257,11 +258,11 @@ fun Client.withdrawExternally(
257258
destination: PublicKey
258259
): Completable {
259260
if (amount.kin.fractionalQuarks().quarks != 0L) {
260-
throw TransactionRepository.WithdrawException.InvalidFractionalKinAmountException()
261+
throw WithdrawException.InvalidFractionalKinAmountException()
261262
}
262263

263264
if (amount.kin > organizer.availableBalance) {
264-
throw TransactionRepository.WithdrawException.InsufficientFundsException()
265+
throw WithdrawException.InsufficientFundsException()
265266
}
266267

267268
val intent = PublicKey.generate()

api/src/main/java/com/getcode/network/repository/IdentityRepository.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,6 @@ class IdentityRepository @Inject constructor(
389389
}
390390
}.first()
391391
} catch (e: Exception) {
392-
e.printStackTrace()
393392
ErrorUtils.handleError(e)
394393
Result.failure(e)
395394
}

api/src/main/java/com/getcode/network/repository/TransactionRepository.kt

Lines changed: 75 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import com.getcode.solana.organizer.GiftCardAccount
4242
import com.getcode.solana.organizer.Organizer
4343
import com.getcode.solana.organizer.Relationship
4444
import com.getcode.utils.ErrorUtils
45+
import com.getcode.utils.TraceType
4546
import com.getcode.utils.bytes
4647
import com.getcode.utils.trace
4748
import com.google.protobuf.Timestamp
@@ -303,8 +304,8 @@ class TransactionRepository @Inject constructor(
303304
val subject = SingleSubject.create<IntentType>()
304305

305306
var serverMessageStream: StreamObserver<TransactionService.SubmitIntentRequest>? = null
306-
val serverResponse = object : StreamObserver<TransactionService.SubmitIntentResponse> {
307-
override fun onNext(value: TransactionService.SubmitIntentResponse?) {
307+
val serverResponse = object : StreamObserver<SubmitIntentResponse> {
308+
override fun onNext(value: SubmitIntentResponse?) {
308309
Timber.i("Received: " + value?.responseCase?.name.orEmpty())
309310

310311
when (value?.responseCase) {
@@ -373,8 +374,9 @@ class TransactionRepository @Inject constructor(
373374
else -> Unit
374375
}
375376

376-
Timber.e(
377-
"Error: ${errors.joinToString("\n")}"
377+
trace(
378+
"Error: ${errors.joinToString("\n")}",
379+
type = TraceType.Error
378380
)
379381
}
380382

@@ -696,90 +698,90 @@ class TransactionRepository @Inject constructor(
696698
}
697699
}
698700
}
701+
}
699702

700-
class ErrorSubmitIntentException(
701-
val errorSubmitIntent: ErrorSubmitIntent,
702-
cause: Throwable? = null,
703-
val messageString: String = ""
704-
) : Exception(cause) {
705-
override val message: String
706-
get() = "${errorSubmitIntent.javaClass.simpleName} $messageString"
707-
}
703+
class ErrorSubmitIntentException(
704+
val errorSubmitIntent: ErrorSubmitIntent,
705+
cause: Throwable? = null,
706+
val messageString: String = ""
707+
) : Exception(cause) {
708+
override val message: String
709+
get() = "${errorSubmitIntent.javaClass.simpleName} $messageString"
710+
}
708711

709-
enum class DeniedReason {
710-
Unspecified,
711-
TooManyFreeAccountsForPhoneNumber,
712-
TooManyFreeAccountsForDevice,
713-
UnsupportedCountry,
714-
UnsupportedDevice;
712+
enum class DeniedReason {
713+
Unspecified,
714+
TooManyFreeAccountsForPhoneNumber,
715+
TooManyFreeAccountsForDevice,
716+
UnsupportedCountry,
717+
UnsupportedDevice;
715718

716-
companion object {
717-
fun fromValue(value: Int): DeniedReason {
718-
return entries.firstOrNull { it.ordinal == value } ?: Unspecified
719-
}
719+
companion object {
720+
fun fromValue(value: Int): DeniedReason {
721+
return entries.firstOrNull { it.ordinal == value } ?: Unspecified
720722
}
721723
}
724+
}
722725

723-
sealed class ErrorSubmitIntent(val value: Int) {
724-
data class Denied(val reasons: List<DeniedReason> = emptyList()): ErrorSubmitIntent(0)
725-
data class InvalidIntent(val reasons: List<String>): ErrorSubmitIntent(1)
726-
data object SignatureError: ErrorSubmitIntent(2)
727-
data class StaleState(val reasons: List<String>): ErrorSubmitIntent(3)
728-
data object Unknown: ErrorSubmitIntent(-1)
729-
data object DeviceTokenUnavailable: ErrorSubmitIntent(-2)
730-
731-
override fun toString(): String {
732-
return when (this) {
733-
is Denied -> "denied(${reasons.joinToString()})"
734-
DeviceTokenUnavailable -> "deviceTokenUnavailable"
735-
is InvalidIntent -> "invalidIntent(${reasons.joinToString()})"
736-
SignatureError -> "signatureError"
737-
is StaleState -> "staleState(${reasons.joinToString()})"
738-
Unknown -> "unknown"
739-
}
726+
sealed class ErrorSubmitIntent(val value: Int) {
727+
data class Denied(val reasons: List<DeniedReason> = emptyList()): ErrorSubmitIntent(0)
728+
data class InvalidIntent(val reasons: List<String>): ErrorSubmitIntent(1)
729+
data object SignatureError: ErrorSubmitIntent(2)
730+
data class StaleState(val reasons: List<String>): ErrorSubmitIntent(3)
731+
data object Unknown: ErrorSubmitIntent(-1)
732+
data object DeviceTokenUnavailable: ErrorSubmitIntent(-2)
733+
734+
override fun toString(): String {
735+
return when (this) {
736+
is Denied -> "denied(${reasons.joinToString()})"
737+
DeviceTokenUnavailable -> "deviceTokenUnavailable"
738+
is InvalidIntent -> "invalidIntent(${reasons.joinToString()})"
739+
SignatureError -> "signatureError"
740+
is StaleState -> "staleState(${reasons.joinToString()})"
741+
Unknown -> "unknown"
740742
}
743+
}
741744

742-
companion object {
743-
operator fun invoke(proto: SubmitIntentResponse.Error): ErrorSubmitIntent {
744-
val reasonStrings = proto.errorDetailsList.mapNotNull {
745-
when (it.typeCase) {
746-
TransactionService.ErrorDetails.TypeCase.REASON_STRING ->
747-
it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() }
748-
else -> null
749-
}
745+
companion object {
746+
operator fun invoke(proto: SubmitIntentResponse.Error): ErrorSubmitIntent {
747+
val reasonStrings = proto.errorDetailsList.mapNotNull {
748+
when (it.typeCase) {
749+
TransactionService.ErrorDetails.TypeCase.REASON_STRING ->
750+
it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() }
751+
else -> null
750752
}
751-
return when (proto.code) {
752-
SubmitIntentResponse.Error.Code.DENIED -> {
753-
val reasons = proto.errorDetailsList.mapNotNull {
754-
if (!it.hasIntentDenied()) return@mapNotNull null
755-
DeniedReason.fromValue(it.intentDenied.reasonValue)
756-
}
757-
758-
Denied(reasons)
753+
}
754+
return when (proto.code) {
755+
SubmitIntentResponse.Error.Code.DENIED -> {
756+
val reasons = proto.errorDetailsList.mapNotNull {
757+
if (!it.hasIntentDenied()) return@mapNotNull null
758+
DeniedReason.fromValue(it.intentDenied.reasonValue)
759759
}
760-
SubmitIntentResponse.Error.Code.INVALID_INTENT -> InvalidIntent(reasonStrings)
761-
SubmitIntentResponse.Error.Code.SIGNATURE_ERROR -> SignatureError
762-
SubmitIntentResponse.Error.Code.STALE_STATE -> StaleState(reasonStrings)
763-
SubmitIntentResponse.Error.Code.UNRECOGNIZED -> Unknown
764-
else -> return Unknown
760+
761+
Denied(reasons)
765762
}
763+
SubmitIntentResponse.Error.Code.INVALID_INTENT -> InvalidIntent(reasonStrings)
764+
SubmitIntentResponse.Error.Code.SIGNATURE_ERROR -> SignatureError
765+
SubmitIntentResponse.Error.Code.STALE_STATE -> StaleState(reasonStrings)
766+
SubmitIntentResponse.Error.Code.UNRECOGNIZED -> Unknown
767+
else -> return Unknown
766768
}
767769
}
768770
}
771+
}
769772

770-
sealed class WithdrawException : Exception() {
771-
class InvalidFractionalKinAmountException : WithdrawException()
772-
class InsufficientFundsException : WithdrawException()
773-
}
773+
sealed class WithdrawException : Exception() {
774+
class InvalidFractionalKinAmountException : WithdrawException()
775+
class InsufficientFundsException : WithdrawException()
776+
}
774777

775-
sealed class FetchUpgradeableIntentsException : Exception() {
776-
class DeserializationException : FetchUpgradeableIntentsException()
777-
class UnknownException : FetchUpgradeableIntentsException()
778-
}
778+
sealed class FetchUpgradeableIntentsException : Exception() {
779+
class DeserializationException : FetchUpgradeableIntentsException()
780+
class UnknownException : FetchUpgradeableIntentsException()
781+
}
779782

780-
sealed class AirdropException : Exception() {
781-
class AlreadyClaimedException : AirdropException()
782-
class UnavailableException : AirdropException()
783-
class UnknownException : AirdropException()
784-
}
783+
sealed class AirdropException : Exception() {
784+
class AlreadyClaimedException : AirdropException()
785+
class UnavailableException : AirdropException()
786+
class UnknownException : AirdropException()
785787
}

api/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.getcode.model.intents.SwapConfigParameters
88
import com.getcode.model.intents.SwapIntent
99
import com.getcode.model.intents.requestToSubmitSignatures
1010
import com.getcode.network.core.BidirectionalStreamReference
11-
import com.getcode.network.repository.TransactionRepository.ErrorSubmitIntent
1211
import com.getcode.solana.SolanaTransaction
1312
import com.getcode.solana.diff
1413
import com.getcode.solana.keys.Signature
@@ -17,7 +16,6 @@ import com.getcode.solana.organizer.Organizer
1716
import com.getcode.utils.ErrorUtils
1817
import io.grpc.stub.StreamObserver
1918
import kotlinx.coroutines.suspendCancellableCoroutine
20-
import org.kin.sdk.base.tools.Base58
2119
import timber.log.Timber
2220
import kotlin.coroutines.resume
2321

api/src/main/java/com/getcode/utils/ErrorUtils.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.getcode.manager.TopBarManager
88
import io.grpc.StatusRuntimeException
99
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
1010
import io.reactivex.rxjava3.exceptions.UndeliverableException
11+
import kotlinx.coroutines.TimeoutCancellationException
1112
import timber.log.Timber
1213
import java.net.ConnectException
1314
import java.net.UnknownHostException
@@ -42,6 +43,7 @@ object ErrorUtils {
4243
BuildConfig.NOTIFY_ERRORS &&
4344
throwable !is UnknownHostException &&
4445
throwable !is TimeoutException &&
46+
throwable !is TimeoutCancellationException &&
4547
throwable !is ConnectException
4648
) {
4749
FirebaseCrashlytics.getInstance().recordException(throwable)
@@ -62,7 +64,7 @@ object ErrorUtils {
6264
throwable.cause is StatusRuntimeException
6365

6466
private fun isSuppressibleError(throwable: Throwable): Boolean =
65-
throwable is SQLException || throwable is net.sqlcipher.SQLException || throwable is SuppressibleException
67+
throwable is SQLException || throwable is net.sqlcipher.SQLException || throwable is SuppressibleException || throwable is TimeoutCancellationException
6668
}
6769

6870
data class SuppressibleException(override val message: String): Throwable(message)

app/proguard-rules.pro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
}
5757

5858
-keep public class * extends java.lang.Exception
59+
-keep public class * extends com.getcode.network.repository.ErrorSubmitIntent
60+
-keep public class * extends com.getcode.network.repository.ErrorSubmitIntentException
61+
-keep public class * extends com.getcode.network.repository.WithdrawException
62+
-keep public class * extends com.getcode.network.repository.FetchUpgradeableIntentsException
63+
-keep public class * extends com.getcode.network.repository.AirdropException
5964

6065
# https://github.com/firebase/firebase-android-sdk/issues/3688
6166
-keep class org.json.** { *; }

app/src/main/java/com/getcode/util/AccountUtils.kt

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ import io.reactivex.rxjava3.core.Single
1515
import io.reactivex.rxjava3.subjects.SingleSubject
1616
import kotlinx.coroutines.CoroutineScope
1717
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.delay
1819
import kotlinx.coroutines.launch
1920
import kotlinx.coroutines.suspendCancellableCoroutine
2021
import kotlinx.datetime.Clock
2122
import kotlin.coroutines.resume
23+
import kotlin.time.Duration
24+
import kotlin.time.Duration.Companion.seconds
25+
import kotlin.time.TimeSource
2226

2327

2428
object AccountUtils {
@@ -51,7 +55,7 @@ object AccountUtils {
5155
val subject = SingleSubject.create<Pair<String?, Account?>>()
5256
return subject.doOnSubscribe {
5357
CoroutineScope(Dispatchers.IO).launch {
54-
val result = getAccountNoActivity(context)
58+
val result = getAccountInternal(context)
5559
subject.onSuccess(result ?: (null to null))
5660
}
5761
}
@@ -66,15 +70,46 @@ object AccountUtils {
6670
}
6771
}
6872

73+
private suspend fun getAccountInternal(
74+
context: Context,
75+
maxRetries: Int = 3,
76+
delayDuration: Duration = 2.seconds
77+
): Pair<String?, Account?>? {
78+
var currentAttempt = 0
79+
val startTime = TimeSource.Monotonic.markNow()
80+
81+
while (currentAttempt < maxRetries) {
82+
val result = try {
83+
getAccountNoActivity(context)
84+
} catch (e: Exception) {
85+
trace(message = "Attempt $currentAttempt failed with exception: ${e.message}", error = e, type = TraceType.Error)
86+
null
87+
}
88+
89+
if (result != null) {
90+
return result
91+
} else {
92+
currentAttempt++
93+
if (currentAttempt < maxRetries) {
94+
trace("Retrying after ${delayDuration.inWholeMilliseconds} ms...", type = TraceType.Log)
95+
delay(delayDuration.inWholeMilliseconds)
96+
}
97+
}
98+
}
99+
100+
trace("Failed to get account after $maxRetries attempts in ${startTime.elapsedNow().inWholeMilliseconds} ms", type = TraceType.Error)
101+
return null
102+
}
103+
69104
private suspend fun getAccountNoActivity(
70105
context: Context
71106
): Pair<String?, Account?>? = suspendCancellableCoroutine { cont ->
72107
trace("getAuthToken", type = TraceType.Silent)
73108
val am: AccountManager = AccountManager.get(context)
74-
val account = am.accounts.getOrNull(0)
109+
val account = am.getAccountsByType(ACCOUNT_TYPE).firstOrNull()
75110
if (account == null) {
76111
trace("no associated account found", type = TraceType.Error)
77-
cont.resume(null to null)
112+
cont.resume(null)
78113
return@suspendCancellableCoroutine
79114
}
80115
val start = Clock.System.now()
@@ -90,9 +125,8 @@ object AccountUtils {
90125

91126
cont.resume(authToken.orEmpty() to account)
92127
} catch (e: AuthenticatorException) {
93-
e.printStackTrace()
94128
trace(message = "failed to read account", error = e, type = TraceType.Error)
95-
cont.resume(null to null)
129+
cont.resume(null)
96130
}
97131
}, handler
98132
)
@@ -110,6 +144,6 @@ object AccountUtils {
110144
}
111145
}
112146

113-
return getAccountNoActivity(context)?.first
147+
return getAccountInternal(context)?.first
114148
}
115149
}

app/src/main/java/com/getcode/util/AuthenticatorService.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import javax.inject.Inject
1010

1111
@AndroidEntryPoint
1212
class AuthenticatorService : Service() {
13-
@Inject
14-
lateinit var accountAuthenticator: AccountAuthenticator
13+
private val accountAuthenticator: AccountAuthenticator by lazy {
14+
AccountAuthenticator(this)
15+
}
1516

1617
override fun onBind(intent: Intent): IBinder? {
1718
var binder: IBinder? = null

0 commit comments

Comments
 (0)