Fix bugs around requesting and entering verification codes.
This commit is contained in:
parent
9823563ef7
commit
04c903c4c4
22 changed files with 381 additions and 307 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
|||
|
||||
private final Callback callback;
|
||||
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 {
|
||||
if (hasLowSignal) {
|
||||
hasLowSignal = false;
|
||||
Log.i(TAG, "Cell signal detected");
|
||||
}
|
||||
debouncer.clear();
|
||||
callback.onCellSignalPresent();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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 {
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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() {
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,9 +763,13 @@ 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()
|
||||
|
||||
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)
|
||||
|
@ -779,6 +783,7 @@ class RegistrationViewModel : ViewModel() {
|
|||
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 -> {
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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 {
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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() {
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
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);
|
||||
Long retryAfterMs = retryAfterLong != -1 ? TimeUnit.SECONDS.toMillis(retryAfterLong) : null;
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue