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)
|
items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
|
||||||
}
|
}
|
||||||
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
|
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
|
||||||
|
Log.i(TAG, "Android Backup Service complete.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cumulativeHashCode(): Int {
|
private fun cumulativeHashCode(): Int {
|
||||||
|
|||||||
@@ -271,13 +271,6 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FeatureFlags.registrationV2()) {
|
|
||||||
if (SignalStore.account().getKeysToIncludeInBackup().contains(keyValue.key)) {
|
|
||||||
Log.i(TAG, "[regv2] skipping restore of " + keyValue.key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyValue.blobValue != null) {
|
if (keyValue.blobValue != null) {
|
||||||
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
|
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
|
||||||
} else if (keyValue.booleanValue != null) {
|
} else if (keyValue.booleanValue != null) {
|
||||||
|
|||||||
@@ -217,6 +217,6 @@ public final class InternalValues extends SignalStoreValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean enterRestoreV2Flow() {
|
public boolean enterRestoreV2Flow() {
|
||||||
return FeatureFlags.registrationV2() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
|
return FeatureFlags.restoreAfterRegistration() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
|
|||||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(root.findViewById(R.id.pin_restore_pin_title));
|
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(root.findViewById(R.id.pin_restore_pin_title));
|
||||||
|
|
||||||
pinEntry = root.findViewById(R.id.pin_restore_pin_input);
|
pinEntry = root.findViewById(R.id.pin_restore_pin_input);
|
||||||
pinButton = root.findViewById(R.id.pin_restore_pin_confirm);
|
pinButton = root.findViewById(R.id.pin_restore_pin_continue);
|
||||||
errorLabel = root.findViewById(R.id.pin_restore_pin_input_label);
|
errorLabel = root.findViewById(R.id.pin_restore_pin_input_label);
|
||||||
keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle);
|
keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle);
|
||||||
helpButton = root.findViewById(R.id.pin_restore_forgot_pin);
|
helpButton = root.findViewById(R.id.pin_restore_forgot_pin);
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class VerifyAccountRepository(private val context: Application) {
|
|||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MasterKeyProducer {
|
fun interface MasterKeyProducer {
|
||||||
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
||||||
fun produceMasterKey(): MasterKey
|
fun produceMasterKey(): MasterKey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.registration.v2.data
|
package org.thoughtcrime.securesms.registration.v2.data
|
||||||
|
|
||||||
|
import android.app.backup.BackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -12,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||||
import org.signal.libsignal.protocol.util.KeyHelper
|
import org.signal.libsignal.protocol.util.KeyHelper
|
||||||
@@ -39,6 +41,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
|||||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest
|
import org.thoughtcrime.securesms.registration.PushChallengeRequest
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||||
|
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
@@ -47,13 +50,16 @@ import org.whispersystems.signalservice.api.account.AccountAttributes
|
|||||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||||
|
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||||
|
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
|
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
|
||||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -227,6 +233,24 @@ object RegistrationRepository {
|
|||||||
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
|
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun canUseLocalRecoveryPassword(): Boolean {
|
||||||
|
val recoveryPassword = SignalStore.svr().recoveryPassword
|
||||||
|
val pinHash = SignalStore.svr().localPinHash
|
||||||
|
return recoveryPassword != null && pinHash != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doesPinMatchLocalHash(pin: String): Boolean {
|
||||||
|
val pinHash = SignalStore.svr().localPinHash ?: throw IllegalStateException("Local PIN hash is not present!")
|
||||||
|
return PinHashUtil.verifyLocalPinHash(pinHash, pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchMasterKeyFromSvrRemote(pin: String, authCredentials: AuthCredentials): MasterKey =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, authCredentials), pin)
|
||||||
|
SignalStore.svr().setMasterKey(masterKey, pin)
|
||||||
|
return@withContext masterKey
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asks the service to send a verification code through one of our supported channels (SMS, phone call).
|
* Asks the service to send a verification code through one of our supported channels (SMS, phone call).
|
||||||
* This requires two or more network calls:
|
* This requires two or more network calls:
|
||||||
@@ -280,9 +304,9 @@ object RegistrationRepository {
|
|||||||
/**
|
/**
|
||||||
* Submit the necessary assets as a verified account so that the user can actually use the service.
|
* Submit the necessary assets as a verified account so that the user can actually use the service.
|
||||||
*/
|
*/
|
||||||
suspend fun registerAccount(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult<AccountRegistrationResult> =
|
suspend fun registerAccount(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult<AccountRegistrationResult> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi
|
||||||
|
|
||||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||||
@@ -335,6 +359,7 @@ object RegistrationRepository {
|
|||||||
val eventBus = EventBus.getDefault()
|
val eventBus = EventBus.getDefault()
|
||||||
eventBus.register(subscriber)
|
eventBus.register(subscriber)
|
||||||
|
|
||||||
|
try {
|
||||||
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
|
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
|
||||||
if (sessionCreationResponse !is NetworkResult.Success) {
|
if (sessionCreationResponse !is NetworkResult.Success) {
|
||||||
return@withContext sessionCreationResponse
|
return@withContext sessionCreationResponse
|
||||||
@@ -356,6 +381,10 @@ object RegistrationRepository {
|
|||||||
}
|
}
|
||||||
Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.")
|
Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.")
|
||||||
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(NullPointerException())
|
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
|
@JvmStatic
|
||||||
@@ -368,6 +397,39 @@ object RegistrationRepository {
|
|||||||
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
|
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): AuthCredentials? =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val usernamePasswords = SignalStore.svr()
|
||||||
|
.authTokenList
|
||||||
|
.take(10)
|
||||||
|
.map {
|
||||||
|
it.replace("Basic ", "").trim()
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
Base64.decode(it) // TODO [regv2]: figure out why Android Studio doesn't like mapCatching
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
String(it, StandardCharsets.ISO_8859_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernamePasswords.isEmpty()) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||||
|
|
||||||
|
val authCheck = api.getSvrAuthCredential(e164, usernamePasswords)
|
||||||
|
if (authCheck !is NetworkResult.Success) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
val removedInvalidTokens = SignalStore.svr().removeAuthTokens(authCheck.result.invalid)
|
||||||
|
if (removedInvalidTokens) {
|
||||||
|
BackupManager(context).dataChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext authCheck.result.match
|
||||||
|
}
|
||||||
|
|
||||||
enum class Mode(val isSmsRetrieverSupported: Boolean) {
|
enum class Mode(val isSmsRetrieverSupported: Boolean) {
|
||||||
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
|
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum class RegistrationCheckpoint {
|
|||||||
BACKUP_RESTORED,
|
BACKUP_RESTORED,
|
||||||
PUSH_NETWORK_AUDITED,
|
PUSH_NETWORK_AUDITED,
|
||||||
PHONE_NUMBER_CONFIRMED,
|
PHONE_NUMBER_CONFIRMED,
|
||||||
|
PIN_CONFIRMED,
|
||||||
VERIFICATION_CODE_REQUESTED,
|
VERIFICATION_CODE_REQUESTED,
|
||||||
CHALLENGE_RECEIVED,
|
CHALLENGE_RECEIVED,
|
||||||
CHALLENGE_COMPLETED,
|
CHALLENGE_COMPLETED,
|
||||||
|
|||||||
@@ -9,9 +9,16 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.navigation.ActivityNavigator
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.BaseActivity
|
import org.thoughtcrime.securesms.BaseActivity
|
||||||
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||||
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||||
|
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to hold the entire registration process.
|
* Activity to hold the entire registration process.
|
||||||
@@ -25,6 +32,43 @@ class RegistrationV2Activity : BaseActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_registration_navigation_v2)
|
setContentView(R.layout.activity_registration_navigation_v2)
|
||||||
|
sharedViewModel.uiState.observe(this) {
|
||||||
|
if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) {
|
||||||
|
handleSuccessfulVerify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSuccessfulVerify() {
|
||||||
|
// TODO [regv2]: add functionality of [RegistrationCompleteFragment]
|
||||||
|
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
|
||||||
|
val isAvatarEmpty = !AvatarHelper.hasAvatar(this, Recipient.self().id)
|
||||||
|
val needsProfile = isProfileNameEmpty || isAvatarEmpty
|
||||||
|
val needsPin = !sharedViewModel.hasPin()
|
||||||
|
|
||||||
|
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
|
||||||
|
|
||||||
|
SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
|
||||||
|
|
||||||
|
if (!needsProfile && !needsPin) {
|
||||||
|
sharedViewModel.completeRegistration()
|
||||||
|
}
|
||||||
|
sharedViewModel.setInProgress(false)
|
||||||
|
|
||||||
|
val startIntent = MainActivity.clearTop(this).apply {
|
||||||
|
if (needsPin) {
|
||||||
|
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsProfile) {
|
||||||
|
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Launching ${startIntent.component}")
|
||||||
|
startActivity(startIntent)
|
||||||
|
finish()
|
||||||
|
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
package org.thoughtcrime.securesms.registration.v2.ui
|
package org.thoughtcrime.securesms.registration.v2.ui
|
||||||
|
|
||||||
import com.google.i18n.phonenumbers.Phonenumber
|
import com.google.i18n.phonenumbers.Phonenumber
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State holder shared across all of registration.
|
* State holder shared across all of registration.
|
||||||
@@ -15,10 +17,16 @@ data class RegistrationV2State(
|
|||||||
val phoneNumber: Phonenumber.PhoneNumber? = null,
|
val phoneNumber: Phonenumber.PhoneNumber? = null,
|
||||||
val inProgress: Boolean = false,
|
val inProgress: Boolean = false,
|
||||||
val isReRegister: Boolean = false,
|
val isReRegister: Boolean = false,
|
||||||
|
val recoveryPassword: String? = SignalStore.svr().getRecoveryPassword(),
|
||||||
val canSkipSms: Boolean = false,
|
val canSkipSms: Boolean = false,
|
||||||
|
val svrAuthCredentials: AuthCredentials? = null,
|
||||||
|
val svrTriesRemaining: Int = 10,
|
||||||
|
val isRegistrationLockEnabled: Boolean = false,
|
||||||
|
val userSkippedReregistration: Boolean = false,
|
||||||
val isFcmSupported: Boolean = false,
|
val isFcmSupported: Boolean = false,
|
||||||
val fcmToken: String? = null,
|
val fcmToken: String? = null,
|
||||||
val nextSms: Long = 0L,
|
val nextSms: Long = 0L,
|
||||||
val nextCall: Long = 0L,
|
val nextCall: Long = 0L,
|
||||||
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION
|
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
|
||||||
|
val networkError: Throwable? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,12 +23,17 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
|
|||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
|
||||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||||
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
|
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||||
|
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||||
|
import org.whispersystems.signalservice.internal.push.LockedException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,50 +83,168 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, isFcmSupported = true, fcmToken = fcmToken)
|
||||||
registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED,
|
|
||||||
isFcmSupported = true,
|
|
||||||
fcmToken = fcmToken
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun updateFcmToken(context: Context): String? {
|
||||||
|
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||||
|
store.update {
|
||||||
|
it.copy(fcmToken = fcmToken)
|
||||||
|
}
|
||||||
|
return fcmToken
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackupSuccessfullyRestored() {
|
||||||
|
val recoveryPassword = SignalStore.svr().recoveryPassword
|
||||||
|
store.update {
|
||||||
|
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onUserConfirmedPhoneNumber(context: Context) {
|
fun onUserConfirmedPhoneNumber(context: Context) {
|
||||||
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
|
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
|
||||||
// TODO [regv2]: check if can skip sms flow
|
|
||||||
val state = store.value
|
val state = store.value
|
||||||
if (state.phoneNumber == null) {
|
if (state.phoneNumber == null) {
|
||||||
Log.w(TAG, "Phone number was null after confirmation.")
|
Log.w(TAG, "Phone number was null after confirmation.")
|
||||||
onErrorOccurred()
|
onErrorOccurred()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (state.canSkipSms) {
|
|
||||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
|
||||||
} else {
|
|
||||||
// TODO [regv2]: initialize Play Services sms retriever
|
// TODO [regv2]: initialize Play Services sms retriever
|
||||||
val mccMncProducer = MccMncProducer(context)
|
val mccMncProducer = MccMncProducer(context)
|
||||||
val e164 = state.phoneNumber.toE164()
|
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 {
|
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()
|
val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow()
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
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)
|
||||||
sessionId = codeRequestResponse.body.id,
|
}
|
||||||
nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms),
|
|
||||||
nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall),
|
|
||||||
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setRecoveryPassword(recoveryPassword: String?) {
|
||||||
|
store.update {
|
||||||
|
it.copy(recoveryPassword = recoveryPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSvrTriesRemaining(remainingTries: Int) {
|
||||||
|
store.update {
|
||||||
|
it.copy(svrTriesRemaining = remainingTries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUserSkippedReRegisterFlow(value: Boolean) {
|
||||||
|
store.update {
|
||||||
|
it.copy(userSkippedReregistration = value, canSkipSms = !value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) {
|
||||||
|
setInProgress(true)
|
||||||
|
|
||||||
|
// Local recovery password
|
||||||
|
if (RegistrationRepository.canUseLocalRecoveryPassword()) {
|
||||||
|
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
|
||||||
|
Log.d(TAG, "Found recovery password, attempting to re-register.")
|
||||||
|
viewModelScope.launch {
|
||||||
|
verifyReRegisterInternal(context, pin, SignalStore.svr().getOrCreateMasterKey())
|
||||||
|
setInProgress(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Entered PIN did not match local PIN hash.")
|
||||||
|
wrongPinHandler()
|
||||||
|
setInProgress(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remote recovery password
|
||||||
|
val authCredentials = store.value.svrAuthCredentials
|
||||||
|
if (authCredentials != null) {
|
||||||
|
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR.")
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, authCredentials)
|
||||||
|
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
|
||||||
|
updateSvrTriesRemaining(10)
|
||||||
|
verifyReRegisterInternal(context, pin, masterKey)
|
||||||
|
} catch (rejectedPin: SvrWrongPinException) {
|
||||||
|
Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin)
|
||||||
|
updateSvrTriesRemaining(rejectedPin.triesRemaining)
|
||||||
|
wrongPinHandler()
|
||||||
|
} catch (noData: SvrNoDataException) {
|
||||||
|
Log.w(TAG, "SVR has no data for these credentials. Aborting skip SMS flow.", noData)
|
||||||
|
setUserSkippedReRegisterFlow(true)
|
||||||
|
}
|
||||||
|
setInProgress(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w(TAG, "Could not get credentials to skip SMS registration, aborting!")
|
||||||
|
// TODO [regv2]: Investigate why in v1, this case throws a [IncorrectRegistrationRecoveryPasswordException], which seems weird.
|
||||||
|
store.update {
|
||||||
|
it.copy(canSkipSms = false, inProgress = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun verifyReRegisterInternal(context: Context, pin: String, masterKey: MasterKey) {
|
||||||
|
updateFcmToken(context)
|
||||||
|
|
||||||
|
val registrationData = getRegistrationData("")
|
||||||
|
|
||||||
|
val resultAndRegLockStatus = registerAccountInternal(context, null, registrationData, pin, masterKey)
|
||||||
|
val result = resultAndRegLockStatus.first
|
||||||
|
val reglockEnabled = resultAndRegLockStatus.second
|
||||||
|
|
||||||
|
if (result !is NetworkResult.Success) {
|
||||||
|
Log.w(TAG, "Error during registration!", result.getCause())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccessfulRegistration(context, registrationData, result.result, reglockEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun registerAccountInternal(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String?, masterKey: MasterKey): Pair<NetworkResult<RegistrationRepository.AccountRegistrationResult>, Boolean> {
|
||||||
|
val registrationResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }
|
||||||
|
|
||||||
|
// TODO: check for wrong recovery password
|
||||||
|
|
||||||
|
// Check if reg lock is enabled
|
||||||
|
if (registrationResult !is NetworkResult.StatusCodeError || registrationResult.exception !is LockedException) {
|
||||||
|
return Pair(registrationResult, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Received a registration lock response when trying to register an account. Retrying with master key.")
|
||||||
|
val lockedException = registrationResult.exception as LockedException
|
||||||
|
store.update {
|
||||||
|
it.copy(svrAuthCredentials = lockedException.svr2Credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }, true)
|
||||||
|
}
|
||||||
|
|
||||||
fun verifyCodeWithoutRegistrationLock(context: Context, code: String) {
|
fun verifyCodeWithoutRegistrationLock(context: Context, code: String) {
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(inProgress = true, registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED)
|
||||||
inProgress = true,
|
|
||||||
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionId = store.value.sessionId
|
val sessionId = store.value.sessionId
|
||||||
@@ -143,17 +266,18 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||||||
|
|
||||||
setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED)
|
setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED)
|
||||||
|
|
||||||
val registrationResponse = RegistrationRepository.registerAccount(context, e164, password, sessionId, registrationData).successOrThrow()
|
val registrationResponse = RegistrationRepository.registerAccount(context, sessionId, registrationData).successOrThrow()
|
||||||
|
onSuccessfulRegistration(context, registrationData, registrationResponse, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
localRegisterAccount(context, registrationData, registrationResponse, false)
|
private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) {
|
||||||
|
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
|
||||||
|
|
||||||
refreshFeatureFlags()
|
refreshFeatureFlags()
|
||||||
|
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
|
||||||
registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,38 +286,31 @@ class RegistrationV2ViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun completeRegistration() {
|
fun completeRegistration() {
|
||||||
ApplicationDependencies.getJobManager()
|
ApplicationDependencies.getJobManager().startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue()
|
||||||
.startChain(ProfileUploadJob())
|
|
||||||
.then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob()))
|
|
||||||
.enqueue()
|
|
||||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun matchesSavedE164(e164: String?): Boolean {
|
||||||
|
return if (e164 == null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
e164 == SignalStore.account().e164
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasRecoveryPassword(): Boolean {
|
||||||
|
return store.value.recoveryPassword != null
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCurrentE164(): String? {
|
private fun getCurrentE164(): String? {
|
||||||
return store.value.phoneNumber?.toE164()
|
return store.value.phoneNumber?.toE164()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun localRegisterAccount(
|
|
||||||
context: Context,
|
|
||||||
registrationData: RegistrationData,
|
|
||||||
remoteResult: RegistrationRepository.AccountRegistrationResult,
|
|
||||||
reglockEnabled: Boolean
|
|
||||||
) {
|
|
||||||
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getRegistrationData(code: String): RegistrationData {
|
private suspend fun getRegistrationData(code: String): RegistrationData {
|
||||||
val e164: String = getCurrentE164() ?: throw IllegalStateException()
|
val currentState = store.value
|
||||||
return RegistrationData(
|
val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException()
|
||||||
code,
|
val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr().getRecoveryPassword() else null
|
||||||
e164,
|
return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), currentState.fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
|
||||||
password,
|
|
||||||
RegistrationRepository.getRegistrationId(),
|
|
||||||
RegistrationRepository.getProfileKey(e164),
|
|
||||||
store.value.fcmToken,
|
|
||||||
RegistrationRepository.getPniRegistrationId(),
|
|
||||||
null // TODO [regv2]: recovery password
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,19 +9,12 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.ActivityNavigator
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.LoggingFragment
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
import org.thoughtcrime.securesms.MainActivity
|
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
|
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
|
||||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||||
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
|
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
|
||||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
||||||
@@ -74,45 +67,6 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedViewModel.uiState.observe(viewLifecycleOwner) {
|
|
||||||
if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) {
|
|
||||||
handleSuccessfulVerify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleSuccessfulVerify() {
|
|
||||||
// TODO [regv2]: add functionality of [RegistrationCompleteFragment]
|
|
||||||
val activity = requireActivity()
|
|
||||||
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
|
|
||||||
val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id)
|
|
||||||
val needsProfile = isProfileNameEmpty || isAvatarEmpty
|
|
||||||
val needsPin = !sharedViewModel.hasPin()
|
|
||||||
|
|
||||||
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
|
|
||||||
|
|
||||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
|
|
||||||
|
|
||||||
if (!needsProfile && !needsPin) {
|
|
||||||
sharedViewModel.completeRegistration()
|
|
||||||
}
|
|
||||||
sharedViewModel.setInProgress(false)
|
|
||||||
|
|
||||||
val startIntent = MainActivity.clearTop(activity).apply {
|
|
||||||
if (needsPin) {
|
|
||||||
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(activity))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsProfile) {
|
|
||||||
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(activity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Launching ${startIntent.component}")
|
|
||||||
activity.startActivity(startIntent)
|
|
||||||
activity.finish()
|
|
||||||
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun popBackStack() {
|
private fun popBackStack() {
|
||||||
|
|||||||
@@ -5,15 +5,18 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
|
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
@@ -22,8 +25,8 @@ import org.thoughtcrime.securesms.compose.ComposeFragment
|
|||||||
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
|
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
|
||||||
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
||||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
||||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
|
|
||||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||||
|
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
import org.thoughtcrime.securesms.util.BackupUtil
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
|
|
||||||
@@ -33,29 +36,31 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
|||||||
@RequiresApi(23)
|
@RequiresApi(23)
|
||||||
class GrantPermissionsV2Fragment : ComposeFragment() {
|
class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||||
|
|
||||||
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
|
|
||||||
|
|
||||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||||
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
|
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
|
||||||
private val isSearchingForBackup = mutableStateOf(false)
|
private val isSearchingForBackup = mutableStateOf(false)
|
||||||
|
|
||||||
private val requestPermissionLauncher = registerForActivityResult(
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestMultiplePermissions(),
|
ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
::permissionsGranted
|
::onPermissionsGranted
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
super.onViewCreated(view, savedInstanceState)
|
when (val resultCode = result.resultCode) {
|
||||||
sharedViewModel.uiState.observe(viewLifecycleOwner) {
|
Activity.RESULT_OK -> {
|
||||||
if (it.registrationCheckpoint >= RegistrationCheckpoint.PERMISSIONS_GRANTED) {
|
sharedViewModel.onBackupSuccessfullyRestored()
|
||||||
proceedToNextScreen(it)
|
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
|
||||||
}
|
}
|
||||||
|
Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.")
|
||||||
|
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun proceedToNextScreen(it: RegistrationV2State) {
|
private lateinit var welcomeAction: WelcomeAction
|
||||||
// TODO [nicholas]: conditionally go to backup flow
|
|
||||||
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionSkipRestore())
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
welcomeAction = args.welcomeAction
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -66,40 +71,41 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
|||||||
deviceBuildVersion = Build.VERSION.SDK_INT,
|
deviceBuildVersion = Build.VERSION.SDK_INT,
|
||||||
isSearchingForBackup = isSearchingForBackup,
|
isSearchingForBackup = isSearchingForBackup,
|
||||||
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
|
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
|
||||||
onNextClicked = this::onNextClicked,
|
onNextClicked = this::launchPermissionRequests,
|
||||||
onNotNowClicked = this::onNotNowClicked
|
onNotNowClicked = this::proceedToNextScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNextClicked() {
|
private fun launchPermissionRequests() {
|
||||||
when (args.welcomeAction) {
|
|
||||||
WelcomeAction.CONTINUE -> continueNext()
|
|
||||||
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun continueNext() {
|
|
||||||
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
||||||
val requiredPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)
|
|
||||||
requestPermissionLauncher.launch(requiredPermissions)
|
val neededPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).filterNot {
|
||||||
|
ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNotNowClicked() {
|
if (neededPermissions.isEmpty()) {
|
||||||
when (args.welcomeAction) {
|
proceedToNextScreen()
|
||||||
WelcomeAction.CONTINUE -> continueNotNow()
|
} else {
|
||||||
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
requestPermissionLauncher.launch(neededPermissions.toTypedArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun continueNotNow() {
|
private fun onPermissionsGranted(permissions: Map<String, Boolean>) {
|
||||||
NavHostFragment.findNavController(this).popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun permissionsGranted(permissions: Map<String, Boolean>) {
|
|
||||||
permissions.forEach {
|
permissions.forEach {
|
||||||
Log.d(TAG, "${it.key} = ${it.value}")
|
Log.d(TAG, "${it.key} = ${it.value}")
|
||||||
}
|
}
|
||||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||||
|
proceedToNextScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun proceedToNextScreen() {
|
||||||
|
when (welcomeAction) {
|
||||||
|
WelcomeAction.CONTINUE -> NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
|
||||||
|
WelcomeAction.RESTORE_BACKUP -> {
|
||||||
|
val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
|
||||||
|
launchRestoreActivity.launch(restoreIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,4 +116,8 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
|||||||
CONTINUE,
|
CONTINUE,
|
||||||
RESTORE_BACKUP
|
RESTORE_BACKUP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.core.widget.addTextChangedListener
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@@ -95,7 +96,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
|||||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||||
presentRegisterButton(sharedState)
|
presentRegisterButton(sharedState)
|
||||||
presentProgressBar(sharedState.inProgress, sharedState.isReRegister)
|
presentProgressBar(sharedState.inProgress, sharedState.isReRegister)
|
||||||
if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
|
if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
|
||||||
|
moveToEnterPinScreen()
|
||||||
|
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
|
||||||
moveToVerificationEntryScreen()
|
moveToVerificationEntryScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,6 +323,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun moveToEnterPinScreen() {
|
||||||
|
sharedViewModel.setInProgress(false)
|
||||||
|
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment())
|
||||||
|
}
|
||||||
|
|
||||||
private fun moveToVerificationEntryScreen() {
|
private fun moveToVerificationEntryScreen() {
|
||||||
NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
|
NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
|
||||||
sharedViewModel.setInProgress(false)
|
sharedViewModel.setInProgress(false)
|
||||||
|
|||||||
@@ -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
|
package org.thoughtcrime.securesms.registration.v2.ui.welcome
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.LoggingFragment
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
@@ -20,8 +23,10 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Bindi
|
|||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||||
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
||||||
|
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
||||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||||
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
|
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
|
||||||
|
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
import org.thoughtcrime.securesms.util.BackupUtil
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
@@ -36,6 +41,20 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
|||||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||||
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
|
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
|
||||||
|
|
||||||
|
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
|
when (val resultCode = result.resultCode) {
|
||||||
|
Activity.RESULT_OK -> {
|
||||||
|
sharedViewModel.onBackupSuccessfullyRestored()
|
||||||
|
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionGoToRegistration())
|
||||||
|
}
|
||||||
|
Activity.RESULT_CANCELED -> {
|
||||||
|
Log.w(TAG, "Backup restoration canceled.")
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
maybePrefillE164()
|
maybePrefillE164()
|
||||||
@@ -43,12 +62,13 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
|||||||
setDebugLogSubmitMultiTapView(binding.title)
|
setDebugLogSubmitMultiTapView(binding.title)
|
||||||
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
|
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
|
||||||
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
|
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
|
||||||
|
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onContinueClicked() {
|
private fun onContinueClicked() {
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
|
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
|
||||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||||
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
|
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
|
||||||
} else {
|
} else {
|
||||||
skipRestore()
|
skipRestore()
|
||||||
}
|
}
|
||||||
@@ -60,13 +80,24 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun skipRestore() {
|
private fun skipRestore() {
|
||||||
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
|
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTermsClicked() {
|
private fun onTermsClicked() {
|
||||||
CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL)
|
CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onTransferOrRestoreClicked() {
|
||||||
|
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||||
|
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.RESTORE_BACKUP))
|
||||||
|
} else {
|
||||||
|
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||||
|
|
||||||
|
val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
|
||||||
|
launchRestoreActivity.launch(restoreIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun maybePrefillE164() {
|
private fun maybePrefillE164() {
|
||||||
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
|
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
|
||||||
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()
|
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()
|
||||||
|
|||||||
@@ -23,12 +23,20 @@ class RestoreActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
|
||||||
setContentView(R.layout.activity_restore)
|
setContentView(R.layout.activity_restore)
|
||||||
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
|
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
|
||||||
sharedViewModel.setNextIntent(it)
|
sharedViewModel.setNextIntent(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun finishActivitySuccessfully() {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getIntentForRestore(context: Context): Intent {
|
fun getIntentForRestore(context: Context): Intent {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Bindin
|
|||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter
|
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter
|
||||||
|
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||||
import org.thoughtcrime.securesms.restore.RestoreRepository
|
import org.thoughtcrime.securesms.restore.RestoreRepository
|
||||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
@@ -52,12 +53,11 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
|||||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||||
Log.i(TAG, "Backup restore.")
|
Log.i(TAG, "Backup restore.")
|
||||||
|
|
||||||
|
binding.restoreButton.setOnClickListener { presentBackupPassPhrasePromptDialog() }
|
||||||
|
|
||||||
restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||||
fragmentState.backupInfo?.let {
|
fragmentState.backupInfo?.let {
|
||||||
presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp)
|
presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp)
|
||||||
if (fragmentState.backupPassphrase.isEmpty()) {
|
|
||||||
presentBackupPassPhrasePromptDialog()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fragmentState.restoreInProgress) {
|
if (fragmentState.restoreInProgress) {
|
||||||
@@ -71,23 +71,23 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
|||||||
if (importResult == null) {
|
if (importResult == null) {
|
||||||
onBackupCompletedSuccessfully()
|
onBackupCompletedSuccessfully()
|
||||||
} else {
|
} else {
|
||||||
handleBackupImportResult(importResult)
|
handleBackupImportError(importResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreLocalBackupViewModel.startRestore(requireContext())
|
restoreLocalBackupViewModel.prepareRestore(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBackupCompletedSuccessfully() {
|
private fun onBackupCompletedSuccessfully() {
|
||||||
Log.d(TAG, "onBackupCompletedSuccessfully()")
|
Log.d(TAG, "onBackupCompletedSuccessfully()")
|
||||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
|
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
|
||||||
val activity = requireActivity()
|
val activity = requireActivity() as RestoreActivity
|
||||||
navigationViewModel.getNextIntent()?.let {
|
navigationViewModel.getNextIntent()?.let {
|
||||||
Log.d(TAG, "Launching ${it.component}")
|
Log.d(TAG, "Launching ${it.component}")
|
||||||
activity.startActivity(it)
|
activity.startActivity(it)
|
||||||
}
|
}
|
||||||
activity.finish()
|
activity.finishActivitySuccessfully()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
@@ -105,12 +105,12 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
|||||||
restoreLocalBackupViewModel.onBackupProgressUpdate(event)
|
restoreLocalBackupViewModel.onBackupProgressUpdate(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBackupImportResult(importResult: RestoreRepository.BackupImportResult) {
|
private fun handleBackupImportError(importResult: RestoreRepository.BackupImportResult) {
|
||||||
when (importResult) {
|
when (importResult) {
|
||||||
RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
|
RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
|
||||||
RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show()
|
RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show()
|
||||||
RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
|
RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
|
||||||
RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled here.", IllegalStateException())
|
RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled in this function.", IllegalStateException())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
|||||||
ViewUtil.hideKeyboard(requireContext(), prompt)
|
ViewUtil.hideKeyboard(requireContext(), prompt)
|
||||||
|
|
||||||
val passphrase = prompt.getText().toString()
|
val passphrase = prompt.getText().toString()
|
||||||
restoreLocalBackupViewModel.confirmPassphrase(requireContext(), passphrase)
|
restoreLocalBackupViewModel.confirmPassphraseAndBeginRestore(requireContext(), passphrase)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
|
|||||||
private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri))
|
private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri))
|
||||||
val uiState = store.asLiveData()
|
val uiState = store.asLiveData()
|
||||||
|
|
||||||
fun startRestore(context: Context) {
|
fun prepareRestore(context: Context) {
|
||||||
val backupFileUri = store.value.uri
|
val backupFileUri = store.value.uri
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri)
|
val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri)
|
||||||
@@ -48,7 +48,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun confirmPassphrase(context: Context, passphrase: String) {
|
fun confirmPassphraseAndBeginRestore(context: Context, passphrase: String) {
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
backupPassphrase = passphrase,
|
backupPassphrase = passphrase,
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ public final class FeatureFlags {
|
|||||||
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
|
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
|
||||||
private static final String REGISTRATION_V2 = "android.registration.v2";
|
private static final String REGISTRATION_V2 = "android.registration.v2";
|
||||||
private static final String LIBSIGNAL_WEB_SOCKET_ENABLED = "android.libsignalWebSocketEnabled";
|
private static final String LIBSIGNAL_WEB_SOCKET_ENABLED = "android.libsignalWebSocketEnabled";
|
||||||
|
private static final String RESTORE_POST_REGISTRATION = "android.registration.restorePostRegistration";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||||
@@ -214,7 +215,7 @@ public final class FeatureFlags {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2);
|
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Values in this map will take precedence over any value. This should only be used for local
|
* Values in this map will take precedence over any value. This should only be used for local
|
||||||
@@ -759,6 +760,11 @@ public final class FeatureFlags {
|
|||||||
/** Whether unauthenticated chat web socket is backed by libsignal-net */
|
/** Whether unauthenticated chat web socket is backed by libsignal-net */
|
||||||
public static boolean libSignalWebSocketEnabled() { return getBoolean(LIBSIGNAL_WEB_SOCKET_ENABLED, false); }
|
public static boolean libSignalWebSocketEnabled() { return getBoolean(LIBSIGNAL_WEB_SOCKET_ENABLED, false); }
|
||||||
|
|
||||||
|
/** Whether or not to launch the restore activity after registration is complete, rather than before. */
|
||||||
|
public static boolean restoreAfterRegistration() {
|
||||||
|
return getBoolean(RESTORE_POST_REGISTRATION, false);
|
||||||
|
}
|
||||||
|
|
||||||
/** Only for rendering debug info. */
|
/** Only for rendering debug info. */
|
||||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||||
return new TreeMap<>(REMOTE_VALUES);
|
return new TreeMap<>(REMOTE_VALUES);
|
||||||
|
|||||||
@@ -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:layout_goneMarginBottom="@dimen/registration_button_bottom_margin"
|
||||||
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
|
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
|
||||||
|
|
||||||
<!-- TODO [regv2]: delete this -->
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/welcome_transfer_or_restore"
|
android:id="@+id/welcome_transfer_or_restore"
|
||||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||||
|
|||||||
@@ -11,14 +11,6 @@
|
|||||||
android:label="fragment_welcome"
|
android:label="fragment_welcome"
|
||||||
tools:layout="@layout/fragment_registration_welcome_v2">
|
tools:layout="@layout/fragment_registration_welcome_v2">
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_restore"
|
|
||||||
app:destination="@id/restoreBackupV2Fragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_skip_restore"
|
android:id="@+id/action_skip_restore"
|
||||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||||
@@ -28,16 +20,8 @@
|
|||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||||
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_transfer_or_restore"
|
android:id="@+id/action_go_to_registration"
|
||||||
app:destination="@id/transferOrRestore"
|
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_welcomeFragment_to_deviceTransferSetup"
|
|
||||||
app:destination="@id/deviceTransferSetup"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
@@ -59,24 +43,10 @@
|
|||||||
android:label="fragment_grant_permissions">
|
android:label="fragment_grant_permissions">
|
||||||
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_restore"
|
android:id="@+id/action_enter_phone_number"
|
||||||
app:destination="@id/restoreBackupV2Fragment"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_skip_restore"
|
|
||||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
app:popUpTo="@+id/welcomeV2Fragment"
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:popUpToInclusive="true"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_transfer_or_restore"
|
|
||||||
app:destination="@id/transferOrRestore"
|
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
@@ -120,15 +90,6 @@
|
|||||||
android:label="fragment_enter_phone_number"
|
android:label="fragment_enter_phone_number"
|
||||||
tools:layout="@layout/fragment_registration_enter_phone_number_v2">
|
tools:layout="@layout/fragment_registration_enter_phone_number_v2">
|
||||||
|
|
||||||
<action
|
|
||||||
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
|
<action
|
||||||
android:id="@+id/action_enterVerificationCode"
|
android:id="@+id/action_enterVerificationCode"
|
||||||
app:destination="@id/enterCodeV2Fragment"
|
app:destination="@id/enterCodeV2Fragment"
|
||||||
@@ -247,7 +208,7 @@
|
|||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/reRegisterWithPinV2Fragment"
|
android:id="@+id/reRegisterWithPinV2Fragment"
|
||||||
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment"
|
android:name="org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin.ReRegisterWithPinV2Fragment"
|
||||||
tools:layout="@layout/fragment_registration_lock">
|
tools:layout="@layout/fragment_registration_lock">
|
||||||
|
|
||||||
<action
|
<action
|
||||||
|
|||||||
@@ -64,6 +64,18 @@ sealed class NetworkResult<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [Throwable] associated with the result, or null if the result is successful.
|
||||||
|
*/
|
||||||
|
fun getCause(): Throwable? {
|
||||||
|
return when (this) {
|
||||||
|
is Success -> null
|
||||||
|
is NetworkError -> exception
|
||||||
|
is StatusCodeError -> exception
|
||||||
|
is ApplicationError -> throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the output of one [NetworkResult] and transforms it into another if the operation is successful.
|
* Takes the output of one [NetworkResult] and transforms it into another if the operation is successful.
|
||||||
* If it's a failure, the original failure will be propagated. Useful for changing the type of a result.
|
* If it's a failure, the original failure will be propagated. Useful for changing the type of a result.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.registration
|
|||||||
import org.whispersystems.signalservice.api.NetworkResult
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||||
|
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||||
@@ -67,4 +68,10 @@ class RegistrationApi(
|
|||||||
pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer)
|
pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSvrAuthCredential(e164: String, usernamePasswords: List<String>): NetworkResult<BackupAuthCheckResponse> {
|
||||||
|
return NetworkResult.fromFetch {
|
||||||
|
pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1137,6 +1137,11 @@ public class PushServiceSocket {
|
|||||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BackupAuthCheckResponse checkBackupAuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||||
|
String response = makeServiceRequest(BACKUP_AUTH_CHECK, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||||
|
return JsonUtil.fromJson(response, BackupAuthCheckResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
private Single<ServiceResponse<BackupAuthCheckResponse>> createBackupAuthCheckSingle(@Nonnull String path,
|
private Single<ServiceResponse<BackupAuthCheckResponse>> createBackupAuthCheckSingle(@Nonnull String path,
|
||||||
@Nonnull BackupAuthCheckRequest request,
|
@Nonnull BackupAuthCheckRequest request,
|
||||||
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
|
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
|
||||||
@@ -3048,7 +3053,6 @@ public class PushServiceSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException {
|
private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException {
|
||||||
long serverDeliveredTimestamp = 0;
|
long serverDeliveredTimestamp = 0;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user