Remove regv2.

This commit is contained in:
Cody Henthorne
2025-09-22 11:49:13 -04:00
committed by Jeffrey Starke
parent 52fa86046b
commit 8dc2077ad0
56 changed files with 35 additions and 5018 deletions

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity;
import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
@@ -189,7 +189,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private boolean userCanTransferOrRestore() {
return !SignalStore.registration().isRegistrationComplete() &&
RemoteConfig.restoreAfterRegistration() &&
RestoreDecisionStateUtil.isDecisionPending(SignalStore.registration().getRestoreDecisionState());
}

View File

@@ -1993,8 +1993,7 @@ object BackupRepository {
private fun isPreRestoreDuringRegistration(): Boolean {
return !SignalStore.registration.isRegistrationComplete &&
SignalStore.registration.restoreDecisionState.isDecisionPending &&
RemoteConfig.restoreAfterRegistration
SignalStore.registration.restoreDecisionState.isDecisionPending
}
private fun scheduleSyncForAccountChange() {

View File

@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.banner.ui.compose.Action
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
import org.thoughtcrime.securesms.banner.ui.compose.Importance
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity
import org.thoughtcrime.securesms.util.TextSecurePreferences
/**

View File

@@ -54,7 +54,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ViewUtil

View File

@@ -33,7 +33,7 @@ class AccountSettingsViewModel : ViewModel() {
registrationLockEnabled = SignalStore.svr.isRegistrationLockEnabled,
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application),
clientDeprecated = SignalStore.misc.isClientDeprecated,
canTransferWhileUnregistered = RemoteConfig.restoreAfterRegistration
canTransferWhileUnregistered = true
)
}
}

View File

@@ -6,7 +6,7 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.registration.ui.captcha.CaptchaFragment
import org.thoughtcrime.securesms.registrationv3.ui.captcha.CaptchaFragment
/**
* Screen visible to the user when they are to solve a captcha. @see [CaptchaFragment]

View File

@@ -23,13 +23,13 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterCodeBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
import org.thoughtcrime.securesms.registration.sms.ReceivedSmsEvent
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible

View File

@@ -26,8 +26,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.registration.ui.registrationlock.RegistrationLockFragment
import org.thoughtcrime.securesms.registration.ui.registrationlock.RegistrationLockFragmentArgs
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
@@ -41,7 +39,7 @@ import java.util.concurrent.TimeUnit
class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_change_number_registration_lock) {
companion object {
private val TAG = Log.tag(RegistrationLockFragment::class.java)
private val TAG = Log.tag(ChangeNumberRegistrationLockFragment::class.java)
}
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(bindingFactory = { rootView ->
@@ -67,7 +65,7 @@ class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_c
}
)
val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments())
val args: ChangeNumberRegistrationLockFragmentArgs = ChangeNumberRegistrationLockFragmentArgs.fromBundle(requireArguments())
timeRemaining = args.getTimeRemaining()

View File

@@ -61,7 +61,7 @@ import kotlin.time.Duration.Companion.seconds
/**
* Repository to perform data operations during change number.
*
* @see [org.thoughtcrime.securesms.registration.data.RegistrationRepository]
* @see [org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository]
*/
class ChangeNumberRepository(
private val accountManager: SignalServiceAccountManager = AppDependencies.signalServiceAccountManager,

View File

@@ -18,9 +18,9 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**

View File

@@ -23,16 +23,16 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.IOException

View File

@@ -293,7 +293,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet

View File

@@ -44,12 +44,7 @@ final class NewDeviceServerTask implements ServerTask {
DataRestoreConstraint.setRestoringData(true);
SQLiteDatabase database = SignalDatabase.getBackupDatabase();
String passphrase;
if (RemoteConfig.restoreAfterRegistration()) {
passphrase = SignalStore.account().getAccountEntropyPool().getValue();
} else {
passphrase = "deadbeef";
}
String passphrase = SignalStore.account().getAccountEntropyPool().getValue();
BackupPassphrase.set(context, passphrase);
FullBackupImporter.importFile(context,
@@ -57,7 +52,7 @@ final class NewDeviceServerTask implements ServerTask {
database,
inputStream,
passphrase,
RemoteConfig.restoreAfterRegistration());
true);
SignalDatabase.runPostBackupRestoreTasks(database);
NotificationChannels.getInstance().restoreContactNotificationChannels();

View File

@@ -40,12 +40,7 @@ final class OldDeviceClientTask implements ClientTask {
EventBus.getDefault().register(this);
try {
String passphrase;
if (RemoteConfig.restoreAfterRegistration()) {
passphrase = SignalStore.account().getAccountEntropyPool().getValue();
} else {
passphrase = "deadbeef";
}
String passphrase = SignalStore.account().getAccountEntropyPool().getValue();
FullBackupExporter.transfer(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),

View File

@@ -16,8 +16,8 @@ import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberD
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.keyvalue.SvrValues;
import org.thoughtcrime.securesms.net.SignalNetwork;
import org.thoughtcrime.securesms.registration.data.RegistrationRepository;
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher;
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.account.AccountAttributes;

View File

@@ -378,7 +378,7 @@ class IncomingMessageObserver(
sleepTimer = if (!SignalStore.account.fcmEnabled || SignalStore.internal.isWebsocketModeForced) AlarmSleepTimer(context) else UptimeSleepTimer()
canProcessMessages = !(RemoteConfig.restoreAfterRegistration && SignalStore.registration.restoreDecisionState.isDecisionPending)
canProcessMessages = !SignalStore.registration.restoreDecisionState.isDecisionPending
}
override fun run() {

View File

@@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.profiles.manage.EditProfileViewModel.AvatarState
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameDeleteResult
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity
import org.thoughtcrime.securesms.util.NameUtil.getAbbreviation
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil

View File

@@ -1,212 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
@Preview
@Composable
fun GrantPermissionsScreenPreview() {
SignalTheme(isDarkMode = false) {
GrantPermissionsScreen(
deviceBuildVersion = 33,
isBackupSelectionRequired = true,
isSearchingForBackup = true,
{},
{}
)
}
}
/**
* Layout that explains permissions rationale to the user.
*/
@Composable
fun GrantPermissionsScreen(
deviceBuildVersion: Int,
isBackupSelectionRequired: Boolean,
isSearchingForBackup: Boolean,
onNextClicked: () -> Unit,
onNotNowClicked: () -> Unit
) {
Surface {
Column(
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 40.dp, bottom = 24.dp)
) {
LazyColumn(
modifier = Modifier.weight(1f)
) {
item {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__allow_permissions),
style = MaterialTheme.typography.headlineMedium
)
}
item {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__to_help_you_message_people_you_know),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 12.dp, bottom = 41.dp)
)
}
if (deviceBuildVersion >= 33) {
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
)
}
}
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_contact),
title = stringResource(id = R.string.GrantPermissionsFragment__contacts),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__find_people_you_know)
)
}
if (deviceBuildVersion < 29 || !isBackupSelectionRequired) {
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_file),
title = stringResource(id = R.string.GrantPermissionsFragment__storage),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__send_photos_videos_and_files)
)
}
}
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_phone),
title = stringResource(id = R.string.GrantPermissionsFragment__phone_calls),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__make_registering_easier)
)
}
}
Row {
TextButton(onClick = onNotNowClicked) {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__not_now)
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSearchingForBackup) {
Box {
NextButton(
isSearchingForBackup = true,
onNextClicked = onNextClicked
)
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
} else {
NextButton(
isSearchingForBackup = false,
onNextClicked = onNextClicked
)
}
}
}
}
}
@Preview
@Composable
fun PermissionRowPreview() {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
)
}
@Composable
fun PermissionRow(
imageVector: ImageVector,
title: String,
subtitle: String
) {
Row(modifier = Modifier.padding(bottom = 32.dp)) {
Image(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Column {
Text(
text = title,
style = MaterialTheme.typography.titleSmall
)
Text(
text = subtitle,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.size(32.dp))
}
}
@Composable
fun NextButton(
isSearchingForBackup: Boolean,
onNextClicked: () -> Unit
) {
val alpha = if (isSearchingForBackup) {
0f
} else {
1f
}
Buttons.LargeTonal(
onClick = onNextClicked,
enabled = !isSearchingForBackup,
modifier = Modifier.alpha(alpha)
) {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__next)
)
}
}

View File

