diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b37ddde6f3..a911fdde67 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -848,13 +848,6 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false"/>
-
-
@@ -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()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt
index f9ab356b63..7eadde7995 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt
@@ -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,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt
index b5ebf97c0f..278c066f04 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt
@@ -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
/**
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt
index f8209b3f65..a57298c373 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
index ceafdd59a8..01e17e9b70 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceServerTask.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceServerTask.java
index 3c1773b576..c7a31e741c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceServerTask.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceServerTask.java
@@ -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();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java
index 25cb1b5a18..36e5f1c4f2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/olddevice/OldDeviceClientTask.java
@@ -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(),
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java
index 24cd6e5ce5..8b68fe0c0a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java
@@ -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;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt
index 8b8cd81a2d..591f122199 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt
@@ -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() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt
index f57478f91a..61e3b5739b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/compose/GrantPermissionsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/compose/GrantPermissionsScreen.kt
deleted file mode 100644
index 24b68c4b01..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/compose/GrantPermissionsScreen.kt
+++ /dev/null
@@ -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)
- )
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt
index d0090f6f29..81317ef588 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt
@@ -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 {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt
deleted file mode 100644
index b31efd4783..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt
+++ /dev/null
@@ -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 = 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 =
- 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(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.toSvrCredentials(): List {
- 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()
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt
deleted file mode 100644
index a0f415bcc7..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt
+++ /dev/null
@@ -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
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationCheckpoint.kt
deleted file mode 100644
index d2c880d75c..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationCheckpoint.kt
+++ /dev/null
@@ -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
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt
deleted file mode 100644
index 09f7f13d80..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt
+++ /dev/null
@@ -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 = 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
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt
deleted file mode 100644
index 9f321ff718..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt
+++ /dev/null
@@ -1,1014 +0,0 @@
-/*
- * Copyright 2024 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-package org.thoughtcrime.securesms.registration.ui
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.Context
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.asLiveData
-import androidx.lifecycle.viewModelScope
-import com.google.i18n.phonenumbers.Phonenumber
-import kotlinx.coroutines.CoroutineExceptionHandler
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.flow.updateAndGet
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.signal.core.util.Base64
-import org.signal.core.util.Stopwatch
-import org.signal.core.util.isNotNullOrBlank
-import org.signal.core.util.logging.Log
-import org.thoughtcrime.securesms.dependencies.AppDependencies
-import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
-import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
-import org.thoughtcrime.securesms.jobs.ProfileUploadJob
-import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
-import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
-import org.thoughtcrime.securesms.jobs.StorageSyncJob
-import org.thoughtcrime.securesms.keyvalue.SignalStore
-import org.thoughtcrime.securesms.permissions.Permissions
-import org.thoughtcrime.securesms.pin.SvrRepository
-import org.thoughtcrime.securesms.pin.SvrWrongPinException
-import org.thoughtcrime.securesms.registration.data.AccountRegistrationResult
-import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil
-import org.thoughtcrime.securesms.registration.data.RegistrationData
-import org.thoughtcrime.securesms.registration.data.RegistrationRepository
-import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
-import org.thoughtcrime.securesms.registration.data.network.Challenge
-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.SessionMetadataResult
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ExternalServiceFailure
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ImpossibleNumber
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MalformedRequest
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NoSuchSession
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NonNormalizedNumber
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RateLimited
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RegistrationLocked
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RequestVerificationCodeRateLimited
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.SubmitVerificationCodeRateLimited
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted
-import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError
-import org.thoughtcrime.securesms.registration.util.RegistrationUtil
-import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
-import org.thoughtcrime.securesms.util.RemoteConfig
-import org.thoughtcrime.securesms.util.Util
-import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
-import org.whispersystems.signalservice.api.SvrNoDataException
-import org.whispersystems.signalservice.api.kbs.MasterKey
-import org.whispersystems.signalservice.api.svr.Svr3Credentials
-import org.whispersystems.signalservice.internal.push.AuthCredentials
-import java.io.IOException
-import java.nio.charset.StandardCharsets
-import java.util.concurrent.TimeUnit
-import kotlin.jvm.optionals.getOrNull
-import kotlin.time.Duration.Companion.minutes
-
-/**
- * ViewModel shared across all of registration.
- */
-class RegistrationViewModel : ViewModel() {
-
- private val store = MutableStateFlow(RegistrationState())
- private val password = Util.getSecret(18)
-
- private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
- Log.w(TAG, "CoroutineExceptionHandler invoked!")
- handleGenericError(exception)
- }
-
- val uiState = store.asLiveData()
-
- val checkpoint = store.map { it.registrationCheckpoint }.asLiveData()
-
- val lockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData()
-
- val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData()
-
- val svrTriesRemaining: Int
- get() = store.value.svrTriesRemaining
-
- var isReregister: Boolean
- get() = store.value.isReRegister
- set(value) {
- store.update {
- it.copy(isReRegister = value)
- }
-
- if (value) {
- SignalStore.misc.needsUsernameRestore = true
- }
- }
-
- val phoneNumber: Phonenumber.PhoneNumber?
- get() = store.value.phoneNumber
-
- var nationalNumber: String
- get() = store.value.nationalNumber
- set(value) {
- store.update {
- it.copy(nationalNumber = value)
- }
- }
-
- @SuppressLint("MissingPermission")
- fun maybePrefillE164(context: Context) {
- Log.v(TAG, "maybePrefillE164()")
- if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
- val localNumber = Util.getDeviceNumber(context).getOrNull()
-
- if (localNumber != null) {
- Log.v(TAG, "Phone number detected.")
- setPhoneNumber(localNumber)
- } else {
- Log.i(TAG, "Could not read phone number.")
- }
- } else {
- Log.i(TAG, "No phone permission.")
- }
- }
-
- fun setInProgress(inProgress: Boolean) {
- store.update {
- it.copy(inProgress = inProgress)
- }
- }
-
- fun setRegistrationCheckpoint(checkpoint: RegistrationCheckpoint) {
- store.update {
- it.copy(registrationCheckpoint = checkpoint)
- }
- }
-
- fun setPhoneNumber(phoneNumber: Phonenumber.PhoneNumber?) {
- store.update {
- it.copy(
- phoneNumber = phoneNumber,
- sessionId = null
- )
- }
- }
-
- fun setCaptchaResponse(token: String) {
- store.update {
- it.copy(
- captchaToken = token
- )
- }
- }
-
- fun sessionCreationErrorShown() {
- store.update {
- it.copy(sessionCreationError = null)
- }
- }
-
- fun sessionStateErrorShown() {
- store.update {
- it.copy(sessionStateError = null)
- }
- }
-
- fun registerAccountErrorShown() {
- store.update {
- it.copy(registerAccountError = null)
- }
- }
-
- fun incrementIncorrectCodeAttempts() {
- store.update {
- it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1)
- }
- }
-
- fun fetchFcmToken(context: Context) {
- viewModelScope.launch(context = coroutineExceptionHandler) {
- val fcmToken = RegistrationRepository.getFcmToken(context)
- store.update {
- it.copy(registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, isFcmSupported = true, fcmToken = fcmToken)
- }
- }
- }
-
- private suspend fun updateFcmToken(context: Context): String? {
- Log.d(TAG, "Fetching FCM token…")
- val fcmToken = RegistrationRepository.getFcmToken(context)
- store.update {
- it.copy(fcmToken = fcmToken)
- }
- Log.d(TAG, "FCM token fetched.")
- return fcmToken
- }
-
- fun togglePinKeyboardType() {
- store.update { previousState ->
- previousState.copy(pinKeyboardType = previousState.pinKeyboardType.other)
- }
- }
-
- fun onBackupSuccessfullyRestored() {
- val recoveryPassword = SignalStore.svr.recoveryPassword
- store.update {
- it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED_OR_SKIPPED, canSkipSms = recoveryPassword != null, isReRegister = true)
- }
- }
-
- fun onUserConfirmedPhoneNumber(context: Context) {
- setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
- val state = store.value
-
- val e164 = state.phoneNumber?.toE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") }
-
- if (!state.userSkippedReregistration) {
- if (SignalStore.svr.recoveryPassword != null && matchesSavedE164(e164)) {
- // Re-registration when the local database is intact.
- Log.d(TAG, "Has recovery password, and therefore can skip SMS verification.")
- store.update {
- it.copy(
- canSkipSms = true,
- isReRegister = true
- )
- }
- return
- }
- }
-
- viewModelScope.launch {
- if (!state.userSkippedReregistration) {
- val svrCredentialsResult: BackupAuthCheckResult = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
-
- when (svrCredentialsResult) {
- is BackupAuthCheckResult.UnknownError -> {
- handleGenericError(svrCredentialsResult.getCause())
- return@launch
- }
-
- is BackupAuthCheckResult.SuccessWithCredentials -> {
- Log.d(TAG, "Found local valid SVR auth credentials.")
- store.update {
- it.copy(
- isReRegister = true,
- canSkipSms = true,
- svr2AuthCredentials = svrCredentialsResult.svr2Credentials,
- svr3AuthCredentials = svrCredentialsResult.svr3Credentials
- )
- }
- return@launch
- }
-
- is BackupAuthCheckResult.SuccessWithoutCredentials -> {
- Log.d(TAG, "No local SVR auth credentials could be found and/or validated.")
- }
- }
- }
-
- val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
-
- if (validSession.verified) {
- Log.i(TAG, "Session is already verified, registering account.")
- registerVerifiedSession(context, validSession.sessionId)
- return@launch
- }
-
- if (!validSession.allowedToRequestCode) {
- Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
- handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
- return@launch
- }
-
- requestSmsCodeInternal(context, validSession.sessionId, e164)
- }
- }
-
- fun requestSmsCode(context: Context) {
- val e164 = getCurrentE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") }
-
- viewModelScope.launch {
- val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
- requestSmsCodeInternal(context, validSession.sessionId, e164)
- }
- }
-
- fun requestVerificationCall(context: Context) {
- val e164 = getCurrentE164()
-
- if (e164 == null) {
- Log.w(TAG, "Phone number was null after confirmation.")
- setInProgress(false)
- return
- }
-
- viewModelScope.launch {
- val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting a verification call.") }
- Log.d(TAG, "Requesting voice call code…")
- val codeRequestResponse = RegistrationRepository.requestSmsCode(
- context = context,
- sessionId = validSession.sessionId,
- e164 = e164,
- password = password,
- mode = RegistrationRepository.E164VerificationMode.PHONE_CALL
- )
- Log.d(TAG, "Voice code request network call completed.")
-
- handleSessionStateResult(context, codeRequestResponse)
- if (codeRequestResponse is Success) {
- Log.d(TAG, "Voice code request was successful.")
- }
- }
- }
-
- private suspend fun requestSmsCodeInternal(context: Context, sessionId: String, e164: String) {
- var smsListenerReady = false
- Log.d(TAG, "Initializing SMS listener.")
- if (store.value.smsListenerTimeout < System.currentTimeMillis()) {
- smsListenerReady = store.value.isFcmSupported && RegistrationRepository.registerSmsListener(context)
-
- if (smsListenerReady) {
- val smsRetrieverTimeout = System.currentTimeMillis() + 5.minutes.inWholeMilliseconds
- Log.d(TAG, "Successfully started verification code SMS retriever, which will last until $smsRetrieverTimeout.")
- store.update { it.copy(smsListenerTimeout = smsRetrieverTimeout) }
- } else {
- Log.d(TAG, "Could not start verification code SMS retriever.")
- }
- }
-
- Log.d(TAG, "Requesting SMS code…")
- val transportMode = if (smsListenerReady) RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER else RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER
- val codeRequestResponse = RegistrationRepository.requestSmsCode(
- context = context,
- sessionId = sessionId,
- e164 = e164,
- password = password,
- mode = transportMode
- )
- Log.d(TAG, "SMS code request network call completed.")
-
- if (codeRequestResponse is AlreadyVerified) {
- Log.d(TAG, "Got session was already verified when requesting SMS code.")
- registerVerifiedSession(context, sessionId)
- return
- }
-
- handleSessionStateResult(context, codeRequestResponse)
-
- if (codeRequestResponse is Success) {
- Log.d(TAG, "SMS code request was successful.")
- store.update {
- it.copy(
- registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
- )
- }
- } else {
- Log.i(TAG, "SMS code request failed: ${codeRequestResponse::class.simpleName}")
- }
- }
-
- private suspend fun getOrCreateValidSession(context: Context): SessionMetadataResult? {
- Log.v(TAG, "getOrCreateValidSession()")
- val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
- val mccMncProducer = MccMncProducer(context)
-
- val existingSessionId = store.value.sessionId
- return getOrCreateValidSession(
- context = context,
- existingSessionId = existingSessionId,
- e164 = e164,
- password = password,
- mcc = mccMncProducer.mcc,
- mnc = mccMncProducer.mnc,
- successListener = { sessionData ->
- Log.i(TAG, "[getOrCreateValidSession] Challenges requested: ${sessionData.challengesRequested}", true)
- store.update {
- it.copy(
- sessionId = sessionData.sessionId,
- nextSmsTimestamp = sessionData.nextSmsTimestamp,
- nextCallTimestamp = sessionData.nextCallTimestamp,
- nextVerificationAttempt = sessionData.nextVerificationAttempt,
- allowedToRequestCode = sessionData.allowedToRequestCode,
- challengesRequested = sessionData.challengesRequested,
- verified = sessionData.verified
- )
- }
- },
- errorHandler = { error ->
- Log.d(TAG, "Setting ${error::class.simpleName} as session creation error.")
- store.update {
- it.copy(
- sessionCreationError = error,
- inProgress = false
- )
- }
- }
- )
- }
-
- fun submitCaptchaToken(context: Context) {
- val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
- val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
-
- store.update {
- it.copy(captchaToken = null, challengeInProgress = true, inProgress = true)
- }
-
- viewModelScope.launch {
- val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") }
- Log.d(TAG, "Submitting captcha token…")
- val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.sessionId, captchaToken)
- Log.d(TAG, "Captcha token submitted.")
-
- handleSessionStateResult(context, captchaSubmissionResult)
-
- store.update { it.copy(challengeInProgress = false) }
-
- if (captchaSubmissionResult is Success) {
- requestSmsCode(context)
- } else {
- setInProgress(false)
- }
- }
- }
-
- fun requestAndSubmitPushToken(context: Context) {
- Log.v(TAG, "validatePushToken()")
-
- val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
-
- viewModelScope.launch {
- Log.d(TAG, "Getting session in order to perform push token verification…")
- val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") }
-
- if (!session.challengesRequested.contains(Challenge.PUSH)) {
- return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") }
- }
-
- Log.d(TAG, "Requesting push challenge token…")
- val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.sessionId, e164, password)
- Log.d(TAG, "Push challenge token submitted.")
- handleSessionStateResult(context, pushSubmissionResult)
- }
- }
-
- /**
- * @return whether the request was successful and execution should continue
- */
- private suspend fun handleSessionStateResult(context: Context, sessionResult: VerificationCodeRequestResult): Boolean {
- Log.v(TAG, "handleSessionStateResult()")
- when (sessionResult) {
- is UnknownError -> {
- handleGenericError(sessionResult.getCause())
- }
-
- is Success -> {
- Log.d(TAG, "New registration session status received.")
- updateFcmToken(context)
- store.update {
- it.copy(
- sessionId = sessionResult.sessionId,
- nextSmsTimestamp = sessionResult.nextSmsTimestamp,
- nextCallTimestamp = sessionResult.nextCallTimestamp,
- isAllowedToRequestCode = sessionResult.allowedToRequestCode,
- challengesRequested = emptyList()
- )
- }
- return true
- }
-
- is ChallengeRequired -> {
- Log.d(TAG, "[${sessionResult.challenges.joinToString()}] registration challenges received.")
- store.update {
- it.copy(
- challengesRequested = sessionResult.challenges
- )
- }
- return false
- }
-
- is ImpossibleNumber -> Log.i(TAG, "Received ImpossibleNumber.", sessionResult.getCause())
-
- is NonNormalizedNumber -> Log.i(TAG, "Received NonNormalizedNumber.", sessionResult.getCause())
-
- is RateLimited -> Log.i(TAG, "Received RateLimited.", sessionResult.getCause())
-
- is ExternalServiceFailure -> Log.i(TAG, "Received ExternalServiceFailure.", sessionResult.getCause())
-
- is InvalidTransportModeFailure -> Log.i(TAG, "Received InvalidTransportModeFailure.", sessionResult.getCause())
-
- is MalformedRequest -> Log.i(TAG, "Received MalformedRequest.", sessionResult.getCause())
-
- is RequestVerificationCodeRateLimited -> {
- Log.i(TAG, "Received RequestVerificationCodeRateLimited.", sessionResult.getCause())
-
- if (sessionResult.willBeAbleToRequestAgain) {
- store.update {
- it.copy(
- nextSmsTimestamp = sessionResult.nextSmsTimestamp,
- nextCallTimestamp = sessionResult.nextCallTimestamp
- )
- }
- } else {
- Log.w(TAG, "Request verification code rate limit is forever, need to start new session")
- SignalStore.registration.sessionId = null
- store.update { RegistrationState() }
- }
- }
-
- is SubmitVerificationCodeRateLimited -> Log.i(TAG, "Received SubmitVerificationCodeRateLimited.", sessionResult.getCause())
-
- is TokenNotAccepted -> Log.i(TAG, "Received TokenNotAccepted.", sessionResult.getCause())
-
- is RegistrationLocked -> {
- store.update {
- it.copy(lockedTimeRemaining = sessionResult.timeRemaining)
- }
- Log.i(TAG, "Received RegistrationLocked.", sessionResult.getCause())
- }
-
- is NoSuchSession -> Log.i(TAG, "Received NoSuchSession.", sessionResult.getCause())
-
- is AlreadyVerified -> Log.i(TAG, "Received AlreadyVerified", sessionResult.getCause())
- }
-
- store.update {
- it.copy(
- inProgress = false,
- sessionStateError = sessionResult
- )
- }
- return false
- }
-
- /**
- * @return whether the request was successful and execution should continue
- */
- private suspend fun handleRegistrationResult(context: Context, registrationData: RegistrationData, registrationResult: RegisterAccountResult, reglockEnabled: Boolean): Boolean {
- Log.v(TAG, "handleRegistrationResult()")
- var stayInProgress = false
- when (registrationResult) {
- is RegisterAccountResult.Success -> {
- Log.i(TAG, "Register account result: Success! Registration lock: $reglockEnabled")
- store.update {
- it.copy(
- registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED
- )
- }
- onSuccessfulRegistration(context, registrationData, registrationResult.accountRegistrationResult, reglockEnabled)
- return true
- }
-
- is RegisterAccountResult.IncorrectRecoveryPassword -> {
- Log.i(TAG, "Registration recovery password was incorrect, falling back to SMS verification.", registrationResult.getCause())
- setUserSkippedReRegisterFlow(true)
- }
-
- is RegisterAccountResult.RegistrationLocked -> {
- Log.i(TAG, "Account is registration locked!", registrationResult.getCause())
- stayInProgress = true
- }
-
- is RegisterAccountResult.SvrWrongPin -> {
- Log.i(TAG, "Received wrong SVR PIN response! ${registrationResult.triesRemaining} tries remaining.")
- updateSvrTriesRemaining(registrationResult.triesRemaining)
- }
-
- is RegisterAccountResult.SvrNoData,
- is RegisterAccountResult.AttemptsExhausted,
- is RegisterAccountResult.RateLimited,
- is RegisterAccountResult.AuthorizationFailed,
- is RegisterAccountResult.MalformedRequest,
- is RegisterAccountResult.ValidationError,
- is RegisterAccountResult.UnknownError -> Log.i(TAG, "Received error when trying to register!", registrationResult.getCause())
- }
- store.update {
- it.copy(
- inProgress = stayInProgress,
- registerAccountError = registrationResult
- )
- }
- return false
- }
-
- private fun handleGenericError(cause: Throwable) {
- Log.w(TAG, "Encountered unknown error!", cause)
- store.update {
- it.copy(inProgress = false, networkError = cause)
- }
- }
-
- private fun updateSvrTriesRemaining(remainingTries: Int) {
- store.update {
- it.copy(svrTriesRemaining = remainingTries)
- }
- }
-
- fun setUserSkippedReRegisterFlow(value: Boolean) {
- store.update {
- it.copy(userSkippedReregistration = value, canSkipSms = !value)
- }
- }
-
- fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) {
- setInProgress(true)
-
- // remote recovery password
- val svr2Credentials = store.value.svr2AuthCredentials ?: SignalStore.svr.svr2AuthTokens.toSvrCredentials()
- val svr3Credentials = store.value.svr3AuthCredentials ?: SignalStore.svr.svr3AuthTokens.toSvrCredentials()?.let { Svr3Credentials(it.username(), it.password(), null) }
-
- if (svr2Credentials != null || svr3Credentials != null) {
- Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR (svr2: ${svr2Credentials != null}, svr3: ${svr3Credentials != null}).")
- viewModelScope.launch(context = coroutineExceptionHandler) {
- try {
- val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, svr2Credentials, svr3Credentials)
- SignalStore.svr.masterKeyForInitialDataRestore = masterKey
- SignalStore.svr.setPin(pin)
-
- updateSvrTriesRemaining(10)
- verifyReRegisterInternal(context, pin, masterKey)
- } catch (rejectedPin: SvrWrongPinException) {
- Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin)
- updateSvrTriesRemaining(rejectedPin.triesRemaining)
- wrongPinHandler()
- } catch (noData: SvrNoDataException) {
- Log.w(TAG, "SVR has no data for these credentials. Aborting skip SMS flow.", noData)
- updateSvrTriesRemaining(0)
- setUserSkippedReRegisterFlow(true)
- }
- }
- return
- }
-
- // Local recovery password
- if (RegistrationRepository.canUseLocalRecoveryPassword()) {
- if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
- Log.d(TAG, "Found recovery password, attempting to re-register.")
- viewModelScope.launch(context = coroutineExceptionHandler) {
- verifyReRegisterInternal(context, pin, SignalStore.svr.masterKey)
- }
- } else {
- Log.d(TAG, "Entered PIN did not match local PIN hash.")
- wrongPinHandler()
- }
- return
- }
-
- Log.w(TAG, "Could not get credentials to skip SMS registration, aborting!")
- store.update {
- it.copy(canSkipSms = false)
- }
- }
-
- private suspend fun verifyReRegisterInternal(context: Context, pin: String, masterKey: MasterKey) {
- Log.v(TAG, "verifyReRegisterInternal()")
- updateFcmToken(context)
-
- val registrationData = getRegistrationData()
-
- val resultAndRegLockStatus = registerAccountInternal(context, null, registrationData, pin, masterKey)
- val result = resultAndRegLockStatus.first
- val reglockEnabled = resultAndRegLockStatus.second
-
- handleRegistrationResult(context, registrationData, result, reglockEnabled)
- }
-
- /**
- * @return a [Pair] containing the server response and a boolean signifying whether the current account is registration locked.
- */
- private suspend fun registerAccountInternal(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String?, masterKey: MasterKey): Pair {
- Log.v(TAG, "registerAccountInternal()")
- var registrationResult: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = masterKey.deriveRegistrationRecoveryPassword(), pin = pin)
-
- // Check if reg lock is enabled
- if (registrationResult !is RegisterAccountResult.RegistrationLocked) {
- if (registrationResult is RegisterAccountResult.Success) {
- registrationResult = RegisterAccountResult.Success(registrationResult.accountRegistrationResult.copy(masterKey = masterKey))
- }
-
- Log.i(TAG, "Received a non-registration lock response to registration. Assuming registration lock as DISABLED")
- return Pair(registrationResult, false)
- }
-
- Log.i(TAG, "Received a registration lock response when trying to register an account. Retrying with master key.")
- store.update {
- it.copy(
- svr2AuthCredentials = registrationResult.svr2Credentials,
- svr3AuthCredentials = registrationResult.svr3Credentials
- )
- }
-
- return Pair(RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = masterKey.deriveRegistrationRecoveryPassword(), pin = pin) { masterKey }, true)
- }
-
- fun verifyCodeWithoutRegistrationLock(context: Context, code: String) {
- Log.v(TAG, "verifyCodeWithoutRegistrationLock()")
- store.update {
- it.copy(
- inProgress = true,
- enteredCode = code,
- registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED
- )
- }
-
- viewModelScope.launch(context = coroutineExceptionHandler) {
- verifyCodeInternal(
- context = context,
- registrationLocked = false,
- pin = null
- )
- }
- }
-
- fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String) {
- Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()")
- SignalStore.pin.keyboardType = store.value.pinKeyboardType
-
- store.update {
- it.copy(
- inProgress = true,
- registrationCheckpoint = RegistrationCheckpoint.PIN_ENTERED
- )
- }
- viewModelScope.launch {
- verifyCodeInternal(
- context = context,
- registrationLocked = true,
- pin = pin
- )
- }
- }
-
- private suspend fun verifyCodeInternal(context: Context, registrationLocked: Boolean, pin: String?) {
- Log.d(TAG, "Getting valid session in order to submit verification code.")
-
- if (registrationLocked && pin.isNullOrBlank()) {
- throw IllegalStateException("Must have PIN to register with registration lock!")
- }
-
- var reglock = registrationLocked
-
- val session: SessionMetadataResult? = getOrCreateValidSession(context)
- val sessionId: String = session?.sessionId ?: return
- val registrationData: RegistrationData = getRegistrationData()
-
- if (session.verified) {
- Log.i(TAG, "Session is already verified, registering account.")
- } else {
- Log.d(TAG, "Submitting verification code…")
-
- val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
-
- val submissionSuccessful = verificationResponse is Success
- val alreadyVerified = verificationResponse is AlreadyVerified
-
- Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
-
- if (!submissionSuccessful && !alreadyVerified) {
- handleSessionStateResult(context, verificationResponse)
- return
- }
- }
-
- Log.d(TAG, "Submitting registration…")
-
- var result: RegisterAccountResult? = null
- var state = store.value
-
- if (!reglock) {
- Log.d(TAG, "Registration lock not enabled, attempting to register account without master key producer.")
- result = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null, pin = pin)
- }
-
- if (result is RegisterAccountResult.RegistrationLocked) {
- Log.d(TAG, "Registration lock response received.")
- val timeRemaining = result.timeRemaining
- store.update {
- it.copy(lockedTimeRemaining = timeRemaining)
- }
- reglock = true
-
- if (pin == null && SignalStore.svr.registrationLockToken != null) {
- Log.d(TAG, "Retrying registration with stored credentials.")
- result = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null, pin = SignalStore.svr.pin) { SignalStore.svr.masterKey }
- }
-
- if (result is RegisterAccountResult.RegistrationLocked && (result.svr2Credentials != null || result.svr3Credentials != null)) {
- Log.d(TAG, "Retrying registration with received credentials (svr2: ${result.svr2Credentials != null}, svr3: ${result.svr3Credentials != null}).")
- val svr2Credentials = result.svr2Credentials
- val svr3Credentials = result.svr3Credentials
- state = store.updateAndGet {
- it.copy(svr2AuthCredentials = svr2Credentials, svr3AuthCredentials = svr3Credentials)
- }
- }
- }
-
- if (reglock && pin.isNotNullOrBlank()) {
- Log.d(TAG, "Registration lock enabled, attempting to register account restore master key from SVR (svr2: ${state.svr2AuthCredentials != null}, svr3: ${state.svr3AuthCredentials != null})")
- result = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null, pin = pin) {
- SvrRepository.restoreMasterKeyPreRegistration(
- credentials = SvrAuthCredentialSet(
- svr2Credentials = state.svr2AuthCredentials,
- svr3Credentials = state.svr3AuthCredentials
- ),
- userPin = pin
- )
- }
- }
-
- if (result != null) {
- handleRegistrationResult(context, registrationData, result, reglock)
- } else {
- Log.w(TAG, "No registration response received!")
- }
- }
-
- private suspend fun registerVerifiedSession(context: Context, sessionId: String) {
- Log.v(TAG, "registerVerifiedSession()")
- val registrationData = getRegistrationData()
- var result: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null)
-
- val reglockEnabled = result is RegisterAccountResult.RegistrationLocked
-
- if (reglockEnabled) {
- Log.i(TAG, "Registration lock response received.")
- store.update { it.copy(lockedTimeRemaining = result.timeRemaining) }
-
- if (SignalStore.svr.registrationLockToken != null) {
- Log.d(TAG, "Retrying registration with stored credentials.")
- result = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null, pin = SignalStore.svr.pin) { SignalStore.svr.masterKey }
- }
-
- if (result is RegisterAccountResult.RegistrationLocked && (result.svr2Credentials != null || result.svr3Credentials != null)) {
- Log.i(TAG, "Saving registration lock received credentials (svr2: ${result.svr2Credentials != null}, svr3: ${result.svr3Credentials != null}).")
- store.update {
- it.copy(
- svr2AuthCredentials = result.svr2Credentials,
- svr3AuthCredentials = result.svr3Credentials
- )
- }
- }
- }
-
- handleRegistrationResult(context, registrationData, result, reglockEnabled)
- }
-
- private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: AccountRegistrationResult, reglockEnabled: Boolean) {
- Log.v(TAG, "onSuccessfulRegistration()")
- val metadata = LocalRegistrationMetadataUtil.createLocalRegistrationMetadata(SignalStore.account.aciIdentityKey, SignalStore.account.pniIdentityKey, registrationData, remoteResult, reglockEnabled)
- RegistrationRepository.registerAccountLocally(context, metadata)
-
- if (reglockEnabled) {
- SignalStore.onboarding.clearAll()
-
- val stopwatch = Stopwatch("post-reg-storage-service")
-
- AppDependencies.jobManager.runSynchronously(StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN)
- stopwatch.split("account-restore")
-
- AppDependencies.jobManager
- .startChain(StorageSyncJob.forAccountRestore())
- .then(ReclaimUsernameAndLinkJob())
- .enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10))
- stopwatch.split("storage-sync")
-
- stopwatch.stop(TAG)
- } else if (SignalStore.misc.needsUsernameRestore) {
- AppDependencies.jobManager.runSynchronously(ReclaimUsernameAndLinkJob(), TimeUnit.SECONDS.toMillis(10))
- }
-
- refreshRemoteConfig()
-
- store.update {
- it.copy(
- registrationCheckpoint = RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE
- )
- }
- }
-
- fun hasPin(): Boolean {
- return RegistrationRepository.hasPin() || store.value.isReRegister
- }
-
- fun completeRegistration() {
- AppDependencies.jobManager.startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue()
- RegistrationUtil.maybeMarkRegistrationComplete()
- }
-
- fun networkErrorShown() {
- store.update {
- it.copy(networkError = null)
- }
- }
-
- private fun matchesSavedE164(e164: String?): Boolean {
- return if (e164 == null) {
- false
- } else {
- e164 == SignalStore.account.e164
- }
- }
-
- private fun getCurrentE164(): String? {
- return store.value.phoneNumber?.toE164()
- }
-
- private suspend fun getRegistrationData(): RegistrationData {
- val currentState = store.value
- val code = currentState.enteredCode
- val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException("Can't construct registration data without E164!")
- val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr.recoveryPassword else null
- return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), currentState.fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
- }
-
- /**
- * Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened.
- *
- * @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages.
- */
- private fun bail(logMessage: () -> Unit) {
- logMessage()
- setInProgress(false)
- }
-
- /** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
- private fun List.toSvrCredentials(): AuthCredentials? {
- return this
- .asSequence()
- .filterNotNull()
- .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) }
- .mapNotNull {
- val colonIndex = it.indexOf(":")
- if (colonIndex < 0) {
- return@mapNotNull null
- }
- AuthCredentials.create(it.substring(0, colonIndex), it.substring(colonIndex + 1))
- }
- .firstOrNull()
- }
-
- companion object {
- private val TAG = Log.tag(RegistrationViewModel::class.java)
-
- private suspend fun refreshRemoteConfig() = withContext(Dispatchers.IO) {
- val startTime = System.currentTimeMillis()
- try {
- RemoteConfig.refreshSync()
- Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags.")
- } catch (e: IOException) {
- Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e)
- }
- }
-
- suspend fun getOrCreateValidSession(
- context: Context,
- existingSessionId: String?,
- e164: String,
- password: String,
- mcc: String?,
- mnc: String?,
- successListener: (SessionMetadataResult) -> Unit,
- errorHandler: (RegistrationSessionResult) -> Unit
- ): SessionMetadataResult? {
- Log.d(TAG, "Validating/creating a registration session.")
- val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc)
- when (sessionResult) {
- is RegistrationSessionCheckResult.Success -> {
- successListener(sessionResult)
- Log.d(TAG, "Registration session validated.")
- return sessionResult
- }
-
- is RegistrationSessionCreationResult.Success -> {
- successListener(sessionResult)
- Log.d(TAG, "Registration session created.")
- return sessionResult
- }
-
- else -> {
- Log.d(TAG, "Handling error during session creation.")
- errorHandler(sessionResult)
- }
- }
- return null
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/accountlocked/AccountLockedFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/accountlocked/AccountLockedFragment.kt
deleted file mode 100644
index 1d6f0ea6d6..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/accountlocked/AccountLockedFragment.kt
+++ /dev/null
@@ -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()
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title))
-
- val description = view.findViewById(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(R.id.account_locked_next).setOnClickListener { v: View? -> onNext() }
- view.findViewById(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
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/CaptchaFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/CaptchaFragment.kt
deleted file mode 100644
index 11f9078e4d..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/CaptchaFragment.kt
+++ /dev/null
@@ -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)
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/RegistrationCaptchaFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/RegistrationCaptchaFragment.kt
deleted file mode 100644
index 867a5f9736..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/RegistrationCaptchaFragment.kt
+++ /dev/null
@@ -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()
-
- override fun handleCaptchaToken(token: String) {
- sharedViewModel.setCaptchaResponse(token)
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeFragment.kt
deleted file mode 100644
index 1f4e4efebd..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeFragment.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeViewModel.kt
index 2025dc609d..3ad82d151d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/countrycode/CountryCodeViewModel.kt
@@ -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() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt
deleted file mode 100644
index a41a8443e9..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt
+++ /dev/null
@@ -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()
- private val fragmentViewModel by viewModels()
- 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) {
- when (remainingChallenges.first()) {
- Challenge.CAPTCHA -> moveToCaptcha()
- Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext())
- }
- }
-
- private fun presentAccountLocked() {
- binding.keyboard.displayLocked().addListener(
- object : AssertedSuccessListener() {
- override fun onSuccess(result: Boolean?) {
- findNavController().safeNavigate(EnterCodeFragmentDirections.actionAccountLocked())
- }
- }
- )
- }
-
- private fun presentRegistrationLocked(timeRemaining: Long) {
- binding.keyboard.displayLocked().addListener(
- object : AssertedSuccessListener() {
- override fun onSuccess(result: Boolean?) {
- findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining))
- sharedViewModel.setInProgress(false)
- }
- }
- )
- }
-
- private fun presentRateLimitedDialog() {
- binding.keyboard.displayFailure().addListener(
- object : AssertedSuccessListener() {
- 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() {
- override fun onSuccess(result: Boolean?) {
- fragmentViewModel.resetAllViews()
- }
- })
- }
-
- private fun presentSmsGenericError(requestResult: RegistrationResult) {
- binding.keyboard.displayFailure().addListener(
- object : AssertedSuccessListener() {
- 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() {
- 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() {
- 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()
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeState.kt
deleted file mode 100644
index 8c570d135b..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeState.kt
+++ /dev/null
@@ -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)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeViewModel.kt
deleted file mode 100644
index a467b5b51f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeViewModel.kt
+++ /dev/null
@@ -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) }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/grantpermissions/GrantPermissionsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/grantpermissions/GrantPermissionsFragment.kt
deleted file mode 100644
index 91a3e84e78..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/grantpermissions/GrantPermissionsFragment.kt
+++ /dev/null
@@ -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()
- private val args by navArgs()
- 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) {
- 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)
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt
deleted file mode 100644
index 0ff7998c5a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt
+++ /dev/null
@@ -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()
- private val fragmentViewModel by viewModels()
- private val binding: FragmentRegistrationEnterPhoneNumberBinding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberBinding::bind)
-
- private val skipToNextScreen: DialogInterface.OnClickListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> moveToVerificationEntryScreen() }
-
- private lateinit var spinnerAdapter: ArrayAdapter
- 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(
- 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) {
- 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(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
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt
deleted file mode 100644
index 275295f473..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt
+++ /dev/null
@@ -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
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt
deleted file mode 100644
index 5c5090d5e8..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt
+++ /dev/null
@@ -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 = 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)
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt
deleted file mode 100644
index cc4861e525..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt
+++ /dev/null
@@ -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()
-
- 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
- )
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt
deleted file mode 100644
index 1acf612bdd..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt
+++ /dev/null
@@ -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()
- private val reRegisterViewModel by viewModels()
-
- 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()
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinState.kt
deleted file mode 100644
index 6f0e179422..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinState.kt
+++ /dev/null
@@ -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
-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinViewModel.kt
deleted file mode 100644
index cb4b3d1b1b..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinViewModel.kt
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/welcome/WelcomeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/welcome/WelcomeFragment.kt
deleted file mode 100644
index 555f8ee5cd..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/welcome/WelcomeFragment.kt
+++ /dev/null
@@ -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()
- 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"
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java
index dc69f189ef..591f973623 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationUtil.java
@@ -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();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt
index b3223f0a90..1656dd28dc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt
index d17879eebc..a391a18235 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt
@@ -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)
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt
deleted file mode 100644
index a4739655fd..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt
+++ /dev/null
@@ -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()
- 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)
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java
index bf16f773c5..69e4adb90c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java
@@ -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) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
index cd6df9ea4a..f8e2347505 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
@@ -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",
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index acadff90cd..75cafa4b57 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -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;
diff --git a/app/src/main/res/layout/activity_registration_navigation_v2.xml b/app/src/main/res/layout/activity_registration_navigation_v2.xml
deleted file mode 100644
index a0e54602d5..0000000000
--- a/app/src/main/res/layout/activity_registration_navigation_v2.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_registration_enter_code.xml b/app/src/main/res/layout/fragment_registration_enter_code.xml
index 8e833dfc7c..83e0812b67 100644
--- a/app/src/main/res/layout/fragment_registration_enter_code.xml
+++ b/app/src/main/res/layout/fragment_registration_enter_code.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- tools:context=".registration.ui.entercode.EnterCodeFragment">
+ tools:context=".registrationv3.ui.entercode.EnterCodeFragment">
+ android:layout_height="wrap_content">
+ tools:context="org.thoughtcrime.securesms.registrationv3.ui.phonenumber.EnterPhoneNumberFragment">
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/navigation/registration.xml b/app/src/main/res/navigation/registration.xml
deleted file mode 100644
index 32630b7cde..0000000000
--- a/app/src/main/res/navigation/registration.xml
+++ /dev/null
@@ -1,240 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/registration_v3.xml b/app/src/main/res/navigation/registration_v3.xml
index 412970726c..ab006e48a9 100644
--- a/app/src/main/res/navigation/registration_v3.xml
+++ b/app/src/main/res/navigation/registration_v3.xml
@@ -9,7 +9,7 @@
android:id="@+id/welcomeFragment"
android:name="org.thoughtcrime.securesms.registrationv3.ui.welcome.WelcomeFragment"
android:label="fragment_welcome"
- tools:layout="@layout/fragment_registration_welcome">
+ tools:layout="@layout/fragment_registration_welcome_v3">
-
-
+ app:startDestination="@id/selectRestoreMethod">
-
-
-
-
-
-
-