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