@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistratio
import org.whispersystems.signalservice.api.account.PreKeyCollection
/**
* Takes the two sources of registration data ([RegistrationData], [RegistrationRepository.AccountRegistrationResult])
* Takes the two sources of registration data ([RegistrationData], [org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository.AccountRegistrationResult])
* and combines them into a proto-backed class [LocalRegistrationMetadata] so they can be serialized & stored.
*/
object LocalRegistrationMetadataUtil {

View File

@@ -1,614 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.data
import android.app.backup.BackupManager
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import com.google.android.gms.auth.api.phone.SmsRetriever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.AppCapabilities
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.gcm.FcmUtil
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.pin.Svr3Migration
import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.pin.SvrWrongPinException
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getAciIdentityKeyPair
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getAciPreKeyCollection
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getPniIdentityKeyPair
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getPniPreKeyCollection
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.account.AccountAttributes
import org.whispersystems.signalservice.api.account.PreKeyCollection
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.kbs.PinHashUtil
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.Locale
import java.util.Optional
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
/**
* A repository that deals with disk I/O during account registration.
*/
object RegistrationRepository {
private val TAG = Log.tag(RegistrationRepository::class.java)
private val PUSH_REQUEST_TIMEOUT = 5.seconds.inWholeMilliseconds
/**
* Retrieve the FCM token from the Firebase service.
*/
suspend fun getFcmToken(context: Context): String? =
withContext(Dispatchers.Default) {
FcmUtil.getToken(context).orElse(null)
}
/**
* Queries the local store for whether a PIN is set.
*/
@JvmStatic
fun hasPin(): Boolean {
return SignalStore.svr.hasPin()
}
/**
* Queries, and creates if needed, the local registration ID.
*/
@JvmStatic
fun getRegistrationId(): Int {
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
var registrationId = SignalStore.account.registrationId
if (registrationId == 0) {
registrationId = KeyHelper.generateRegistrationId(false)
SignalStore.account.registrationId = registrationId
}
return registrationId
}
/**
* Queries, and creates if needed, the local PNI registration ID.
*/
@JvmStatic
fun getPniRegistrationId(): Int {
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
var pniRegistrationId = SignalStore.account.pniRegistrationId
if (pniRegistrationId == 0) {
pniRegistrationId = KeyHelper.generateRegistrationId(false)
SignalStore.account.pniRegistrationId = pniRegistrationId
}
return pniRegistrationId
}
/**
* Queries, and creates if needed, the local profile key.
*/
@JvmStatic
suspend fun getProfileKey(e164: String): ProfileKey =
withContext(Dispatchers.IO) {
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
val recipientTable = SignalDatabase.recipients
val recipient = recipientTable.getByE164(e164)
var profileKey = if (recipient.isPresent) {
ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).profileKey)
} else {
null
}
if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew()
Log.i(TAG, "No profile key found, created a new one")
}
profileKey
}
/**
* Takes a server response from a successful registration and persists the relevant data.
*/
@JvmStatic
suspend fun registerAccountLocally(context: Context, data: LocalRegistrationMetadata) =
withContext(Dispatchers.IO) {
Log.v(TAG, "registerAccountLocally()")
val aciIdentityKeyPair = data.getAciIdentityKeyPair()
val pniIdentityKeyPair = data.getPniIdentityKeyPair()
SignalStore.account.restoreAciIdentityKeyFromBackup(aciIdentityKeyPair.publicKey.serialize(), aciIdentityKeyPair.privateKey.serialize())
SignalStore.account.restorePniIdentityKeyFromBackup(pniIdentityKeyPair.publicKey.serialize(), pniIdentityKeyPair.privateKey.serialize())
val aciPreKeyCollection = data.getAciPreKeyCollection()
val pniPreKeyCollection = data.getPniPreKeyCollection()
val aci: ACI = ACI.parseOrThrow(data.aci)
val pni: PNI = PNI.parseOrThrow(data.pni)
val hasPin: Boolean = data.hasPin
SignalStore.account.setAci(aci)
SignalStore.account.setPni(pni)
AppDependencies.resetProtocolStores()
AppDependencies.protocolStore.aci().sessions().archiveAllSessions()
AppDependencies.protocolStore.pni().sessions().archiveAllSessions()
SenderKeyUtil.clearAllState()
val aciProtocolStore = AppDependencies.protocolStore.aci()
val aciMetadataStore = SignalStore.account.aciPreKeys
val pniProtocolStore = AppDependencies.protocolStore.pni()
val pniMetadataStore = SignalStore.account.pniPreKeys
storeSignedAndLastResortPreKeys(aciProtocolStore, aciMetadataStore, aciPreKeyCollection)
storeSignedAndLastResortPreKeys(pniProtocolStore, pniMetadataStore, pniPreKeyCollection)
val recipientTable = SignalDatabase.recipients
val selfId = Recipient.trustedPush(aci, pni, data.e164).id
recipientTable.setProfileSharing(selfId, true)
recipientTable.markRegisteredOrThrow(selfId, aci)
recipientTable.linkIdsForSelf(aci, pni, data.e164)
recipientTable.setProfileKey(selfId, ProfileKey(data.profileKey.toByteArray()))
AppDependencies.recipientCache.clearSelf()
SignalStore.account.setE164(data.e164)
SignalStore.account.fcmToken = data.fcmToken
SignalStore.account.fcmEnabled = data.fcmEnabled
val now = System.currentTimeMillis()
saveOwnIdentityKey(selfId, aci, aciProtocolStore, now)
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
SignalStore.account.setServicePassword(data.servicePassword)
SignalStore.account.setRegistered(true)
TextSecurePreferences.setPromptedPushRegistration(context, true)
TextSecurePreferences.setUnauthorizedReceived(context, false)
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
val masterKey = if (data.masterKey != null) MasterKey(data.masterKey.toByteArray()) else null
SvrRepository.onRegistrationComplete(masterKey, data.pin, hasPin, data.reglockEnabled, false)
AppDependencies.resetNetwork()
AppDependencies.startNetwork()
PreKeysSyncJob.enqueue()
val jobManager = AppDependencies.jobManager
jobManager.add(DirectoryRefreshJob(false))
jobManager.add(RotateCertificateJob())
DirectoryRefreshListener.schedule(context)
RotateSignedPreKeyListener.schedule(context)
}
@JvmStatic
private fun saveOwnIdentityKey(selfId: RecipientId, serviceId: ServiceId, protocolStore: SignalServiceAccountDataStoreImpl, now: Long) {
protocolStore.identities().saveIdentityWithoutSideEffects(
selfId,
serviceId,
protocolStore.identityKeyPair.publicKey,
IdentityTable.VerifiedStatus.VERIFIED,
true,
now,
true
)
}
@JvmStatic
private fun storeSignedAndLastResortPreKeys(protocolStore: SignalServiceAccountDataStoreImpl, metadataStore: PreKeyMetadataStore, preKeyCollection: PreKeyCollection) {
PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, preKeyCollection.signedPreKey)
metadataStore.isSignedPreKeyRegistered = true
metadataStore.activeSignedPreKeyId = preKeyCollection.signedPreKey.id
metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis()
PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, preKeyCollection.lastResortKyberPreKey)
metadataStore.lastResortKyberPreKeyId = preKeyCollection.lastResortKyberPreKey.id
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, svr2Credentials: AuthCredentials?, svr3Credentials: Svr3Credentials?): MasterKey =
withContext(Dispatchers.IO) {
val credentialSet = SvrAuthCredentialSet(svr2Credentials = svr2Credentials, svr3Credentials = svr3Credentials)
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(credentialSet, pin)
return@withContext masterKey
}
/**
* Validates a session ID.
*/
private suspend fun validateSession(context: Context, sessionId: String, e164: String, password: String): RegistrationSessionCheckResult =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
Log.d(TAG, "Validating registration session with service.")
val registrationSessionResult = api.getRegistrationSessionStatus(sessionId)
return@withContext RegistrationSessionCheckResult.from(registrationSessionResult)
}
/**
* Initiates a new registration session on the service.
*/
suspend fun createSession(context: Context, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionCreationResult =
withContext(Dispatchers.IO) {
Log.d(TAG, "About to create a registration session…")
val fcmToken: String? = FcmUtil.getToken(context).orElse(null)
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val registrationSessionResult = if (fcmToken == null) {
Log.d(TAG, "Creating registration session without FCM token.")
api.createRegistrationSession(null, mcc, mnc)
} else {
Log.d(TAG, "Creating registration session with FCM token.")
createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc)
}
val result = RegistrationSessionCreationResult.from(registrationSessionResult)
if (result is RegistrationSessionCreationResult.Success) {
Log.d(TAG, "Updating registration session and E164 in value store.")
SignalStore.registration.sessionId = result.sessionId
SignalStore.registration.sessionE164 = e164
}
return@withContext result
}
/**
* Validates an existing session, if its ID is provided. If the session is expired/invalid, or none is provided, it will attempt to initiate a new session.
*/
suspend fun createOrValidateSession(context: Context, sessionId: String?, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionResult {
val savedSessionId = if (sessionId == null && e164 == SignalStore.registration.sessionE164) {
SignalStore.registration.sessionId
} else {
sessionId
}
if (savedSessionId != null) {
Log.d(TAG, "Validating existing registration session.")
val sessionValidationResult = validateSession(context, savedSessionId, e164, password)
when (sessionValidationResult) {
is RegistrationSessionCheckResult.Success -> {
Log.d(TAG, "Existing registration session is valid.")
return sessionValidationResult
}
is RegistrationSessionCheckResult.UnknownError -> {
Log.w(TAG, "Encountered error when validating existing session.", sessionValidationResult.getCause())
return sessionValidationResult
}
is RegistrationSessionCheckResult.SessionNotFound -> {
Log.i(TAG, "Current session is invalid or has expired. Must create new one.")
// fall through to creation
}
}
}
return createSession(context, e164, password, mcc, mnc)
}
/**
* Asks the service to send a verification code through one of our supported channels (SMS, phone call).
*/
suspend fun requestSmsCode(context: Context, sessionId: String, e164: String, password: String, mode: E164VerificationMode): VerificationCodeRequestResult =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val codeRequestResult = api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported, mode.transport)
return@withContext VerificationCodeRequestResult.from(codeRequestResult)
}
/**
* Submits the user-entered verification code to the service.
*/
suspend fun submitVerificationCode(context: Context, sessionId: String, registrationData: RegistrationData): VerificationCodeRequestResult =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi
val result = api.verifyAccount(sessionId = sessionId, verificationCode = registrationData.code)
return@withContext VerificationCodeRequestResult.from(result)
}
/**
* Submits the solved captcha token to the service.
*/
suspend fun submitCaptchaToken(context: Context, e164: String, password: String, sessionId: String, captchaToken: String): VerificationCodeRequestResult =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val captchaSubmissionResult = api.submitCaptchaToken(sessionId = sessionId, captchaToken = captchaToken)
return@withContext VerificationCodeRequestResult.from(captchaSubmissionResult)
}
suspend fun requestAndVerifyPushToken(context: Context, sessionId: String, e164: String, password: String) =
withContext(Dispatchers.IO) {
val fcmToken = getFcmToken(context)
val accountManager = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, sessionId, Optional.ofNullable(fcmToken), PUSH_REQUEST_TIMEOUT).orElse(null)
val pushSubmissionResult = accountManager.registrationApi.submitPushChallengeToken(sessionId = sessionId, pushChallengeToken = pushChallenge)
return@withContext VerificationCodeRequestResult.from(pushSubmissionResult)
}
/**
* Submit the necessary assets as a verified account so that the user can actually use the service.
*/
suspend fun registerAccount(context: Context, sessionId: String?, registrationData: RegistrationData, recoveryPassword: String?, pin: String? = null, masterKeyProducer: MasterKeyProducer? = null): RegisterAccountResult =
withContext(Dispatchers.IO) {
Log.v(TAG, "registerAccount()")
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
val masterKey: MasterKey?
try {
masterKey = masterKeyProducer?.produceMasterKey()
} catch (e: SvrNoDataException) {
return@withContext RegisterAccountResult.SvrNoData(e)
} catch (e: SvrWrongPinException) {
return@withContext RegisterAccountResult.SvrWrongPin(e)
} catch (e: IOException) {
return@withContext RegisterAccountResult.UnknownError(e)
}
val registrationLock: String? = masterKey?.deriveRegistrationLock()
val accountAttributes = AccountAttributes(
signalingKey = null,
registrationId = registrationData.registrationId,
fetchesMessages = registrationData.isNotFcm,
registrationLock = registrationLock,
unidentifiedAccessKey = unidentifiedAccessKey,
unrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
capabilities = AppCapabilities.getCapabilities(true),
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE,
name = null,
pniRegistrationId = registrationData.pniRegistrationId,
recoveryPassword = recoveryPassword
)
SignalStore.account.generateAciIdentityKeyIfNecessary()
val aciIdentity: IdentityKeyPair = SignalStore.account.aciIdentityKey
SignalStore.account.generatePniIdentityKeyIfNecessary()
val pniIdentity: IdentityKeyPair = SignalStore.account.pniIdentityKey
val aciPreKeyCollection = generateSignedAndLastResortPreKeys(aciIdentity, SignalStore.account.aciPreKeys)
val pniPreKeyCollection = generateSignedAndLastResortPreKeys(pniIdentity, SignalStore.account.pniPreKeys)
val result: NetworkResult<AccountRegistrationResult> = api.registerAccount(sessionId, recoveryPassword, accountAttributes, aciPreKeyCollection, pniPreKeyCollection, registrationData.fcmToken, true)
.map { accountRegistrationResponse: VerifyAccountResponse ->
AccountRegistrationResult(
uuid = accountRegistrationResponse.uuid,
pni = accountRegistrationResponse.pni,
storageCapable = accountRegistrationResponse.storageCapable,
number = accountRegistrationResponse.number,
masterKey = masterKey,
pin = pin,
aciPreKeyCollection = aciPreKeyCollection,
pniPreKeyCollection = pniPreKeyCollection,
reRegistration = accountRegistrationResponse.reregistration
)
}
return@withContext RegisterAccountResult.from(result)
}
private suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> =
withContext(Dispatchers.IO) {
// TODO [regv2]: do not use event bus nor latch
val subscriber = PushTokenChallengeSubscriber()
val eventBus = EventBus.getDefault()
eventBus.register(subscriber)
try {
Log.d(TAG, "Requesting a registration session with FCM token…")
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
if (sessionCreationResponse !is NetworkResult.Success) {
return@withContext sessionCreationResponse
}
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
eventBus.unregister(subscriber)
if (receivedPush) {
val challenge = subscriber.challenge
if (challenge != null) {
Log.i(TAG, "Push challenge token received.")
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.metadata.id, challenge)
} else {
Log.w(TAG, "Push received but challenge token was null.")
}
} else {
Log.i(TAG, "Push challenge timed out.")
}
Log.i(TAG, "Push challenge unsuccessful. Continuing with session created without one.")
return@withContext sessionCreationResponse
} catch (ex: Exception) {
Log.w(TAG, "Exception caught, but the earlier try block should have caught it?", ex)
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(ex)
}
}
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val svr3Result = SignalStore.svr.svr3AuthTokens
?.takeIf { Svr3Migration.shouldReadFromSvr3 }
?.takeIf { it.isNotEmpty() }
?.toSvrCredentials()
?.let { authTokens ->
api
.validateSvr3AuthCredential(e164, authTokens)
.runIfSuccessful {
val removedInvalidTokens = SignalStore.svr.removeSvr3AuthTokens(it.invalid)
if (removedInvalidTokens) {
BackupManager(context).dataChanged()
}
}
.let { BackupAuthCheckResult.fromV3(it) }
}
if (svr3Result is BackupAuthCheckResult.SuccessWithCredentials) {
Log.d(TAG, "Found valid SVR3 credentials.")
return@withContext svr3Result
}
Log.d(TAG, "No valid SVR3 credentials, looking for SVR2.")
return@withContext SignalStore.svr.svr2AuthTokens
?.takeIf { it.isNotEmpty() }
?.toSvrCredentials()
?.let { authTokens ->
api
.validateSvr2AuthCredential(e164, authTokens)
.runIfSuccessful {
val removedInvalidTokens = SignalStore.svr.removeSvr2AuthTokens(it.invalid)
if (removedInvalidTokens) {
BackupManager(context).dataChanged()
}
}
.let { BackupAuthCheckResult.fromV2(it) }
} ?: BackupAuthCheckResult.SuccessWithoutCredentials()
}
/** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
private fun List<String?>.toSvrCredentials(): List<String> {
return this
.asSequence()
.filterNotNull()
.take(10)
.map { it.replace("Basic ", "").trim() }
.mapNotNull {
try {
Base64.decode(it)
} catch (e: IOException) {
Log.w(TAG, "Encountered error trying to decode a token!", e)
null
}
}
.map { String(it, StandardCharsets.ISO_8859_1) }
.toList()
}
/**
* Starts an SMS listener to auto-enter a verification code.
*
* The listener [lives for 5 minutes](https://developers.google.com/android/reference/com/google/android/gms/auth/api/phone/SmsRetrieverApi).
*
* @return whether or not the Play Services SMS Listener was successfully registered.
*/
suspend fun registerSmsListener(context: Context): Boolean {
Log.d(TAG, "Attempting to start verification code SMS retriever.")
val started = withTimeoutOrNull(5.seconds.inWholeMilliseconds) {
try {
SmsRetriever.getClient(context).startSmsRetriever().await()
Log.d(TAG, "Successfully started verification code SMS retriever.")
return@withTimeoutOrNull true
} catch (ex: Exception) {
Log.w(TAG, "Could not start verification code SMS retriever due to exception.", ex)
return@withTimeoutOrNull false
}
}
if (started == null) {
Log.w(TAG, "Could not start verification code SMS retriever due to timeout.")
}
return started == true
}
@VisibleForTesting
fun generateSignedAndLastResortPreKeys(identity: IdentityKeyPair, metadataStore: PreKeyMetadataStore): PreKeyCollection {
val signedPreKey = PreKeyUtil.generateSignedPreKey(metadataStore.nextSignedPreKeyId, identity.privateKey)
val lastResortKyberPreKey = PreKeyUtil.generateLastResortKyberPreKey(metadataStore.nextKyberPreKeyId, identity.privateKey)
return PreKeyCollection(
identity.publicKey,
signedPreKey,
lastResortKyberPreKey
)
}
fun interface MasterKeyProducer {
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
fun produceMasterKey(): MasterKey
}
enum class E164VerificationMode(val isSmsRetrieverSupported: Boolean, val transport: PushServiceSocket.VerificationCodeTransport) {
SMS_WITH_LISTENER(true, PushServiceSocket.VerificationCodeTransport.SMS),
SMS_WITHOUT_LISTENER(false, PushServiceSocket.VerificationCodeTransport.SMS),
PHONE_CALL(false, PushServiceSocket.VerificationCodeTransport.VOICE)
}
private class PushTokenChallengeSubscriber {
var challenge: String? = null
val latch = CountDownLatch(1)
@Subscribe
fun onChallengeEvent(pushChallengeEvent: PushChallengeRequest.PushChallengeEvent) {
Log.d(TAG, "Push challenge received!")
challenge = pushChallengeEvent.challenge
latch.countDown()
}
}
}

View File

@@ -1,139 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.ActivityNavigator
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.pin.PinRestoreActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* Activity to hold the entire registration process.
*/
class RegistrationActivity : BaseActivity() {
private val TAG = Log.tag(RegistrationActivity::class.java)
private val dynamicTheme = DynamicNoActionBarTheme()
val sharedViewModel: RegistrationViewModel by viewModels()
private var smsRetrieverReceiver: SmsRetrieverReceiver? = null
init {
lifecycle.addObserver(SmsRetrieverObserver())
}
override fun onCreate(savedInstanceState: Bundle?) {
dynamicTheme.onCreate(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration_navigation_v2)
sharedViewModel.isReregister = intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false)
sharedViewModel.checkpoint.observe(this) {
if (it >= RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE) {
handleSuccessfulVerify()
}
}
}
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
private fun handleSuccessfulVerify() {
if (SignalStore.account.isPrimaryDevice && SignalStore.account.isMultiDevice) {
SignalStore.misc.shouldShowLinkedDevicesReminder = sharedViewModel.isReregister
}
if (SignalStore.storageService.needsAccountRestore) {
Log.i(TAG, "Performing pin restore.")
startActivity(Intent(this, PinRestoreActivity::class.java))
finish()
} else {
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")
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
val startIntent = MainActivity.clearTop(this).apply {
if (needsPin) {
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationActivity))
} else if (SignalStore.registration.restoreDecisionState.isDecisionPending && RemoteConfig.messageBackups) {
putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationActivity))
} else if (needsProfile) {
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationActivity))
}
}
Log.d(TAG, "Launching ${startIntent.component}")
startActivity(startIntent)
finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
}
private inner class SmsRetrieverObserver : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
smsRetrieverReceiver = SmsRetrieverReceiver(application)
smsRetrieverReceiver?.registerReceiver()
}
override fun onDestroy(owner: LifecycleOwner) {
smsRetrieverReceiver?.unregisterReceiver()
smsRetrieverReceiver = null
}
}
companion object {
const val RE_REGISTRATION_EXTRA: String = "re_registration"
@JvmStatic
fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent {
return Intent(context, getRegistrationClass()).apply {
putExtra(RE_REGISTRATION_EXTRA, false)
setData(originalIntent.data)
}
}
@JvmStatic
fun newIntentForReRegistration(context: Context): Intent {
return Intent(context, getRegistrationClass()).apply {
putExtra(RE_REGISTRATION_EXTRA, true)
}
}
private fun getRegistrationClass(): Class<*> {
return if (RemoteConfig.restoreAfterRegistration) org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity::class.java else RegistrationActivity::class.java
}
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui
/**
* An ordered list of checkpoints of the registration process.
* This is used for screens to know when to advance, as well as restoring state after process death.
*/
enum class RegistrationCheckpoint {
INITIALIZATION,
PERMISSIONS_GRANTED,
BACKUP_RESTORED_OR_SKIPPED,
PUSH_NETWORK_AUDITED,
PHONE_NUMBER_CONFIRMED,
PIN_CONFIRMED,
VERIFICATION_CODE_REQUESTED,
VERIFICATION_CODE_ENTERED,
PIN_ENTERED,
VERIFICATION_CODE_VALIDATED,
SERVICE_REGISTRATION_COMPLETED,
LOCAL_REGISTRATION_COMPLETE
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* State holder shared across all of registration.
*/
data class RegistrationState(
val sessionId: String? = null,
val enteredCode: String = "",
val phoneNumber: Phonenumber.PhoneNumber? = fetchExistingE164FromValues(),
val nationalNumber: String = "",
val inProgress: Boolean = false,
val isReRegister: Boolean = false,
val canSkipSms: Boolean = false,
val svr2AuthCredentials: AuthCredentials? = null,
val svr3AuthCredentials: Svr3Credentials? = null,
val svrTriesRemaining: Int = 10,
val incorrectCodeAttempts: Int = 0,
val isRegistrationLockEnabled: Boolean = false,
val lockedTimeRemaining: Long = 0L,
val userSkippedReregistration: Boolean = false,
val isFcmSupported: Boolean = false,
val isAllowedToRequestCode: Boolean = false,
val fcmToken: String? = null,
val challengesRequested: List<Challenge> = emptyList(),
val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Duration = 0.seconds,
val nextCallTimestamp: Duration = 0.seconds,
val nextVerificationAttempt: Duration = 0.seconds,
val verified: Boolean = false,
val smsListenerTimeout: Long = 0L,
val pinKeyboardType: PinKeyboardType = SignalStore.pin.keyboardType,
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
val networkError: Throwable? = null,
val sessionCreationError: RegistrationSessionResult? = null,
val sessionStateError: VerificationCodeRequestResult? = null,
val registerAccountError: RegisterAccountResult? = null,
val challengeInProgress: Boolean = false
) {
companion object {
private val TAG = Log.tag(RegistrationState::class)
private fun fetchExistingE164FromValues(): Phonenumber.PhoneNumber? {
val existingE164 = SignalStore.registration.sessionE164
if (existingE164 != null) {
try {
return PhoneNumberUtil.getInstance().parse(existingE164, null)
} catch (ex: NumberParseException) {
Log.w(TAG, "Could not parse stored E164.", ex)
return null
}
} else {
return null
}
}
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.accountlocked
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import kotlin.time.Duration.Companion.milliseconds
/**
* Screen educating the user that they need to wait some number of days to register.
*/
class AccountLockedFragment : LoggingFragment(R.layout.account_locked_fragment) {
private val viewModel by activityViewModels<RegistrationViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title))
val description = view.findViewById<TextView>(R.id.account_locked_description)
viewModel.lockedTimeRemaining.observe(
viewLifecycleOwner
) { t: Long? -> description.text = getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t!!)) }
view.findViewById<View>(R.id.account_locked_next).setOnClickListener { v: View? -> onNext() }
view.findViewById<View>(R.id.account_locked_learn_more).setOnClickListener { v: View? -> learnMore() }
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onNext()
}
}
)
}
private fun learnMore() {
val intent = Intent(Intent.ACTION_VIEW)
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)))
startActivity(intent)
}
fun onNext() {
requireActivity().finish()
}
private fun durationToDays(duration: Long): Long {
return if (duration != 0L) getLockoutDays(duration).toLong() else 7
}
private fun getLockoutDays(timeRemainingMs: Long): Int {
return timeRemainingMs.milliseconds.inWholeDays.toInt() + 1
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.captcha
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaBinding
import org.thoughtcrime.securesms.registration.fragments.RegistrationConstants
abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_captcha) {
private val binding: FragmentRegistrationCaptchaBinding by ViewBinderDelegate(FragmentRegistrationCaptchaBinding::bind)
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.registrationCaptchaWebView.settings.javaScriptEnabled = true
binding.registrationCaptchaWebView.clearCache(true)
binding.registrationCaptchaWebView.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
val token = url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length)
handleCaptchaToken(token)
findNavController().navigateUp()
return true
}
return false
}
}
binding.registrationCaptchaWebView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL)
}
abstract fun handleCaptchaToken(token: String)
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.captcha
import androidx.fragment.app.activityViewModels
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
/**
* Screen that displays a captcha as part of the registration flow.
* This subclass plugs in [RegistrationViewModel] to the shared super class.
*
* @see CaptchaFragment
*/
class RegistrationCaptchaFragment : CaptchaFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
override fun handleCaptchaToken(token: String) {
sharedViewModel.setCaptchaResponse(token)
}
}

View File

@@ -1,62 +0,0 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package org.thoughtcrime.securesms.registration.ui.countrycode
import android.os.Bundle
import android.view.View
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
/**
* Country picker fragment used in registration V1
*/
class CountryCodeFragment : ComposeFragment() {
companion object {
private val TAG = Log.tag(CountryCodeFragment::class.java)
const val REQUEST_KEY_COUNTRY = "request_key_country"
const val REQUEST_COUNTRY = "country"
const val RESULT_COUNTRY = "country"
}
private val viewModel: CountryCodeViewModel by viewModels()
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsStateWithLifecycle()
CountryCodeSelectScreen(
state = state,
title = stringResource(R.string.CountryCodeFragment__your_country),
onSearch = { search -> viewModel.filterCountries(search) },
onDismissed = { findNavController().popBackStack() },
onClick = { country ->
setFragmentResult(
REQUEST_KEY_COUNTRY,
bundleOf(
RESULT_COUNTRY to country
)
)
findNavController().popBackStack()
}
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val initialCountry = arguments?.getParcelableCompat(REQUEST_COUNTRY, Country::class.java)
viewModel.loadCountries(initialCountry)
}
}

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
* View model to support [CountryCodeFragment] and track the countries
* View model to support [org.thoughtcrime.securesms.registrationv3.ui.countrycode.CountryCodeFragment] and track the countries
*/
class CountryCodeViewModel : ViewModel() {

View File

@@ -1,432 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.entercode
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.ThreadUtil
import org.signal.core.util.isNotNullOrBlank
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.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
import org.thoughtcrime.securesms.registration.sms.ReceivedSmsEvent
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
import kotlin.time.Duration.Companion.milliseconds
/**
* The final screen of account registration, where the user enters their verification code.
*/
class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) {
companion object {
private val TAG = Log.tag(EnterCodeFragment::class.java)
private const val BOTTOM_SHEET_TAG = "support_bottom_sheet"
}
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val fragmentViewModel by viewModels<EnterCodeViewModel>()
private val bottomSheet = ContactSupportBottomSheetFragment()
private val binding: FragmentRegistrationEnterCodeBinding by ViewBinderDelegate(FragmentRegistrationEnterCodeBinding::bind)
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
private var autopilotCodeEntryActive = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(binding.verifyHeader)
phoneStateListener = SignalStrengthPhoneStateListener(this, PhoneStateCallback())
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
popBackStack()
}
}
)
binding.wrongNumber.setOnClickListener {
popBackStack()
}
binding.code.setOnCompleteListener {
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it)
}
binding.havingTroubleButton.setOnClickListener {
bottomSheet.showSafely(childFragmentManager, BOTTOM_SHEET_TAG)
}
binding.callMeCountDown.apply {
setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in)
setOnClickListener {
sharedViewModel.requestVerificationCall(requireContext())
}
}
binding.resendSmsCountDown.apply {
setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in)
setOnClickListener {
sharedViewModel.requestSmsCode(requireContext())
}
}
binding.keyboard.setOnKeyPressListener { key ->
if (!autopilotCodeEntryActive) {
if (key >= 0) {
binding.code.append(key)
} else {
binding.code.delete()
}
}
}
sharedViewModel.incorrectCodeAttempts.observe(viewLifecycleOwner) { attempts: Int ->
if (attempts >= 3) {
binding.havingTroubleButton.visible = true
}
}
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
sharedState.sessionCreationError?.let { error ->
handleSessionCreationError(error)
sharedViewModel.sessionCreationErrorShown()
}
sharedState.sessionStateError?.let { error ->
handleSessionErrorResponse(error)
sharedViewModel.sessionStateErrorShown()
}
sharedState.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error)
sharedViewModel.registerAccountErrorShown()
}
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRequested.isNotEmpty() && !sharedState.challengeInProgress) {
handleChallenges(sharedState.challengesRequested)
}
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(sharedState.nextCallTimestamp)
if (sharedState.inProgress) {
binding.keyboard.displayProgress()
} else {
binding.keyboard.displayKeyboard()
}
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) {
if (it.resetRequiredAfterFailure) {
binding.callMeCountDown.visibility = View.VISIBLE
binding.resendSmsCountDown.visibility = View.VISIBLE
binding.wrongNumber.visibility = View.VISIBLE
binding.code.clear()
binding.keyboard.displayKeyboard()
fragmentViewModel.allViewsResetCompleted()
} else if (it.showKeyboard) {
binding.keyboard.displayKeyboard()
fragmentViewModel.keyboardShown()
}
}
EventBus.getDefault().registerForLifecycle(subscriber = this, lifecycleOwner = viewLifecycleOwner)
}
override fun onResume() {
super.onResume()
sharedViewModel.phoneNumber?.let {
val formatted = PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
binding.verificationSubheader.text = requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, formatted)
}
}
private fun handleSessionCreationError(result: RegistrationSessionResult) {
if (!result.isSuccess()) {
Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause())
}
when (result) {
is RegistrationSessionCheckResult.Success,
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
is RegistrationSessionCreationResult.RateLimited -> {
val timeRemaining = result.timeRemaining?.milliseconds
Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining")
if (timeRemaining != null) {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
} else {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
}
}
is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result)
is RegistrationSessionCheckResult.SessionNotFound -> presentGenericError(result)
is RegistrationSessionCheckResult.UnknownError,
is RegistrationSessionCreationResult.UnknownError -> presentGenericError(result)
}
}
private fun handleSessionErrorResponse(result: VerificationCodeRequestResult) {
if (!result.isSuccess()) {
Log.i(TAG, "[sessionError] Handling error response of ${result.javaClass.name}", result.getCause())
}
when (result) {
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is VerificationCodeRequestResult.RateLimited -> {
val timeRemaining = result.timeRemaining?.milliseconds
Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining")
if (timeRemaining != null) {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
} else {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
}
}
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is VerificationCodeRequestResult.ExternalServiceFailure -> presentSmsGenericError(result)
is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> {
Log.i(TAG, result.log())
handleRequestVerificationCodeRateLimited(result)
}
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited()
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
else -> presentGenericError(result)
}
}
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
if (!result.isSuccess()) {
Log.i(TAG, "[registrationError] Handling error response of ${result.javaClass.name}", result.getCause())
}
when (result) {
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is RegisterAccountResult.AuthorizationFailed -> presentIncorrectCodeDialog()
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
else -> presentGenericError(result)
}
}
private fun handleChallenges(remainingChallenges: List<Challenge>) {
when (remainingChallenges.first()) {
Challenge.CAPTCHA -> moveToCaptcha()
Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext())
}
}
private fun presentAccountLocked() {
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
findNavController().safeNavigate(EnterCodeFragmentDirections.actionAccountLocked())
}
}
)
}
private fun presentRegistrationLocked(timeRemaining: Long) {
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining))
sharedViewModel.setInProgress(false)
}
}
)
}
private fun presentRateLimitedDialog() {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean?>() {
override fun onSuccess(result: Boolean?) {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_too_many_attempts)
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
fragmentViewModel.resetAllViews()
}
show()
}
}
}
)
}
private fun presentIncorrectCodeDialog() {
sharedViewModel.incrementIncorrectCodeAttempts()
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show()
binding.keyboard.displayFailure().addListener(object : AssertedSuccessListener<Boolean?>() {
override fun onSuccess(result: Boolean?) {
fragmentViewModel.resetAllViews()
}
})
}
private fun presentSmsGenericError(requestResult: RegistrationResult) {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
Log.w(TAG, "Encountered sms provider error!", requestResult.getCause())
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_sms_provider_error)
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.showKeyboard() }
show()
}
}
}
)
}
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(message)
setPositiveButton(android.R.string.ok, positiveButtonListener)
show()
}
}
private fun presentGenericError(requestResult: RegistrationResult) {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
Log.w(TAG, "Encountered unexpected error!", requestResult.getCause())
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_error_connecting_to_service)
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.showKeyboard() }
show()
}
}
}
)
}
private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) {
if (result.willBeAbleToRequestAgain) {
Log.i(TAG, "Attempted to request new code too soon, timers should be updated")
} else {
Log.w(TAG, "Request for new verification code impossible, need to restart registration")
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() }
setCancelable(false)
show()
}
}
}
private fun presentSubmitVerificationCodeRateLimited() {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
Log.w(TAG, "Submit verification code impossible, need to request a new code and restart registration")
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() }
setCancelable(false)
show()
}
}
}
)
}
private fun popBackStack() {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PUSH_NETWORK_AUDITED)
NavHostFragment.findNavController(this).popBackStack()
sharedViewModel.setInProgress(false)
}
private fun moveToCaptcha() {
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequestCaptcha())
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
Log.i(TAG, "Received verification code via EventBus.")
binding.code.clear()
if (event.code.isBlank() || event.code.length != ReceivedSmsEvent.CODE_LENGTH) {
Log.i(TAG, "Received invalid code of length ${event.code.length}. Ignoring.")
return
}
val finalIndex = ReceivedSmsEvent.CODE_LENGTH - 1
autopilotCodeEntryActive = true
try {
event.code
.map { it.digitToInt() }
.forEachIndexed { i, digit ->
binding.code.postDelayed({
binding.code.append(digit)
if (i == finalIndex) {
autopilotCodeEntryActive = false
}
}, i * 200L)
}
Log.i(TAG, "Finished auto-filling code.")
} catch (notADigit: IllegalArgumentException) {
Log.w(TAG, "Failed to convert code into digits.", notADigit)
autopilotCodeEntryActive = false
}
}
private inner class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
override fun onNoCellSignalPresent() {
if (isAdded) {
bottomSheet.showSafely(childFragmentManager, BOTTOM_SHEET_TAG)
}
}
override fun onCellSignalPresent() {
if (bottomSheet.isResumed) {
bottomSheet.dismiss()
}
}
}
}

