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