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 androidx.annotation.StringRes
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(
context: Context,
@ -17,7 +19,7 @@ class ActionCountDownButton @JvmOverloads constructor(
@StringRes
private var disabledText = 0
private var countDownToTime: Long = 0
private var countDownToTime: Duration = 0.seconds
private var listener: Listener? = null
private var updateRunnable = Runnable {
@ -27,8 +29,8 @@ class ActionCountDownButton @JvmOverloads constructor(
/**
* Starts a count down to the specified {@param time}.
*/
fun startCountDownTo(time: Long) {
if (time > 0) {
fun startCountDownTo(time: Duration) {
if (time > 0.seconds) {
countDownToTime = time
removeCallbacks(updateRunnable)
updateCountDown()
@ -46,11 +48,11 @@ class ActionCountDownButton @JvmOverloads constructor(
}
private fun updateCountDown() {
val remainingMillis = countDownToTime - System.currentTimeMillis()
if (remainingMillis > 1000) {
val remaining = countDownToTime - System.currentTimeMillis().milliseconds
if (remaining > 1.seconds) {
isEnabled = false
alpha = 0.5f
val totalRemainingSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingMillis).toInt()
val totalRemainingSeconds = remaining.inWholeSeconds.toInt()
val minutesRemaining = 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.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
import kotlin.time.Duration.Companion.milliseconds
/**
* 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) {
binding.codeEntryLayout.resendSmsCountDown.startCountDownTo(state.nextSmsTimestamp)
binding.codeEntryLayout.callMeCountDown.startCountDownTo(state.nextCallTimestamp)
binding.codeEntryLayout.resendSmsCountDown.startCountDownTo(state.nextSmsTimestamp.milliseconds)
binding.codeEntryLayout.callMeCountDown.startCountDownTo(state.nextCallTimestamp.milliseconds)
when (val outcome = state.changeNumberOutcome) {
is ChangeNumberOutcome.RecoveryPasswordWorked,
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.network.Challenge
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.sms.SmsRetrieverReceiver
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.util.dualsim.MccMncProducer
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException
import java.util.concurrent.locks.ReentrantLock
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) {
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 verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
@ -285,8 +285,8 @@ class ChangeNumberViewModel : ViewModel() {
viewModelScope.launch {
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.") }
if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.CAPTCHA)) {
val sessionData = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") }
if (!sessionData.challengesRequested.contains(Challenge.CAPTCHA)) {
Log.d(TAG, "Captcha submission no longer necessary, bailing.")
store.update {
it.copy(
@ -297,7 +297,7 @@ class ChangeNumberViewModel : ViewModel() {
return@launch
}
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.")
store.update {
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult))
@ -314,9 +314,9 @@ class ChangeNumberViewModel : ViewModel() {
viewModelScope.launch {
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.")
store.update {
it.copy(
@ -328,7 +328,7 @@ class ChangeNumberViewModel : ViewModel() {
}
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.")
store.update {
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult))
@ -361,14 +361,18 @@ class ChangeNumberViewModel : ViewModel() {
// region Private actions
private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) {
private fun updateLocalStateFromSession(sessionData: SessionMetadataResult) {
Log.v(TAG, "updateLocalStateFromSession()")
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()")
val e164 = number.e164Number
val mccMncProducer = MccMncProducer(context)
@ -477,15 +481,15 @@ class ChangeNumberViewModel : ViewModel() {
return
}
val result = if (!validSession.metadata.allowedToRequestCode) {
val challenges = validSession.metadata.requestedInformation.joinToString()
val result = if (!validSession.allowedToRequestCode) {
val challenges = validSession.challengesRequested.joinToString()
Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation))
VerificationCodeRequestResult.ChallengeRequired(validSession.challengesRequested)
} else {
store.update {
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")
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.internal.push.AuthCredentials
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.VerifyAccountResponse
import java.io.IOException
@ -307,7 +306,7 @@ object RegistrationRepository {
val result = RegistrationSessionCreationResult.from(registrationSessionResult)
if (result is RegistrationSessionCreationResult.Success) {
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
}
@ -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 =
withContext(Dispatchers.IO) {
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
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.whispersystems.signalservice.api.NetworkResult
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.RateLimitException
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
sealed class RegistrationSessionResult(cause: Throwable?) : RegistrationResult(cause)
interface SessionMetadataHolder {
fun getMetadata(): RegistrationSessionMetadataResponse
interface SessionMetadataResult {
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) {
companion object {
private val TAG = Log.tag(RegistrationSessionResult::class.java)
@JvmStatic
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCreationResult {
return when (networkResult) {
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)
@ -49,11 +62,15 @@ sealed class RegistrationSessionCreationResult(cause: Throwable?) : Registration
}
}
class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCreationResult(null), SessionMetadataHolder {
override fun getMetadata(): RegistrationSessionMetadataResponse {
return metadata
}
}
class Success(
override val sessionId: String,
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 AttemptsExhausted(cause: Throwable) : RegistrationSessionCreationResult(cause)
@ -67,7 +84,15 @@ sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSes
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCheckResult {
return when (networkResult) {
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)
@ -82,11 +107,15 @@ sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSes
}
}
class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCheckResult(null), SessionMetadataHolder {
override fun getMetadata(): RegistrationSessionMetadataResponse {
return metadata
}
}
class Success(
override val sessionId: String,
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 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.orNull
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException
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 kotlin.time.Duration
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.
@ -47,9 +47,9 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
} else {
Success(
sessionId = networkResult.result.metadata.id,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextSms),
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextCall),
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextVerificationAttempt),
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
@ -75,8 +75,8 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
is RequestVerificationCodeRateLimitException -> {
RequestVerificationCodeRateLimited(
cause = cause,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextSms),
nextCallTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextCall)
nextSmsTimestamp = cause.sessionMetadata.deriveTimestamp(delta = cause.sessionMetadata.metadata.nextSms?.seconds),
nextCallTimestamp = cause.sessionMetadata.deriveTimestamp(delta = cause.sessionMetadata.metadata.nextCall?.seconds)
)
}
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)
@ -111,17 +111,17 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause)
class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Long, val nextCallTimestamp: Long) : VerificationCodeRequestResult(cause) {
val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0 || nextCallTimestamp > 0
class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Duration, val nextCallTimestamp: Duration) : VerificationCodeRequestResult(cause) {
val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0.seconds || nextCallTimestamp > 0.seconds
fun log(now: Duration = System.currentTimeMillis().milliseconds): String {
val sms = if (nextSmsTimestamp > 0) {
"${(nextSmsTimestamp.milliseconds - now).inWholeSeconds}s"
val sms = if (nextSmsTimestamp > 0.seconds) {
"${(nextSmsTimestamp - now).inWholeSeconds}s"
} else {
"Never"
}
val call = if (nextCallTimestamp > 0) {
"${(nextCallTimestamp.milliseconds - now).inWholeSeconds}s"
val call = if (nextCallTimestamp > 0.seconds) {
"${(nextCallTimestamp - now).inWholeSeconds}s"
} else {
"Never"
}

View file

@ -26,7 +26,8 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
private static final String TAG = Log.tag(SignalStrengthPhoneStateListener.class);
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")
public SignalStrengthPhoneStateListener(@NonNull LifecycleOwner lifecycleOwner, @NonNull Callback callback) {
@ -40,10 +41,14 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
if (signalStrength == null) return;
if (isLowLevel(signalStrength)) {
hasLowSignal = true;
Log.w(TAG, "No cell signal detected");
debouncer.publish(callback::onNoCellSignalPresent);
} else {
Log.i(TAG, "Cell signal detected");
if (hasLowSignal) {
hasLowSignal = false;
Log.i(TAG, "Cell signal detected");
}
debouncer.clear();
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.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* State holder shared across all of registration.
@ -41,9 +43,9 @@ data class RegistrationState(
val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Long = 0L,
val nextCallTimestamp: Long = 0L,
val nextVerificationAttempt: Long = 0L,
val nextSmsTimestamp: Duration = 0.seconds,
val nextCallTimestamp: Duration = 0.seconds,
val nextVerificationAttempt: Duration = 0.seconds,
val verified: Boolean = false,
val smsListenerTimeout: Long = 0L,
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.RegistrationSessionCreationResult
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.AlreadyVerified
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.svr.Svr3Credentials
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.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
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.") }
if (validSession.metadata.verified) {
if (validSession.verified) {
Log.i(TAG, "Session is already verified, registering account.")
registerVerifiedSession(context, validSession.metadata.id)
registerVerifiedSession(context, validSession.sessionId)
return@launch
}
if (!validSession.metadata.allowedToRequestCode) {
if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) {
if (!validSession.allowedToRequestCode) {
if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
}
} else {
val challenges = validSession.metadata.requestedInformation
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)))
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
}
return@launch
}
requestSmsCodeInternal(context, validSession.metadata.id, e164)
requestSmsCodeInternal(context, validSession.sessionId, e164)
}
}
@ -299,7 +298,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch {
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…")
val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context,
sessionId = validSession.metadata.id,
sessionId = validSession.sessionId,
e164 = e164,
password = password,
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()")
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
val mccMncProducer = MccMncProducer(context)
@ -388,16 +387,16 @@ class RegistrationViewModel : ViewModel() {
password = password,
mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc,
successListener = { networkResult ->
successListener = { sessionData ->
store.update {
it.copy(
sessionId = networkResult.metadata.id,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms),
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall),
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt),
allowedToRequestCode = networkResult.metadata.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation),
verified = networkResult.metadata.verified
sessionId = sessionData.sessionId,
nextSmsTimestamp = sessionData.nextSmsTimestamp,
nextCallTimestamp = sessionData.nextCallTimestamp,
nextVerificationAttempt = sessionData.nextVerificationAttempt,
allowedToRequestCode = sessionData.allowedToRequestCode,
challengesRequested = sessionData.challengesRequested,
verified = sessionData.verified
)
}
},
@ -424,7 +423,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch {
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…")
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.")
handleSessionStateResult(context, captchaSubmissionResult)
@ -442,12 +441,12 @@ class RegistrationViewModel : ViewModel() {
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.") }
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.") }
}
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.")
handleSessionStateResult(context, pushSubmissionResult)
}
@ -748,8 +747,8 @@ class RegistrationViewModel : ViewModel() {
var reglock = registrationLocked
val session: RegistrationSessionMetadataJson? = getOrCreateValidSession(context)?.metadata
val sessionId: String = session?.id ?: return
val session: SessionMetadataResult? = getOrCreateValidSession(context)
val sessionId: String = session?.sessionId ?: return
val registrationData: RegistrationData = getRegistrationData()
if (session.verified) {
@ -965,24 +964,22 @@ class RegistrationViewModel : ViewModel() {
password: String,
mcc: String?,
mnc: String?,
successListener: (RegistrationSessionMetadataResponse) -> Unit,
successListener: (SessionMetadataResult) -> Unit,
errorHandler: (RegistrationSessionResult) -> Unit
): RegistrationSessionMetadataResponse? {
): SessionMetadataResult? {
Log.d(TAG, "Validating/creating a registration session.")
val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc)
when (sessionResult) {
is RegistrationSessionCheckResult.Success -> {
val metadata = sessionResult.getMetadata()
successListener(metadata)
successListener(sessionResult)
Log.d(TAG, "Registration session validated.")
return metadata
return sessionResult
}
is RegistrationSessionCreationResult.Success -> {
val metadata = sessionResult.getMetadata()
successListener(metadata)
successListener(sessionResult)
Log.d(TAG, "Registration session created.")
return metadata
return sessionResult
}
else -> {

View file

@ -19,12 +19,15 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
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.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
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.RegistrationResult
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) {
it.sessionCreationError?.let { error ->
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
sharedState.sessionCreationError?.let { error ->
handleSessionCreationError(error)
sharedViewModel.sessionCreationErrorShown()
}
it.sessionStateError?.let { error ->
sharedState.sessionStateError?.let { error ->
handleSessionErrorResponse(error)
sharedViewModel.sessionStateErrorShown()
}
it.registerAccountError?.let { error ->
sharedState.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error)
sharedViewModel.registerAccountErrorShown()
}
binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp)
if (it.inProgress) {
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
}
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(sharedState.nextCallTimestamp)
if (sharedState.inProgress) {
binding.keyboard.displayProgress()
} else {
binding.keyboard.displayKeyboard()
@ -173,6 +182,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
if (!result.isSuccess()) {
Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause())
}
when (result) {
is RegistrationSessionCheckResult.Success,
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)
}
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited()
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
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() {
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
@ -363,6 +381,11 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
sharedViewModel.setInProgress(false)
}
private fun moveToCaptcha() {
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequestCaptcha())
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
Log.i(TAG, "Received verification code via EventBus.")

View file

@ -10,7 +10,6 @@ import android.content.DialogInterface
import android.os.Bundle
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
@ -25,6 +24,8 @@ import androidx.core.view.MenuProvider
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
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.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText
import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
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 spinnerView: MaterialAutoCompleteTextView
private var currentPhoneNumberFormatter: TextWatcher? = null
private var currentPhoneNumberFormatter: AsYouTypeFormatter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -148,13 +150,17 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
}
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
fragmentState.phoneNumberFormatter?.let {
bindPhoneNumberFormatter(it)
fragmentViewModel
.uiState
.map { it.phoneNumberRegionCode }
.distinctUntilChanged()
.observe(viewLifecycleOwner) { regionCode ->
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus()
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) {
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
} else {
@ -172,9 +178,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString())
fragmentViewModel.formatter?.let {
bindPhoneNumberFormatter(it)
}
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
} else {
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
@ -183,15 +186,24 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
}
private fun bindPhoneNumberFormatter(formatter: TextWatcher) {
if (formatter != currentPhoneNumberFormatter) {
currentPhoneNumberFormatter?.let { oldWatcher ->
Log.d(TAG, "Removing current phone number formatter in fragment")
phoneNumberInputLayout.removeTextChangedListener(oldWatcher)
private fun reformatText(text: Editable?) {
if (text.isNullOrEmpty()) {
return
}
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 {
fragmentViewModel.setPhoneNumber(it?.toString())
}
phoneNumberInputLayout.addTextChangedListener(
afterTextChanged = {
reformatText(it)
fragmentViewModel.setPhoneNumber(it?.toString())
}
)
val scrollView = binding.scrollView
val registerButton = binding.registerButton
@ -325,6 +340,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
when (result) {
is RegistrationSessionCheckResult.Success,
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.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())
handleRequestVerificationCodeRateLimited(result)
}
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result)
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode)
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))
}
}
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is VerificationCodeRequestResult.AlreadyVerified -> presentGenericError(result)
@ -477,6 +495,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
when (mode) {
RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER,
RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext())
RegistrationRepository.E164VerificationMode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext())
}
dialogInterface.dismiss()
@ -503,7 +522,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
sharedViewModel.fetchFcmToken(requireContext())
} else {
sharedViewModel.uiState.value?.let { value ->
val now = System.currentTimeMillis()
val now = System.currentTimeMillis().milliseconds
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)

View file

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

View file

@ -5,19 +5,13 @@
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.telephony.PhoneNumberFormattingTextWatcher
import android.text.TextWatcher
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.CountryPrefix
@ -27,14 +21,22 @@ import org.thoughtcrime.securesms.registration.util.CountryPrefix
*/
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 formatter: TextWatcher?
get() = store.value.phoneNumberFormatter
val phoneNumber: PhoneNumber?
get() = try {
parsePhoneNumber(store.value)
@ -43,10 +45,6 @@ class EnterPhoneNumberViewModel : ViewModel() {
null
}
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
var mode: RegistrationRepository.E164VerificationMode
get() = store.value.mode
set(value) = store.update {
@ -69,19 +67,10 @@ class EnterPhoneNumberViewModel : ViewModel() {
}
store.update {
it.copy(countryPrefixIndex = matchingIndex)
}
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)
}
}
it.copy(
countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode
)
}
}
@ -103,6 +92,7 @@ class EnterPhoneNumberViewModel : ViewModel() {
store.update {
it.copy(
countryPrefixIndex = prefixIndex,
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
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.internal.push.AuthCredentials
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.VerifyAccountResponse
import java.io.IOException
@ -302,7 +301,7 @@ object RegistrationRepository {
val result = RegistrationSessionCreationResult.from(registrationSessionResult)
if (result is RegistrationSessionCreationResult.Success) {
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
}
@ -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 =
withContext(Dispatchers.IO) {
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.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* State holder shared across all of registration.
@ -42,9 +44,9 @@ data class RegistrationState(
val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Long = 0L,
val nextCallTimestamp: Long = 0L,
val nextVerificationAttempt: Long = 0L,
val nextSmsTimestamp: Duration = 0.seconds,
val nextCallTimestamp: Duration = 0.seconds,
val nextVerificationAttempt: Duration = 0.seconds,
val verified: Boolean = false,
val smsListenerTimeout: Long = 0L,
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.RegistrationSessionCreationResult
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.AlreadyVerified
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.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.milliseconds
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.") }
if (validSession.metadata.verified) {
if (validSession.verified) {
Log.i(TAG, "Session is already verified, registering account.")
registerVerifiedSession(context, validSession.metadata.id)
registerVerifiedSession(context, validSession.sessionId)
return@launch
}
if (!validSession.metadata.allowedToRequestCode) {
if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) {
if (!validSession.allowedToRequestCode) {
if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
}
} else {
val challenges = validSession.metadata.requestedInformation
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)))
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
}
return@launch
}
requestSmsCodeInternal(context, validSession.metadata.id, e164)
requestSmsCodeInternal(context, validSession.sessionId, e164)
}
}
@ -305,7 +305,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch {
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…")
val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context,
sessionId = validSession.metadata.id,
sessionId = validSession.sessionId,
e164 = e164,
password = password,
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()")
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
val mccMncProducer = MccMncProducer(context)
@ -394,16 +394,16 @@ class RegistrationViewModel : ViewModel() {
password = password,
mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc,
successListener = { networkResult ->
successListener = { sessionData ->
store.update {
it.copy(
sessionId = networkResult.metadata.id,
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms),
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall),
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt),
allowedToRequestCode = networkResult.metadata.allowedToRequestCode,
challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation),
verified = networkResult.metadata.verified
sessionId = sessionData.sessionId,
nextSmsTimestamp = sessionData.nextSmsTimestamp,
nextCallTimestamp = sessionData.nextCallTimestamp,
nextVerificationAttempt = sessionData.nextVerificationAttempt,
allowedToRequestCode = sessionData.allowedToRequestCode,
challengesRequested = sessionData.challengesRequested,
verified = sessionData.verified
)
}
},
@ -430,7 +430,7 @@ class RegistrationViewModel : ViewModel() {
viewModelScope.launch {
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…")
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.")
handleSessionStateResult(context, captchaSubmissionResult)
@ -448,12 +448,12 @@ class RegistrationViewModel : ViewModel() {
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.") }
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.") }
}
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.")
handleSessionStateResult(context, pushSubmissionResult)
}
@ -763,21 +763,26 @@ class RegistrationViewModel : ViewModel() {
var reglock = registrationLocked
val sessionId = getOrCreateValidSession(context)?.metadata?.id ?: return
val registrationData = getRegistrationData()
val session: SessionMetadataResult? = getOrCreateValidSession(context)
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 alreadyVerified = verificationResponse is AlreadyVerified
val submissionSuccessful = verificationResponse is Success
val alreadyVerified = verificationResponse is AlreadyVerified
Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
if (!submissionSuccessful && !alreadyVerified) {
handleSessionStateResult(context, verificationResponse)
return
if (!submissionSuccessful && !alreadyVerified) {
handleSessionStateResult(context, verificationResponse)
return
}
}
Log.d(TAG, "Submitting registration…")
@ -1002,24 +1007,22 @@ class RegistrationViewModel : ViewModel() {
password: String,
mcc: String?,
mnc: String?,
successListener: (RegistrationSessionMetadataResponse) -> Unit,
successListener: (SessionMetadataResult) -> Unit,
errorHandler: (RegistrationSessionResult) -> Unit
): RegistrationSessionMetadataResponse? {
): SessionMetadataResult? {
Log.d(TAG, "Validating/creating a registration session.")
val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc)
when (sessionResult) {
is RegistrationSessionCheckResult.Success -> {
val metadata = sessionResult.getMetadata()
successListener(metadata)
successListener(sessionResult)
Log.d(TAG, "Registration session validated.")
return metadata
return sessionResult
}
is RegistrationSessionCreationResult.Success -> {
val metadata = sessionResult.getMetadata()
successListener(metadata)
successListener(sessionResult)
Log.d(TAG, "Registration session created.")
return metadata
return sessionResult
}
else -> {

View file

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

View file

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

View file

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

View file

@ -5,19 +5,13 @@
package org.thoughtcrime.securesms.registrationv3.ui.phonenumber
import android.telephony.PhoneNumberFormattingTextWatcher
import android.text.TextWatcher
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
@ -27,14 +21,22 @@ import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
*/
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 formatter: TextWatcher?
get() = store.value.phoneNumberFormatter
val phoneNumber: PhoneNumber?
get() = try {
parsePhoneNumber(store.value)
@ -43,10 +45,6 @@ class EnterPhoneNumberViewModel : ViewModel() {
null
}
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
var e164VerificationMode: RegistrationRepository.E164VerificationMode
get() = store.value.mode
set(value) = store.update {
@ -69,19 +67,10 @@ class EnterPhoneNumberViewModel : ViewModel() {
}
store.update {
it.copy(countryPrefixIndex = matchingIndex)
}
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)
}
}
it.copy(
countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode
)
}
}
@ -103,6 +92,7 @@ class EnterPhoneNumberViewModel : ViewModel() {
store.update {
it.copy(
countryPrefixIndex = prefixIndex,
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
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 {
long serverDeliveredTimestamp = 0;
try {
String stringValue = getHeader.apply(SERVER_DELIVERED_TIMESTAMP_HEADER);
stringValue = stringValue != null ? stringValue : "0";
long retryAfterLong = Util.parseLong(getHeader.apply("Retry-After"), -1);
Long retryAfterMs = retryAfterLong != -1 ? TimeUnit.SECONDS.toMillis(retryAfterLong) : null;
RegistrationSessionMetadataJson responseBody = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class);
serverDeliveredTimestamp = Long.parseLong(stringValue);
} 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);
return new RegistrationSessionMetadataResponse(responseBody, System.currentTimeMillis(), retryAfterMs);
}
private static @Nonnull String urlEncode(@Nonnull String data) throws IOException {

View file

@ -1,21 +1,30 @@
package org.whispersystems.signalservice.internal.push
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.
* The useful headers and the request body are wrapped in a single holder class.
*/
data class RegistrationSessionMetadataResponse(
val headers: RegistrationSessionMetadataHeaders,
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(
val serverDeliveredTimestamp: Long,
val retryAfterTimestamp: Long? = null
)
fun deriveTimestamp(delta: Duration?): Duration {
if (delta == null) {
return 0.milliseconds
}
val now = System.currentTimeMillis().milliseconds
val base = clientReceivedAt.takeIf { clientReceivedAt <= now } ?: now
return base + delta
}
}
data class RegistrationSessionMetadataJson(
@JsonProperty("id") val id: String,
@ -34,7 +43,3 @@ data class RegistrationSessionMetadataJson(
return requestedInformation.contains("captcha")
}
}
data class RegistrationSessionState(
var pushChallengeTimedOut: Boolean
)