From 9c5bb4aa179706feceef5d9dc4a30ca505c00a63 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Mon, 6 May 2024 15:07:59 -0400 Subject: [PATCH] Initial error handling for registration v2. --- .../v2/data/RegistrationRepository.kt | 89 +++++++------ .../v2/data/network/BackupAuthCheckResult.kt | 41 ++++++ .../v2/data/network/RegistrationResult.kt | 27 ++++ .../network/VerificationCodeRequestResult.kt | 117 ++++++++++++++++++ .../v2/ui/RegistrationV2ViewModel.kt | 93 +++++++++++--- .../phonenumber/EnterPhoneNumberV2Fragment.kt | 13 ++ .../signalservice/api/NetworkResult.kt | 2 +- 7 files changed, 328 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt index b043ae431b..fda81c2f06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -7,9 +7,9 @@ package org.thoughtcrime.securesms.registration.v2.data import android.app.backup.BackupManager import android.content.Context -import androidx.annotation.WorkerThread import androidx.core.app.NotificationManagerCompat import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.registration.PushChallengeRequest import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.VerifyAccountRepository +import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.service.DirectoryRefreshListener import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener @@ -59,6 +61,7 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse +import java.io.IOException import java.nio.charset.StandardCharsets import java.util.Locale import java.util.concurrent.CountDownLatch @@ -142,7 +145,6 @@ object RegistrationRepository { /** * Takes a server response from a successful registration and persists the relevant data. */ - @WorkerThread @JvmStatic suspend fun registerAccountLocally(context: Context, registrationData: RegistrationData, response: AccountRegistrationResult, reglockEnabled: Boolean) = withContext(Dispatchers.IO) { @@ -258,20 +260,21 @@ object RegistrationRepository { * 2. (Optional) If the session has any proof requirements ("challenges"), the user must solve them and submit the proof. * 3. Once the service responds we are allowed to, we request the verification code. */ - suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): NetworkResult = + suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): VerificationCodeRequestResult = withContext(Dispatchers.IO) { val fcmToken: String? = FcmUtil.getToken(context).orElse(null) val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi - val activeSession = if (fcmToken == null) { - // TODO [regv2] - val notImplementedError = NotImplementedError() - Log.w(TAG, "Not yet implemented!", notImplementedError) - NetworkResult.ApplicationError(notImplementedError) - } else { - createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc) - } - activeSession.then { session -> + val result = ( + if (fcmToken == null) { + // TODO [regv2] + val notImplementedError = NotImplementedError() + Log.w(TAG, "Not yet implemented!", notImplementedError) + NetworkResult.ApplicationError(notImplementedError) + } else { + createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc) + } + ).then { session -> val sessionId = session.body.id SignalStore.registrationValues().sessionId = sessionId SignalStore.registrationValues().sessionE164 = e164 @@ -290,6 +293,8 @@ object RegistrationRepository { api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported) } } + + return@withContext VerificationCodeRequestResult.from(result) } /** @@ -397,39 +402,45 @@ object RegistrationRepository { return timestamp + deltaSeconds.seconds.inWholeMilliseconds } - suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): AuthCredentials? = + suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult = withContext(Dispatchers.IO) { - val usernamePasswords = SignalStore.svr() - .authTokenList - .take(10) - .map { - it.replace("Basic ", "").trim() - } - .map { - Base64.decode(it) // TODO [regv2]: figure out why Android Studio doesn't like mapCatching - } - .map { - String(it, StandardCharsets.ISO_8859_1) - } - - if (usernamePasswords.isEmpty()) { - return@withContext null - } + val usernamePasswords = async { retrieveLocalSvrCredentials() } val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi - val authCheck = api.getSvrAuthCredential(e164, usernamePasswords) - if (authCheck !is NetworkResult.Success) { - return@withContext null - } + val result = api.getSvrAuthCredential(e164, usernamePasswords.await()) + .runIfSuccessful { + val removedInvalidTokens = SignalStore.svr().removeAuthTokens(it.invalid) + if (removedInvalidTokens) { + BackupManager(context).dataChanged() + } + } - val removedInvalidTokens = SignalStore.svr().removeAuthTokens(authCheck.result.invalid) - if (removedInvalidTokens) { - BackupManager(context).dataChanged() - } - - return@withContext authCheck.result.match + return@withContext BackupAuthCheckResult.from(result) } + private suspend fun retrieveLocalSvrCredentials(): List = withContext(Dispatchers.IO) { + return@withContext SignalStore.svr() + .authTokenList + .asSequence() + .filterNotNull() + .take(10) + .map { + it.replace("Basic ", "").trim() + } + .mapNotNull { + try { + Base64.decode(it) + } catch (e: IOException) { + Log.w(TAG, "Encountered error trying to decode a token!", e) + null + } + } + .map { + String(it, StandardCharsets.ISO_8859_1) + } + .toList() + } + enum class Mode(val isSmsRetrieverSupported: Boolean) { SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt new file mode 100644 index 0000000000..10acf03c8b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.data.network + +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse + +/** + * This is a processor to map a [BackupAuthCheckResponse] to all the known outcomes. + */ +sealed class BackupAuthCheckResult(cause: Throwable?) : RegistrationResult(cause) { + companion object { + @JvmStatic + fun from(networkResult: NetworkResult): BackupAuthCheckResult { + return when (networkResult) { + is NetworkResult.Success -> { + val match = networkResult.result.match + if (match != null) { + SuccessWithCredentials(match) + } else { + SuccessWithoutCredentials() + } + } + + is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) + is NetworkResult.NetworkError -> UnknownError(networkResult.exception) + is NetworkResult.StatusCodeError -> UnknownError(networkResult.exception) + } + } + } + + class SuccessWithCredentials(val authCredentials: AuthCredentials) : BackupAuthCheckResult(null) + + class SuccessWithoutCredentials : BackupAuthCheckResult(null) + + class UnknownError(cause: Throwable) : BackupAuthCheckResult(cause) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt new file mode 100644 index 0000000000..b4b38baffa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.data.network + +/** + * This is a merging of the NetworkResult pattern and the Processor pattern of registration v1. + * The goal is to enumerate all possible responses as sealed classes, which means the consumer will be able to handle them in an exhaustive when clause + * + * @property errorCause the [Throwable] that caused the Error. Null if the network request was successful. + * + */ +abstract class RegistrationResult(private val errorCause: Throwable?) { + fun isSuccess(): Boolean { + return errorCause == null + } + + fun getCause(): Throwable { + if (errorCause == null) { + throw IllegalStateException("Cannot get cause from successful processor!") + } + + return errorCause + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt new file mode 100644 index 0000000000..46be63716e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.data.network + +import okio.IOException +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException +import org.whispersystems.signalservice.api.push.exceptions.ExternalServiceFailureException +import org.whispersystems.signalservice.api.push.exceptions.ImpossiblePhoneNumberException +import org.whispersystems.signalservice.api.push.exceptions.InvalidTransportModeException +import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException +import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException +import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequiredException +import org.whispersystems.signalservice.api.push.exceptions.RateLimitException +import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException +import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse +import org.whispersystems.signalservice.internal.util.JsonUtil + +/** + * This is a processor to map a [RegistrationSessionMetadataResponse] to all the known outcomes. + */ +sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResult(cause) { + companion object { + + private val TAG = Log.tag(VerificationCodeRequestResult::class.java) + + @JvmStatic + fun from(networkResult: NetworkResult): VerificationCodeRequestResult { + return when (networkResult) { + is NetworkResult.Success -> { + val challenges = networkResult.result.body.requestedInformation + if (challenges.isNotEmpty()) { + ChallengeRequired(challenges) + } else { + Success( + sessionId = networkResult.result.body.id, + nextSms = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms), + nextCall = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall) + ) + } + } + + is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) + is NetworkResult.NetworkError -> UnknownError(networkResult.exception) + is NetworkResult.StatusCodeError -> { + when (val cause = networkResult.exception) { + is PushChallengeRequiredException -> createChallengeRequiredProcessor(networkResult) + is CaptchaRequiredException -> createChallengeRequiredProcessor(networkResult) + is RateLimitException -> createRateLimitProcessor(cause) + is ImpossiblePhoneNumberException -> ImpossibleNumber(cause) + is NonNormalizedPhoneNumberException -> NonNormalizedNumber(cause) + is TokenNotAcceptedException -> TokenNotAccepted(cause) + is ExternalServiceFailureException -> ExternalServiceFailure(cause) + is InvalidTransportModeException -> InvalidTransportModeFailure(cause) + is MalformedRequestException -> MalformedRequest(cause) + is RegistrationRetryException -> MustRetry(cause) + else -> UnknownError(cause) + } + } + } + } + + private fun createChallengeRequiredProcessor(errorResult: NetworkResult.StatusCodeError): VerificationCodeRequestResult { + if (errorResult.body == null) { + Log.w(TAG, "Attempted to parse error body with response code ${errorResult.code} for list of requested information, but body was null.") + return UnknownError(errorResult.exception) + } + + try { + val response = JsonUtil.fromJson(errorResult.body, RegistrationSessionMetadataJson::class.java) + return ChallengeRequired(response.requestedInformation) + } catch (parseException: IOException) { + Log.w(TAG, "Attempted to parse error body for list of requested information, but encountered exception.", parseException) + return UnknownError(parseException) + } + } + + private fun createRateLimitProcessor(exception: RateLimitException): VerificationCodeRequestResult { + return if (exception.retryAfterMilliseconds.isPresent) { + RateLimited(exception, exception.retryAfterMilliseconds.get()) + } else { + AttemptsExhausted(exception) + } + } + } + + class Success(val sessionId: String, val nextSms: Long, val nextCall: Long) : VerificationCodeRequestResult(null) + + class ChallengeRequired(val challenges: List) : VerificationCodeRequestResult(null) + + class RateLimited(cause: Throwable, val timeRemaining: Long) : VerificationCodeRequestResult(cause) + + class AttemptsExhausted(cause: Throwable) : VerificationCodeRequestResult(cause) + + class ImpossibleNumber(cause: Throwable) : VerificationCodeRequestResult(cause) + + class NonNormalizedNumber(cause: Throwable) : VerificationCodeRequestResult(cause) + + class TokenNotAccepted(cause: Throwable) : VerificationCodeRequestResult(cause) + + class ExternalServiceFailure(cause: Throwable) : VerificationCodeRequestResult(cause) + + class InvalidTransportModeFailure(cause: Throwable) : VerificationCodeRequestResult(cause) + + class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause) + + class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause) + + class UnknownError(cause: Throwable) : VerificationCodeRequestResult(cause) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index f779d0c061..ba38744085 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.Phonenumber +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -27,6 +28,19 @@ import org.thoughtcrime.securesms.pin.SvrWrongPinException import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ExternalServiceFailure +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ImpossibleNumber +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MalformedRequest +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NonNormalizedNumber +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RateLimited +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted +import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.dualsim.MccMncProducer @@ -42,9 +56,15 @@ import java.io.IOException class RegistrationV2ViewModel : ViewModel() { private val store = MutableStateFlow(RegistrationV2State()) - private val password = Util.getSecret(18) // TODO [regv2]: persist this + private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> + Log.w(TAG, "CoroutineExceptionHandler invoked.", exception) + store.update { + it.copy(networkError = exception) + } + } + val uiState = store.asLiveData() init { @@ -80,7 +100,7 @@ class RegistrationV2ViewModel : ViewModel() { } fun fetchFcmToken(context: Context) { - viewModelScope.launch { + viewModelScope.launch(context = coroutineExceptionHandler) { val fcmToken = RegistrationRepository.getFcmToken(context) store.update { it.copy(registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, isFcmSupported = true, fcmToken = fcmToken) @@ -120,25 +140,70 @@ class RegistrationV2ViewModel : ViewModel() { store.update { it.copy(canSkipSms = true) } - } else { - viewModelScope.launch { - val svrCredentials = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password) + return + } - if (svrCredentials != null) { - // Re-registration when credentials stored in backup. + viewModelScope.launch { + val svrCredentialsResult = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password) + + when (svrCredentialsResult) { + is BackupAuthCheckResult.UnknownError -> { + handleGenericError(svrCredentialsResult.getCause()) + return@launch + } + + is BackupAuthCheckResult.SuccessWithCredentials -> { + Log.d(TAG, "Found local valid SVR auth credentials.") store.update { - it.copy(canSkipSms = true, svrAuthCredentials = svrCredentials) + it.copy(canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials) } - } else { - val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow() + return@launch + } + + is BackupAuthCheckResult.SuccessWithoutCredentials -> Log.d(TAG, "No local SVR auth credentials could be found and/or validated.") + } + + val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc) + + when (codeRequestResponse) { + is UnknownError -> { + handleGenericError(codeRequestResponse.getCause()) + return@launch + } + + is Success -> { + updateFcmToken(context) store.update { - it.copy(sessionId = codeRequestResponse.body.id, nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms), nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall), registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) + it.copy( + sessionId = codeRequestResponse.sessionId, + nextSms = codeRequestResponse.nextSms, + nextCall = codeRequestResponse.nextCall, + registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED + ) } } + + is AttemptsExhausted -> Log.w(TAG, "TODO") + is ChallengeRequired -> Log.w(TAG, "TODO") + is ImpossibleNumber -> Log.w(TAG, "TODO") + is NonNormalizedNumber -> Log.w(TAG, "TODO") + is RateLimited -> Log.w(TAG, "TODO") + is ExternalServiceFailure -> Log.w(TAG, "TODO") + is InvalidTransportModeFailure -> Log.w(TAG, "TODO") + is MalformedRequest -> Log.w(TAG, "TODO") + is MustRetry -> Log.w(TAG, "TODO") + is TokenNotAccepted -> Log.w(TAG, "TODO") } } } + private fun handleGenericError(cause: Throwable) { + Log.w(TAG, "Encountered unknown error!", cause) + store.update { + it.copy(inProgress = false, networkError = cause) + } + } + private fun setRecoveryPassword(recoveryPassword: String?) { store.update { it.copy(recoveryPassword = recoveryPassword) @@ -164,7 +229,7 @@ class RegistrationV2ViewModel : ViewModel() { if (RegistrationRepository.canUseLocalRecoveryPassword()) { if (RegistrationRepository.doesPinMatchLocalHash(pin)) { Log.d(TAG, "Found recovery password, attempting to re-register.") - viewModelScope.launch { + viewModelScope.launch(context = coroutineExceptionHandler) { verifyReRegisterInternal(context, pin, SignalStore.svr().getOrCreateMasterKey()) setInProgress(false) } @@ -180,7 +245,7 @@ class RegistrationV2ViewModel : ViewModel() { val authCredentials = store.value.svrAuthCredentials if (authCredentials != null) { Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR.") - viewModelScope.launch { + viewModelScope.launch(context = coroutineExceptionHandler) { try { val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, authCredentials) setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword()) @@ -254,7 +319,7 @@ class RegistrationV2ViewModel : ViewModel() { } val e164: String = getCurrentE164() ?: throw IllegalStateException() - viewModelScope.launch { + viewModelScope.launch(context = coroutineExceptionHandler) { val registrationData = getRegistrationData(code) val verificationResponse = RegistrationRepository.submitVerificationCode(context, e164, password, sessionId, registrationData).successOrThrow() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt index d60b3ab968..696b0a134e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt @@ -96,6 +96,10 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState -> presentRegisterButton(sharedState) presentProgressBar(sharedState.inProgress, sharedState.isReRegister) + + sharedState.networkError?.let { + presentNetworkError(it) + } if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) { moveToEnterPinScreen() } else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) { @@ -226,6 +230,15 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } } + private fun presentNetworkError(networkError: Throwable) { + // TODO [regv2]: check specific errors with a when clause + Log.i(TAG, "Unknown error during verification code request", networkError) + MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.RegistrationActivity_unable_to_connect_to_service) + .setPositiveButton(android.R.string.ok, null) + .show() + } + private fun onRegistrationButtonClicked() { ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout) sharedViewModel.setInProgress(true) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt index 6465bd5f88..700d07603c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt @@ -51,7 +51,7 @@ sealed class NetworkResult( data class NetworkError(val exception: IOException) : NetworkResult() /** Indicates we got a response, but it was a non-2xx response. */ - data class StatusCodeError(val code: Int, val body: String?, val exception: IOException) : NetworkResult() + data class StatusCodeError(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult() /** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */ data class ApplicationError(val throwable: Throwable) : NetworkResult()