Initial support for restoring backups and skipping SMS in registration v2.

This commit is contained in:
Nicholas Tinsley
2024-04-29 15:17:22 -04:00
committed by Greyson Parrelli
parent fd4864b3b1
commit f23476a4e9
27 changed files with 881 additions and 233 deletions

View File

@@ -54,6 +54,7 @@ class SignalBackupAgent : BackupAgent() {
items.find { dataInput.key == it.getKey() }?.restoreData(buffer) items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
} }
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) } DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
Log.i(TAG, "Android Backup Service complete.")
} }
private fun cumulativeHashCode(): Int { private fun cumulativeHashCode(): Int {

View File

@@ -271,13 +271,6 @@ public class FullBackupImporter extends FullBackupBase {
return; return;
} }
if (FeatureFlags.registrationV2()) {
if (SignalStore.account().getKeysToIncludeInBackup().contains(keyValue.key)) {
Log.i(TAG, "[regv2] skipping restore of " + keyValue.key);
return;
}
}
if (keyValue.blobValue != null) { if (keyValue.blobValue != null) {
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray()); dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
} else if (keyValue.booleanValue != null) { } else if (keyValue.booleanValue != null) {

View File

@@ -217,6 +217,6 @@ public final class InternalValues extends SignalStoreValues {
} }
public boolean enterRestoreV2Flow() { public boolean enterRestoreV2Flow() {
return FeatureFlags.registrationV2() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false); return FeatureFlags.restoreAfterRegistration() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
} }
} }

View File

@@ -72,7 +72,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(root.findViewById(R.id.pin_restore_pin_title)); RegistrationViewDelegate.setDebugLogSubmitMultiTapView(root.findViewById(R.id.pin_restore_pin_title));
pinEntry = root.findViewById(R.id.pin_restore_pin_input); pinEntry = root.findViewById(R.id.pin_restore_pin_input);
pinButton = root.findViewById(R.id.pin_restore_pin_confirm); pinButton = root.findViewById(R.id.pin_restore_pin_continue);
errorLabel = root.findViewById(R.id.pin_restore_pin_input_label); errorLabel = root.findViewById(R.id.pin_restore_pin_input_label);
keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle); keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle);
helpButton = root.findViewById(R.id.pin_restore_forgot_pin); helpButton = root.findViewById(R.id.pin_restore_forgot_pin);

View File

@@ -207,7 +207,7 @@ class VerifyAccountRepository(private val context: Application) {
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
} }
interface MasterKeyProducer { fun interface MasterKeyProducer {
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class) @Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
fun produceMasterKey(): MasterKey fun produceMasterKey(): MasterKey
} }

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.registration.v2.data package org.thoughtcrime.securesms.registration.v2.data
import android.app.backup.BackupManager
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -12,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.util.KeyHelper import org.signal.libsignal.protocol.util.KeyHelper
@@ -39,6 +41,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.PushChallengeRequest import org.thoughtcrime.securesms.registration.PushChallengeRequest
import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.VerifyAccountRepository import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.service.DirectoryRefreshListener import org.thoughtcrime.securesms.service.DirectoryRefreshListener
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -47,13 +50,16 @@ import org.whispersystems.signalservice.api.account.AccountAttributes
import org.whispersystems.signalservice.api.account.PreKeyCollection import org.whispersystems.signalservice.api.account.PreKeyCollection
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.kbs.PinHashUtil
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.registration.RegistrationApi import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.nio.charset.StandardCharsets
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -227,6 +233,24 @@ object RegistrationRepository {
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis() metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
} }
fun canUseLocalRecoveryPassword(): Boolean {
val recoveryPassword = SignalStore.svr().recoveryPassword
val pinHash = SignalStore.svr().localPinHash
return recoveryPassword != null && pinHash != null
}
fun doesPinMatchLocalHash(pin: String): Boolean {
val pinHash = SignalStore.svr().localPinHash ?: throw IllegalStateException("Local PIN hash is not present!")
return PinHashUtil.verifyLocalPinHash(pinHash, pin)
}
suspend fun fetchMasterKeyFromSvrRemote(pin: String, authCredentials: AuthCredentials): MasterKey =
withContext(Dispatchers.IO) {
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, authCredentials), pin)
SignalStore.svr().setMasterKey(masterKey, pin)
return@withContext masterKey
}
/** /**
* Asks the service to send a verification code through one of our supported channels (SMS, phone call). * Asks the service to send a verification code through one of our supported channels (SMS, phone call).
* This requires two or more network calls: * This requires two or more network calls:
@@ -280,9 +304,9 @@ object RegistrationRepository {
/** /**
* Submit the necessary assets as a verified account so that the user can actually use the service. * Submit the necessary assets as a verified account so that the user can actually use the service.
*/ */
suspend fun registerAccount(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult<AccountRegistrationResult> = suspend fun registerAccount(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult<AccountRegistrationResult> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context) val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey) val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
@@ -335,27 +359,32 @@ object RegistrationRepository {
val eventBus = EventBus.getDefault() val eventBus = EventBus.getDefault()
eventBus.register(subscriber) eventBus.register(subscriber)
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc) try {
if (sessionCreationResponse !is NetworkResult.Success) { val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
return@withContext sessionCreationResponse if (sessionCreationResponse !is NetworkResult.Success) {
} return@withContext sessionCreationResponse
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
eventBus.unregister(subscriber)
if (receivedPush) {
val challenge = subscriber.challenge
if (challenge != null) {
Log.w(TAG, "Push challenge token received.")
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge)
} else {
Log.w(TAG, "Push received but challenge token was null.")
} }
} else {
Log.i(TAG, "Push challenge timed out.") val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
eventBus.unregister(subscriber)
if (receivedPush) {
val challenge = subscriber.challenge
if (challenge != null) {
Log.w(TAG, "Push challenge token received.")
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge)
} else {
Log.w(TAG, "Push received but challenge token was null.")
}
} else {
Log.i(TAG, "Push challenge timed out.")
}
Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.")
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(NullPointerException())
} catch (ex: Exception) {
Log.w(TAG, "Exception caught, but the earlier try block should have caught it?", ex) // TODO [regv2]: figure out why this exception is not caught
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(ex)
} }
Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.")
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(NullPointerException())
} }
@JvmStatic @JvmStatic
@@ -368,6 +397,39 @@ object RegistrationRepository {
return timestamp + deltaSeconds.seconds.inWholeMilliseconds return timestamp + deltaSeconds.seconds.inWholeMilliseconds
} }
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): AuthCredentials? =
withContext(Dispatchers.IO) {
val usernamePasswords = SignalStore.svr()
.authTokenList
.take(10)
.map {
it.replace("Basic ", "").trim()
}
.map {
Base64.decode(it) // TODO [regv2]: figure out why Android Studio doesn't like mapCatching
}
.map {
String(it, StandardCharsets.ISO_8859_1)
}
if (usernamePasswords.isEmpty()) {
return@withContext null
}
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val authCheck = api.getSvrAuthCredential(e164, usernamePasswords)
if (authCheck !is NetworkResult.Success) {
return@withContext null
}
val removedInvalidTokens = SignalStore.svr().removeAuthTokens(authCheck.result.invalid)
if (removedInvalidTokens) {
BackupManager(context).dataChanged()
}
return@withContext authCheck.result.match
}
enum class Mode(val isSmsRetrieverSupported: Boolean) { enum class Mode(val isSmsRetrieverSupported: Boolean) {
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false) SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
} }