View File

@@ -1,8 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.entercode
data class EnterCodeState(val resetRequiredAfterFailure: Boolean = false, val showKeyboard: Boolean = false)

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.entercode
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
class EnterCodeViewModel : ViewModel() {
private val store = MutableStateFlow(EnterCodeState())
val uiState = store.asLiveData()
fun resetAllViews() {
store.update { it.copy(resetRequiredAfterFailure = true) }
}
fun allViewsResetCompleted() {
store.update {
it.copy(
resetRequiredAfterFailure = false,
showKeyboard = false
)
}
}
fun showKeyboard() {
store.update { it.copy(showKeyboard = true) }
}
fun keyboardShown() {
store.update { it.copy(showKeyboard = false) }
}
}

View File

@@ -1,125 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.grantpermissions
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Screen in account registration that provides rationales for the suggested runtime permissions.
*/
@RequiresApi(23)
class GrantPermissionsFragment : ComposeFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val args by navArgs<GrantPermissionsFragmentArgs>()
private val isSearchingForBackup = mutableStateOf(false)
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
::onPermissionsGranted
)
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
when (val resultCode = result.resultCode) {
Activity.RESULT_OK -> {
sharedViewModel.onBackupSuccessfullyRestored()
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
}
Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.")
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
}
}
private lateinit var welcomeAction: WelcomeAction
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
welcomeAction = args.welcomeAction
}
@Composable
override fun FragmentContent() {
val isSearchingForBackup by this.isSearchingForBackup
GrantPermissionsScreen(
deviceBuildVersion = Build.VERSION.SDK_INT,
isSearchingForBackup = isSearchingForBackup,
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
onNextClicked = this::launchPermissionRequests,
onNotNowClicked = this::proceedToNextScreen
)
}
private fun launchPermissionRequests() {
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
val neededPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).filterNot {
ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
}
if (neededPermissions.isEmpty()) {
proceedToNextScreen()
} else {
requestPermissionLauncher.launch(neededPermissions.toTypedArray())
}
}
private fun onPermissionsGranted(permissions: Map<String, Boolean>) {
permissions.forEach {
Log.d(TAG, "${it.key} = ${it.value}")
}
sharedViewModel.maybePrefillE164(requireContext())
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
proceedToNextScreen()
}
private fun proceedToNextScreen() {
when (welcomeAction) {
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
WelcomeAction.RESTORE_BACKUP -> {
val restoreIntent = RestoreActivity.getRestoreIntent(requireActivity())
launchRestoreActivity.launch(restoreIntent)
}
}
}
/**
* Which welcome action the user selected which prompted this
* screen.
*/
enum class WelcomeAction {
CONTINUE,
RESTORE_BACKUP
}
companion object {
private val TAG = Log.tag(GrantPermissionsFragment::class.java)
}
}

View File

