diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt index 93b7656989..1d18304eb8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt index 5df537abc9..8e2b13e583 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt index c71a1ca695..fabf0df98b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt index 54d406c3d8..d3e4a46291 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt index 0eabe11c7d..b7f12b3d2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt @@ -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 + 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): 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, + 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): 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, + override val verified: Boolean + ) : RegistrationSessionCheckResult(null), SessionMetadataResult class SessionNotFound(cause: Throwable) : RegistrationSessionCheckResult(cause) class UnknownError(cause: Throwable) : RegistrationSessionCheckResult(cause) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt index d5ead2a23c..3cff779bda 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt @@ -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, val verified: Boolean) : VerificationCodeRequestResult(null) + class Success(val sessionId: String, val nextSmsTimestamp: Duration, val nextCallTimestamp: Duration, nextVerificationAttempt: Duration, val allowedToRequestCode: Boolean, challengesRequested: List, val verified: Boolean) : VerificationCodeRequestResult(null) class ChallengeRequired(val challenges: List) : 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" } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java index a21eb99994..abbe6d0048 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java @@ -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(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt index 8a3a067ddf..45af3ceb2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt @@ -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 = 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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 81855f005a..ba657a4428 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -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 -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt index 5943d1491c..879a4d2341 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt @@ -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) { + when (remainingChallenges.first()) { + Challenge.CAPTCHA -> moveToCaptcha() + Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext()) + } + } + private fun presentAccountLocked() { binding.keyboard.displayLocked().addListener( object : AssertedSuccessListener() { @@ -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.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt index 21079fe2f3..358854661d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt index 707879cafa..b037311133 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt @@ -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 ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt index 128a3b04f5..e9c5f7c061 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt @@ -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 = 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 = 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() ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt index b3ad999f13..fa58d969eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationState.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationState.kt index 4bcffe037d..7853313fef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationState.kt @@ -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 = 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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt index 4cc9711b04..d00a813ea0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt @@ -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 -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt index 4585b185d8..65e5e69a9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt @@ -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) { + when (remainingChallenges.first()) { + Challenge.CAPTCHA -> moveToCaptcha() + Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext()) + } + } + private fun presentAccountLocked() { binding.keyboard.displayLocked().addListener( object : AssertedSuccessListener() { @@ -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.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt index e4a03ca13d..a1a4db21c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberState.kt index eb4f0f0ca6..cac2d6466c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberState.kt @@ -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 ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt index 0d10f620fc..fb6e89f913 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberViewModel.kt @@ -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 = 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 = 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() ) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 151b48c3da..146d3eaf61 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -3309,23 +3309,11 @@ public class PushServiceSocket { } private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(ResponseBody body, Function 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 { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt index b0012b70f3..f1d34df168 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt @@ -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 -)