Fix bugs around requesting and entering verification codes.

This commit is contained in:
Cody Henthorne 2025-01-23 11:10:14 -05:00 committed by GitHub
parent 9823563ef7
commit 04c903c4c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 381 additions and 307 deletions

View file

@ -4,7 +4,9 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import java.util.concurrent.TimeUnit import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class ActionCountDownButton @JvmOverloads constructor( class ActionCountDownButton @JvmOverloads constructor(
context: Context, context: Context,
@ -17,7 +19,7 @@ class ActionCountDownButton @JvmOverloads constructor(
@StringRes @StringRes
private var disabledText = 0 private var disabledText = 0
private var countDownToTime: Long = 0 private var countDownToTime: Duration = 0.seconds
private var listener: Listener? = null private var listener: Listener? = null
private var updateRunnable = Runnable { private var updateRunnable = Runnable {
@ -27,8 +29,8 @@ class ActionCountDownButton @JvmOverloads constructor(
/** /**
* Starts a count down to the specified {@param time}. * Starts a count down to the specified {@param time}.
*/ */
fun startCountDownTo(time: Long) { fun startCountDownTo(time: Duration) {
if (time > 0) { if (time > 0.seconds) {
countDownToTime = time countDownToTime = time
removeCallbacks(updateRunnable) removeCallbacks(updateRunnable)
updateCountDown() updateCountDown()
@ -46,11 +48,11 @@ class ActionCountDownButton @JvmOverloads constructor(
} }
private fun updateCountDown() { private fun updateCountDown() {
val remainingMillis = countDownToTime - System.currentTimeMillis() val remaining = countDownToTime - System.currentTimeMillis().milliseconds
if (remainingMillis > 1000) { if (remaining > 1.seconds) {
isEnabled = false isEnabled = false
alpha = 0.5f alpha = 0.5f
val totalRemainingSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingMillis).toInt() val totalRemainingSeconds = remaining.inWholeSeconds.toInt()
val minutesRemaining = totalRemainingSeconds / 60 val minutesRemaining = totalRemainingSeconds / 60
val secondsRemaining = totalRemainingSeconds % 60 val secondsRemaining = totalRemainingSeconds % 60

View file

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.registration.sms.ReceivedSmsEvent
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible import org.thoughtcrime.securesms.util.visible
import kotlin.time.Duration.Companion.milliseconds
/** /**
* Screen used to enter the registration code provided by the service. * Screen used to enter the registration code provided by the service.
@ -132,8 +133,8 @@ class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_n
} }
private fun onStateUpdate(state: ChangeNumberState) { private fun onStateUpdate(state: ChangeNumberState) {
binding.codeEntryLayout.resendSmsCountDown.startCountDownTo(state.nextSmsTimestamp) binding.codeEntryLayout.resendSmsCountDown.startCountDownTo(state.nextSmsTimestamp.milliseconds)
binding.codeEntryLayout.callMeCountDown.startCountDownTo(state.nextCallTimestamp) binding.codeEntryLayout.callMeCountDown.startCountDownTo(state.nextCallTimestamp.milliseconds)
when (val outcome = state.changeNumberOutcome) { when (val outcome = state.changeNumberOutcome) {
is ChangeNumberOutcome.RecoveryPasswordWorked, is ChangeNumberOutcome.RecoveryPasswordWorked,
is ChangeNumberOutcome.VerificationCodeWorked -> changeNumberSuccess() is ChangeNumberOutcome.VerificationCodeWorked -> changeNumberSuccess()

View file

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
@ -33,7 +34,6 @@ import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException import java.io.IOException
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@ -240,7 +240,7 @@ class ChangeNumberViewModel : ViewModel() {
} }
private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
val sessionId = getOrCreateValidSession(context)?.metadata?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } val sessionId = getOrCreateValidSession(context)?.sessionId ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") }
val registrationData = getRegistrationData(context) val registrationData = getRegistrationData(context)
val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
@ -285,8 +285,8 @@ class ChangeNumberViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
Log.d(TAG, "Getting session in order to submit captcha token…") Log.d(TAG, "Getting session in order to submit captcha token…")
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") } val sessionData = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") }
if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.CAPTCHA)) { if (!sessionData.challengesRequested.contains(Challenge.CAPTCHA)) {
Log.d(TAG, "Captcha submission no longer necessary, bailing.") Log.d(TAG, "Captcha submission no longer necessary, bailing.")
store.update { store.update {
it.copy( it.copy(
@ -297,7 +297,7 @@ class ChangeNumberViewModel : ViewModel() {
return@launch return@launch
} }
Log.d(TAG, "Submitting captcha token…") Log.d(TAG, "Submitting captcha token…")
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken) val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, sessionData.sessionId, captchaToken)
Log.d(TAG, "Captcha token submitted.") Log.d(TAG, "Captcha token submitted.")
store.update { store.update {
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult)) it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult))
@ -314,9 +314,9 @@ class ChangeNumberViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
Log.d(TAG, "Getting session in order to perform push token verification…") Log.d(TAG, "Getting session in order to perform push token verification…")
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") } val sessionData = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") }
if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) { if (!sessionData.challengesRequested.contains(Challenge.PUSH)) {
Log.d(TAG, "Push submission no longer necessary, bailing.") Log.d(TAG, "Push submission no longer necessary, bailing.")
store.update { store.update {
it.copy( it.copy(
@ -328,7 +328,7 @@ class ChangeNumberViewModel : ViewModel() {
} }
Log.d(TAG, "Requesting push challenge token…") Log.d(TAG, "Requesting push challenge token…")
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password) val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, sessionData.sessionId, e164, password)
Log.d(TAG, "Push challenge token submitted.") Log.d(TAG, "Push challenge token submitted.")
store.update { store.update {
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult)) it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult))
@ -361,14 +361,18 @@ class ChangeNumberViewModel : ViewModel() {
// region Private actions // region Private actions
private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) { private fun updateLocalStateFromSession(sessionData: SessionMetadataResult) {
Log.v(TAG, "updateLocalStateFromSession()") Log.v(TAG, "updateLocalStateFromSession()")
store.update { store.update {
it.copy(sessionId = response.metadata.id, challengesRequested = Challenge.parse(response.metadata.requestedInformation), allowedToRequestCode = response.metadata.allowedToRequestCode) it.copy(
sessionId = sessionData.sessionId,
challengesRequested = sessionData.challengesRequested,
allowedToRequestCode = sessionData.allowedToRequestCode
)
} }
} }
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { private suspend fun getOrCreateValidSession(context: Context): SessionMetadataResult? {
Log.v(TAG, "getOrCreateValidSession()") Log.v(TAG, "getOrCreateValidSession()")
val e164 = number.e164Number val e164 = number.e164Number
val mccMncProducer = MccMncProducer(context) val mccMncProducer = MccMncProducer(context)
@ -477,15 +481,15 @@ class ChangeNumberViewModel : ViewModel() {
return return
} }
val result = if (!validSession.metadata.allowedToRequestCode) { val result = if (!validSession.allowedToRequestCode) {
val challenges = validSession.metadata.requestedInformation.joinToString() val challenges = validSession.challengesRequested.joinToString()
Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)) VerificationCodeRequestResult.ChallengeRequired(validSession.challengesRequested)
} else { } else {
store.update { store.update {
it.copy(changeNumberOutcome = null, challengesRequested = emptyList()) it.copy(changeNumberOutcome = null, challengesRequested = emptyList())
} }
val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.metadata.id, e164 = e164, password = password, mode = mode) val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.sessionId, e164 = e164, password = password, mode = mode)
Log.d(TAG, "SMS code request submitted") Log.d(TAG, "SMS code request submitted")
response response
} }

View file

@ -74,7 +74,6 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.io.IOException import java.io.IOException
@ -307,7 +306,7 @@ object RegistrationRepository {
val result = RegistrationSessionCreationResult.from(registrationSessionResult) val result = RegistrationSessionCreationResult.from(registrationSessionResult)
if (result is RegistrationSessionCreationResult.Success) { if (result is RegistrationSessionCreationResult.Success) {
Log.d(TAG, "Updating registration session and E164 in value store.") Log.d(TAG, "Updating registration session and E164 in value store.")
SignalStore.registration.sessionId = result.getMetadata().metadata.id SignalStore.registration.sessionId = result.sessionId
SignalStore.registration.sessionE164 = e164 SignalStore.registration.sessionE164 = e164
} }
@ -488,16 +487,6 @@ object RegistrationRepository {
} }
} }
@JvmStatic
fun deriveTimestamp(headers: RegistrationSessionMetadataHeaders, deltaSeconds: Int?): Long {
if (deltaSeconds == null) {
return 0L
}
val timestamp: Long = headers.serverDeliveredTimestamp
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
}
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult = suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi

View file

@ -5,7 +5,6 @@
package org.thoughtcrime.securesms.registration.data.network package org.thoughtcrime.securesms.registration.data.network
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
@ -13,23 +12,37 @@ import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionExcepti
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException import org.whispersystems.signalservice.api.push.exceptions.NotFoundException
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
sealed class RegistrationSessionResult(cause: Throwable?) : RegistrationResult(cause) sealed class RegistrationSessionResult(cause: Throwable?) : RegistrationResult(cause)
interface SessionMetadataHolder { interface SessionMetadataResult {
fun getMetadata(): RegistrationSessionMetadataResponse val sessionId: String
val nextSmsTimestamp: Duration
val nextCallTimestamp: Duration
val nextVerificationAttempt: Duration
val allowedToRequestCode: Boolean
val challengesRequested: List<Challenge>
val verified: Boolean
} }
sealed class RegistrationSessionCreationResult(cause: Throwable?) : RegistrationSessionResult(cause) { sealed class RegistrationSessionCreationResult(cause: Throwable?) : RegistrationSessionResult(cause) {
companion object { companion object {
private val TAG = Log.tag(RegistrationSessionResult::class.java)
@JvmStatic @JvmStatic
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCreationResult { fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCreationResult {
return when (networkResult) { return when (networkResult) {
is NetworkResult.Success -> { is NetworkResult.Success -> {
Success(networkResult.result) Success(
sessionId = networkResult.result.metadata.id,
nextSmsTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextSms?.seconds),
nextCallTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextCall?.seconds),
nextVerificationAttempt = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextVerificationAttempt?.seconds),
allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation),
verified = networkResult.result.metadata.verified
)
} }
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
@ -49,11 +62,15 @@ sealed class RegistrationSessionCreationResult(cause: Throwable?) : Registration
} }
} }
class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCreationResult(null), SessionMetadataHolder { class Success(
override fun getMetadata(): RegistrationSessionMetadataResponse { override val sessionId: String,
return metadata override val nextSmsTimestamp: Duration,
} override val nextCallTimestamp: Duration,
} override val nextVerificationAttempt: Duration,
override val allowedToRequestCode: Boolean,
override val challengesRequested: List<Challenge>,
override val verified: Boolean
) : RegistrationSessionCreationResult(null), SessionMetadataResult
class RateLimited(cause: Throwable, val timeRemaining: Long?) : RegistrationSessionCreationResult(cause) class RateLimited(cause: Throwable, val timeRemaining: Long?) : RegistrationSessionCreationResult(cause)
class AttemptsExhausted(cause: Throwable) : RegistrationSessionCreationResult(cause) class AttemptsExhausted(cause: Throwable) : RegistrationSessionCreationResult(cause)
@ -67,7 +84,15 @@ sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSes
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCheckResult { fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCheckResult {
return when (networkResult) { return when (networkResult) {
is NetworkResult.Success -> { is NetworkResult.Success -> {
Success(networkResult.result) Success(
sessionId = networkResult.result.metadata.id,
nextSmsTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextSms?.seconds),
nextCallTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextCall?.seconds),
nextVerificationAttempt = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextVerificationAttempt?.seconds),
allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation),
verified = networkResult.result.metadata.verified
)
} }
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
@ -82,11 +107,15 @@ sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSes
} }
} }
class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCheckResult(null), SessionMetadataHolder { class Success(
override fun getMetadata(): RegistrationSessionMetadataResponse { override val sessionId: String,
return metadata override val nextSmsTimestamp: Duration,
} override val nextCallTimestamp: Duration,
} override val nextVerificationAttempt: Duration,
override val allowedToRequestCode: Boolean,
override val challengesRequested: List<Challenge>,
override val verified: Boolean
) : RegistrationSessionCheckResult(null), SessionMetadataResult
class SessionNotFound(cause: Throwable) : RegistrationSessionCheckResult(cause) class SessionNotFound(cause: Throwable) : RegistrationSessionCheckResult(cause)
class UnknownError(cause: Throwable) : RegistrationSessionCheckResult(cause) class UnknownError(cause: Throwable) : RegistrationSessionCheckResult(cause)

View file

@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.registration.data.network
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException
import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException
@ -27,6 +26,7 @@ import org.whispersystems.signalservice.internal.push.LockedException
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/** /**
* This is a processor to map a [RegistrationSessionMetadataResponse] to all the known outcomes. * This is a processor to map a [RegistrationSessionMetadataResponse] to all the known outcomes.
@ -47,9 +47,9 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
} else { } else {
Success( Success(
sessionId = networkResult.result.metadata.id, sessionId = networkResult.result.metadata.id,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextSms), nextSmsTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextSms?.seconds),
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextCall), nextCallTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextCall?.seconds),
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextVerificationAttempt), nextVerificationAttempt = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextVerificationAttempt?.seconds),
allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode, allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation), challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation),
verified = networkResult.result.metadata.verified verified = networkResult.result.metadata.verified
@ -75,8 +75,8 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
is RequestVerificationCodeRateLimitException -> { is RequestVerificationCodeRateLimitException -> {
RequestVerificationCodeRateLimited( RequestVerificationCodeRateLimited(
cause = cause, cause = cause,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextSms), nextSmsTimestamp = cause.sessionMetadata.deriveTimestamp(delta = cause.sessionMetadata.metadata.nextSms?.seconds),
nextCallTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextCall) nextCallTimestamp = cause.sessionMetadata.deriveTimestamp(delta = cause.sessionMetadata.metadata.nextCall?.seconds)
) )
} }
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials) is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials)
@ -93,7 +93,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
} }
} }
class Success(val sessionId: String, val nextSmsTimestamp: Long, val nextCallTimestamp: Long, nextVerificationAttempt: Long, val allowedToRequestCode: Boolean, challengesRequested: List<Challenge>, val verified: Boolean) : VerificationCodeRequestResult(null) class Success(val sessionId: String, val nextSmsTimestamp: Duration, val nextCallTimestamp: Duration, nextVerificationAttempt: Duration, val allowedToRequestCode: Boolean, challengesRequested: List<Challenge>, val verified: Boolean) : VerificationCodeRequestResult(null)
class ChallengeRequired(val challenges: List<Challenge>) : VerificationCodeRequestResult(null) class ChallengeRequired(val challenges: List<Challenge>) : VerificationCodeRequestResult(null)
@ -111,17 +111,17 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause) class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause)
class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Long, val nextCallTimestamp: Long) : VerificationCodeRequestResult(cause) { class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Duration, val nextCallTimestamp: Duration) : VerificationCodeRequestResult(cause) {
val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0 || nextCallTimestamp > 0 val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0.seconds || nextCallTimestamp > 0.seconds
fun log(now: Duration = System.currentTimeMillis().milliseconds): String { fun log(now: Duration = System.currentTimeMillis().milliseconds): String {
val sms = if (nextSmsTimestamp > 0) { val sms = if (nextSmsTimestamp > 0.seconds) {
"${(nextSmsTimestamp.milliseconds - now).inWholeSeconds}s" "${(nextSmsTimestamp - now).inWholeSeconds}s"
} else { } else {
"Never" "Never"
} }
val call = if (nextCallTimestamp > 0) { val call = if (nextCallTimestamp > 0.seconds) {
"${(nextCallTimestamp.milliseconds - now).inWholeSeconds}s" "${(nextCallTimestamp - now).inWholeSeconds}s"
} else { } else {
"Never" "Never"
} }

View file

@ -26,7 +26,8 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
private static final String TAG = Log.tag(SignalStrengthPhoneStateListener.class); private static final String TAG = Log.tag(SignalStrengthPhoneStateListener.class);
private final Callback callback; private final Callback callback;
private final Debouncer debouncer = new Debouncer(1000); private final Debouncer debouncer = new Debouncer(1000);
private volatile boolean hasLowSignal = true;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public SignalStrengthPhoneStateListener(@NonNull LifecycleOwner lifecycleOwner, @NonNull Callback callback) { public SignalStrengthPhoneStateListener(@NonNull LifecycleOwner lifecycleOwner, @NonNull Callback callback) {
@ -40,10 +41,14 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
if (signalStrength == null) return; if (signalStrength == null) return;
if (isLowLevel(signalStrength)) { if (isLowLevel(signalStrength)) {
hasLowSignal = true;
Log.w(TAG, "No cell signal detected"); Log.w(TAG, "No cell signal detected");
debouncer.publish(callback::onNoCellSignalPresent); debouncer.publish(callback::onNoCellSignalPresent);
} else { } else {
Log.i(TAG, "Cell signal detected"); if (hasLowSignal) {
hasLowSignal = false;
Log.i(TAG, "Cell signal detected");
}
debouncer.clear(); debouncer.clear();
callback.onCellSignalPresent(); callback.onCellSignalPresent();
} }

View file

@ -16,6 +16,8 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionR
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/** /**
* State holder shared across all of registration. * State holder shared across all of registration.
@ -41,9 +43,9 @@ data class RegistrationState(
val challengesPresented: Set<Challenge> = emptySet(), val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null, val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false, val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Long = 0L, val nextSmsTimestamp: Duration = 0.seconds,
val nextCallTimestamp: Long = 0L, val nextCallTimestamp: Duration = 0.seconds,
val nextVerificationAttempt: Long = 0L, val nextVerificationAttempt: Duration = 0.seconds,
val verified: Boolean = false, val verified: Boolean = false,
val smsListenerTimeout: Long = 0L, val smsListenerTimeout: Long = 0L,
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION, val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,

View file

@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResul
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
@ -69,12 +70,11 @@ import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException import java.io.IOException
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
/** /**
@ -271,26 +271,25 @@ class RegistrationViewModel : ViewModel() {
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") } val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
if (validSession.metadata.verified) { if (validSession.verified) {
Log.i(TAG, "Session is already verified, registering account.") Log.i(TAG, "Session is already verified, registering account.")
registerVerifiedSession(context, validSession.metadata.id) registerVerifiedSession(context, validSession.sessionId)
return@launch return@launch
} }
if (!validSession.metadata.allowedToRequestCode) { if (!validSession.allowedToRequestCode) {
if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) { if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
store.update { store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
} }
} else { } else {
val challenges = validSession.metadata.requestedInformation Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}") handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)))
} }
return@launch return@launch
} }
requestSmsCodeInternal(context, validSession.metadata.id, e164) requestSmsCodeInternal(context, validSession.sessionId, e164)
} }
} }
@ -299,7 +298,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") } val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
requestSmsCodeInternal(context, validSession.metadata.id, e164) requestSmsCodeInternal(context, validSession.sessionId, e164)
} }
} }
@ -317,7 +316,7 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Requesting voice call code…") Log.d(TAG, "Requesting voice call code…")
val codeRequestResponse = RegistrationRepository.requestSmsCode( val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context, context = context,
sessionId = validSession.metadata.id, sessionId = validSession.sessionId,
e164 = e164, e164 = e164,
password = password, password = password,
mode = RegistrationRepository.E164VerificationMode.PHONE_CALL mode = RegistrationRepository.E164VerificationMode.PHONE_CALL
@ -375,7 +374,7 @@ class RegistrationViewModel : ViewModel() {
} }
} }
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { private suspend fun getOrCreateValidSession(context: Context): SessionMetadataResult? {
Log.v(TAG, "getOrCreateValidSession()") Log.v(TAG, "getOrCreateValidSession()")
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!") val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
val mccMncProducer = MccMncProducer(context) val mccMncProducer = MccMncProducer(context)
@ -388,16 +387,16 @@ class RegistrationViewModel : ViewModel() {
password = password, password = password,
mcc = mccMncProducer.mcc, mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc, mnc = mccMncProducer.mnc,
successListener = { networkResult -> successListener = { sessionData ->
store.update { store.update {
it.copy( it.copy(
sessionId = networkResult.metadata.id, sessionId = sessionData.sessionId,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms), nextSmsTimestamp = sessionData.nextSmsTimestamp,
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall), nextCallTimestamp = sessionData.nextCallTimestamp,
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt), nextVerificationAttempt = sessionData.nextVerificationAttempt,
allowedToRequestCode = networkResult.metadata.allowedToRequestCode, allowedToRequestCode = sessionData.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation), challengesRequested = sessionData.challengesRequested,
verified = networkResult.metadata.verified verified = sessionData.verified
) )
} }
}, },
@ -424,7 +423,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") } val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") }
Log.d(TAG, "Submitting captcha token…") Log.d(TAG, "Submitting captcha token…")
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken) val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.sessionId, captchaToken)
Log.d(TAG, "Captcha token submitted.") Log.d(TAG, "Captcha token submitted.")
handleSessionStateResult(context, captchaSubmissionResult) handleSessionStateResult(context, captchaSubmissionResult)
@ -442,12 +441,12 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Getting session in order to perform push token verification…") Log.d(TAG, "Getting session in order to perform push token verification…")
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") } val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") }
if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) { if (!session.challengesRequested.contains(Challenge.PUSH)) {
return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") } return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") }
} }
Log.d(TAG, "Requesting push challenge token…") Log.d(TAG, "Requesting push challenge token…")
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password) val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.sessionId, e164, password)
Log.d(TAG, "Push challenge token submitted.") Log.d(TAG, "Push challenge token submitted.")
handleSessionStateResult(context, pushSubmissionResult) handleSessionStateResult(context, pushSubmissionResult)
} }
@ -748,8 +747,8 @@ class RegistrationViewModel : ViewModel() {
var reglock = registrationLocked var reglock = registrationLocked
val session: RegistrationSessionMetadataJson? = getOrCreateValidSession(context)?.metadata val session: SessionMetadataResult? = getOrCreateValidSession(context)
val sessionId: String = session?.id ?: return val sessionId: String = session?.sessionId ?: return
val registrationData: RegistrationData = getRegistrationData() val registrationData: RegistrationData = getRegistrationData()
if (session.verified) { if (session.verified) {
@ -965,24 +964,22 @@ class RegistrationViewModel : ViewModel() {
password: String, password: String,
mcc: String?, mcc: String?,
mnc: String?, mnc: String?,
successListener: (RegistrationSessionMetadataResponse) -> Unit, successListener: (SessionMetadataResult) -> Unit,
errorHandler: (RegistrationSessionResult) -> Unit errorHandler: (RegistrationSessionResult) -> Unit
): RegistrationSessionMetadataResponse? { ): SessionMetadataResult? {
Log.d(TAG, "Validating/creating a registration session.") Log.d(TAG, "Validating/creating a registration session.")
val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc) val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc)
when (sessionResult) { when (sessionResult) {
is RegistrationSessionCheckResult.Success -> { is RegistrationSessionCheckResult.Success -> {
val metadata = sessionResult.getMetadata() successListener(sessionResult)
successListener(metadata)
Log.d(TAG, "Registration session validated.") Log.d(TAG, "Registration session validated.")
return metadata return sessionResult
} }
is RegistrationSessionCreationResult.Success -> { is RegistrationSessionCreationResult.Success -> {
val metadata = sessionResult.getMetadata() successListener(sessionResult)
successListener(metadata)
Log.d(TAG, "Registration session created.") Log.d(TAG, "Registration session created.")
return metadata return sessionResult
} }
else -> { else -> {

View file

@ -19,12 +19,15 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.ThreadUtil
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
@ -119,25 +122,31 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
} }
} }
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
it.sessionCreationError?.let { error -> sharedState.sessionCreationError?.let { error ->
handleSessionCreationError(error) handleSessionCreationError(error)
sharedViewModel.sessionCreationErrorShown() sharedViewModel.sessionCreationErrorShown()
} }
it.sessionStateError?.let { error -> sharedState.sessionStateError?.let { error ->
handleSessionErrorResponse(error) handleSessionErrorResponse(error)
sharedViewModel.sessionStateErrorShown() sharedViewModel.sessionStateErrorShown()
} }
it.registerAccountError?.let { error -> sharedState.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error) handleRegistrationErrorResponse(error)
sharedViewModel.registerAccountErrorShown() sharedViewModel.registerAccountErrorShown()
} }
binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp) if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp) sharedViewModel.submitCaptchaToken(requireContext())
if (it.inProgress) { } else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
}
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(sharedState.nextCallTimestamp)
if (sharedState.inProgress) {
binding.keyboard.displayProgress() binding.keyboard.displayProgress()
} else { } else {
binding.keyboard.displayKeyboard() binding.keyboard.displayKeyboard()
@ -173,6 +182,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
if (!result.isSuccess()) { if (!result.isSuccess()) {
Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause()) Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause())
} }
when (result) { when (result) {
is RegistrationSessionCheckResult.Success, is RegistrationSessionCheckResult.Success,
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!") is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
@ -219,6 +229,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
handleRequestVerificationCodeRateLimited(result) handleRequestVerificationCodeRateLimited(result)
} }
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited() is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited()
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
else -> presentGenericError(result) else -> presentGenericError(result)
} }
} }
@ -239,6 +250,13 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
} }
} }
private fun handleChallenges(remainingChallenges: List<Challenge>) {
when (remainingChallenges.first()) {
Challenge.CAPTCHA -> moveToCaptcha()
Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext())
}
}
private fun presentAccountLocked() { private fun presentAccountLocked() {
binding.keyboard.displayLocked().addListener( binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() { object : AssertedSuccessListener<Boolean>() {
@ -363,6 +381,11 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
sharedViewModel.setInProgress(false) sharedViewModel.setInProgress(false)
} }
private fun moveToCaptcha() {
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequestCaptcha())
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onVerificationCodeReceived(event: ReceivedSmsEvent) { fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
Log.i(TAG, "Received verification code via EventBus.") Log.i(TAG, "Received verification code via EventBus.")

View file

@ -10,7 +10,6 @@ import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -25,6 +24,8 @@ import androidx.core.view.MenuProvider
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
@ -32,6 +33,7 @@ import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
@ -85,7 +87,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private lateinit var phoneNumberInputLayout: TextInputEditText private lateinit var phoneNumberInputLayout: TextInputEditText
private lateinit var spinnerView: MaterialAutoCompleteTextView private lateinit var spinnerView: MaterialAutoCompleteTextView
private var currentPhoneNumberFormatter: TextWatcher? = null private var currentPhoneNumberFormatter: AsYouTypeFormatter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -148,13 +150,17 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
} }
} }
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState -> fragmentViewModel
.uiState
fragmentState.phoneNumberFormatter?.let { .map { it.phoneNumberRegionCode }
bindPhoneNumberFormatter(it) .distinctUntilChanged()
.observe(viewLifecycleOwner) { regionCode ->
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus() phoneNumberInputLayout.requestFocus()
} }
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) { if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) {
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState)) sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
} else { } else {
@ -172,9 +178,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (existingPhoneNumber != null) { if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber) fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString()) spinnerView.setText(existingPhoneNumber.countryCode.toString())
fragmentViewModel.formatter?.let {
bindPhoneNumberFormatter(it)
}
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString()) phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
} else { } else {
spinnerView.setText(fragmentViewModel.countryPrefix().toString()) spinnerView.setText(fragmentViewModel.countryPrefix().toString())
@ -183,15 +186,24 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout) ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
} }
private fun bindPhoneNumberFormatter(formatter: TextWatcher) { private fun reformatText(text: Editable?) {
if (formatter != currentPhoneNumberFormatter) { if (text.isNullOrEmpty()) {
currentPhoneNumberFormatter?.let { oldWatcher -> return
Log.d(TAG, "Removing current phone number formatter in fragment") }
phoneNumberInputLayout.removeTextChangedListener(oldWatcher)
currentPhoneNumberFormatter?.let { formatter ->
formatter.clear()
var formattedNumber: String? = null
text.forEach {
if (it.isDigit()) {
formattedNumber = formatter.inputDigit(it)
}
}
if (formattedNumber != null && text.toString() != formattedNumber) {
text.replace(0, text.length, formattedNumber)
} }
phoneNumberInputLayout.addTextChangedListener(formatter)
currentPhoneNumberFormatter = formatter
Log.d(TAG, "Updated phone number formatter in fragment")
} }
} }
@ -215,9 +227,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
} }
} }
phoneNumberInputLayout.addTextChangedListener { phoneNumberInputLayout.addTextChangedListener(
fragmentViewModel.setPhoneNumber(it?.toString()) afterTextChanged = {
} reformatText(it)
fragmentViewModel.setPhoneNumber(it?.toString())
}
)
val scrollView = binding.scrollView val scrollView = binding.scrollView
val registerButton = binding.registerButton val registerButton = binding.registerButton
@ -325,6 +340,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
when (result) { when (result) {
is RegistrationSessionCheckResult.Success, is RegistrationSessionCheckResult.Success,
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!") is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service)) is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
@ -377,6 +393,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
Log.i(TAG, result.log()) Log.i(TAG, result.log())
handleRequestVerificationCodeRateLimited(result) handleRequestVerificationCodeRateLimited(result)
} }
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result) is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result)
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode) is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode)
is VerificationCodeRequestResult.RateLimited -> { is VerificationCodeRequestResult.RateLimited -> {
@ -388,6 +405,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
} }
} }
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() } is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is VerificationCodeRequestResult.AlreadyVerified -> presentGenericError(result) is VerificationCodeRequestResult.AlreadyVerified -> presentGenericError(result)
@ -477,6 +495,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
when (mode) { when (mode) {
RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER, RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER,
RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext()) RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext())
RegistrationRepository.E164VerificationMode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext()) RegistrationRepository.E164VerificationMode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext())
} }
dialogInterface.dismiss() dialogInterface.dismiss()
@ -503,7 +522,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
sharedViewModel.fetchFcmToken(requireContext()) sharedViewModel.fetchFcmToken(requireContext())
} else { } else {
sharedViewModel.uiState.value?.let { value -> sharedViewModel.uiState.value?.let { value ->
val now = System.currentTimeMillis() val now = System.currentTimeMillis().milliseconds
if (value.phoneNumber == null) { if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER) fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false) sharedViewModel.setInProgress(false)

View file

@ -5,16 +5,15 @@
package org.thoughtcrime.securesms.registration.ui.phonenumber package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.text.TextWatcher
import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.thoughtcrime.securesms.registration.data.RegistrationRepository
/** /**
* State holder for the phone number entry screen, including phone number and Play Services errors. * State holder for the phone number entry screen, including phone number and Play Services errors.
*/ */
data class EnterPhoneNumberState( data class EnterPhoneNumberState(
val countryPrefixIndex: Int = 0, val countryPrefixIndex: Int,
val phoneNumber: String = "", val phoneNumber: String = "",
val phoneNumberFormatter: TextWatcher? = null, val phoneNumberRegionCode: String,
val mode: RegistrationRepository.E164VerificationMode = RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER, val mode: RegistrationRepository.E164VerificationMode = RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER,
val error: Error = Error.NONE val error: Error = Error.NONE
) { ) {

View file

@ -5,19 +5,13 @@
package org.thoughtcrime.securesms.registration.ui.phonenumber package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.telephony.PhoneNumberFormattingTextWatcher
import android.text.TextWatcher
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.registration.util.CountryPrefix
@ -27,14 +21,22 @@ import org.thoughtcrime.securesms.registration.util.CountryPrefix
*/ */
class EnterPhoneNumberViewModel : ViewModel() { class EnterPhoneNumberViewModel : ViewModel() {
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java) companion object {
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
}
private val store = MutableStateFlow(EnterPhoneNumberState()) val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
private val store = MutableStateFlow(
EnterPhoneNumberState(
countryPrefixIndex = 0,
phoneNumberRegionCode = supportedCountryPrefixes[0].regionCode
)
)
val uiState = store.asLiveData() val uiState = store.asLiveData()
val formatter: TextWatcher?
get() = store.value.phoneNumberFormatter
val phoneNumber: PhoneNumber? val phoneNumber: PhoneNumber?
get() = try { get() = try {
parsePhoneNumber(store.value) parsePhoneNumber(store.value)
@ -43,10 +45,6 @@ class EnterPhoneNumberViewModel : ViewModel() {
null null
} }
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
var mode: RegistrationRepository.E164VerificationMode var mode: RegistrationRepository.E164VerificationMode
get() = store.value.mode get() = store.value.mode
set(value) = store.update { set(value) = store.update {
@ -69,19 +67,10 @@ class EnterPhoneNumberViewModel : ViewModel() {
} }
store.update { store.update {
it.copy(countryPrefixIndex = matchingIndex) it.copy(
} countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode
viewModelScope.launch { )
withContext(Dispatchers.Default) {
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(digits)
val textWatcher = PhoneNumberFormattingTextWatcher(regionCode)
store.update {
Log.d(TAG, "Updating phone number formatter in state")
it.copy(phoneNumberFormatter = textWatcher)
}
}
} }
} }
@ -103,6 +92,7 @@ class EnterPhoneNumberViewModel : ViewModel() {
store.update { store.update {
it.copy( it.copy(
countryPrefixIndex = prefixIndex, countryPrefixIndex = prefixIndex,
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
phoneNumber = value.nationalNumber.toString() phoneNumber = value.nationalNumber.toString()
) )
} }

View file

@ -77,7 +77,6 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.io.IOException import java.io.IOException
@ -302,7 +301,7 @@ object RegistrationRepository {
val result = RegistrationSessionCreationResult.from(registrationSessionResult) val result = RegistrationSessionCreationResult.from(registrationSessionResult)
if (result is RegistrationSessionCreationResult.Success) { if (result is RegistrationSessionCreationResult.Success) {
Log.d(TAG, "Updating registration session and E164 in value store.") Log.d(TAG, "Updating registration session and E164 in value store.")
SignalStore.registration.sessionId = result.getMetadata().metadata.id SignalStore.registration.sessionId = result.sessionId
SignalStore.registration.sessionE164 = e164 SignalStore.registration.sessionE164 = e164
} }
@ -483,16 +482,6 @@ object RegistrationRepository {
} }
} }
@JvmStatic
fun deriveTimestamp(headers: RegistrationSessionMetadataHeaders, deltaSeconds: Int?): Long {
if (deltaSeconds == null) {
return 0L
}
val timestamp: Long = headers.serverDeliveredTimestamp
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
}
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult = suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi

View file

@ -16,6 +16,8 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionR
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/** /**
* State holder shared across all of registration. * State holder shared across all of registration.
@ -42,9 +44,9 @@ data class RegistrationState(
val challengesPresented: Set<Challenge> = emptySet(), val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null, val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false, val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Long = 0L, val nextSmsTimestamp: Duration = 0.seconds,
val nextCallTimestamp: Long = 0L, val nextCallTimestamp: Duration = 0.seconds,
val nextVerificationAttempt: Long = 0L, val nextVerificationAttempt: Duration = 0.seconds,
val verified: Boolean = false, val verified: Boolean = false,
val smsListenerTimeout: Long = 0L, val smsListenerTimeout: Long = 0L,
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION, val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,

View file

@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResul
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
@ -74,11 +75,11 @@ import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException import java.io.IOException
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
/** /**
@ -277,26 +278,25 @@ class RegistrationViewModel : ViewModel() {
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") } val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
if (validSession.metadata.verified) { if (validSession.verified) {
Log.i(TAG, "Session is already verified, registering account.") Log.i(TAG, "Session is already verified, registering account.")
registerVerifiedSession(context, validSession.metadata.id) registerVerifiedSession(context, validSession.sessionId)
return@launch return@launch
} }
if (!validSession.metadata.allowedToRequestCode) { if (!validSession.allowedToRequestCode) {
if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) { if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
store.update { store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
} }
} else { } else {
val challenges = validSession.metadata.requestedInformation Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}") handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)))
} }
return@launch return@launch
} }
requestSmsCodeInternal(context, validSession.metadata.id, e164) requestSmsCodeInternal(context, validSession.sessionId, e164)
} }
} }
@ -305,7 +305,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") } val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
requestSmsCodeInternal(context, validSession.metadata.id, e164) requestSmsCodeInternal(context, validSession.sessionId, e164)
} }
} }
@ -323,7 +323,7 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Requesting voice call code…") Log.d(TAG, "Requesting voice call code…")
val codeRequestResponse = RegistrationRepository.requestSmsCode( val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context, context = context,
sessionId = validSession.metadata.id, sessionId = validSession.sessionId,
e164 = e164, e164 = e164,
password = password, password = password,
mode = RegistrationRepository.E164VerificationMode.PHONE_CALL mode = RegistrationRepository.E164VerificationMode.PHONE_CALL
@ -381,7 +381,7 @@ class RegistrationViewModel : ViewModel() {
} }
} }
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { private suspend fun getOrCreateValidSession(context: Context): SessionMetadataResult? {
Log.v(TAG, "getOrCreateValidSession()") Log.v(TAG, "getOrCreateValidSession()")
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!") val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
val mccMncProducer = MccMncProducer(context) val mccMncProducer = MccMncProducer(context)
@ -394,16 +394,16 @@ class RegistrationViewModel : ViewModel() {
password = password, password = password,
mcc = mccMncProducer.mcc, mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc, mnc = mccMncProducer.mnc,
successListener = { networkResult -> successListener = { sessionData ->
store.update { store.update {
it.copy( it.copy(
sessionId = networkResult.metadata.id, sessionId = sessionData.sessionId,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms), nextSmsTimestamp = sessionData.nextSmsTimestamp,
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall), nextCallTimestamp = sessionData.nextCallTimestamp,
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt), nextVerificationAttempt = sessionData.nextVerificationAttempt,
allowedToRequestCode = networkResult.metadata.allowedToRequestCode, allowedToRequestCode = sessionData.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation), challengesRequested = sessionData.challengesRequested,
verified = networkResult.metadata.verified verified = sessionData.verified
) )
} }
}, },
@ -430,7 +430,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") } val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") }
Log.d(TAG, "Submitting captcha token…") Log.d(TAG, "Submitting captcha token…")
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken) val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.sessionId, captchaToken)
Log.d(TAG, "Captcha token submitted.") Log.d(TAG, "Captcha token submitted.")
handleSessionStateResult(context, captchaSubmissionResult) handleSessionStateResult(context, captchaSubmissionResult)
@ -448,12 +448,12 @@ class RegistrationViewModel : ViewModel() {
Log.d(TAG, "Getting session in order to perform push token verification…") Log.d(TAG, "Getting session in order to perform push token verification…")
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") } val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") }
if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) { if (!session.challengesRequested.contains(Challenge.PUSH)) {
return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") } return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") }
} }
Log.d(TAG, "Requesting push challenge token…") Log.d(TAG, "Requesting push challenge token…")
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password) val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.sessionId, e164, password)
Log.d(TAG, "Push challenge token submitted.") Log.d(TAG, "Push challenge token submitted.")
handleSessionStateResult(context, pushSubmissionResult) handleSessionStateResult(context, pushSubmissionResult)
} }
@ -763,21 +763,26 @@ class RegistrationViewModel : ViewModel() {
var reglock = registrationLocked var reglock = registrationLocked
val sessionId = getOrCreateValidSession(context)?.metadata?.id ?: return val session: SessionMetadataResult? = getOrCreateValidSession(context)
val registrationData = getRegistrationData() val sessionId: String = session?.sessionId ?: return
val registrationData: RegistrationData = getRegistrationData()
Log.d(TAG, "Submitting verification code…") if (session.verified) {
Log.i(TAG, "Session is already verified, registering account.")
} else {
Log.d(TAG, "Submitting verification code…")
val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
val submissionSuccessful = verificationResponse is Success val submissionSuccessful = verificationResponse is Success
val alreadyVerified = verificationResponse is AlreadyVerified val alreadyVerified = verificationResponse is AlreadyVerified
Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified") Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
if (!submissionSuccessful && !alreadyVerified) { if (!submissionSuccessful && !alreadyVerified) {
handleSessionStateResult(context, verificationResponse) handleSessionStateResult(context, verificationResponse)
return return
}
} }
Log.d(TAG, "Submitting registration…") Log.d(TAG, "Submitting registration…")
@ -1002,24 +1007,22 @@ class RegistrationViewModel : ViewModel() {
password: String, password: String,
mcc: String?, mcc: String?,
mnc: String?, mnc: String?,
successListener: (RegistrationSessionMetadataResponse) -> Unit, successListener: (SessionMetadataResult) -> Unit,
errorHandler: (RegistrationSessionResult) -> Unit errorHandler: (RegistrationSessionResult) -> Unit
): RegistrationSessionMetadataResponse? { ): SessionMetadataResult? {
Log.d(TAG, "Validating/creating a registration session.") Log.d(TAG, "Validating/creating a registration session.")
val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc) val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc)
when (sessionResult) { when (sessionResult) {
is RegistrationSessionCheckResult.Success -> { is RegistrationSessionCheckResult.Success -> {
val metadata = sessionResult.getMetadata() successListener(sessionResult)
successListener(metadata)
Log.d(TAG, "Registration session validated.") Log.d(TAG, "Registration session validated.")
return metadata return sessionResult
} }
is RegistrationSessionCreationResult.Success -> { is RegistrationSessionCreationResult.Success -> {
val metadata = sessionResult.getMetadata() successListener(sessionResult)
successListener(metadata)
Log.d(TAG, "Registration session created.") Log.d(TAG, "Registration session created.")
return metadata return sessionResult
} }
else -> { else -> {

View file

@ -19,12 +19,15 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.ThreadUtil
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
@ -119,25 +122,31 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
} }
} }
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
it.sessionCreationError?.let { error -> sharedState.sessionCreationError?.let { error ->
handleSessionCreationError(error) handleSessionCreationError(error)
sharedViewModel.sessionCreationErrorShown() sharedViewModel.sessionCreationErrorShown()
} }
it.sessionStateError?.let { error -> sharedState.sessionStateError?.let { error ->
handleSessionErrorResponse(error) handleSessionErrorResponse(error)
sharedViewModel.sessionStateErrorShown() sharedViewModel.sessionStateErrorShown()
} }
it.registerAccountError?.let { error -> sharedState.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error) handleRegistrationErrorResponse(error)
sharedViewModel.registerAccountErrorShown() sharedViewModel.registerAccountErrorShown()
} }
binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp) if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp) sharedViewModel.submitCaptchaToken(requireContext())
if (it.inProgress) { } else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
}
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(sharedState.nextCallTimestamp)
if (sharedState.inProgress) {
binding.keyboard.displayProgress() binding.keyboard.displayProgress()
} else { } else {
binding.keyboard.displayKeyboard() binding.keyboard.displayKeyboard()
@ -219,6 +228,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
handleRequestVerificationCodeRateLimited(result) handleRequestVerificationCodeRateLimited(result)
} }
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited() is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited()
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
else -> presentGenericError(result) else -> presentGenericError(result)
} }
} }
@ -239,6 +249,13 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
} }
} }
private fun handleChallenges(remainingChallenges: List<Challenge>) {
when (remainingChallenges.first()) {
Challenge.CAPTCHA -> moveToCaptcha()
Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext())
}
}
private fun presentAccountLocked() { private fun presentAccountLocked() {
binding.keyboard.displayLocked().addListener( binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() { object : AssertedSuccessListener<Boolean>() {
@ -363,6 +380,11 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
sharedViewModel.setInProgress(false) sharedViewModel.setInProgress(false)
} }
private fun moveToCaptcha() {
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequestCaptcha())
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onVerificationCodeReceived(event: ReceivedSmsEvent) { fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
Log.i(TAG, "Received verification code via EventBus.") Log.i(TAG, "Received verification code via EventBus.")

View file

@ -10,7 +10,6 @@ import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -25,6 +24,8 @@ import androidx.core.view.MenuProvider
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -33,6 +34,7 @@ import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
@ -90,7 +92,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private lateinit var phoneNumberInputLayout: TextInputEditText private lateinit var phoneNumberInputLayout: TextInputEditText
private lateinit var spinnerView: MaterialAutoCompleteTextView private lateinit var spinnerView: MaterialAutoCompleteTextView
private var currentPhoneNumberFormatter: TextWatcher? = null private var currentPhoneNumberFormatter: AsYouTypeFormatter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -153,13 +155,17 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
} }
} }
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState -> fragmentViewModel
.uiState
fragmentState.phoneNumberFormatter?.let { .map { it.phoneNumberRegionCode }
bindPhoneNumberFormatter(it) .distinctUntilChanged()
.observe(viewLifecycleOwner) { regionCode ->
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus() phoneNumberInputLayout.requestFocus()
} }
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) { if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) {
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState)) sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
} else { } else {
@ -177,9 +183,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (existingPhoneNumber != null) { if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber) fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString()) spinnerView.setText(existingPhoneNumber.countryCode.toString())
fragmentViewModel.formatter?.let {
bindPhoneNumberFormatter(it)
}
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString()) phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
} else { } else {
spinnerView.setText(fragmentViewModel.countryPrefix().toString()) spinnerView.setText(fragmentViewModel.countryPrefix().toString())
@ -193,15 +196,24 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
} }
} }
private fun bindPhoneNumberFormatter(formatter: TextWatcher) { private fun reformatText(text: Editable?) {
if (formatter != currentPhoneNumberFormatter) { if (text.isNullOrEmpty()) {
currentPhoneNumberFormatter?.let { oldWatcher -> return
Log.d(TAG, "Removing current phone number formatter in fragment") }
phoneNumberInputLayout.removeTextChangedListener(oldWatcher)
currentPhoneNumberFormatter?.let { formatter ->
formatter.clear()
var formattedNumber: String? = null
text.forEach {
if (it.isDigit()) {
formattedNumber = formatter.inputDigit(it)
}
}
if (formattedNumber != null && text.toString() != formattedNumber) {
text.replace(0, text.length, formattedNumber)
} }
phoneNumberInputLayout.addTextChangedListener(formatter)
currentPhoneNumberFormatter = formatter
Log.d(TAG, "Updated phone number formatter in fragment")
} }
} }
@ -225,9 +237,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
} }
} }
phoneNumberInputLayout.addTextChangedListener { phoneNumberInputLayout.addTextChangedListener(
fragmentViewModel.setPhoneNumber(it?.toString()) afterTextChanged = {
} reformatText(it)
fragmentViewModel.setPhoneNumber(it?.toString())
}
)
val scrollView = binding.scrollView val scrollView = binding.scrollView
val registerButton = binding.registerButton val registerButton = binding.registerButton
@ -388,6 +403,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
Log.i(TAG, result.log()) Log.i(TAG, result.log())
handleRequestVerificationCodeRateLimited(result) handleRequestVerificationCodeRateLimited(result)
} }
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result) is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result)
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.e164VerificationMode) is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.e164VerificationMode)
is VerificationCodeRequestResult.RateLimited -> { is VerificationCodeRequestResult.RateLimited -> {
@ -525,7 +541,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
sharedViewModel.fetchFcmToken(requireContext()) sharedViewModel.fetchFcmToken(requireContext())
} else { } else {
sharedViewModel.uiState.value?.let { value -> sharedViewModel.uiState.value?.let { value ->
val now = System.currentTimeMillis() val now = System.currentTimeMillis().milliseconds
if (value.phoneNumber == null) { if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER) fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false) sharedViewModel.setInProgress(false)

View file

@ -5,16 +5,15 @@
package org.thoughtcrime.securesms.registrationv3.ui.phonenumber package org.thoughtcrime.securesms.registrationv3.ui.phonenumber
import android.text.TextWatcher
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
/** /**
* State holder for the phone number entry screen, including phone number and Play Services errors. * State holder for the phone number entry screen, including phone number and Play Services errors.
*/ */
data class EnterPhoneNumberState( data class EnterPhoneNumberState(
val countryPrefixIndex: Int = 0, val countryPrefixIndex: Int,
val phoneNumber: String = "", val phoneNumber: String = "",
val phoneNumberFormatter: TextWatcher? = null, val phoneNumberRegionCode: String,
val mode: RegistrationRepository.E164VerificationMode = RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER, val mode: RegistrationRepository.E164VerificationMode = RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER,
val error: Error = Error.NONE val error: Error = Error.NONE
) { ) {

View file

@ -5,19 +5,13 @@
package org.thoughtcrime.securesms.registrationv3.ui.phonenumber package org.thoughtcrime.securesms.registrationv3.ui.phonenumber
import android.telephony.PhoneNumberFormattingTextWatcher
import android.text.TextWatcher
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.util.CountryPrefix import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
@ -27,14 +21,22 @@ import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
*/ */
class EnterPhoneNumberViewModel : ViewModel() { class EnterPhoneNumberViewModel : ViewModel() {
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java) companion object {
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
}
private val store = MutableStateFlow(EnterPhoneNumberState()) val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
private val store = MutableStateFlow(
EnterPhoneNumberState(
countryPrefixIndex = 0,
phoneNumberRegionCode = supportedCountryPrefixes[0].regionCode
)
)
val uiState = store.asLiveData() val uiState = store.asLiveData()
val formatter: TextWatcher?
get() = store.value.phoneNumberFormatter
val phoneNumber: PhoneNumber? val phoneNumber: PhoneNumber?
get() = try { get() = try {
parsePhoneNumber(store.value) parsePhoneNumber(store.value)
@ -43,10 +45,6 @@ class EnterPhoneNumberViewModel : ViewModel() {
null null
} }
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
var e164VerificationMode: RegistrationRepository.E164VerificationMode var e164VerificationMode: RegistrationRepository.E164VerificationMode
get() = store.value.mode get() = store.value.mode
set(value) = store.update { set(value) = store.update {
@ -69,19 +67,10 @@ class EnterPhoneNumberViewModel : ViewModel() {
} }
store.update { store.update {
it.copy(countryPrefixIndex = matchingIndex) it.copy(
} countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode
viewModelScope.launch { )
withContext(Dispatchers.Default) {
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(digits)
val textWatcher = PhoneNumberFormattingTextWatcher(regionCode)
store.update {
Log.d(TAG, "Updating phone number formatter in state")
it.copy(phoneNumberFormatter = textWatcher)
}
}
} }
} }
@ -103,6 +92,7 @@ class EnterPhoneNumberViewModel : ViewModel() {
store.update { store.update {
it.copy( it.copy(
countryPrefixIndex = prefixIndex, countryPrefixIndex = prefixIndex,
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
phoneNumber = value.nationalNumber.toString() phoneNumber = value.nationalNumber.toString()
) )
} }

View file

@ -3309,23 +3309,11 @@ public class PushServiceSocket {
} }
private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(ResponseBody body, Function<String, String> getHeader) throws IOException { private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(ResponseBody body, Function<String, String> getHeader) throws IOException {
long serverDeliveredTimestamp = 0; long retryAfterLong = Util.parseLong(getHeader.apply("Retry-After"), -1);
try { Long retryAfterMs = retryAfterLong != -1 ? TimeUnit.SECONDS.toMillis(retryAfterLong) : null;
String stringValue = getHeader.apply(SERVER_DELIVERED_TIMESTAMP_HEADER); RegistrationSessionMetadataJson responseBody = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class);
stringValue = stringValue != null ? stringValue : "0";
serverDeliveredTimestamp = Long.parseLong(stringValue); return new RegistrationSessionMetadataResponse(responseBody, System.currentTimeMillis(), retryAfterMs);
} catch (NumberFormatException e) {
Log.w(TAG, e);
}
long retryAfterLong = Util.parseLong(getHeader.apply("Retry-After"), -1);
Long retryAfter = retryAfterLong != -1 ? TimeUnit.SECONDS.toMillis(retryAfterLong) : null;
RegistrationSessionMetadataHeaders responseHeaders = new RegistrationSessionMetadataHeaders(serverDeliveredTimestamp, retryAfter);
RegistrationSessionMetadataJson responseBody = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class);
return new RegistrationSessionMetadataResponse(responseHeaders, responseBody, null);
} }
private static @Nonnull String urlEncode(@Nonnull String data) throws IOException { private static @Nonnull String urlEncode(@Nonnull String data) throws IOException {

View file

@ -1,21 +1,30 @@
package org.whispersystems.signalservice.internal.push package org.whispersystems.signalservice.internal.push
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/** /**
* This is a parsed, POJO representation of the server response describing the state of the registration session. * This is a parsed, POJO representation of the server response describing the state of the registration session.
* The useful headers and the request body are wrapped in a single holder class.
*/ */
data class RegistrationSessionMetadataResponse( data class RegistrationSessionMetadataResponse(
val headers: RegistrationSessionMetadataHeaders,
val metadata: RegistrationSessionMetadataJson, val metadata: RegistrationSessionMetadataJson,
val state: RegistrationSessionState? val clientReceivedAt: Duration,
) val retryAfterTimestamp: Duration? = null
) {
constructor(metadata: RegistrationSessionMetadataJson, clientReceivedAt: Long, retryAfterTimestamp: Long?) : this(metadata, clientReceivedAt.milliseconds, retryAfterTimestamp?.milliseconds)
data class RegistrationSessionMetadataHeaders( fun deriveTimestamp(delta: Duration?): Duration {
val serverDeliveredTimestamp: Long, if (delta == null) {
val retryAfterTimestamp: Long? = null return 0.milliseconds
) }
val now = System.currentTimeMillis().milliseconds
val base = clientReceivedAt.takeIf { clientReceivedAt <= now } ?: now
return base + delta
}
}
data class RegistrationSessionMetadataJson( data class RegistrationSessionMetadataJson(
@JsonProperty("id") val id: String, @JsonProperty("id") val id: String,
@ -34,7 +43,3 @@ data class RegistrationSessionMetadataJson(
return requestedInformation.contains("captcha") return requestedInformation.contains("captcha")
} }
} }
data class RegistrationSessionState(
var pushChallengeTimedOut: Boolean
)