@@ -1,711 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.text.Editable
import android.text.SpannableStringBuilder
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import org.signal.core.util.ThreadUtil
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.isNotNullOrBlank
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.FragmentRegistrationEnterPhoneNumberBinding
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationState
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeFragment
import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.SignalE164Util
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.livedata.LiveDataObserverCallback
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
import kotlin.time.Duration.Companion.milliseconds
/**
* Screen in registration where the user enters their phone number.
*/
class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number) {
private val TAG = Log.tag(EnterPhoneNumberFragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val fragmentViewModel by viewModels<EnterPhoneNumberViewModel>()
private val binding: FragmentRegistrationEnterPhoneNumberBinding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberBinding::bind)
private val skipToNextScreen: DialogInterface.OnClickListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> moveToVerificationEntryScreen() }
private lateinit var spinnerAdapter: ArrayAdapter<CountryPrefix>
private lateinit var phoneNumberInputLayout: TextInputEditText
private lateinit var spinnerView: TextInputEditText
private lateinit var countryPickerView: View
private var currentPhoneNumberFormatter: AsYouTypeFormatter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(binding.verifyHeader)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
popBackStack()
}
}
)
phoneNumberInputLayout = binding.number.editText as TextInputEditText
spinnerView = binding.countryCode.editText as TextInputEditText
countryPickerView = binding.countryPicker
countryPickerView.setOnClickListener {
moveToCountryPickerScreen()
}
parentFragmentManager.setFragmentResultListener(
CountryCodeFragment.REQUEST_KEY_COUNTRY,
this
) { _, bundle ->
val country: Country = bundle.getParcelableCompat(CountryCodeFragment.RESULT_COUNTRY, Country::class.java)!!
fragmentViewModel.setCountry(country.countryCode, country)
}
spinnerAdapter = ArrayAdapter<CountryPrefix>(
requireContext(),
R.layout.registration_country_code_dropdown_item,
fragmentViewModel.supportedCountryPrefixes
)
binding.registerButton.setOnClickListener { onRegistrationButtonClicked() }
binding.cancelButton.setOnClickListener { popBackStack() }
binding.toolbar.title = ""
val activity = requireActivity() as AppCompatActivity
activity.setSupportActionBar(binding.toolbar)
requireActivity().addMenuProvider(UseProxyMenuProvider(), viewLifecycleOwner)
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
presentRegisterButton(sharedState)
updateEnabledControls(sharedState.inProgress, sharedState.isReRegister)
sharedState.networkError?.let {
presentNetworkError(it)
sharedViewModel.networkErrorShown()
}
sharedState.sessionCreationError?.let {
handleSessionCreationError(it)
sharedViewModel.sessionCreationErrorShown()
}
sharedState.sessionStateError?.let {
handleSessionStateError(it)
sharedViewModel.sessionStateErrorShown()
}
sharedState.registerAccountError?.let {
handleRegistrationErrorResponse(it)
sharedViewModel.registerAccountErrorShown()
}
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRequested.isNotEmpty()) {
if (!sharedState.challengeInProgress) {
handleChallenges(sharedState.challengesRequested)
}
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
moveToEnterPinScreen()
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
moveToVerificationEntryScreen()
}
}
fragmentViewModel
.uiState
.map { it.phoneNumberRegionCode }
.distinctUntilChanged()
.observe(viewLifecycleOwner) { regionCode ->
if (regionCode.isNotNullOrBlank()) {
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus()
}
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) {
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
sharedViewModel.nationalNumber = ""
} else {
sharedViewModel.setPhoneNumber(null)
}
updateCountrySelection(fragmentState.country)
if (fragmentState.error != EnterPhoneNumberState.Error.NONE) {
presentLocalError(fragmentState)
}
}
initializeInputFields()
val existingPhoneNumber = sharedViewModel.phoneNumber
val existingNationalNumber = sharedViewModel.nationalNumber
if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
} else if (spinnerView.text?.isEmpty() == true) {
spinnerView.setText(fragmentViewModel.getDefaultCountryCode(requireContext()).toString())
phoneNumberInputLayout.setText(existingNationalNumber)
} else {
phoneNumberInputLayout.setText(existingNationalNumber)
}
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
}
private fun updateCountrySelection(country: Country?) {
if (country != null) {
binding.countryEmoji.visible = true
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
if (spinnerView.text.toString() != country.countryCode.toString()) {
spinnerView.setText(country.countryCode.toString())
}
} else {
binding.countryEmoji.visible = false
binding.country.text = getString(R.string.RegistrationActivity_select_a_country)
}
}
private fun reformatText(text: Editable?) {
if (text.isNullOrEmpty()) {
return
}
currentPhoneNumberFormatter?.let { formatter ->
formatter.clear()
var formattedNumber: String? = null
text.forEach {
if (it.isDigit()) {
formattedNumber = formatter.inputDigit(it)
}
}
if (formattedNumber != null && text.toString() != formattedNumber) {
text.replace(0, text.length, formattedNumber)
}
}
}
private fun handleChallenges(remainingChallenges: List<Challenge>) {
when (remainingChallenges.first()) {
Challenge.CAPTCHA -> moveToCaptcha()
Challenge.PUSH -> performPushChallenge()
}
}
private fun performPushChallenge() {
sharedViewModel.requestAndSubmitPushToken(requireContext())
}
private fun initializeInputFields() {
binding.countryCode.editText?.addTextChangedListener { s ->
val sanitized = s.toString().filter { c -> c.isDigit() }
if (sanitized.isNotNullOrBlank()) {
val countryCode: Int = sanitized.toInt()
fragmentViewModel.setCountry(countryCode)
} else {
binding.countryCode.editText?.setHint(R.string.RegistrationActivity_default_country_code)
fragmentViewModel.clearCountry()
}
}
phoneNumberInputLayout.addTextChangedListener(
afterTextChanged = {
reformatText(it)
fragmentViewModel.setPhoneNumber(it?.toString())
sharedViewModel.nationalNumber = it?.toString() ?: ""
}
)
val scrollView = binding.scrollView
val registerButton = binding.registerButton
phoneNumberInputLayout.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
if (hasFocus) {
scrollView.postDelayed({
scrollView.smoothScrollTo(0, registerButton.bottom)
}, 250)
}
}
phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE
phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
if (actionId == EditorInfo.IME_ACTION_DONE && v != null) {
onRegistrationButtonClicked()
return@setOnEditorActionListener true
}
false
}
}
private fun presentRegisterButton(sharedState: RegistrationState) {
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isPossibleNumber(sharedState.phoneNumber)
if (sharedState.inProgress) {
binding.registerButton.setSpinning()
} else {
binding.registerButton.cancelSpinning()
}
}
private fun presentLocalError(state: EnterPhoneNumberState) {
when (state.error) {
EnterPhoneNumberState.Error.NONE -> Unit
EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_invalid_number)
setMessage(
String.format(
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid),
state.phoneNumber
)
)
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.clearError() }
setOnCancelListener { fragmentViewModel.clearError() }
setOnDismissListener { fragmentViewModel.clearError() }
show()
}
}
EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING -> {
handlePromptForNoPlayServices()
}
EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show()
}
EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_play_services_error)
setMessage(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.clearError() }
setOnCancelListener { fragmentViewModel.clearError() }
setOnDismissListener { fragmentViewModel.clearError() }
show()
}
}
}
}
private fun presentNetworkError(networkError: Throwable) {
Log.i(TAG, "Unknown error during verification code request", networkError)
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_unable_to_connect_to_service)
setPositiveButton(android.R.string.ok, null)
show()
}
}
private fun handleSessionCreationError(result: RegistrationSessionResult) {
if (!result.isSuccess()) {
Log.i(TAG, "Handling error response of ${result.javaClass.name}", result.getCause())
}
when (result) {
is RegistrationSessionCheckResult.Success,
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
is RegistrationSessionCreationResult.RateLimited -> {
val timeRemaining = result.timeRemaining?.milliseconds
Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining")
if (timeRemaining != null) {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
} else {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
}
}
is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result)
is RegistrationSessionCheckResult.SessionNotFound -> presentGenericError(result)
is RegistrationSessionCheckResult.UnknownError,
is RegistrationSessionCreationResult.UnknownError -> presentGenericError(result)
}
}
private fun handleSessionStateError(result: VerificationCodeRequestResult) {
if (!result.isSuccess()) {
Log.i(TAG, "Handling error response.", result.getCause())
}
when (result) {
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is VerificationCodeRequestResult.ChallengeRequired -> handleChallenges(result.challenges)
is VerificationCodeRequestResult.ExternalServiceFailure -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_sms_provider_error))
is VerificationCodeRequestResult.ImpossibleNumber -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid, fragmentViewModel.phoneNumber?.toE164()))
setPositiveButton(android.R.string.ok, null)
show()
}
}
is VerificationCodeRequestResult.InvalidTransportModeFailure -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_we_couldnt_send_you_a_verification_code)
setPositiveButton(R.string.RegistrationActivity_voice_call) { _, _ ->
sharedViewModel.requestVerificationCall(requireContext())
}
setNegativeButton(R.string.RegistrationActivity_cancel, null)
show()
}
}
is VerificationCodeRequestResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> {
Log.i(TAG, result.log())
handleRequestVerificationCodeRateLimited(result)
}
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result)
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode)
is VerificationCodeRequestResult.RateLimited -> {
val timeRemaining = result.timeRemaining?.milliseconds
Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining")
if (timeRemaining != null) {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
} else {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
}
}
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is VerificationCodeRequestResult.AlreadyVerified -> presentGenericError(result)
is VerificationCodeRequestResult.NoSuchSession -> presentGenericError(result)
is VerificationCodeRequestResult.UnknownError -> presentGenericError(result)
}
}
private fun presentGenericError(result: RegistrationResult) {
Log.i(TAG, "Received unhandled response: ${result.javaClass.name}", result.getCause())
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
}
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
when (result) {
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
is RegisterAccountResult.SvrNoData -> presentAccountLocked()
else -> presentGenericError(result)
}
}
private fun presentRegistrationLocked(timeRemaining: Long) {
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionPhoneNumberRegistrationLock(timeRemaining))
sharedViewModel.setInProgress(false)
}
private fun presentRateLimitedDialog() {
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
}
private fun presentAccountLocked() {
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionPhoneNumberAccountLocked())
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
}
private fun moveToCaptcha() {
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha())
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
}
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(message)
setPositiveButton(android.R.string.ok, positiveButtonListener)
show()
}
}
private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) {
if (result.willBeAbleToRequestAgain) {
Log.i(TAG, "New verification code cannot be requested yet but can soon, moving to enter code to show timers")
moveToVerificationEntryScreen()
} else {
Log.w(TAG, "Unable to request new verification code, prompting to start new session")
MaterialAlertDialogBuilder(requireContext()).apply {
setMessage(R.string.RegistrationActivity_unable_to_connect_to_service)
setPositiveButton(R.string.NetworkFailure__retry) { _, _ ->
onRegistrationButtonClicked()
}
setNegativeButton(android.R.string.cancel, null)
show()
}
}
}
private fun handleNonNormalizedNumberError(originalNumber: String, normalizedNumber: String, mode: RegistrationRepository.E164VerificationMode) {
try {
val phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null)
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_non_standard_number_format)
setMessage(getString(R.string.RegistrationActivity_the_number_you_entered_appears_to_be_a_non_standard, originalNumber, normalizedNumber))
setNegativeButton(android.R.string.no) { d: DialogInterface, i: Int -> d.dismiss() }
setNeutralButton(R.string.RegistrationActivity_contact_signal_support) { dialogInterface, _ ->
val subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format)
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null)
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body)
dialogInterface.dismiss()
}
setPositiveButton(R.string.yes) { dialogInterface, _ ->
spinnerView.setText(phoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(phoneNumber.nationalNumber.toString())
when (mode) {
RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER,
RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext())
RegistrationRepository.E164VerificationMode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext())
}
dialogInterface.dismiss()
}
show()
}
} catch (e: NumberParseException) {
Log.w(TAG, "Failed to parse number!", e)
Dialogs.showAlertDialog(
requireContext(),
getString(R.string.RegistrationActivity_invalid_number),
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid, fragmentViewModel.phoneNumber?.toE164())
)
}
}
private fun onRegistrationButtonClicked() {
ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout)
sharedViewModel.setInProgress(true)
val hasFcm = validateFcmStatus(requireContext())
if (hasFcm) {
sharedViewModel.uiState.observe(viewLifecycleOwner, FcmTokenRetrievedObserver())
sharedViewModel.fetchFcmToken(requireContext())
} else {
sharedViewModel.uiState.value?.let { value ->
val now = System.currentTimeMillis().milliseconds
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)
} else if (now < value.nextSmsTimestamp) {
moveToVerificationEntryScreen()
} else {
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = true)
}
}
}
}
private fun onFcmTokenRetrieved(value: RegistrationState) {
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)
} else {
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = false)
}
}
private fun updateEnabledControls(showProgress: Boolean, isReRegister: Boolean) {
binding.countryCode.isEnabled = !showProgress
binding.number.isEnabled = !showProgress
binding.cancelButton.visible = !showProgress && isReRegister
}
private fun validateFcmStatus(context: Context): Boolean {
val fcmStatus = PlayServicesUtil.getPlayServicesStatus(context)
Log.d(TAG, "Got $fcmStatus for Play Services status.")
when (fcmStatus) {
PlayServicesUtil.PlayServicesStatus.SUCCESS -> {
return true
}
PlayServicesUtil.PlayServicesStatus.MISSING -> {
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
return false
}
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> {
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE)
return false
}
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> {
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT)
return false
}
null -> {
Log.w(TAG, "Null result received from PlayServicesUtil, marking Play Services as missing.")
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
return false
}
}
}
private fun handleConfirmNumberDialogCanceled() {
Log.d(TAG, "User canceled confirm number, returning to edit number.")
sharedViewModel.setInProgress(false)
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(phoneNumberInputLayout)
}
private fun presentConfirmNumberDialog(phoneNumber: PhoneNumber, isReRegister: Boolean, canSkipSms: Boolean, missingFcmConsentRequired: Boolean) {
val title = if (isReRegister) {
R.string.RegistrationActivity_additional_verification_required
} else {
R.string.RegistrationActivity_phone_number_verification_dialog_title
}
val message: CharSequence = SpannableStringBuilder().apply {
append(SpanUtil.bold(SignalE164Util.prettyPrint(phoneNumber.toE164())))
if (!canSkipSms) {
append("\n\n")
append(getString(R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number))
}
}
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(title)
setMessage(message)
setPositiveButton(android.R.string.ok) { _, _ ->
Log.d(TAG, "User confirmed number.")
if (missingFcmConsentRequired) {
handlePromptForNoPlayServices()
} else {
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
}
}
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> handleConfirmNumberDialogCanceled() }
setOnCancelListener { _ -> handleConfirmNumberDialogCanceled() }
}.show()
}
private fun handlePromptForNoPlayServices() {
val context = activity
if (context != null) {
Log.d(TAG, "Device does not have Play Services, showing consent dialog.")
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.RegistrationActivity_missing_google_play_services)
setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
setPositiveButton(R.string.RegistrationActivity_i_understand) { _, _ ->
Log.d(TAG, "User confirmed number.")
sharedViewModel.onUserConfirmedPhoneNumber(AppDependencies.application)
}
setNegativeButton(android.R.string.cancel, null)
setOnCancelListener { fragmentViewModel.clearError() }
setOnDismissListener { fragmentViewModel.clearError() }
show()
}
}
}
private fun moveToEnterPinScreen() {
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment())
sharedViewModel.setInProgress(false)
}
private fun moveToVerificationEntryScreen() {
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode())
sharedViewModel.setInProgress(false)
}
private fun moveToCountryPickerScreen() {
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionCountryPicker(fragmentViewModel.country))
}
private fun popBackStack() {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.INITIALIZATION)
findNavController().popBackStack()
}
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationState>(sharedViewModel.uiState) {
override fun onValue(value: RegistrationState): Boolean {
val fcmRetrieved = value.isFcmSupported
if (fcmRetrieved) {
onFcmTokenRetrieved(value)
}
return fcmRetrieved
}
}
private inner class UseProxyMenuProvider : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.enter_phone_number, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return if (menuItem.itemId == R.id.phone_menu_use_proxy) {
NavHostFragment.findNavController(this@EnterPhoneNumberFragment).safeNavigate(EnterPhoneNumberFragmentDirections.actionEditProxy())
true
} else {
false
}
}
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.phonenumber
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
/**
* State holder for the phone number entry screen, including phone number and Play Services errors.
*/
data class EnterPhoneNumberState(
val countryPrefixIndex: Int,
val phoneNumber: String = "",
val phoneNumberRegionCode: String,
val mode: RegistrationRepository.E164VerificationMode = RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER,
val error: Error = Error.NONE,
val country: Country? = null
) {
enum class Error {
NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT
}
}

