From 303100bb6bdbe05e8e31e16d67062397f19b0922 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Fri, 31 May 2024 15:15:32 -0400 Subject: [PATCH] Further registration lock improvements in Registration V2. --- .../registration/ReceivedSmsEvent.java | 2 + .../v2/data/network/RegisterAccountResult.kt | 1 + .../network/VerificationCodeRequestResult.kt | 11 +- .../v2/ui/RegistrationV2Activity.kt | 21 ++ .../registration/v2/ui/RegistrationV2State.kt | 6 +- .../v2/ui/RegistrationV2ViewModel.kt | 270 ++++++++++++------ .../v2/ui/entercode/EnterCodeV2Fragment.kt | 71 ++++- .../GrantPermissionsV2Fragment.kt | 1 + .../phonenumber/EnterPhoneNumberV2Fragment.kt | 36 ++- .../ui/phonenumber/EnterPhoneNumberV2State.kt | 8 +- .../RegistrationLockV2Fragment.kt | 41 +-- .../v2/ui/welcome/WelcomeV2Fragment.kt | 26 +- .../main/res/navigation/registration_v2.xml | 1 - 13 files changed, 336 insertions(+), 159 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ReceivedSmsEvent.java b/app/src/main/java/org/thoughtcrime/securesms/registration/ReceivedSmsEvent.java index 6f88ca36e4..6f7227c815 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ReceivedSmsEvent.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ReceivedSmsEvent.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; public final class ReceivedSmsEvent { + public static final int CODE_LENGTH = 6; + private final @NonNull String code; public ReceivedSmsEvent(@NonNull String code) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt index 575395f4db..29d4deb007 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt @@ -43,6 +43,7 @@ sealed class RegisterAccountResult(cause: Throwable?) : RegistrationResult(cause } } } + private fun createRateLimitProcessor(exception: RateLimitException): RegisterAccountResult { return if (exception.retryAfterMilliseconds.isPresent) { RateLimited(exception, exception.retryAfterMilliseconds.get()) 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 index 2bf992b53a..aaf5268c28 100644 --- 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 @@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequire 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.AuthCredentials import org.whispersystems.signalservice.internal.push.LockedException import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse @@ -45,9 +46,11 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } else { Success( sessionId = networkResult.result.body.id, - allowedToRequestCode = networkResult.result.body.allowedToRequestCode, nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms), nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall), + nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextVerificationAttempt), + allowedToRequestCode = networkResult.result.body.allowedToRequestCode, + challengesRequested = Challenge.parse(networkResult.result.body.requestedInformation), verified = networkResult.result.body.verified ) } @@ -67,7 +70,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu is InvalidTransportModeException -> InvalidTransportModeFailure(cause) is MalformedRequestException -> MalformedRequest(cause) is RegistrationRetryException -> MustRetry(cause) - is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining) + is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials) is NoSuchSessionException -> NoSuchSession(cause) is AlreadyVerifiedException -> AlreadyVerified(cause) else -> UnknownError(cause) @@ -100,7 +103,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } } - class Success(val sessionId: String, val allowedToRequestCode: Boolean, val nextSmsTimestamp: Long, val nextCallTimestamp: Long, val verified: Boolean) : VerificationCodeRequestResult(null) + class Success(val sessionId: String, val nextSmsTimestamp: Long, val nextCallTimestamp: Long, nextVerificationAttempt: Long, val allowedToRequestCode: Boolean, challengesRequested: List, val verified: Boolean) : VerificationCodeRequestResult(null) class ChallengeRequired(val challenges: List) : VerificationCodeRequestResult(null) @@ -122,7 +125,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause) - class RegistrationLocked(cause: Throwable, val timeRemaining: Long) : VerificationCodeRequestResult(cause) + class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials) : VerificationCodeRequestResult(cause) class NoSuchSession(cause: Throwable) : VerificationCodeRequestResult(cause) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt index 95c36a6bfa..d5d9706f3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt @@ -9,6 +9,8 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.navigation.ActivityNavigator import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.BaseActivity @@ -20,6 +22,7 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver /** * Activity to hold the entire registration process. @@ -30,6 +33,12 @@ class RegistrationV2Activity : BaseActivity() { val sharedViewModel: RegistrationV2ViewModel by viewModels() + private var smsRetrieverReceiver: SmsRetrieverReceiver? = null + + init { + lifecycle.addObserver(SmsRetrieverObserver()) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_registration_navigation_v2) @@ -80,6 +89,18 @@ class RegistrationV2Activity : BaseActivity() { } } + private inner class SmsRetrieverObserver : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + smsRetrieverReceiver = SmsRetrieverReceiver(application) + smsRetrieverReceiver?.registerReceiver() + } + + override fun onDestroy(owner: LifecycleOwner) { + smsRetrieverReceiver?.unregisterReceiver() + smsRetrieverReceiver = null + } + } + companion object { @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt index befd7abd9f..80fd051252 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt @@ -15,7 +15,7 @@ import org.whispersystems.signalservice.internal.push.AuthCredentials */ data class RegistrationV2State( val sessionId: String? = null, - val enteredCode: String? = null, + val enteredCode: String = "", val phoneNumber: Phonenumber.PhoneNumber? = null, val inProgress: Boolean = false, val isReRegister: Boolean = false, @@ -23,6 +23,7 @@ data class RegistrationV2State( val canSkipSms: Boolean = false, val svrAuthCredentials: AuthCredentials? = null, val svrTriesRemaining: Int = 10, + val incorrectCodeAttempts: Int = 0, val isRegistrationLockEnabled: Boolean = false, val lockedTimeRemaining: Long = 0L, val userSkippedReregistration: Boolean = false, @@ -32,8 +33,11 @@ data class RegistrationV2State( val challengesRequested: List = emptyList(), val challengesPresented: Set = emptySet(), val captchaToken: String? = null, + val allowedToRequestCode: Boolean = false, val nextSmsTimestamp: Long = 0L, val nextCallTimestamp: Long = 0L, + val nextVerificationAttempt: Long = 0L, + val verified: Boolean = false, val smsListenerTimeout: Long = 0L, val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION, val networkError: Throwable? = null 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 9b39372eb3..922b679a20 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 @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.registration.v2.ui +import android.Manifest import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData @@ -17,14 +18,18 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob import org.thoughtcrime.securesms.jobs.ProfileUploadJob import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.pin.SvrRepository import org.thoughtcrime.securesms.pin.SvrWrongPinException import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationUtil @@ -51,6 +56,7 @@ import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeR 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.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.dualsim.MccMncProducer @@ -58,6 +64,7 @@ import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import java.io.IOException +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration.Companion.minutes /** @@ -84,6 +91,8 @@ class RegistrationV2ViewModel : ViewModel() { val lockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData() + val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData() + val svrTriesRemaining: Int get() = store.value.svrTriesRemaining @@ -104,6 +113,22 @@ class RegistrationV2ViewModel : ViewModel() { } } + fun maybePrefillE164(context: Context) { + Log.v(TAG, "maybePrefillE164()") + if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { + val localNumber = Util.getDeviceNumber(context).getOrNull() + + if (localNumber != null) { + Log.v(TAG, "Phone number detected.") + setPhoneNumber(localNumber) + } else { + Log.i(TAG, "Could not read phone number.") + } + } else { + Log.i(TAG, "No phone permission.") + } + } + fun setInProgress(inProgress: Boolean) { store.update { it.copy(inProgress = inProgress) @@ -131,6 +156,12 @@ class RegistrationV2ViewModel : ViewModel() { } } + fun incrementIncorrectCodeAttempts() { + store.update { + it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1) + } + } + fun addPresentedChallenge(challenge: Challenge) { store.update { it.copy(challengesPresented = it.challengesPresented.plus(challenge)) @@ -165,60 +196,73 @@ class RegistrationV2ViewModel : ViewModel() { fun onBackupSuccessfullyRestored() { val recoveryPassword = SignalStore.svr().recoveryPassword store.update { - it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED_OR_SKIPPED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null) + it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED_OR_SKIPPED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null, isReRegister = true) } } fun onUserConfirmedPhoneNumber(context: Context, errorHandler: (RegistrationResult) -> Unit) { setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED) val state = store.value - if (state.phoneNumber == null) { - Log.w(TAG, "Phone number was null after confirmation.") - onErrorOccurred() - return - } - val e164 = state.phoneNumber.toE164() - if (hasRecoveryPassword() && matchesSavedE164(e164)) { - // Re-registration when the local database is intact. - Log.d(TAG, "Has recovery password, and therefore can skip SMS verification.") - store.update { - it.copy( - canSkipSms = true, - inProgress = false - ) + val e164 = state.phoneNumber?.toE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") } + + if (!state.userSkippedReregistration) { + if (hasRecoveryPassword() && matchesSavedE164(e164)) { + // Re-registration when the local database is intact. + Log.d(TAG, "Has recovery password, and therefore can skip SMS verification.") + store.update { + it.copy( + canSkipSms = true, + isReRegister = true, + inProgress = false + ) + } + return } - return } viewModelScope.launch { - val svrCredentialsResult: BackupAuthCheckResult = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password) + if (!state.userSkippedReregistration) { + val svrCredentialsResult: BackupAuthCheckResult = 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 = svrCredentialsResult.authCredentials, inProgress = false) + when (svrCredentialsResult) { + is BackupAuthCheckResult.UnknownError -> { + handleGenericError(svrCredentialsResult.getCause()) + return@launch } - return@launch - } - is BackupAuthCheckResult.SuccessWithoutCredentials -> { - Log.d(TAG, "No local SVR auth credentials could be found and/or validated.") + is BackupAuthCheckResult.SuccessWithCredentials -> { + Log.d(TAG, "Found local valid SVR auth credentials.") + store.update { + it.copy(isReRegister = true, canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials, inProgress = false) + } + return@launch + } + + is BackupAuthCheckResult.SuccessWithoutCredentials -> { + Log.d(TAG, "No local SVR auth credentials could be found and/or validated.") + } } } - val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch + val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") } + + if (validSession.body.verified) { + Log.i(TAG, "Session is already verified, registering account.") + registerVerifiedSession(context, validSession.body.id, errorHandler) + return@launch + } if (!validSession.body.allowedToRequestCode) { - val challenges = validSession.body.requestedInformation.joinToString() - Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") - handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)), errorHandler) + if (System.currentTimeMillis() > (validSession.body.nextVerificationAttempt ?: Int.MAX_VALUE)) { + store.update { + it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) + } + } else { + val challenges = validSession.body.requestedInformation + Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}") + handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)), errorHandler) + } return@launch } @@ -227,16 +271,10 @@ class RegistrationV2ViewModel : ViewModel() { } fun requestSmsCode(context: Context, errorHandler: (RegistrationResult) -> Unit) { - val e164 = getCurrentE164() - - if (e164 == null) { - Log.w(TAG, "Phone number was null after confirmation.") - onErrorOccurred() - return - } + val e164 = getCurrentE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") } viewModelScope.launch { - val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch + val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") } requestSmsCodeInternal(context, validSession.body.id, e164, errorHandler) } } @@ -251,7 +289,7 @@ class RegistrationV2ViewModel : ViewModel() { } viewModelScope.launch { - val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch + val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting a verification call.") } Log.d(TAG, "Requesting voice call code…") val codeRequestResponse = RegistrationRepository.requestSmsCode( context = context, @@ -260,9 +298,12 @@ class RegistrationV2ViewModel : ViewModel() { password = password, mode = RegistrationRepository.Mode.PHONE_CALL ) - Log.d(TAG, "Voice call code request submitted.") + Log.d(TAG, "Voice code request network call completed.") handleSessionStateResult(context, codeRequestResponse, errorHandler) + if (codeRequestResponse is Success) { + Log.d(TAG, "Voice code request was successful.") + } } } @@ -305,6 +346,7 @@ class RegistrationV2ViewModel : ViewModel() { } private suspend fun getOrCreateValidSession(context: Context, errorHandler: (RegistrationResult) -> Unit): RegistrationSessionMetadataResponse? { + Log.v(TAG, "getOrCreateValidSession()") val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!") val mccMncProducer = MccMncProducer(context) @@ -316,12 +358,17 @@ class RegistrationV2ViewModel : ViewModel() { password = password, mcc = mccMncProducer.mcc, mnc = mccMncProducer.mnc, - successListener = { freshSession -> - val freshSessionId = freshSession.body.id - if (freshSessionId != existingSessionId) { - store.update { - it.copy(sessionId = freshSessionId) - } + successListener = { networkResult -> + store.update { + it.copy( + sessionId = networkResult.body.id, + nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextSms), + nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextCall), + nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextVerificationAttempt), + allowedToRequestCode = networkResult.body.allowedToRequestCode, + challengesRequested = Challenge.parse(networkResult.body.requestedInformation), + verified = networkResult.body.verified + ) } }, errorHandler = errorHandler @@ -333,7 +380,7 @@ class RegistrationV2ViewModel : ViewModel() { val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!") viewModelScope.launch { - val session = getOrCreateValidSession(context, errorHandler) ?: return@launch + val session = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") } Log.d(TAG, "Submitting captcha token…") val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) Log.d(TAG, "Captcha token submitted.") @@ -353,7 +400,7 @@ class RegistrationV2ViewModel : ViewModel() { viewModelScope.launch { Log.d(TAG, "Getting session in order to perform push token verification…") - val session = getOrCreateValidSession(context, errorHandler) ?: return@launch + val session = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") } if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) { Log.d(TAG, "Push submission no longer necessary, bailing.") @@ -362,7 +409,7 @@ class RegistrationV2ViewModel : ViewModel() { inProgress = false ) } - return@launch + return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") } } Log.d(TAG, "Requesting push challenge token…") @@ -376,6 +423,7 @@ class RegistrationV2ViewModel : ViewModel() { * @return whether the request was successful and execution should continue */ private suspend fun handleSessionStateResult(context: Context, sessionResult: RegistrationResult, errorHandler: (RegistrationResult) -> Unit): Boolean { + Log.v(TAG, "handleSessionStateResult()") when (sessionResult) { is UnknownError -> { handleGenericError(sessionResult.getCause()) @@ -564,8 +612,6 @@ class RegistrationV2ViewModel : ViewModel() { private suspend fun registerAccountInternal(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String?, masterKey: MasterKey): Pair { val registrationResult: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey } - // TODO: check for wrong recovery password - // Check if reg lock is enabled if (registrationResult !is RegisterAccountResult.RegistrationLocked) { return Pair(registrationResult, false) @@ -593,7 +639,7 @@ class RegistrationV2ViewModel : ViewModel() { verifyCodeInternal( context = context, pin = null, - reglockEnabled = false, + registrationLocked = false, submissionErrorHandler = submissionErrorHandler, registrationErrorHandler = registrationErrorHandler ) @@ -612,24 +658,82 @@ class RegistrationV2ViewModel : ViewModel() { verifyCodeInternal( context = context, pin = pin, - reglockEnabled = true, + registrationLocked = true, submissionErrorHandler = submissionErrorHandler, registrationErrorHandler = registrationErrorHandler ) } } - private suspend fun verifyCodeInternal(context: Context, reglockEnabled: Boolean, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) { + private suspend fun verifyCodeInternal(context: Context, registrationLocked: Boolean, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) { + Log.d(TAG, "Getting valid session in order to submit verification code.") + + if (registrationLocked && pin.isNullOrBlank()) { + throw IllegalStateException("Must have PIN to register with registration lock!") + } + + var reglock = registrationLocked + val sessionId = getOrCreateValidSession(context, submissionErrorHandler)?.body?.id ?: return val registrationData = getRegistrationData() - val registrationResponse = verifyCode(context, sessionId, registrationData, pin) { - viewModelScope.launch { // TODO: validate the scopes are correct here - handleSessionStateResult(context, it, submissionErrorHandler) - } - } ?: return + Log.d(TAG, "Submitting verification code…") - handleRegistrationResult(context, registrationData, registrationResponse, reglockEnabled, registrationErrorHandler) + val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) + + val submissionSuccessful = verificationResponse is Success + val alreadyVerified = verificationResponse is AlreadyVerified + + Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified") + + if (!submissionSuccessful && !alreadyVerified) { + handleSessionStateResult(context, verificationResponse, submissionErrorHandler) + return + } + + Log.d(TAG, "Submitting registration…") + + var result: RegisterAccountResult? = null + var state = store.value + + if (!reglock) { + Log.d(TAG, "Registration lock not enabled, attempting to register account without master key producer.") + result = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin) + } + + if (result is RegisterAccountResult.RegistrationLocked) { + Log.d(TAG, "Registration lock response received.") + reglock = true + if (pin == null && SignalStore.svr().registrationLockToken != null) { + Log.d(TAG, "Retrying registration with stored credentials.") + result = RegistrationRepository.registerAccount(context, sessionId, registrationData, SignalStore.svr().pin) { SignalStore.svr().getOrCreateMasterKey() } + } else if (result.svr2Credentials != null) { + Log.d(TAG, "Retrying registration with received credentials.") + val credentials = result.svr2Credentials + state = store.updateAndGet { + it.copy(svrAuthCredentials = credentials) + } + } + } + + if (reglock && pin.isNotNullOrBlank()) { + Log.d(TAG, "Registration lock enabled, attempting to register account restore master key from SVR.") + result = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin) { + SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, state.svrAuthCredentials), pin) + } + } + + if (result != null) { + handleRegistrationResult(context, registrationData, result, reglock, registrationErrorHandler) + } else { + Log.w(TAG, "No registration response received!") + } + } + + private suspend fun registerVerifiedSession(context: Context, sessionId: String, registrationErrorHandler: (RegisterAccountResult) -> Unit) { + val registrationData = getRegistrationData() + val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context, sessionId, registrationData) + handleRegistrationResult(context, registrationData, registrationResponse, false, registrationErrorHandler) } private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) { @@ -679,7 +783,7 @@ class RegistrationV2ViewModel : ViewModel() { private suspend fun getRegistrationData(): RegistrationData { val currentState = store.value - val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!") + val code = currentState.enteredCode val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException("Can't construct registration data without E164!") val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr().getRecoveryPassword() else null return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), currentState.fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword) @@ -693,6 +797,16 @@ class RegistrationV2ViewModel : ViewModel() { setInProgress(false) } + /** + * Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened. + * + * @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages. + */ + private fun bail(logMessage: () -> Unit) { + logMessage() + setInProgress(false) + } + companion object { private val TAG = Log.tag(RegistrationV2ViewModel::class.java) @@ -737,29 +851,5 @@ class RegistrationV2ViewModel : ViewModel() { } return null } - - suspend fun verifyCode(context: Context, sessionId: String, registrationData: RegistrationData, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit): RegisterAccountResult? { - Log.d(TAG, "Getting valid session in order to submit verification code.") - - Log.d(TAG, "Submitting verification code…") - - val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) - - val submissionSuccessful = verificationResponse is Success - val alreadyVerified = verificationResponse is AlreadyVerified - - Log.i(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified") - - if (!submissionSuccessful && !alreadyVerified) { - submissionErrorHandler(verificationResponse) - return null - } - - Log.d(TAG, "Submitting registration…") - - val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin) - Log.d(TAG, "Registration network call completed.") - return registrationResponse - } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt index ff02a9f5cb..2587023ea1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt @@ -8,16 +8,20 @@ package org.thoughtcrime.securesms.registration.v2.ui.entercode import android.content.DialogInterface import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding +import org.thoughtcrime.securesms.registration.ReceivedSmsEvent import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener @@ -28,6 +32,7 @@ import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.thoughtcrime.securesms.util.visible /** * The final screen of account registration, where the user enters their verification code. @@ -101,6 +106,12 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter } } + sharedViewModel.incorrectCodeAttempts.observe(viewLifecycleOwner) { attempts: Int -> + if (attempts >= 3) { + binding.havingTroubleButton.visible = true + } + } + sharedViewModel.uiState.observe(viewLifecycleOwner) { binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp) binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp) @@ -116,6 +127,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter when (result) { is VerificationCodeRequestResult.Success -> binding.keyboard.displaySuccess() is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog() + is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked() is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) else -> presentGenericError(result) } @@ -125,12 +137,24 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter when (result) { is RegisterAccountResult.Success -> binding.keyboard.displaySuccess() is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) - is RegisterAccountResult.AttemptsExhausted, + is RegisterAccountResult.AuthorizationFailed -> presentIncorrectCodeDialog() + is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked() is RegisterAccountResult.RateLimited -> presentRateLimitedDialog() + else -> presentGenericError(result) } } + private fun presentAccountLocked() { + binding.keyboard.displayLocked().addListener( + object : AssertedSuccessListener() { + override fun onSuccess(result: Boolean?) { + findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionAccountLocked()) + } + } + ) + } + private fun presentRegistrationLocked(timeRemaining: Long) { binding.keyboard.displayLocked().addListener( object : AssertedSuccessListener() { @@ -162,6 +186,21 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter ) } + private fun presentIncorrectCodeDialog() { + sharedViewModel.incrementIncorrectCodeAttempts() + + Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show() + binding.keyboard.displayFailure().addListener(object : AssertedSuccessListener() { + override fun onSuccess(result: Boolean?) { + binding.callMeCountDown.setVisibility(View.VISIBLE) + binding.resendSmsCountDown.setVisibility(View.VISIBLE) + binding.wrongNumber.setVisibility(View.VISIBLE) + binding.code.clear() + binding.keyboard.displayKeyboard() + } + }) + } + private fun presentGenericError(requestResult: RegistrationResult) { binding.keyboard.displayFailure().addListener( object : AssertedSuccessListener() { @@ -172,7 +211,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter setTitle(it) } setMessage(getString(R.string.RegistrationActivity_error_connecting_to_service)) - setPositiveButton(android.R.string.ok, null) + setPositiveButton(android.R.string.ok) { _, _ -> binding.keyboard.displayKeyboard() } show() } } @@ -185,6 +224,34 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter NavHostFragment.findNavController(this).popBackStack() } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onVerificationCodeReceived(event: ReceivedSmsEvent) { + binding.code.clear() + + if (event.code.isBlank() || event.code.length != ReceivedSmsEvent.CODE_LENGTH) { + Log.i(TAG, "Received invalid code of length ${event.code.length}. Ignoring.") + return + } + + val finalIndex = ReceivedSmsEvent.CODE_LENGTH - 1 + autopilotCodeEntryActive = true + try { + event.code + .map { it.digitToInt() } + .forEachIndexed { i, digit -> + binding.code.postDelayed({ + binding.code.append(digit) + if (i == finalIndex) { + autopilotCodeEntryActive = false + } + }, i * 200L) + } + } catch (notADigit: IllegalArgumentException) { + Log.w(TAG, "Failed to convert code into digits.", notADigit) + autopilotCodeEntryActive = false + } + } + private inner class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback { override fun onNoCellSignalPresent() { bottomSheet.show(childFragmentManager, BOTTOM_SHEET_TAG) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt index 114e5ca311..2cec49dbf0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt @@ -94,6 +94,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() { permissions.forEach { Log.d(TAG, "${it.key} = ${it.value}") } + sharedViewModel.maybePrefillE164(requireContext()) sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED) proceedToNextScreen() } 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 94b7552a4c..ad7e2b48ad 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 @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.registration.v2.ui.phonenumber import android.content.Context import android.content.DialogInterface import android.os.Bundle +import android.text.Editable import android.text.SpannableStringBuilder import android.text.TextWatcher import android.view.KeyEvent @@ -202,20 +203,22 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio spinnerView.threshold = 100 spinnerView.setAdapter(spinnerAdapter) - spinnerView.addTextChangedListener { s -> - if (s.isNullOrEmpty()) { - return@addTextChangedListener - } + spinnerView.addTextChangedListener(afterTextChanged = ::onCountryDropDownChanged) + } - if (s[0] != '+') { - s.insert(0, "+") - } + private fun onCountryDropDownChanged(s: Editable?) { + if (s.isNullOrEmpty()) { + return + } - fragmentViewModel.supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let { - fragmentViewModel.setCountry(it.digits) - val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0 - phoneNumberInputLayout.setSelection(numberLength, numberLength) - } + if (s[0] != '+') { + s.insert(0, "+") + } + + fragmentViewModel.supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let { + fragmentViewModel.setCountry(it.digits) + val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0 + phoneNumberInputLayout.setSelection(numberLength, numberLength) } } @@ -380,9 +383,12 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedViewModel.fetchFcmToken(requireContext()) } else { sharedViewModel.uiState.value?.let { value -> + val now = System.currentTimeMillis() if (value.phoneNumber == null) { fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER) sharedViewModel.setInProgress(false) + } else if (now < value.nextSmsTimestamp) { + moveToVerificationEntryScreen() } else { presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = true) } @@ -441,7 +447,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } } - private fun onConfirmNumberDialogCanceled() { + private fun handleConfirmNumberDialogCanceled() { Log.d(TAG, "User canceled confirm number, returning to edit number.") sharedViewModel.setInProgress(false) ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(phoneNumberInputLayout) @@ -473,8 +479,8 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedViewModel.onUserConfirmedPhoneNumber(requireContext(), ::handleErrorResponse) } } - setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onConfirmNumberDialogCanceled() } - setOnCancelListener { _ -> onConfirmNumberDialogCanceled() } + setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> handleConfirmNumberDialogCanceled() } + setOnCancelListener { _ -> handleConfirmNumberDialogCanceled() } }.show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt index 3cc7b03bd2..9a098f5e40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt @@ -11,12 +11,8 @@ import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository /** * State holder for the phone number entry screen, including phone number and Play Services errors. */ -data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 1, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) { +data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) { enum class Error { - NONE, - INVALID_PHONE_NUMBER, - PLAY_SERVICES_MISSING, - PLAY_SERVICES_NEEDS_UPDATE, - PLAY_SERVICES_TRANSIENT + NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt index e097c59700..b5a519eb6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt @@ -49,6 +49,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio companion object { private val TAG = Log.tag(RegistrationLockV2Fragment::class.java) } + private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind) private val viewModel by activityViewModels() @@ -145,17 +146,25 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio when (requestResult) { is VerificationCodeRequestResult.Success -> Unit is VerificationCodeRequestResult.RateLimited -> onRateLimited() - is VerificationCodeRequestResult.AttemptsExhausted, - is VerificationCodeRequestResult.RegistrationLocked -> onKbsAccountLocked() + is VerificationCodeRequestResult.AttemptsExhausted -> navigateToAccountLocked() + is VerificationCodeRequestResult.RegistrationLocked -> { + Log.i(TAG, "Registration locked response to verify account!") + binding.kbsLockPinConfirm.cancelSpinning() + enableAndFocusPinEntry() + Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show() + } + else -> when (val cause = requestResult.getCause()) { is SvrWrongPinException -> { Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") onIncorrectKbsRegistrationLockPin(cause.triesRemaining) } + is SvrNoDataException -> { Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") - onKbsAccountLocked() + navigateToAccountLocked() } + else -> { Log.w(TAG, "Unable to verify code with registration lock", cause) onError() @@ -168,17 +177,25 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio when (result) { is RegisterAccountResult.Success -> Unit is RegisterAccountResult.RateLimited -> onRateLimited() - is RegisterAccountResult.AttemptsExhausted, - is RegisterAccountResult.RegistrationLocked -> onKbsAccountLocked() + is RegisterAccountResult.AttemptsExhausted -> navigateToAccountLocked() + is RegisterAccountResult.RegistrationLocked -> { + Log.i(TAG, "Registration locked response to register account!") + binding.kbsLockPinConfirm.cancelSpinning() + enableAndFocusPinEntry() + Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show() + } + else -> when (val cause = result.getCause()) { is SvrWrongPinException -> { Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") onIncorrectKbsRegistrationLockPin(cause.triesRemaining) } + is SvrNoDataException -> { Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.") - onKbsAccountLocked() + navigateToAccountLocked() } + else -> { Log.w(TAG, "Unable to register account with registration lock", cause) onError() @@ -194,7 +211,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio if (svrTriesRemaining == 0) { Log.w(TAG, "Account locked. User out of attempts on KBS.") - onAccountLocked() + navigateToAccountLocked() return } @@ -227,10 +244,6 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio .show() } - private fun onKbsAccountLocked() { - onAccountLocked() - } - fun onError() { binding.kbsLockPinConfirm.cancelSpinning() enableAndFocusPinEntry() @@ -252,10 +265,6 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1 } - private fun onAccountLocked() { - navigateToAccountLocked() - } - private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String { val resources = requireContext().resources val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining) @@ -316,7 +325,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio stopwatch.stop(TAG) null - }, { none: Any? -> + }, { binding.kbsLockPinConfirm.cancelSpinning() findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionSuccessfulRegistration()) }) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt index d2a7d36ba1..da93a068b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt @@ -5,7 +5,6 @@ package org.thoughtcrime.securesms.registration.v2.ui.welcome -import android.Manifest import android.app.Activity import android.content.pm.PackageManager import android.os.Bundle @@ -30,9 +29,7 @@ import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.navigation.safeNavigate -import kotlin.jvm.optionals.getOrNull /** * First screen that is displayed on the very first app launch. @@ -57,7 +54,6 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - maybePrefillE164() setDebugLogSubmitMultiTapView(binding.image) setDebugLogSubmitMultiTapView(binding.title) binding.welcomeContinueButton.setOnClickListener { onContinueClicked() } @@ -70,7 +66,8 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) { findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE)) } else { - skipRestore() + sharedViewModel.maybePrefillE164(requireContext()) + findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore()) } } @@ -79,10 +76,6 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome return WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED } } - private fun skipRestore() { - findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore()) - } - private fun onTermsClicked() { CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL) } @@ -98,21 +91,6 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome } } - private fun maybePrefillE164() { - if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { - val localNumber = Util.getDeviceNumber(requireContext()).getOrNull() - - if (localNumber != null) { - Log.v(TAG, "Phone number detected.") - sharedViewModel.setPhoneNumber(localNumber) - } else { - Log.i(TAG, "Could not read phone number.") - } - } else { - Log.i(TAG, "No phone permission.") - } - } - companion object { private val TAG = Log.tag(WelcomeV2Fragment::class.java) private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal" diff --git a/app/src/main/res/navigation/registration_v2.xml b/app/src/main/res/navigation/registration_v2.xml index df81ab4bde..7e561f46c9 100644 --- a/app/src/main/res/navigation/registration_v2.xml +++ b/app/src/main/res/navigation/registration_v2.xml @@ -46,7 +46,6 @@ android:id="@+id/action_enter_phone_number" app:destination="@id/enterPhoneNumberV2Fragment" app:popUpTo="@+id/welcomeV2Fragment" - app:popUpToInclusive="true" app:enterAnim="@anim/nav_default_enter_anim" app:exitAnim="@anim/nav_default_exit_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim"