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)
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
Log.i(TAG, "Android Backup Service complete.")
}
private fun cumulativeHashCode(): Int {

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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)
}

View File

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

View File

@@ -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 {

View File

@@ -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
)

View File

@@ -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)
}
/**

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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)

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
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()

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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,

View File

@@ -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);

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: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"

View File

@@ -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

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.
* 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.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)
}
}
}

View File

@@ -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 {