View File

@@ -1,179 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.signal.core.util.E164Util
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.util.Util
/**
* ViewModel for the phone number entry screen.
*/
class EnterPhoneNumberViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
}
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits }
private val store = MutableStateFlow(
EnterPhoneNumberState(
countryPrefixIndex = 0,
phoneNumberRegionCode = supportedCountryPrefixes[0].regionCode
)
)
val uiState = store.asLiveData()
val phoneNumber: PhoneNumber?
get() = try {
parsePhoneNumber(store.value)
} catch (ex: NumberParseException) {
Log.w(TAG, "Could not parse phone number in current state.", ex)
null
}
var mode: RegistrationRepository.E164VerificationMode
get() = store.value.mode
set(value) = store.update {
it.copy(mode = value)
}
fun getDefaultCountryCode(context: Context): Int {
val existingCountry = store.value.country
val maybeRegionCode = Util.getNetworkCountryIso(context)
val regionCode = if (maybeRegionCode != null && supportedCountryPrefixes.any { it.regionCode == maybeRegionCode }) {
maybeRegionCode
} else {
Log.w(TAG, "Could not find region code")
"US"
}
val countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(regionCode)
val prefixIndex = countryCodeToAdapterIndex(countryCode)
store.update {
it.copy(
countryPrefixIndex = prefixIndex,
phoneNumberRegionCode = regionCode,
country = existingCountry ?: Country(
name = E164Util.getRegionDisplayName(regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(regionCode),
countryCode = countryCode,
regionCode = regionCode
)
)
}
return existingCountry?.countryCode ?: countryCode
}
val country: Country?
get() = store.value.country
fun setPhoneNumber(phoneNumber: String?) {
store.update { it.copy(phoneNumber = phoneNumber ?: "") }
}
fun clearCountry() {
store.update {
it.copy(
country = null,
phoneNumberRegionCode = "",
countryPrefixIndex = 0
)
}
}
fun setCountry(digits: Int, country: Country? = null) {
if (country == null && digits == store.value.country?.countryCode) {
return
}
val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits")
store.update {
it.copy(
country = null,
phoneNumberRegionCode = "",
countryPrefixIndex = 0
)
}
return
}
val regionCode = supportedCountryPrefixes[matchingIndex].regionCode
val matchedCountry = Country(
name = E164Util.getRegionDisplayName(regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(regionCode),
countryCode = digits,
regionCode = regionCode
)
store.update {
it.copy(
countryPrefixIndex = matchingIndex,
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode,
country = country ?: matchedCountry
)
}
}
fun parsePhoneNumber(state: EnterPhoneNumberState): PhoneNumber {
return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode)
}
fun isEnteredNumberPossible(state: EnterPhoneNumberState): Boolean {
return try {
state.country != null &&
PhoneNumberUtil.getInstance().isPossibleNumber(parsePhoneNumber(state))
} catch (ex: NumberParseException) {
false
}
}
fun restoreState(value: PhoneNumber) {
val prefixIndex = countryCodeToAdapterIndex(value.countryCode)
if (prefixIndex != -1) {
store.update {
it.copy(
countryPrefixIndex = prefixIndex,
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
phoneNumber = value.nationalNumber.toString()
)
}
}
}
private fun countryCodeToAdapterIndex(countryCode: Int): Int {
return supportedCountryPrefixes.indexOfFirst { prefix -> prefix.digits == countryCode }
}
fun clearError() {
setError(EnterPhoneNumberState.Error.NONE)
}
fun setError(error: EnterPhoneNumberState.Error) {
store.update {
it.copy(error = error)
}
}
}

View File

@@ -1,277 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.registrationlock
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.activityViewModels
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.FragmentRegistrationLockBinding
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
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
import java.util.concurrent.TimeUnit
class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_lock) {
companion object {
private val TAG = Log.tag(RegistrationLockFragment::class.java)
}
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind)
private val viewModel by activityViewModels<RegistrationViewModel>()
private var timeRemaining: Long = 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments())
timeRemaining = args.getTimeRemaining()
binding.kbsLockForgotPin.visibility = View.GONE
binding.kbsLockForgotPin.setOnClickListener { handleForgottenPin(timeRemaining) }
binding.kbsLockPinInput.setImeOptions(EditorInfo.IME_ACTION_DONE)
binding.kbsLockPinInput.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v!!)
handlePinEntry()
return@setOnEditorActionListener true
}
false
}
enableAndFocusPinEntry()
binding.kbsLockPinConfirm.setOnClickListener {
ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput)
handlePinEntry()
}
binding.kbsLockKeyboardToggle.setOnClickListener { viewModel.togglePinKeyboardType() }
viewModel.lockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> timeRemaining = t }
val triesRemaining: Int = viewModel.svrTriesRemaining
if (triesRemaining <= 3) {
val daysRemaining = getLockoutDays(timeRemaining)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() }
.show()
}
if (triesRemaining < 5) {
binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining)
}
viewModel.uiState.observe(viewLifecycleOwner) {
if (it.inProgress) {
binding.kbsLockPinConfirm.setSpinning()
} else {
binding.kbsLockPinConfirm.cancelSpinning()
}
it.sessionStateError?.let { error ->
handleSessionErrorResponse(error)
viewModel.sessionStateErrorShown()
}
it.registerAccountError?.let { error ->
handleRegistrationErrorResponse(error)
viewModel.registerAccountErrorShown()
}
it.pinKeyboardType.applyTo(
pinEditText = binding.kbsLockPinInput,
toggleTypeButton = binding.kbsLockKeyboardToggle
)
}
}
private fun handlePinEntry() {
binding.kbsLockPinInput.setEnabled(false)
val pin: String = binding.kbsLockPinInput.getText().toString()
val trimmedLength = pin.replace(" ", "").length
if (trimmedLength == 0) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) {
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
binding.kbsLockPinConfirm.setSpinning()
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin)
}
private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) {
when (requestResult) {
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
is VerificationCodeRequestResult.RegistrationLocked -> {
Log.i(TAG, "Registration locked response to verify account!")
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show()
}
else -> {
Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause())
onError()
}
}
}
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
when (result) {
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.RateLimited -> onRateLimited()
is RegisterAccountResult.AttemptsExhausted -> {
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
}
is RegisterAccountResult.RegistrationLocked -> {
Log.i(TAG, "Registration locked response to register account!")
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show()
}
is RegisterAccountResult.SvrWrongPin -> onIncorrectKbsRegistrationLockPin(result.triesRemaining)
is RegisterAccountResult.SvrNoData -> {
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
}
else -> {
Log.w(TAG, "Unable to register account with registration lock", result.getCause())
onError()
}
}
}
private fun onIncorrectKbsRegistrationLockPin(svrTriesRemaining: Int) {
binding.kbsLockPinConfirm.cancelSpinning()
binding.kbsLockPinInput.getText()?.clear()
enableAndFocusPinEntry()
if (svrTriesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.")
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
return
}
if (svrTriesRemaining == 3) {
val daysRemaining = getLockoutDays(timeRemaining)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
.setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.show()
}
if (svrTriesRemaining > 5) {
binding.kbsLockPinInputLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again)
} else {
binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining)
binding.kbsLockForgotPin.visibility = View.VISIBLE
}
}
private fun onRateLimited() {
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show()
}
fun onError() {
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show()
}
private fun handleForgottenPin(timeRemainingMs: Long) {
val lockoutDays = getLockoutDays(timeRemainingMs)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
.setMessage(requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() }
.show()
}
private fun getLockoutDays(timeRemainingMs: Long): Int {
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1
}
private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String {
val resources = requireContext().resources
val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining)
val days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining)
return "$tries $days"
}
private fun enableAndFocusPinEntry() {
binding.kbsLockPinInput.setEnabled(true)
binding.kbsLockPinInput.setFocusable(true)
ViewUtil.focusAndShowKeyboard(binding.kbsLockPinInput)
}
private fun sendEmailToSupport() {
val subject = R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin
val body = SupportEmailUtil.generateSupportEmailBody(
requireContext(),
subject,
null,
null
)
CommunicationActions.openEmail(
requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(subject),
body
)
}
}

View File