View File

@@ -17,6 +17,7 @@ enum class RegistrationCheckpoint {
BACKUP_RESTORED, BACKUP_RESTORED,
PUSH_NETWORK_AUDITED, PUSH_NETWORK_AUDITED,
PHONE_NUMBER_CONFIRMED, PHONE_NUMBER_CONFIRMED,
PIN_CONFIRMED,
VERIFICATION_CODE_REQUESTED, VERIFICATION_CODE_REQUESTED,
CHALLENGE_RECEIVED, CHALLENGE_RECEIVED,
CHALLENGE_COMPLETED, CHALLENGE_COMPLETED,

View File

@@ -9,9 +9,16 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.navigation.ActivityNavigator
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
/** /**
* Activity to hold the entire registration process. * Activity to hold the entire registration process.
@@ -25,6 +32,43 @@ class RegistrationV2Activity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration_navigation_v2) setContentView(R.layout.activity_registration_navigation_v2)
sharedViewModel.uiState.observe(this) {
if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) {
handleSuccessfulVerify()
}
}
}
private fun handleSuccessfulVerify() {
// TODO [regv2]: add functionality of [RegistrationCompleteFragment]
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
val isAvatarEmpty = !AvatarHelper.hasAvatar(this, Recipient.self().id)
val needsProfile = isProfileNameEmpty || isAvatarEmpty
val needsPin = !sharedViewModel.hasPin()
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
sharedViewModel.setInProgress(false)
val startIntent = MainActivity.clearTop(this).apply {
if (needsPin) {
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity))
}
if (needsProfile) {
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity))
}
}
Log.d(TAG, "Launching ${startIntent.component}")
startActivity(startIntent)
finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
} }
companion object { companion object {

View File

@@ -6,6 +6,8 @@
package org.thoughtcrime.securesms.registration.v2.ui package org.thoughtcrime.securesms.registration.v2.ui
import com.google.i18n.phonenumbers.Phonenumber import com.google.i18n.phonenumbers.Phonenumber
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.internal.push.AuthCredentials
/** /**
* State holder shared across all of registration. * State holder shared across all of registration.
@@ -15,10 +17,16 @@ data class RegistrationV2State(
val phoneNumber: Phonenumber.PhoneNumber? = null, val phoneNumber: Phonenumber.PhoneNumber? = null,
val inProgress: Boolean = false, val inProgress: Boolean = false,
val isReRegister: Boolean = false, val isReRegister: Boolean = false,
val recoveryPassword: String? = SignalStore.svr().getRecoveryPassword(),
val canSkipSms: Boolean = false, val canSkipSms: Boolean = false,
val svrAuthCredentials: AuthCredentials? = null,
val svrTriesRemaining: Int = 10,
val isRegistrationLockEnabled: Boolean = false,
val userSkippedReregistration: Boolean = false,
val isFcmSupported: Boolean = false, val isFcmSupported: Boolean = false,
val fcmToken: String? = null, val fcmToken: String? = null,
val nextSms: Long = 0L, val nextSms: Long = 0L,
val nextCall: Long = 0L, val nextCall: Long = 0L,
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
val networkError: Throwable? = null
) )

View File

@@ -23,12 +23,17 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.SvrWrongPinException
import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.internal.push.LockedException
import java.io.IOException import java.io.IOException
/** /**
@@ -78,50 +83,168 @@ class RegistrationV2ViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
val fcmToken = RegistrationRepository.getFcmToken(context) val fcmToken = RegistrationRepository.getFcmToken(context)
store.update { store.update {
it.copy( it.copy(registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, isFcmSupported = true, fcmToken = fcmToken)
registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED,
isFcmSupported = true,
fcmToken = fcmToken
)
} }
} }
} }
private suspend fun updateFcmToken(context: Context): String? {
val fcmToken = RegistrationRepository.getFcmToken(context)
store.update {
it.copy(fcmToken = fcmToken)
}
return fcmToken
}
fun onBackupSuccessfullyRestored() {
val recoveryPassword = SignalStore.svr().recoveryPassword
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null)
}
}
fun onUserConfirmedPhoneNumber(context: Context) { fun onUserConfirmedPhoneNumber(context: Context) {
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED) setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
// TODO [regv2]: check if can skip sms flow
val state = store.value val state = store.value
if (state.phoneNumber == null) { if (state.phoneNumber == null) {
Log.w(TAG, "Phone number was null after confirmation.") Log.w(TAG, "Phone number was null after confirmation.")
onErrorOccurred() onErrorOccurred()
return return
} }
if (state.canSkipSms) {
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2] // TODO [regv2]: initialize Play Services sms retriever
val mccMncProducer = MccMncProducer(context)
val e164 = state.phoneNumber.toE164()
if (hasRecoveryPassword() && matchesSavedE164(e164)) {
// Re-registration when the local database is intact.
store.update {
it.copy(canSkipSms = true)
}
} else { } else {
// TODO [regv2]: initialize Play Services sms retriever
val mccMncProducer = MccMncProducer(context)
val e164 = state.phoneNumber.toE164()
viewModelScope.launch { viewModelScope.launch {
val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow() val svrCredentials = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
store.update {
it.copy( if (svrCredentials != null) {
sessionId = codeRequestResponse.body.id, // Re-registration when credentials stored in backup.
nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms), store.update {
nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall), it.copy(canSkipSms = true, svrAuthCredentials = svrCredentials)
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED }
) } else {
val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow()
store.update {
it.copy(sessionId = codeRequestResponse.body.id, nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms), nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall), registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
}
} }
} }
} }
} }
private fun setRecoveryPassword(recoveryPassword: String?) {
store.update {
it.copy(recoveryPassword = recoveryPassword)
}
}
private fun updateSvrTriesRemaining(remainingTries: Int) {
store.update {
it.copy(svrTriesRemaining = remainingTries)
}
}
fun setUserSkippedReRegisterFlow(value: Boolean) {
store.update {
it.copy(userSkippedReregistration = value, canSkipSms = !value)
}
}
fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) {
setInProgress(true)
// Local recovery password
if (RegistrationRepository.canUseLocalRecoveryPassword()) {
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
Log.d(TAG, "Found recovery password, attempting to re-register.")
viewModelScope.launch {
verifyReRegisterInternal(context, pin, SignalStore.svr().getOrCreateMasterKey())
setInProgress(false)
}
} else {
Log.d(TAG, "Entered PIN did not match local PIN hash.")
wrongPinHandler()
setInProgress(false)
}
return
}
// remote recovery password
val authCredentials = store.value.svrAuthCredentials
if (authCredentials != null) {
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR.")
viewModelScope.launch {
try {
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, authCredentials)
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
updateSvrTriesRemaining(10)
verifyReRegisterInternal(context, pin, masterKey)
} catch (rejectedPin: SvrWrongPinException) {
Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin)
updateSvrTriesRemaining(rejectedPin.triesRemaining)
wrongPinHandler()
} catch (noData: SvrNoDataException) {
Log.w(TAG, "SVR has no data for these credentials. Aborting skip SMS flow.", noData)
setUserSkippedReRegisterFlow(true)
}
setInProgress(false)
}
return
}
Log.w(TAG, "Could not get credentials to skip SMS registration, aborting!")
// TODO [regv2]: Investigate why in v1, this case throws a [IncorrectRegistrationRecoveryPasswordException], which seems weird.
store.update {
it.copy(canSkipSms = false, inProgress = false)
}
}
private suspend fun verifyReRegisterInternal(context: Context, pin: String, masterKey: MasterKey) {
updateFcmToken(context)
val registrationData = getRegistrationData("")
val resultAndRegLockStatus = registerAccountInternal(context, null, registrationData, pin, masterKey)
val result = resultAndRegLockStatus.first
val reglockEnabled = resultAndRegLockStatus.second
if (result !is NetworkResult.Success) {
Log.w(TAG, "Error during registration!", result.getCause())
return
}
onSuccessfulRegistration(context, registrationData, result.result, reglockEnabled)
}
private suspend fun registerAccountInternal(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String?, masterKey: MasterKey): Pair<NetworkResult<RegistrationRepository.AccountRegistrationResult>, Boolean> {
val registrationResult = 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 NetworkResult.StatusCodeError || registrationResult.exception !is LockedException) {
return Pair(registrationResult, false)
}
Log.i(TAG, "Received a registration lock response when trying to register an account. Retrying with master key.")
val lockedException = registrationResult.exception as LockedException
store.update {
it.copy(svrAuthCredentials = lockedException.svr2Credentials)
}
return Pair(RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }, true)
}
fun verifyCodeWithoutRegistrationLock(context: Context, code: String) { fun verifyCodeWithoutRegistrationLock(context: Context, code: String) {
store.update { store.update {
it.copy( it.copy(inProgress = true, registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED)
inProgress = true,
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED
)
} }
val sessionId = store.value.sessionId val sessionId = store.value.sessionId
@@ -143,17 +266,18 @@ class RegistrationV2ViewModel : ViewModel() {
setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED) setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED)
val registrationResponse = RegistrationRepository.registerAccount(context, e164, password, sessionId, registrationData).successOrThrow() val registrationResponse = RegistrationRepository.registerAccount(context, sessionId, registrationData).successOrThrow()
onSuccessfulRegistration(context, registrationData, registrationResponse, false)
}
}
localRegisterAccount(context, registrationData, registrationResponse, false) private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) {
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
refreshFeatureFlags() refreshFeatureFlags()
store.update { store.update {
it.copy( it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED
)
}
} }
} }
@@ -162,38 +286,31 @@ class RegistrationV2ViewModel : ViewModel() {
} }
fun completeRegistration() { fun completeRegistration() {
ApplicationDependencies.getJobManager() ApplicationDependencies.getJobManager().startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue()
.startChain(ProfileUploadJob())
.then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob()))
.enqueue()
RegistrationUtil.maybeMarkRegistrationComplete() RegistrationUtil.maybeMarkRegistrationComplete()
} }
private fun matchesSavedE164(e164: String?): Boolean {
return if (e164 == null) {
false
} else {
e164 == SignalStore.account().e164
}
}
private fun hasRecoveryPassword(): Boolean {
return store.value.recoveryPassword != null
}
private fun getCurrentE164(): String? { private fun getCurrentE164(): String? {
return store.value.phoneNumber?.toE164() return store.value.phoneNumber?.toE164()
} }
private suspend fun localRegisterAccount(
context: Context,
registrationData: RegistrationData,
remoteResult: RegistrationRepository.AccountRegistrationResult,
reglockEnabled: Boolean
) {
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
}
private suspend fun getRegistrationData(code: String): RegistrationData { private suspend fun getRegistrationData(code: String): RegistrationData {
val e164: String = getCurrentE164() ?: throw IllegalStateException() val currentState = store.value
return RegistrationData( val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException()
code, val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr().getRecoveryPassword() else null
e164, return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), currentState.fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
password,
RegistrationRepository.getRegistrationId(),
RegistrationRepository.getProfileKey(e164),
store.value.fcmToken,
RegistrationRepository.getPniRegistrationId(),
null // TODO [regv2]: recovery password
)
} }
/** /**

View File

@@ -9,19 +9,12 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.ActivityNavigator
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
@@ -74,45 +67,6 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
} }
} }
} }
sharedViewModel.uiState.observe(viewLifecycleOwner) {
if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) {
handleSuccessfulVerify()
}
}
}
private fun handleSuccessfulVerify() {
// TODO [regv2]: add functionality of [RegistrationCompleteFragment]
val activity = requireActivity()
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id)
val needsProfile = isProfileNameEmpty || isAvatarEmpty
val needsPin = !sharedViewModel.hasPin()
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
sharedViewModel.setInProgress(false)
val startIntent = MainActivity.clearTop(activity).apply {
if (needsPin) {
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(activity))
}
if (needsProfile) {
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(activity))
}
}
Log.d(TAG, "Launching ${startIntent.component}")
activity.startActivity(startIntent)
activity.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
} }
private fun popBackStack() { private fun popBackStack() {

View File

@@ -5,15 +5,18 @@
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@@ -22,8 +25,8 @@ import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -33,29 +36,31 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
@RequiresApi(23) @RequiresApi(23)
class GrantPermissionsV2Fragment : ComposeFragment() { class GrantPermissionsV2Fragment : ComposeFragment() {
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>() private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val args by navArgs<GrantPermissionsV2FragmentArgs>() private val args by navArgs<GrantPermissionsV2FragmentArgs>()
private val isSearchingForBackup = mutableStateOf(false) private val isSearchingForBackup = mutableStateOf(false)
private val requestPermissionLauncher = registerForActivityResult( private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(), ActivityResultContracts.RequestMultiplePermissions(),
::permissionsGranted ::onPermissionsGranted
) )
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
super.onViewCreated(view, savedInstanceState) when (val resultCode = result.resultCode) {
sharedViewModel.uiState.observe(viewLifecycleOwner) { Activity.RESULT_OK -> {
if (it.registrationCheckpoint >= RegistrationCheckpoint.PERMISSIONS_GRANTED) { sharedViewModel.onBackupSuccessfullyRestored()
proceedToNextScreen(it) NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
} }
Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.")
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
} }
} }
private fun proceedToNextScreen(it: RegistrationV2State) { private lateinit var welcomeAction: WelcomeAction
// TODO [nicholas]: conditionally go to backup flow
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionSkipRestore()) override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
welcomeAction = args.welcomeAction
} }
@Composable @Composable
@@ -66,40 +71,41 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
deviceBuildVersion = Build.VERSION.SDK_INT, deviceBuildVersion = Build.VERSION.SDK_INT,
isSearchingForBackup = isSearchingForBackup, isSearchingForBackup = isSearchingForBackup,
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current), isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
onNextClicked = this::onNextClicked, onNextClicked = this::launchPermissionRequests,
onNotNowClicked = this::onNotNowClicked onNotNowClicked = this::proceedToNextScreen
) )
} }
private fun onNextClicked() { private fun launchPermissionRequests() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> continueNext()
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
}
}
private fun continueNext() {
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext()) val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
val requiredPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)
requestPermissionLauncher.launch(requiredPermissions)
}
private fun onNotNowClicked() { val neededPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).filterNot {
when (args.welcomeAction) { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
WelcomeAction.CONTINUE -> continueNotNow() }
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
if (neededPermissions.isEmpty()) {
proceedToNextScreen()
} else {
requestPermissionLauncher.launch(neededPermissions.toTypedArray())
} }
} }
private fun continueNotNow() { private fun onPermissionsGranted(permissions: Map<String, Boolean>) {
NavHostFragment.findNavController(this).popBackStack()
}
private fun permissionsGranted(permissions: Map<String, Boolean>) {
permissions.forEach { permissions.forEach {
Log.d(TAG, "${it.key} = ${it.value}") Log.d(TAG, "${it.key} = ${it.value}")
} }
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED) sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
proceedToNextScreen()
}
private fun proceedToNextScreen() {
when (welcomeAction) {
WelcomeAction.CONTINUE -> NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
WelcomeAction.RESTORE_BACKUP -> {
val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
launchRestoreActivity.launch(restoreIntent)
}
}
} }
/** /**
@@ -110,4 +116,8 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
CONTINUE, CONTINUE,
RESTORE_BACKUP RESTORE_BACKUP
} }
companion object {
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
}
} }

View File

@@ -24,6 +24,7 @@ import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -95,7 +96,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState -> sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
presentRegisterButton(sharedState) presentRegisterButton(sharedState)
presentProgressBar(sharedState.inProgress, sharedState.isReRegister) presentProgressBar(sharedState.inProgress, sharedState.isReRegister)
if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) { if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
moveToEnterPinScreen()
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
moveToVerificationEntryScreen() moveToVerificationEntryScreen()
} }
} }
@@ -320,6 +323,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}.show() }.show()
} }
private fun moveToEnterPinScreen() {
sharedViewModel.setInProgress(false)
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment())
}
private fun moveToVerificationEntryScreen() { private fun moveToVerificationEntryScreen() {
NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode()) NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
sharedViewModel.setInProgress(false) sharedViewModel.setInProgress(false)

View File

@@ -0,0 +1,243 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.FragmentRegistrationPinRestoreEntryV2Binding
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
companion object {
private val TAG = Log.tag(ReRegisterWithPinV2Fragment::class.java)
}
private val registrationViewModel by activityViewModels<RegistrationV2ViewModel>()
private val reRegisterViewModel by viewModels<ReRegisterWithPinV2ViewModel>()
private val binding: FragmentRegistrationPinRestoreEntryV2Binding by ViewBinderDelegate(FragmentRegistrationPinRestoreEntryV2Binding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.pinRestorePinTitle)
binding.pinRestorePinDescription.setText(R.string.RegistrationLockFragment__enter_the_pin_you_created_for_your_account)
binding.pinRestoreForgotPin.visibility = View.GONE
binding.pinRestoreForgotPin.setOnClickListener { onNeedHelpClicked() }
binding.pinRestoreSkipButton.setOnClickListener { onSkipClicked() }
binding.pinRestorePinInput.imeOptions = EditorInfo.IME_ACTION_DONE
binding.pinRestorePinInput.setOnEditorActionListener { v, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v!!)
handlePinEntry()
return@setOnEditorActionListener true
}
false
}
enableAndFocusPinEntry()
binding.pinRestorePinContinue.setOnClickListener {
handlePinEntry()
}
binding.pinRestoreKeyboardToggle.setOnClickListener {
val currentKeyboardType: PinKeyboardType = getPinEntryKeyboardType()
updateKeyboard(currentKeyboardType.other)
binding.pinRestoreKeyboardToggle.setIconResource(currentKeyboardType.iconResource)
}
binding.pinRestoreKeyboardToggle.setIconResource(getPinEntryKeyboardType().other.iconResource)
registrationViewModel.uiState.observe(viewLifecycleOwner, ::updateViewState)
}
private fun updateViewState(state: RegistrationV2State) {
if (state.networkError != null) {
genericErrorDialog()
} else if (!state.canSkipSms) {
abortSkipSmsFlow()
} else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) {
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
onAccountLocked()
} else {
presentProgress(state.inProgress)
presentTriesRemaining(state.svrTriesRemaining)
}
}
private fun abortSkipSmsFlow() {
findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
}
private fun presentProgress(inProgress: Boolean) {
if (inProgress) {
ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput)
binding.pinRestorePinInput.isEnabled = false
binding.pinRestorePinContinue.setSpinning()
} else {
binding.pinRestorePinInput.isEnabled = true
binding.pinRestorePinContinue.cancelSpinning()
}
}
private fun handlePinEntry() {
val pin: String? = binding.pinRestorePinInput.text?.toString()
if (pin.isNullOrBlank()) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
if (pin.trim().length < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) {
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
registrationViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PIN_CONFIRMED)
registrationViewModel.verifyReRegisterWithPin(requireContext(), pin) {
reRegisterViewModel.markIncorrectGuess()
}
// TODO [regv2]: check for registration lock + wrong pin and decrement SVR tries remaining
}
private fun presentTriesRemaining(triesRemaining: Int) {
if (reRegisterViewModel.hasIncorrectGuess) {
if (triesRemaining == 1 && !reRegisterViewModel.isLocalVerification) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
if (triesRemaining > 5) {
binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin)
} else {
binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)
}
binding.pinRestoreForgotPin.visibility = View.VISIBLE
} else {
if (triesRemaining == 1) {
binding.pinRestoreForgotPin.visibility = View.VISIBLE
if (!reRegisterViewModel.isLocalVerification) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
if (triesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.")
onAccountLocked()
}
}
private fun onAccountLocked() {
Log.d(TAG, "Showing Incorrect PIN dialog. Is local verification: ${reRegisterViewModel.isLocalVerification}")
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() }
.setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) }
.show()
}
private fun enableAndFocusPinEntry() {
binding.pinRestorePinInput.isEnabled = true
binding.pinRestorePinInput.isFocusable = true
ViewUtil.focusAndShowKeyboard(binding.pinRestorePinInput)
}
private fun getPinEntryKeyboardType(): PinKeyboardType {
val isNumeric = binding.pinRestorePinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
}
private fun updateKeyboard(keyboard: PinKeyboardType) {
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
binding.pinRestorePinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
binding.pinRestorePinInput.text?.clear()
}
private fun onNeedHelpClicked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_need_help)
.setMessage(getString(message, SvrConstants.MINIMUM_PIN_LENGTH))
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ ->
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null)
CommunicationActions.openEmail(
requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(R.string.ReRegisterWithPinFragment_support_email_subject),
body
)
}
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show()
}
private fun onSkipClicked() {
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
.setMessage(message)
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
.show()
}
private fun onSkipPinEntry() {
Log.d(TAG, "User skipping PIN entry.")
registrationViewModel.setUserSkippedReRegisterFlow(true)
}
private fun genericErrorDialog() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.RegistrationActivity_error_connecting_to_service)
.setPositiveButton(android.R.string.ok, null)
.create()
.show()
}
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
data class ReRegisterWithPinV2State(
val isLocalVerification: Boolean = false,
val hasIncorrectGuess: Boolean = false,
val localPinMatches: Boolean = false
)

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.signal.core.util.logging.Log
class ReRegisterWithPinV2ViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(ReRegisterWithPinV2ViewModel::class.java)
}
private val store = MutableStateFlow(ReRegisterWithPinV2State())
val uiState = store.asLiveData()
val isLocalVerification: Boolean
get() = store.value.isLocalVerification
val hasIncorrectGuess: Boolean
get() = store.value.hasIncorrectGuess
fun markIncorrectGuess() {
store.update {
it.copy(hasIncorrectGuess = true)
}
}
}

View File

@@ -6,12 +6,15 @@
package org.thoughtcrime.securesms.registration.v2.ui.welcome package org.thoughtcrime.securesms.registration.v2.ui.welcome
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
@@ -20,8 +23,10 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Bindi
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -36,6 +41,20 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>() private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind) private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
when (val resultCode = result.resultCode) {
Activity.RESULT_OK -> {
sharedViewModel.onBackupSuccessfullyRestored()
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionGoToRegistration())
}
Activity.RESULT_CANCELED -> {
Log.w(TAG, "Backup restoration canceled.")
findNavController().popBackStack()
}
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
maybePrefillE164() maybePrefillE164()
@@ -43,12 +62,13 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
setDebugLogSubmitMultiTapView(binding.title) setDebugLogSubmitMultiTapView(binding.title)
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() } binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() } binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
} }
private fun onContinueClicked() { private fun onContinueClicked() {
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true) TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) { if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE)) findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
} else { } else {
skipRestore() skipRestore()
} }
@@ -60,13 +80,24 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
} }
private fun skipRestore() { private fun skipRestore() {
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore()) findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
} }
private fun onTermsClicked() { private fun onTermsClicked() {
CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL) CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL)
} }
private fun onTransferOrRestoreClicked() {
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.RESTORE_BACKUP))
} else {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
launchRestoreActivity.launch(restoreIntent)
}
}
private fun maybePrefillE164() { private fun maybePrefillE164() {
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull() val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()

View File

@@ -23,12 +23,20 @@ class RestoreActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
setContentView(R.layout.activity_restore) setContentView(R.layout.activity_restore)
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let { intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
sharedViewModel.setNextIntent(it) sharedViewModel.setNextIntent(it)
} }
} }
fun finishActivitySuccessfully() {
setResult(RESULT_OK)
finish()
}
companion object { companion object {
@JvmStatic @JvmStatic
fun getIntentForRestore(context: Context): Intent { fun getIntentForRestore(context: Context): Intent {

View File

@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Bindin
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.restore.RestoreRepository import org.thoughtcrime.securesms.restore.RestoreRepository
import org.thoughtcrime.securesms.restore.RestoreViewModel import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
@@ -52,12 +53,11 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
setDebugLogSubmitMultiTapView(binding.verifyHeader) setDebugLogSubmitMultiTapView(binding.verifyHeader)
Log.i(TAG, "Backup restore.") Log.i(TAG, "Backup restore.")
binding.restoreButton.setOnClickListener { presentBackupPassPhrasePromptDialog() }
restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState -> restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
fragmentState.backupInfo?.let { fragmentState.backupInfo?.let {
presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp) presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp)
if (fragmentState.backupPassphrase.isEmpty()) {
presentBackupPassPhrasePromptDialog()
}
} }
if (fragmentState.restoreInProgress) { if (fragmentState.restoreInProgress) {
@@ -71,23 +71,23 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
if (importResult == null) { if (importResult == null) {
onBackupCompletedSuccessfully() onBackupCompletedSuccessfully()
} else { } else {
handleBackupImportResult(importResult) handleBackupImportError(importResult)
} }
} }
} }
restoreLocalBackupViewModel.startRestore(requireContext()) restoreLocalBackupViewModel.prepareRestore(requireContext())
} }
private fun onBackupCompletedSuccessfully() { private fun onBackupCompletedSuccessfully() {
Log.d(TAG, "onBackupCompletedSuccessfully()") Log.d(TAG, "onBackupCompletedSuccessfully()")
SignalStore.internalValues().setForceEnterRestoreV2Flow(false) SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
val activity = requireActivity() val activity = requireActivity() as RestoreActivity
navigationViewModel.getNextIntent()?.let { navigationViewModel.getNextIntent()?.let {
Log.d(TAG, "Launching ${it.component}") Log.d(TAG, "Launching ${it.component}")
activity.startActivity(it) activity.startActivity(it)
} }
activity.finish() activity.finishActivitySuccessfully()
} }
override fun onStart() { override fun onStart() {
@@ -105,12 +105,12 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
restoreLocalBackupViewModel.onBackupProgressUpdate(event) restoreLocalBackupViewModel.onBackupProgressUpdate(event)
} }
private fun handleBackupImportResult(importResult: RestoreRepository.BackupImportResult) { private fun handleBackupImportError(importResult: RestoreRepository.BackupImportResult) {
when (importResult) { when (importResult) {
RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show() RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show() RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show()
RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show() RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled here.", IllegalStateException()) RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled in this function.", IllegalStateException())
} }
} }
@@ -143,7 +143,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
ViewUtil.hideKeyboard(requireContext(), prompt) ViewUtil.hideKeyboard(requireContext(), prompt)
val passphrase = prompt.getText().toString() val passphrase = prompt.getText().toString()
restoreLocalBackupViewModel.confirmPassphrase(requireContext(), passphrase) restoreLocalBackupViewModel.confirmPassphraseAndBeginRestore(requireContext(), passphrase)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()

View File

@@ -24,7 +24,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri)) private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri))
val uiState = store.asLiveData() val uiState = store.asLiveData()
fun startRestore(context: Context) { fun prepareRestore(context: Context) {
val backupFileUri = store.value.uri val backupFileUri = store.value.uri
viewModelScope.launch { viewModelScope.launch {
val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri) val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri)
@@ -48,7 +48,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
} }
} }
fun confirmPassphrase(context: Context, passphrase: String) { fun confirmPassphraseAndBeginRestore(context: Context, passphrase: String) {
store.update { store.update {
it.copy( it.copy(
backupPassphrase = passphrase, backupPassphrase = passphrase,

View File

@@ -130,6 +130,7 @@ public final class FeatureFlags {
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController"; private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
private static final String REGISTRATION_V2 = "android.registration.v2"; private static final String REGISTRATION_V2 = "android.registration.v2";
private static final String LIBSIGNAL_WEB_SOCKET_ENABLED = "android.libsignalWebSocketEnabled"; private static final String LIBSIGNAL_WEB_SOCKET_ENABLED = "android.libsignalWebSocketEnabled";
private static final String RESTORE_POST_REGISTRATION = "android.registration.restorePostRegistration";
/** /**
* We will only store remote values for flags in this set. If you want a flag to be controllable * We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -214,7 +215,7 @@ public final class FeatureFlags {
); );
@VisibleForTesting @VisibleForTesting
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2); static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION);
/** /**
* Values in this map will take precedence over any value. This should only be used for local * Values in this map will take precedence over any value. This should only be used for local
@@ -759,6 +760,11 @@ public final class FeatureFlags {
/** Whether unauthenticated chat web socket is backed by libsignal-net */ /** Whether unauthenticated chat web socket is backed by libsignal-net */
public static boolean libSignalWebSocketEnabled() { return getBoolean(LIBSIGNAL_WEB_SOCKET_ENABLED, false); } public static boolean libSignalWebSocketEnabled() { return getBoolean(LIBSIGNAL_WEB_SOCKET_ENABLED, false); }
/** Whether or not to launch the restore activity after registration is complete, rather than before. */
public static boolean restoreAfterRegistration() {
return getBoolean(RESTORE_POST_REGISTRATION, false);
}
/** Only for rendering debug info. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() { public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES); return new TreeMap<>(REMOTE_VALUES);

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/pin_restore_pin_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="24dp"
android:text="@string/RegistrationLockFragment__enter_your_pin"
android:textAppearance="@style/Signal.Text.HeadlineMedium"
app:layout_constraintBottom_toTopOf="@id/pin_restore_pin_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/pin_restore_pin_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:minHeight="66dp"
android:text="@string/RegistrationLockFragment__enter_the_pin_you_created"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_kbs_textinputlayout"
style="@style/Widget.Signal.TextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:minWidth="210dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pin_restore_pin_description"
app:materialThemeOverlay="@style/Signal.ThemeOverlay.TextInputLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin_restore_pin_input"
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:imeOptions="actionDone"
android:inputType="numberPassword"
tools:text="1234567890" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/pin_restore_pin_input_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
android:textAppearance="@style/Signal.Text.BodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_textinputlayout"
tools:text="@string/RegistrationLockFragment__incorrect_pin_try_again" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pin_restore_forgot_pin"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/PinRestoreEntryFragment_need_help"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_input_label"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pin_restore_keyboard_toggle"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/RegistrationLockFragment__switch_keyboard"
app:icon="@drawable/ic_keyboard_24"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:layout_constraintTop_toBottomOf="@id/pin_restore_forgot_pin"
app:layout_constraintVertical_bias="0.0"
tools:layout_editor_absoluteX="32dp" />
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
android:id="@+id/pin_restore_pin_continue"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
app:circularProgressMaterialButton__label="@string/RegistrationActivity_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pin_restore_skip_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/PinRestoreEntryFragment_skip"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -55,7 +55,6 @@
app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin" app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin"
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" /> app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
<!-- TODO [regv2]: delete this -->
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/welcome_transfer_or_restore" android:id="@+id/welcome_transfer_or_restore"
style="@style/Signal.Widget.Button.Large.Secondary" style="@style/Signal.Widget.Button.Large.Secondary"

View File

@@ -11,14 +11,6 @@
android:label="fragment_welcome" android:label="fragment_welcome"
tools:layout="@layout/fragment_registration_welcome_v2"> tools:layout="@layout/fragment_registration_welcome_v2">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action <action
android:id="@+id/action_skip_restore" android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberV2Fragment" app:destination="@id/enterPhoneNumberV2Fragment"
@@ -28,16 +20,8 @@
app:popExitAnim="@anim/nav_default_pop_exit_anim" /> app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action <action
android:id="@+id/action_transfer_or_restore" android:id="@+id/action_go_to_registration"
app:destination="@id/transferOrRestore" app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_welcomeFragment_to_deviceTransferSetup"
app:destination="@id/deviceTransferSetup"
app:enterAnim="@anim/nav_default_enter_anim" app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim" app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim"
@@ -59,24 +43,10 @@
android:label="fragment_grant_permissions"> android:label="fragment_grant_permissions">
<action <action
android:id="@+id/action_restore" android:id="@+id/action_enter_phone_number"
app:destination="@id/restoreBackupV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberV2Fragment" app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim" app:popUpTo="@+id/welcomeV2Fragment"
app:exitAnim="@anim/nav_default_exit_anim" app:popUpToInclusive="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_transfer_or_restore"
app:destination="@id/transferOrRestore"
app:enterAnim="@anim/nav_default_enter_anim" app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim" app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim"
@@ -120,16 +90,7 @@
android:label="fragment_enter_phone_number" android:label="fragment_enter_phone_number"
tools:layout="@layout/fragment_registration_enter_phone_number_v2"> tools:layout="@layout/fragment_registration_enter_phone_number_v2">
<action <action
android:id="@+id/action_pickCountry"
app:destination="@id/countryPickerV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:launchSingleTop="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_enterVerificationCode" android:id="@+id/action_enterVerificationCode"
app:destination="@id/enterCodeV2Fragment" app:destination="@id/enterCodeV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim" app:enterAnim="@anim/nav_default_enter_anim"
@@ -247,7 +208,7 @@
<fragment <fragment
android:id="@+id/reRegisterWithPinV2Fragment" android:id="@+id/reRegisterWithPinV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment" android:name="org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin.ReRegisterWithPinV2Fragment"
tools:layout="@layout/fragment_registration_lock"> tools:layout="@layout/fragment_registration_lock">
<action <action

View File

@@ -64,6 +64,18 @@ sealed class NetworkResult<T> {
} }
} }
/**
* Returns the [Throwable] associated with the result, or null if the result is successful.
*/
fun getCause(): Throwable? {
return when (this) {
is Success -> null
is NetworkError -> exception
is StatusCodeError -> exception
is ApplicationError -> throwable
}
}
/** /**
* Takes the output of one [NetworkResult] and transforms it into another if the operation is successful. * Takes the output of one [NetworkResult] and transforms it into another if the operation is successful.
* If it's a failure, the original failure will be propagated. Useful for changing the type of a result. * If it's a failure, the original failure will be propagated. Useful for changing the type of a result.

View File

@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.registration
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.account.AccountAttributes import org.whispersystems.signalservice.api.account.AccountAttributes
import org.whispersystems.signalservice.api.account.PreKeyCollection import org.whispersystems.signalservice.api.account.PreKeyCollection
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse
import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
@@ -67,4 +68,10 @@ class RegistrationApi(
pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer) pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer)
} }
} }
fun getSvrAuthCredential(e164: String, usernamePasswords: List<String>): NetworkResult<BackupAuthCheckResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords)
}
}
} }

View File

@@ -1137,6 +1137,11 @@ public class PushServiceSocket {
.onErrorReturn(ServiceResponse::forUnknownError); .onErrorReturn(ServiceResponse::forUnknownError);
} }
public BackupAuthCheckResponse checkBackupAuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
String response = makeServiceRequest(BACKUP_AUTH_CHECK, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
return JsonUtil.fromJson(response, BackupAuthCheckResponse.class);
}
private Single<ServiceResponse<BackupAuthCheckResponse>> createBackupAuthCheckSingle(@Nonnull String path, private Single<ServiceResponse<BackupAuthCheckResponse>> createBackupAuthCheckSingle(@Nonnull String path,
@Nonnull BackupAuthCheckRequest request, @Nonnull BackupAuthCheckRequest request,
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper) @Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
@@ -3048,7 +3053,6 @@ public class PushServiceSocket {
} }
} }
private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException { private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException {
long serverDeliveredTimestamp = 0; long serverDeliveredTimestamp = 0;
try { try {