mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Initial support for restoring backups and skipping SMS in registration v2.
This commit is contained in:
committed by
Greyson Parrelli
parent
fd4864b3b1
commit
f23476a4e9
@@ -54,6 +54,7 @@ class SignalBackupAgent : BackupAgent() {
|
||||
items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
|
||||
}
|
||||
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
|
||||
Log.i(TAG, "Android Backup Service complete.")
|
||||
}
|
||||
|
||||
private fun cumulativeHashCode(): Int {
|
||||
|
||||
@@ -271,13 +271,6 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
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) {
|
||||
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
|
||||
} else if (keyValue.booleanValue != null) {
|
||||
|
||||
@@ -217,6 +217,6 @@ public final class InternalValues extends SignalStoreValues {
|
||||
}
|
||||
|
||||
public boolean enterRestoreV2Flow() {
|
||||
return FeatureFlags.registrationV2() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
|
||||
return FeatureFlags.restoreAfterRegistration() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(root.findViewById(R.id.pin_restore_pin_title));
|
||||
|
||||
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);
|
||||
keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle);
|
||||
helpButton = root.findViewById(R.id.pin_restore_forgot_pin);
|
||||
|
||||
@@ -207,7 +207,7 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
interface MasterKeyProducer {
|
||||
fun interface MasterKeyProducer {
|
||||
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
||||
fun produceMasterKey(): MasterKey
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -12,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
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.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
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.crypto.UnidentifiedAccess
|
||||
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.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
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.RegistrationSessionMetadataResponse
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -227,6 +233,24 @@ object RegistrationRepository {
|
||||
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).
|
||||
* 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.
|
||||
*/
|
||||
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) {
|
||||
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 unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
@@ -335,6 +359,7 @@ object RegistrationRepository {
|
||||
val eventBus = EventBus.getDefault()
|
||||
eventBus.register(subscriber)
|
||||
|
||||
try {
|
||||
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
|
||||
if (sessionCreationResponse !is NetworkResult.Success) {
|
||||
return@withContext sessionCreationResponse
|
||||
@@ -356,6 +381,10 @@ object RegistrationRepository {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -368,6 +397,39 @@ object RegistrationRepository {
|
||||
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) {
|
||||
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class RegistrationCheckpoint {
|
||||
BACKUP_RESTORED,
|
||||
PUSH_NETWORK_AUDITED,
|
||||
PHONE_NUMBER_CONFIRMED,
|
||||
PIN_CONFIRMED,
|
||||
VERIFICATION_CODE_REQUESTED,
|
||||
CHALLENGE_RECEIVED,
|
||||
CHALLENGE_COMPLETED,
|
||||
|
||||
@@ -9,9 +9,16 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
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.
|
||||
@@ -25,6 +32,43 @@ class RegistrationV2Activity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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 {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
|
||||
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.
|
||||
@@ -15,10 +17,16 @@ data class RegistrationV2State(
|
||||
val phoneNumber: Phonenumber.PhoneNumber? = null,
|
||||
val inProgress: Boolean = false,
|
||||
val isReRegister: Boolean = false,
|
||||
val recoveryPassword: String? = SignalStore.svr().getRecoveryPassword(),
|
||||
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 fcmToken: String? = null,
|
||||
val nextSms: Long = 0L,
|
||||
val nextCall: Long = 0L,
|
||||
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION
|
||||
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
|
||||
val networkError: Throwable? = null
|
||||
)
|
||||
|
||||
@@ -23,12 +23,17 @@ 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.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -78,50 +83,168 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||
store.update {
|
||||
it.copy(
|
||||
registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED,
|
||||
isFcmSupported = true,
|
||||
fcmToken = fcmToken
|
||||
)
|
||||
it.copy(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) {
|
||||
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
|
||||
// TODO [regv2]: check if can skip sms flow
|
||||
val state = store.value
|
||||
if (state.phoneNumber == null) {
|
||||
Log.w(TAG, "Phone number was null after confirmation.")
|
||||
onErrorOccurred()
|
||||
return
|
||||
}
|
||||
if (state.canSkipSms) {
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
} else {
|
||||
|
||||
// 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 {
|
||||
viewModelScope.launch {
|
||||
val svrCredentials = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
|
||||
|
||||
if (svrCredentials != null) {
|
||||
// Re-registration when credentials stored in backup.
|
||||
store.update {
|
||||
it.copy(canSkipSms = true, svrAuthCredentials = svrCredentials)
|
||||
}
|
||||
} 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
|
||||
)
|
||||
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) {
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = true,
|
||||
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED
|
||||
)
|
||||
it.copy(inProgress = true, registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED)
|
||||
}
|
||||
|
||||
val sessionId = store.value.sessionId
|
||||
@@ -143,17 +266,18 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
|
||||
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()
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED
|
||||
)
|
||||
}
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,38 +286,31 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun completeRegistration() {
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(ProfileUploadJob())
|
||||
.then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob()))
|
||||
.enqueue()
|
||||
ApplicationDependencies.getJobManager().startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue()
|
||||
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? {
|
||||
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 {
|
||||
val e164: String = getCurrentE164() ?: throw IllegalStateException()
|
||||
return RegistrationData(
|
||||
code,
|
||||
e164,
|
||||
password,
|
||||
RegistrationRepository.getRegistrationId(),
|
||||
RegistrationRepository.getProfileKey(e164),
|
||||
store.value.fcmToken,
|
||||
RegistrationRepository.getPniRegistrationId(),
|
||||
null // TODO [regv2]: recovery password
|
||||
)
|
||||
val currentState = store.value
|
||||
val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException()
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,19 +9,12 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
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.SignalStrengthPhoneStateListener
|
||||
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() {
|
||||
|
||||
@@ -5,15 +5,18 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
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.fragments.WelcomePermissions
|
||||
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.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -33,29 +36,31 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@RequiresApi(23)
|
||||
class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
|
||||
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
|
||||
private val isSearchingForBackup = mutableStateOf(false)
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions(),
|
||||
::permissionsGranted
|
||||
::onPermissionsGranted
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) {
|
||||
if (it.registrationCheckpoint >= RegistrationCheckpoint.PERMISSIONS_GRANTED) {
|
||||
proceedToNextScreen(it)
|
||||
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
when (val resultCode = result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
sharedViewModel.onBackupSuccessfullyRestored()
|
||||
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) {
|
||||
// TODO [nicholas]: conditionally go to backup flow
|
||||
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionSkipRestore())
|
||||
private lateinit var welcomeAction: WelcomeAction
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
welcomeAction = args.welcomeAction
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -66,40 +71,41 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
deviceBuildVersion = Build.VERSION.SDK_INT,
|
||||
isSearchingForBackup = isSearchingForBackup,
|
||||
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
|
||||
onNextClicked = this::onNextClicked,
|
||||
onNotNowClicked = this::onNotNowClicked
|
||||
onNextClicked = this::launchPermissionRequests,
|
||||
onNotNowClicked = this::proceedToNextScreen
|
||||
)
|
||||
}
|
||||
|
||||
private fun onNextClicked() {
|
||||
when (args.welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> continueNext()
|
||||
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueNext() {
|
||||
private fun launchPermissionRequests() {
|
||||
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
||||
val requiredPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)
|
||||
requestPermissionLauncher.launch(requiredPermissions)
|
||||
|
||||
val neededPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).filterNot {
|
||||
ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun onNotNowClicked() {
|
||||
when (args.welcomeAction) {
|
||||
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() {
|
||||
NavHostFragment.findNavController(this).popBackStack()
|
||||
}
|
||||
|
||||
private fun permissionsGranted(permissions: Map<String, Boolean>) {
|
||||
private fun onPermissionsGranted(permissions: Map<String, Boolean>) {
|
||||
permissions.forEach {
|
||||
Log.d(TAG, "${it.key} = ${it.value}")
|
||||
}
|
||||
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,
|
||||
RESTORE_BACKUP
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -95,7 +96,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||
presentRegisterButton(sharedState)
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -320,6 +323,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun moveToEnterPinScreen() {
|
||||
sharedViewModel.setInProgress(false)
|
||||
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment())
|
||||
}
|
||||
|
||||
private fun moveToVerificationEntryScreen() {
|
||||
NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
|
||||
sharedViewModel.setInProgress(false)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,15 @@
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.welcome
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
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.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
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.grantpermissions.GrantPermissionsV2Fragment
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
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 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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
maybePrefillE164()
|
||||
@@ -43,12 +62,13 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
setDebugLogSubmitMultiTapView(binding.title)
|
||||
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
|
||||
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
|
||||
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
|
||||
}
|
||||
|
||||
private fun onContinueClicked() {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
|
||||
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
|
||||
} else {
|
||||
skipRestore()
|
||||
}
|
||||
@@ -60,13 +80,24 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
}
|
||||
|
||||
private fun skipRestore() {
|
||||
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
|
||||
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
|
||||
}
|
||||
|
||||
private fun onTermsClicked() {
|
||||
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() {
|
||||
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
|
||||
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()
|
||||
|
||||
@@ -23,12 +23,20 @@ class RestoreActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
setContentView(R.layout.activity_restore)
|
||||
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
|
||||
sharedViewModel.setNextIntent(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun finishActivitySuccessfully() {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getIntentForRestore(context: Context): Intent {
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Bindin
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
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.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
@@ -52,12 +53,11 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||
Log.i(TAG, "Backup restore.")
|
||||
|
||||
binding.restoreButton.setOnClickListener { presentBackupPassPhrasePromptDialog() }
|
||||
|
||||
restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||
fragmentState.backupInfo?.let {
|
||||
presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp)
|
||||
if (fragmentState.backupPassphrase.isEmpty()) {
|
||||
presentBackupPassPhrasePromptDialog()
|
||||
}
|
||||
}
|
||||
|
||||
if (fragmentState.restoreInProgress) {
|
||||
@@ -71,23 +71,23 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
if (importResult == null) {
|
||||
onBackupCompletedSuccessfully()
|
||||
} else {
|
||||
handleBackupImportResult(importResult)
|
||||
handleBackupImportError(importResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restoreLocalBackupViewModel.startRestore(requireContext())
|
||||
restoreLocalBackupViewModel.prepareRestore(requireContext())
|
||||
}
|
||||
|
||||
private fun onBackupCompletedSuccessfully() {
|
||||
Log.d(TAG, "onBackupCompletedSuccessfully()")
|
||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
|
||||
val activity = requireActivity()
|
||||
val activity = requireActivity() as RestoreActivity
|
||||
navigationViewModel.getNextIntent()?.let {
|
||||
Log.d(TAG, "Launching ${it.component}")
|
||||
activity.startActivity(it)
|
||||
}
|
||||
activity.finish()
|
||||
activity.finishActivitySuccessfully()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -105,12 +105,12 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
restoreLocalBackupViewModel.onBackupProgressUpdate(event)
|
||||
}
|
||||
|
||||
private fun handleBackupImportResult(importResult: RestoreRepository.BackupImportResult) {
|
||||
private fun handleBackupImportError(importResult: RestoreRepository.BackupImportResult) {
|
||||
when (importResult) {
|
||||
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_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)
|
||||
|
||||
val passphrase = prompt.getText().toString()
|
||||
restoreLocalBackupViewModel.confirmPassphrase(requireContext(), passphrase)
|
||||
restoreLocalBackupViewModel.confirmPassphraseAndBeginRestore(requireContext(), passphrase)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
@@ -24,7 +24,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
|
||||
private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri))
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
fun startRestore(context: Context) {
|
||||
fun prepareRestore(context: Context) {
|
||||
val backupFileUri = store.value.uri
|
||||
viewModelScope.launch {
|
||||
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 {
|
||||
it.copy(
|
||||
backupPassphrase = passphrase,
|
||||
|
||||
@@ -130,6 +130,7 @@ public final class FeatureFlags {
|
||||
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
|
||||
private static final String REGISTRATION_V2 = "android.registration.v2";
|
||||
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
|
||||
@@ -214,7 +215,7 @@ public final class FeatureFlags {
|
||||
);
|
||||
|
||||
@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
|
||||
@@ -759,6 +760,11 @@ public final class FeatureFlags {
|
||||
/** Whether unauthenticated chat web socket is backed by libsignal-net */
|
||||
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. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
@@ -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>
|
||||
@@ -55,7 +55,6 @@
|
||||
app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin"
|
||||
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
|
||||
|
||||
<!-- TODO [regv2]: delete this -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/welcome_transfer_or_restore"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
|
||||
@@ -11,14 +11,6 @@
|
||||
android:label="fragment_welcome"
|
||||
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
|
||||
android:id="@+id/action_skip_restore"
|
||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||
@@ -28,16 +20,8 @@
|
||||
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: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"
|
||||
android:id="@+id/action_go_to_registration"
|
||||
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"
|
||||
@@ -59,24 +43,10 @@
|
||||
android:label="fragment_grant_permissions">
|
||||
|
||||
<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
|
||||
android:id="@+id/action_skip_restore"
|
||||
android:id="@+id/action_enter_phone_number"
|
||||
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_transfer_or_restore"
|
||||
app:destination="@id/transferOrRestore"
|
||||
app:popUpTo="@+id/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
@@ -120,15 +90,6 @@
|
||||
android:label="fragment_enter_phone_number"
|
||||
tools:layout="@layout/fragment_registration_enter_phone_number_v2">
|
||||
|
||||
<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"
|
||||
app:destination="@id/enterCodeV2Fragment"
|
||||
@@ -247,7 +208,7 @@
|
||||
|
||||
<fragment
|
||||
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">
|
||||
|
||||
<action
|
||||
|
||||
@@ -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.
|
||||
* If it's a failure, the original failure will be propagated. Useful for changing the type of a result.
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.registration
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
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.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
@@ -67,4 +68,10 @@ class RegistrationApi(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1137,6 +1137,11 @@ public class PushServiceSocket {
|
||||
.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,
|
||||
@Nonnull BackupAuthCheckRequest request,
|
||||
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
|
||||
@@ -3048,7 +3053,6 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException {
|
||||
long serverDeliveredTimestamp = 0;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user