@@ -1,277 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
import android.os.Bundle
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.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationState
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
companion object {
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
}
private val registrationViewModel by activityViewModels<RegistrationViewModel>()
private val reRegisterViewModel by viewModels<ReRegisterWithPinViewModel>()
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 { reRegisterViewModel.toggleKeyboardType() }
LiveDataUtil
.combineLatest(registrationViewModel.uiState, reRegisterViewModel.uiState) { reg, rereg -> reg to rereg }
.observe(viewLifecycleOwner) { (registrationState, reRegisterState) -> updateViewState(registrationState, reRegisterState) }
}
private fun updateViewState(state: RegistrationState, reRegisterState: ReRegisterWithPinState) {
if (state.networkError != null) {
genericErrorDialog()
registrationViewModel.networkErrorShown()
} else if (!state.canSkipSms) {
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberFragment())
registrationViewModel.setInProgress(false)
} else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) {
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
onAccountLocked()
} else {
presentProgress(state.inProgress)
presentTriesRemaining(reRegisterState, state.svrTriesRemaining)
}
reRegisterState.pinKeyboardType.applyTo(
pinEditText = binding.pinRestorePinInput,
toggleTypeButton = binding.pinRestoreKeyboardToggle
)
state.registerAccountError?.let { error ->
registrationErrorHandler(error)
registrationViewModel.registerAccountErrorShown()
}
}
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 < SvrConstants.MINIMUM_PIN_LENGTH) {
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
enableAndFocusPinEntry()
return
}
registrationViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PIN_CONFIRMED)
registrationViewModel.verifyReRegisterWithPin(
context = requireContext(),
pin = pin,
wrongPinHandler = {
registrationViewModel.setInProgress(false)
reRegisterViewModel.markIncorrectGuess()
}
)
}
private fun presentTriesRemaining(reRegisterState: ReRegisterWithPinState, triesRemaining: Int) {
if (reRegisterState.hasIncorrectGuess) {
if (triesRemaining == 1 && !reRegisterState.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 (!reRegisterState.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 onNeedHelpClicked() {
Log.i(TAG, "User clicked need help dialog.")
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() {
Log.i(TAG, "User clicked the skip PIN button.")
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 presentRateLimitedDialog() {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.RegistrationActivity_too_many_attempts)
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
setPositiveButton(android.R.string.ok, null)
show()
}
}
private fun genericErrorDialog() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.RegistrationActivity_error_connecting_to_service)
.setPositiveButton(android.R.string.ok, null)
.create()
.show()
}
private fun registrationErrorHandler(result: RegisterAccountResult) {
when (result) {
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
is RegisterAccountResult.AuthorizationFailed,
is RegisterAccountResult.MalformedRequest,
is RegisterAccountResult.UnknownError,
is RegisterAccountResult.ValidationError,
is RegisterAccountResult.RegistrationLocked -> {
Log.i(TAG, "Registration failed.", result.getCause())
genericErrorDialog()
}
is RegisterAccountResult.IncorrectRecoveryPassword -> {
registrationViewModel.setUserSkippedReRegisterFlow(true)
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberFragment())
}
is RegisterAccountResult.AttemptsExhausted,
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
is RegisterAccountResult.SvrNoData -> onAccountLocked()
is RegisterAccountResult.SvrWrongPin -> {
reRegisterViewModel.markIncorrectGuess()
reRegisterViewModel.markAsRemoteVerification()
}
}
}
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class ReRegisterWithPinState(
val isLocalVerification: Boolean = false,
val hasIncorrectGuess: Boolean = false,
val localPinMatches: Boolean = false,
val pinKeyboardType: PinKeyboardType = SignalStore.pin.keyboardType
)

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
class ReRegisterWithPinViewModel : ViewModel() {
private val store = MutableStateFlow(ReRegisterWithPinState())
val uiState = store.asLiveData()
val isLocalVerification: Boolean
get() = store.value.isLocalVerification
fun markAsRemoteVerification() {
store.update {
it.copy(isLocalVerification = false)
}
}
fun markIncorrectGuess() {
store.update {
it.copy(hasIncorrectGuess = true)
}
}
fun toggleKeyboardType() {
store.update { previousState ->
previousState.copy(pinKeyboardType = previousState.pinKeyboardType.other)
}
}
}

View File

@@ -1,95 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.welcome
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
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.FragmentRegistrationWelcomeBinding
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.grantpermissions.GrantPermissionsFragment
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* First screen that is displayed on the very first app launch.
*/
class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome) {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val binding: FragmentRegistrationWelcomeBinding by ViewBinderDelegate(FragmentRegistrationWelcomeBinding::bind)
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
when (val resultCode = result.resultCode) {
Activity.RESULT_OK -> {
sharedViewModel.onBackupSuccessfullyRestored()
findNavController().safeNavigate(WelcomeFragmentDirections.actionGoToRegistration())
}
Activity.RESULT_CANCELED -> {
Log.w(TAG, "Backup restoration canceled.")
}
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(binding.image)
setDebugLogSubmitMultiTapView(binding.title)
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
}
private fun onContinueClicked() {
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.CONTINUE))
} else {
sharedViewModel.maybePrefillE164(requireContext())
findNavController().safeNavigate(WelcomeFragmentDirections.actionSkipRestore())
}
}
private fun hasAllPermissions(): Boolean {
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
return WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED }
}
private fun onTermsClicked() {
CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL)
}
private fun onTransferOrRestoreClicked() {
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP))
} else {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
val restoreIntent = RestoreActivity.getRestoreIntent(requireActivity())
launchRestoreActivity.launch(restoreIntent)
}
}
companion object {
private val TAG = Log.tag(WelcomeFragment::class.java)
private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"
}
}

View File

@@ -37,7 +37,7 @@ public final class RegistrationUtil {
SignalStore.account().isRegistered() &&
!Recipient.self().getProfileName().isEmpty() &&
(SignalStore.svr().hasPin() || SignalStore.svr().hasOptedOut()) &&
(!RemoteConfig.restoreAfterRegistration() || RestoreDecisionStateUtil.isTerminal(SignalStore.registration().getRestoreDecisionState())))
RestoreDecisionStateUtil.isTerminal(SignalStore.registration().getRestoreDecisionState()))
{
Log.i(TAG, "Marking registration completed.", new Throwable());
SignalStore.registration().markRegistrationComplete();

View File

@@ -55,13 +55,13 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionR
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeFragment
import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationState
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.CountryCodeFragment
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.PlayServicesUtil

View File

@@ -60,20 +60,17 @@ class RestoreActivity : BaseActivity() {
sharedViewModel.setNextIntent(it)
}
val navTarget = NavTarget.deserialize(intent.getIntExtra(EXTRA_NAV_TARGET, NavTarget.LEGACY_LANDING.value))
val navTarget = NavTarget.deserialize(intent.getIntExtra(EXTRA_NAV_TARGET, NavTarget.NEW_LANDING.value))
when (navTarget) {
NavTarget.NEW_LANDING -> {
if (sharedViewModel.hasMultipleRestoreMethods()) {
navController.safeNavigate(RestoreDirections.goDirectlyToNewLanding())
} else {
if (!sharedViewModel.hasMultipleRestoreMethods()) {
startActivity(RemoteRestoreActivity.getIntent(this, isOnlyOption = true))
finish()
}
}
NavTarget.LOCAL_RESTORE -> navController.safeNavigate(RestoreDirections.goDirectlyToChooseLocalBackup())
NavTarget.TRANSFER -> navController.safeNavigate(RestoreDirections.goDirectlyToDeviceTransfer())
else -> Unit
}
onBackPressedDispatcher.addCallback(
@@ -115,27 +112,19 @@ class RestoreActivity : BaseActivity() {
private val TAG = Log.tag(RestoreActivity::class)
enum class NavTarget(val value: Int) {
LEGACY_LANDING(0),
NEW_LANDING(1),
TRANSFER(2),
LOCAL_RESTORE(3);
companion object {
fun deserialize(value: Int): NavTarget {
return entries.firstOrNull { it.value == value } ?: LEGACY_LANDING
return entries.firstOrNull { it.value == value } ?: NEW_LANDING
}
}
}
private const val EXTRA_NAV_TARGET = "nav_target"
@JvmStatic
fun getDeviceTransferIntent(context: Context): Intent {
return Intent(context, RestoreActivity::class.java).apply {
putExtra(EXTRA_NAV_TARGET, NavTarget.TRANSFER.value)
}
}
@JvmStatic
fun getLocalRestoreIntent(context: Context): Intent {
return Intent(context, RestoreActivity::class.java).apply {
@@ -145,11 +134,7 @@ class RestoreActivity : BaseActivity() {
@JvmStatic
fun getRestoreIntent(context: Context): Intent {
return Intent(context, RestoreActivity::class.java).apply {
if (RemoteConfig.restoreAfterRegistration) {
putExtra(EXTRA_NAV_TARGET, NavTarget.NEW_LANDING.value)
}
}
return Intent(context, RestoreActivity::class.java)
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore.transferorrestore
import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
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.FragmentTransferRestoreV2Binding
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* This presents a list of options for the user to restore (or skip) a backup.
*/
class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_restore_v2) {
private val sharedViewModel by activityViewModels<RestoreViewModel>()
private val binding: FragmentTransferRestoreV2Binding by ViewBinderDelegate(FragmentTransferRestoreV2Binding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.transferOrRestoreTitle)
binding.transferOrRestoreFragmentTransfer.setOnClickListener { sharedViewModel.onTransferFromAndroidDeviceSelected() }
binding.transferOrRestoreFragmentRestore.setOnClickListener { sharedViewModel.onRestoreFromLocalBackupSelected() }
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(sharedViewModel.getBackupRestorationType()) }
val description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device)
val toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device)
binding.transferOrRestoreFragmentTransferDescription.text = SpanUtil.boldSubstring(description, toBold)
sharedViewModel.uiState.observe(viewLifecycleOwner) { state ->
updateSelection(state.restorationType)
}
// TODO [regv2]: port backup file detection to here
}
private fun updateSelection(restorationType: BackupRestorationType) {
binding.transferOrRestoreFragmentTransferCard.isSelected = restorationType == BackupRestorationType.DEVICE_TRANSFER
binding.transferOrRestoreFragmentRestoreCard.isSelected = restorationType == BackupRestorationType.LOCAL_BACKUP
}
private fun launchSelection(restorationType: BackupRestorationType) {
when (restorationType) {
BackupRestorationType.DEVICE_TRANSFER -> {
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionNewDeviceTransferInstructions())
}
BackupRestorationType.LOCAL_BACKUP -> {
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToLocalRestore())
}
else -> {
throw IllegalArgumentException()
}
}
}
companion object {
private val TAG = Log.tag(TransferOrRestoreV2Fragment::class.java)
}
}

View File

@@ -24,7 +24,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity;
public class Dialogs {
public static void showAlertDialog(Context context, String title, String message) {

View File

@@ -1099,17 +1099,6 @@ object RemoteConfig {
hotSwappable = false
)
/** Whether or not to launch the restore activity after registration is complete, rather than before. */
@JvmStatic
@get:JvmName("restoreAfterRegistration")
val restoreAfterRegistration: Boolean by remoteValue(
key = "android.registration.restorePostRegistration",
hotSwappable = false,
active = false
) { value ->
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || BuildConfig.LINK_DEVICE_UX_ENABLED || value.asBoolean(false)
}
@JvmStatic
val backgroundMessageProcessInterval: Long by remoteValue(
key = "android.messageProcessor.alarmIntervalMins",

View File

@@ -36,7 +36,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity;
import java.util.ArrayList;
import java.util.Arrays;