mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Remove regv2.
This commit is contained in:
committed by
Jeffrey Starke
parent
52fa86046b
commit
8dc2077ad0
@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
@@ -189,7 +189,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
|
||||
private boolean userCanTransferOrRestore() {
|
||||
return !SignalStore.registration().isRegistrationComplete() &&
|
||||
RemoteConfig.restoreAfterRegistration() &&
|
||||
RestoreDecisionStateUtil.isDecisionPending(SignalStore.registration().getRestoreDecisionState());
|
||||
}
|
||||
|
||||
|
||||
@@ -1993,8 +1993,7 @@ object BackupRepository {
|
||||
|
||||
private fun isPreRestoreDuringRegistration(): Boolean {
|
||||
return !SignalStore.registration.isRegistrationComplete &&
|
||||
SignalStore.registration.restoreDecisionState.isDecisionPending &&
|
||||
RemoteConfig.restoreAfterRegistration
|
||||
SignalStore.registration.restoreDecisionState.isDecisionPending
|
||||
}
|
||||
|
||||
private fun scheduleSyncForAccountChange() {
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Importance
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
@@ -33,7 +33,7 @@ class AccountSettingsViewModel : ViewModel() {
|
||||
registrationLockEnabled = SignalStore.svr.isRegistrationLockEnabled,
|
||||
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application),
|
||||
clientDeprecated = SignalStore.misc.isClientDeprecated,
|
||||
canTransferWhileUnregistered = RemoteConfig.restoreAfterRegistration
|
||||
canTransferWhileUnregistered = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.thoughtcrime.securesms.registration.ui.captcha.CaptchaFragment
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.captcha.CaptchaFragment
|
||||
|
||||
/**
|
||||
* Screen visible to the user when they are to solve a captcha. @see [CaptchaFragment]
|
||||
|
||||
@@ -23,13 +23,13 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterCodeBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
|
||||
import org.thoughtcrime.securesms.registration.sms.ReceivedSmsEvent
|
||||
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
@@ -26,8 +26,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.registration.ui.registrationlock.RegistrationLockFragment
|
||||
import org.thoughtcrime.securesms.registration.ui.registrationlock.RegistrationLockFragmentArgs
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -41,7 +39,7 @@ import java.util.concurrent.TimeUnit
|
||||
class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_change_number_registration_lock) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationLockFragment::class.java)
|
||||
private val TAG = Log.tag(ChangeNumberRegistrationLockFragment::class.java)
|
||||
}
|
||||
|
||||
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(bindingFactory = { rootView ->
|
||||
@@ -67,7 +65,7 @@ class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_c
|
||||
}
|
||||
)
|
||||
|
||||
val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments())
|
||||
val args: ChangeNumberRegistrationLockFragmentArgs = ChangeNumberRegistrationLockFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
timeRemaining = args.getTimeRemaining()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,614 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.data
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.AppCapabilities
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.gcm.FcmUtil
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.pin.Svr3Migration
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getAciIdentityKeyPair
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getAciPreKeyCollection
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getPniIdentityKeyPair
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getPniPreKeyCollection
|
||||
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* A repository that deals with disk I/O during account registration.
|
||||
*/
|
||||
object RegistrationRepository {
|
||||
|
||||
private val TAG = Log.tag(RegistrationRepository::class.java)
|
||||
|
||||
private val PUSH_REQUEST_TIMEOUT = 5.seconds.inWholeMilliseconds
|
||||
|
||||
/**
|
||||
* Retrieve the FCM token from the Firebase service.
|
||||
*/
|
||||
suspend fun getFcmToken(context: Context): String? =
|
||||
withContext(Dispatchers.Default) {
|
||||
FcmUtil.getToken(context).orElse(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the local store for whether a PIN is set.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasPin(): Boolean {
|
||||
return SignalStore.svr.hasPin()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries, and creates if needed, the local registration ID.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getRegistrationId(): Int {
|
||||
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
|
||||
var registrationId = SignalStore.account.registrationId
|
||||
if (registrationId == 0) {
|
||||
registrationId = KeyHelper.generateRegistrationId(false)
|
||||
SignalStore.account.registrationId = registrationId
|
||||
}
|
||||
return registrationId
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries, and creates if needed, the local PNI registration ID.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPniRegistrationId(): Int {
|
||||
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
|
||||
var pniRegistrationId = SignalStore.account.pniRegistrationId
|
||||
if (pniRegistrationId == 0) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
SignalStore.account.pniRegistrationId = pniRegistrationId
|
||||
}
|
||||
return pniRegistrationId
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries, and creates if needed, the local profile key.
|
||||
*/
|
||||
@JvmStatic
|
||||
suspend fun getProfileKey(e164: String): ProfileKey =
|
||||
withContext(Dispatchers.IO) {
|
||||
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
|
||||
val recipientTable = SignalDatabase.recipients
|
||||
val recipient = recipientTable.getByE164(e164)
|
||||
var profileKey = if (recipient.isPresent) {
|
||||
ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).profileKey)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (profileKey == null) {
|
||||
profileKey = ProfileKeyUtil.createNew()
|
||||
Log.i(TAG, "No profile key found, created a new one")
|
||||
}
|
||||
profileKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a server response from a successful registration and persists the relevant data.
|
||||
*/
|
||||
@JvmStatic
|
||||
suspend fun registerAccountLocally(context: Context, data: LocalRegistrationMetadata) =
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.v(TAG, "registerAccountLocally()")
|
||||
val aciIdentityKeyPair = data.getAciIdentityKeyPair()
|
||||
val pniIdentityKeyPair = data.getPniIdentityKeyPair()
|
||||
SignalStore.account.restoreAciIdentityKeyFromBackup(aciIdentityKeyPair.publicKey.serialize(), aciIdentityKeyPair.privateKey.serialize())
|
||||
SignalStore.account.restorePniIdentityKeyFromBackup(pniIdentityKeyPair.publicKey.serialize(), pniIdentityKeyPair.privateKey.serialize())
|
||||
|
||||
val aciPreKeyCollection = data.getAciPreKeyCollection()
|
||||
val pniPreKeyCollection = data.getPniPreKeyCollection()
|
||||
val aci: ACI = ACI.parseOrThrow(data.aci)
|
||||
val pni: PNI = PNI.parseOrThrow(data.pni)
|
||||
val hasPin: Boolean = data.hasPin
|
||||
|
||||
SignalStore.account.setAci(aci)
|
||||
SignalStore.account.setPni(pni)
|
||||
|
||||
AppDependencies.resetProtocolStores()
|
||||
|
||||
AppDependencies.protocolStore.aci().sessions().archiveAllSessions()
|
||||
AppDependencies.protocolStore.pni().sessions().archiveAllSessions()
|
||||
SenderKeyUtil.clearAllState()
|
||||
|
||||
val aciProtocolStore = AppDependencies.protocolStore.aci()
|
||||
val aciMetadataStore = SignalStore.account.aciPreKeys
|
||||
|
||||
val pniProtocolStore = AppDependencies.protocolStore.pni()
|
||||
val pniMetadataStore = SignalStore.account.pniPreKeys
|
||||
|
||||
storeSignedAndLastResortPreKeys(aciProtocolStore, aciMetadataStore, aciPreKeyCollection)
|
||||
storeSignedAndLastResortPreKeys(pniProtocolStore, pniMetadataStore, pniPreKeyCollection)
|
||||
|
||||
val recipientTable = SignalDatabase.recipients
|
||||
val selfId = Recipient.trustedPush(aci, pni, data.e164).id
|
||||
|
||||
recipientTable.setProfileSharing(selfId, true)
|
||||
recipientTable.markRegisteredOrThrow(selfId, aci)
|
||||
recipientTable.linkIdsForSelf(aci, pni, data.e164)
|
||||
recipientTable.setProfileKey(selfId, ProfileKey(data.profileKey.toByteArray()))
|
||||
|
||||
AppDependencies.recipientCache.clearSelf()
|
||||
|
||||
SignalStore.account.setE164(data.e164)
|
||||
SignalStore.account.fcmToken = data.fcmToken
|
||||
SignalStore.account.fcmEnabled = data.fcmEnabled
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
saveOwnIdentityKey(selfId, aci, aciProtocolStore, now)
|
||||
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
|
||||
|
||||
SignalStore.account.setServicePassword(data.servicePassword)
|
||||
SignalStore.account.setRegistered(true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false)
|
||||
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
|
||||
|
||||
val masterKey = if (data.masterKey != null) MasterKey(data.masterKey.toByteArray()) else null
|
||||
SvrRepository.onRegistrationComplete(masterKey, data.pin, hasPin, data.reglockEnabled, false)
|
||||
|
||||
AppDependencies.resetNetwork()
|
||||
AppDependencies.startNetwork()
|
||||
PreKeysSyncJob.enqueue()
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
jobManager.add(DirectoryRefreshJob(false))
|
||||
jobManager.add(RotateCertificateJob())
|
||||
|
||||
DirectoryRefreshListener.schedule(context)
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun saveOwnIdentityKey(selfId: RecipientId, serviceId: ServiceId, protocolStore: SignalServiceAccountDataStoreImpl, now: Long) {
|
||||
protocolStore.identities().saveIdentityWithoutSideEffects(
|
||||
selfId,
|
||||
serviceId,
|
||||
protocolStore.identityKeyPair.publicKey,
|
||||
IdentityTable.VerifiedStatus.VERIFIED,
|
||||
true,
|
||||
now,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun storeSignedAndLastResortPreKeys(protocolStore: SignalServiceAccountDataStoreImpl, metadataStore: PreKeyMetadataStore, preKeyCollection: PreKeyCollection) {
|
||||
PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, preKeyCollection.signedPreKey)
|
||||
metadataStore.isSignedPreKeyRegistered = true
|
||||
metadataStore.activeSignedPreKeyId = preKeyCollection.signedPreKey.id
|
||||
metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis()
|
||||
|
||||
PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, preKeyCollection.lastResortKyberPreKey)
|
||||
metadataStore.lastResortKyberPreKeyId = preKeyCollection.lastResortKyberPreKey.id
|
||||
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun canUseLocalRecoveryPassword(): Boolean {
|
||||
val recoveryPassword = SignalStore.svr.recoveryPassword
|
||||
val pinHash = SignalStore.svr.localPinHash
|
||||
return recoveryPassword != null && pinHash != null
|
||||
}
|
||||
|
||||
fun doesPinMatchLocalHash(pin: String): Boolean {
|
||||
val pinHash = SignalStore.svr.localPinHash ?: throw IllegalStateException("Local PIN hash is not present!")
|
||||
return PinHashUtil.verifyLocalPinHash(pinHash, pin)
|
||||
}
|
||||
|
||||
suspend fun fetchMasterKeyFromSvrRemote(pin: String, svr2Credentials: AuthCredentials?, svr3Credentials: Svr3Credentials?): MasterKey =
|
||||
withContext(Dispatchers.IO) {
|
||||
val credentialSet = SvrAuthCredentialSet(svr2Credentials = svr2Credentials, svr3Credentials = svr3Credentials)
|
||||
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(credentialSet, pin)
|
||||
return@withContext masterKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a session ID.
|
||||
*/
|
||||
private suspend fun validateSession(context: Context, sessionId: String, e164: String, password: String): RegistrationSessionCheckResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
Log.d(TAG, "Validating registration session with service.")
|
||||
val registrationSessionResult = api.getRegistrationSessionStatus(sessionId)
|
||||
return@withContext RegistrationSessionCheckResult.from(registrationSessionResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a new registration session on the service.
|
||||
*/
|
||||
suspend fun createSession(context: Context, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionCreationResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "About to create a registration session…")
|
||||
val fcmToken: String? = FcmUtil.getToken(context).orElse(null)
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
|
||||
val registrationSessionResult = if (fcmToken == null) {
|
||||
Log.d(TAG, "Creating registration session without FCM token.")
|
||||
api.createRegistrationSession(null, mcc, mnc)
|
||||
} else {
|
||||
Log.d(TAG, "Creating registration session with FCM token.")
|
||||
createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc)
|
||||
}
|
||||
val result = RegistrationSessionCreationResult.from(registrationSessionResult)
|
||||
if (result is RegistrationSessionCreationResult.Success) {
|
||||
Log.d(TAG, "Updating registration session and E164 in value store.")
|
||||
SignalStore.registration.sessionId = result.sessionId
|
||||
SignalStore.registration.sessionE164 = e164
|
||||
}
|
||||
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an existing session, if its ID is provided. If the session is expired/invalid, or none is provided, it will attempt to initiate a new session.
|
||||
*/
|
||||
suspend fun createOrValidateSession(context: Context, sessionId: String?, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionResult {
|
||||
val savedSessionId = if (sessionId == null && e164 == SignalStore.registration.sessionE164) {
|
||||
SignalStore.registration.sessionId
|
||||
} else {
|
||||
sessionId
|
||||
}
|
||||
|
||||
if (savedSessionId != null) {
|
||||
Log.d(TAG, "Validating existing registration session.")
|
||||
val sessionValidationResult = validateSession(context, savedSessionId, e164, password)
|
||||
when (sessionValidationResult) {
|
||||
is RegistrationSessionCheckResult.Success -> {
|
||||
Log.d(TAG, "Existing registration session is valid.")
|
||||
return sessionValidationResult
|
||||
}
|
||||
|
||||
is RegistrationSessionCheckResult.UnknownError -> {
|
||||
Log.w(TAG, "Encountered error when validating existing session.", sessionValidationResult.getCause())
|
||||
return sessionValidationResult
|
||||
}
|
||||
|
||||
is RegistrationSessionCheckResult.SessionNotFound -> {
|
||||
Log.i(TAG, "Current session is invalid or has expired. Must create new one.")
|
||||
// fall through to creation
|
||||
}
|
||||
}
|
||||
}
|
||||
return createSession(context, e164, password, mcc, mnc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the service to send a verification code through one of our supported channels (SMS, phone call).
|
||||
*/
|
||||
suspend fun requestSmsCode(context: Context, sessionId: String, e164: String, password: String, mode: E164VerificationMode): VerificationCodeRequestResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
|
||||
val codeRequestResult = api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported, mode.transport)
|
||||
|
||||
return@withContext VerificationCodeRequestResult.from(codeRequestResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the user-entered verification code to the service.
|
||||
*/
|
||||
suspend fun submitVerificationCode(context: Context, sessionId: String, registrationData: RegistrationData): VerificationCodeRequestResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi
|
||||
val result = api.verifyAccount(sessionId = sessionId, verificationCode = registrationData.code)
|
||||
return@withContext VerificationCodeRequestResult.from(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the solved captcha token to the service.
|
||||
*/
|
||||
suspend fun submitCaptchaToken(context: Context, e164: String, password: String, sessionId: String, captchaToken: String): VerificationCodeRequestResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
val captchaSubmissionResult = api.submitCaptchaToken(sessionId = sessionId, captchaToken = captchaToken)
|
||||
return@withContext VerificationCodeRequestResult.from(captchaSubmissionResult)
|
||||
}
|
||||
|
||||
suspend fun requestAndVerifyPushToken(context: Context, sessionId: String, e164: String, password: String) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val fcmToken = getFcmToken(context)
|
||||
val accountManager = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password)
|
||||
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, sessionId, Optional.ofNullable(fcmToken), PUSH_REQUEST_TIMEOUT).orElse(null)
|
||||
val pushSubmissionResult = accountManager.registrationApi.submitPushChallengeToken(sessionId = sessionId, pushChallengeToken = pushChallenge)
|
||||
return@withContext VerificationCodeRequestResult.from(pushSubmissionResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the necessary assets as a verified account so that the user can actually use the service.
|
||||
*/
|
||||
suspend fun registerAccount(context: Context, sessionId: String?, registrationData: RegistrationData, recoveryPassword: String?, pin: String? = null, masterKeyProducer: MasterKeyProducer? = null): RegisterAccountResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.v(TAG, "registerAccount()")
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi
|
||||
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
val masterKey: MasterKey?
|
||||
try {
|
||||
masterKey = masterKeyProducer?.produceMasterKey()
|
||||
} catch (e: SvrNoDataException) {
|
||||
return@withContext RegisterAccountResult.SvrNoData(e)
|
||||
} catch (e: SvrWrongPinException) {
|
||||
return@withContext RegisterAccountResult.SvrWrongPin(e)
|
||||
} catch (e: IOException) {
|
||||
return@withContext RegisterAccountResult.UnknownError(e)
|
||||
}
|
||||
|
||||
val registrationLock: String? = masterKey?.deriveRegistrationLock()
|
||||
|
||||
val accountAttributes = AccountAttributes(
|
||||
signalingKey = null,
|
||||
registrationId = registrationData.registrationId,
|
||||
fetchesMessages = registrationData.isNotFcm,
|
||||
registrationLock = registrationLock,
|
||||
unidentifiedAccessKey = unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
|
||||
capabilities = AppCapabilities.getCapabilities(true),
|
||||
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE,
|
||||
name = null,
|
||||
pniRegistrationId = registrationData.pniRegistrationId,
|
||||
recoveryPassword = recoveryPassword
|
||||
)
|
||||
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
val aciIdentity: IdentityKeyPair = SignalStore.account.aciIdentityKey
|
||||
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
val pniIdentity: IdentityKeyPair = SignalStore.account.pniIdentityKey
|
||||
|
||||
val aciPreKeyCollection = generateSignedAndLastResortPreKeys(aciIdentity, SignalStore.account.aciPreKeys)
|
||||
val pniPreKeyCollection = generateSignedAndLastResortPreKeys(pniIdentity, SignalStore.account.pniPreKeys)
|
||||
|
||||
val result: NetworkResult<AccountRegistrationResult> = api.registerAccount(sessionId, recoveryPassword, accountAttributes, aciPreKeyCollection, pniPreKeyCollection, registrationData.fcmToken, true)
|
||||
.map { accountRegistrationResponse: VerifyAccountResponse ->
|
||||
AccountRegistrationResult(
|
||||
uuid = accountRegistrationResponse.uuid,
|
||||
pni = accountRegistrationResponse.pni,
|
||||
storageCapable = accountRegistrationResponse.storageCapable,
|
||||
number = accountRegistrationResponse.number,
|
||||
masterKey = masterKey,
|
||||
pin = pin,
|
||||
aciPreKeyCollection = aciPreKeyCollection,
|
||||
pniPreKeyCollection = pniPreKeyCollection,
|
||||
reRegistration = accountRegistrationResponse.reregistration
|
||||
)
|
||||
}
|
||||
|
||||
return@withContext RegisterAccountResult.from(result)
|
||||
}
|
||||
|
||||
private suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
// TODO [regv2]: do not use event bus nor latch
|
||||
val subscriber = PushTokenChallengeSubscriber()
|
||||
val eventBus = EventBus.getDefault()
|
||||
eventBus.register(subscriber)
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Requesting a registration session with FCM token…")
|
||||
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
|
||||
if (sessionCreationResponse !is NetworkResult.Success) {
|
||||
return@withContext sessionCreationResponse
|
||||
}
|
||||
|
||||
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
eventBus.unregister(subscriber)
|
||||
|
||||
if (receivedPush) {
|
||||
val challenge = subscriber.challenge
|
||||
if (challenge != null) {
|
||||
Log.i(TAG, "Push challenge token received.")
|
||||
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.metadata.id, challenge)
|
||||
} else {
|
||||
Log.w(TAG, "Push received but challenge token was null.")
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Push challenge timed out.")
|
||||
}
|
||||
Log.i(TAG, "Push challenge unsuccessful. Continuing with session created without one.")
|
||||
return@withContext sessionCreationResponse
|
||||
} catch (ex: Exception) {
|
||||
Log.w(TAG, "Exception caught, but the earlier try block should have caught it?", ex)
|
||||
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
|
||||
val svr3Result = SignalStore.svr.svr3AuthTokens
|
||||
?.takeIf { Svr3Migration.shouldReadFromSvr3 }
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.toSvrCredentials()
|
||||
?.let { authTokens ->
|
||||
api
|
||||
.validateSvr3AuthCredential(e164, authTokens)
|
||||
.runIfSuccessful {
|
||||
val removedInvalidTokens = SignalStore.svr.removeSvr3AuthTokens(it.invalid)
|
||||
if (removedInvalidTokens) {
|
||||
BackupManager(context).dataChanged()
|
||||
}
|
||||
}
|
||||
.let { BackupAuthCheckResult.fromV3(it) }
|
||||
}
|
||||
|
||||
if (svr3Result is BackupAuthCheckResult.SuccessWithCredentials) {
|
||||
Log.d(TAG, "Found valid SVR3 credentials.")
|
||||
return@withContext svr3Result
|
||||
}
|
||||
|
||||
Log.d(TAG, "No valid SVR3 credentials, looking for SVR2.")
|
||||
|
||||
return@withContext SignalStore.svr.svr2AuthTokens
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.toSvrCredentials()
|
||||
?.let { authTokens ->
|
||||
api
|
||||
.validateSvr2AuthCredential(e164, authTokens)
|
||||
.runIfSuccessful {
|
||||
val removedInvalidTokens = SignalStore.svr.removeSvr2AuthTokens(it.invalid)
|
||||
if (removedInvalidTokens) {
|
||||
BackupManager(context).dataChanged()
|
||||
}
|
||||
}
|
||||
.let { BackupAuthCheckResult.fromV2(it) }
|
||||
} ?: BackupAuthCheckResult.SuccessWithoutCredentials()
|
||||
}
|
||||
|
||||
/** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
|
||||
private fun List<String?>.toSvrCredentials(): List<String> {
|
||||
return this
|
||||
.asSequence()
|
||||
.filterNotNull()
|
||||
.take(10)
|
||||
.map { it.replace("Basic ", "").trim() }
|
||||
.mapNotNull {
|
||||
try {
|
||||
Base64.decode(it)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Encountered error trying to decode a token!", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
.map { String(it, StandardCharsets.ISO_8859_1) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an SMS listener to auto-enter a verification code.
|
||||
*
|
||||
* The listener [lives for 5 minutes](https://developers.google.com/android/reference/com/google/android/gms/auth/api/phone/SmsRetrieverApi).
|
||||
*
|
||||
* @return whether or not the Play Services SMS Listener was successfully registered.
|
||||
*/
|
||||
suspend fun registerSmsListener(context: Context): Boolean {
|
||||
Log.d(TAG, "Attempting to start verification code SMS retriever.")
|
||||
val started = withTimeoutOrNull(5.seconds.inWholeMilliseconds) {
|
||||
try {
|
||||
SmsRetriever.getClient(context).startSmsRetriever().await()
|
||||
Log.d(TAG, "Successfully started verification code SMS retriever.")
|
||||
return@withTimeoutOrNull true
|
||||
} catch (ex: Exception) {
|
||||
Log.w(TAG, "Could not start verification code SMS retriever due to exception.", ex)
|
||||
return@withTimeoutOrNull false
|
||||
}
|
||||
}
|
||||
|
||||
if (started == null) {
|
||||
Log.w(TAG, "Could not start verification code SMS retriever due to timeout.")
|
||||
}
|
||||
|
||||
return started == true
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun generateSignedAndLastResortPreKeys(identity: IdentityKeyPair, metadataStore: PreKeyMetadataStore): PreKeyCollection {
|
||||
val signedPreKey = PreKeyUtil.generateSignedPreKey(metadataStore.nextSignedPreKeyId, identity.privateKey)
|
||||
val lastResortKyberPreKey = PreKeyUtil.generateLastResortKyberPreKey(metadataStore.nextKyberPreKeyId, identity.privateKey)
|
||||
|
||||
return PreKeyCollection(
|
||||
identity.publicKey,
|
||||
signedPreKey,
|
||||
lastResortKyberPreKey
|
||||
)
|
||||
}
|
||||
|
||||
fun interface MasterKeyProducer {
|
||||
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
|
||||
fun produceMasterKey(): MasterKey
|
||||
}
|
||||
|
||||
enum class E164VerificationMode(val isSmsRetrieverSupported: Boolean, val transport: PushServiceSocket.VerificationCodeTransport) {
|
||||
SMS_WITH_LISTENER(true, PushServiceSocket.VerificationCodeTransport.SMS),
|
||||
SMS_WITHOUT_LISTENER(false, PushServiceSocket.VerificationCodeTransport.SMS),
|
||||
PHONE_CALL(false, PushServiceSocket.VerificationCodeTransport.VOICE)
|
||||
}
|
||||
|
||||
private class PushTokenChallengeSubscriber {
|
||||
var challenge: String? = null
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
@Subscribe
|
||||
fun onChallengeEvent(pushChallengeEvent: PushChallengeRequest.PushChallengeEvent) {
|
||||
Log.d(TAG, "Push challenge received!")
|
||||
challenge = pushChallengeEvent.challenge
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui
|
||||
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* State holder shared across all of registration.
|
||||
*/
|
||||
data class RegistrationState(
|
||||
val sessionId: String? = null,
|
||||
val enteredCode: String = "",
|
||||
val phoneNumber: Phonenumber.PhoneNumber? = fetchExistingE164FromValues(),
|
||||
val nationalNumber: String = "",
|
||||
val inProgress: Boolean = false,
|
||||
val isReRegister: Boolean = false,
|
||||
val canSkipSms: Boolean = false,
|
||||
val svr2AuthCredentials: AuthCredentials? = null,
|
||||
val svr3AuthCredentials: Svr3Credentials? = null,
|
||||
val svrTriesRemaining: Int = 10,
|
||||
val incorrectCodeAttempts: Int = 0,
|
||||
val isRegistrationLockEnabled: Boolean = false,
|
||||
val lockedTimeRemaining: Long = 0L,
|
||||
val userSkippedReregistration: Boolean = false,
|
||||
val isFcmSupported: Boolean = false,
|
||||
val isAllowedToRequestCode: Boolean = false,
|
||||
val fcmToken: String? = null,
|
||||
val challengesRequested: List<Challenge> = emptyList(),
|
||||
val captchaToken: String? = null,
|
||||
val allowedToRequestCode: Boolean = false,
|
||||
val nextSmsTimestamp: Duration = 0.seconds,
|
||||
val nextCallTimestamp: Duration = 0.seconds,
|
||||
val nextVerificationAttempt: Duration = 0.seconds,
|
||||
val verified: Boolean = false,
|
||||
val smsListenerTimeout: Long = 0L,
|
||||
val pinKeyboardType: PinKeyboardType = SignalStore.pin.keyboardType,
|
||||
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
|
||||
val networkError: Throwable? = null,
|
||||
val sessionCreationError: RegistrationSessionResult? = null,
|
||||
val sessionStateError: VerificationCodeRequestResult? = null,
|
||||
val registerAccountError: RegisterAccountResult? = null,
|
||||
val challengeInProgress: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationState::class)
|
||||
|
||||
private fun fetchExistingE164FromValues(): Phonenumber.PhoneNumber? {
|
||||
val existingE164 = SignalStore.registration.sessionE164
|
||||
if (existingE164 != null) {
|
||||
try {
|
||||
return PhoneNumberUtil.getInstance().parse(existingE164, null)
|
||||
} catch (ex: NumberParseException) {
|
||||
Log.w(TAG, "Could not parse stored E164.", ex)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.accountlocked
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Screen educating the user that they need to wait some number of days to register.
|
||||
*/
|
||||
class AccountLockedFragment : LoggingFragment(R.layout.account_locked_fragment) {
|
||||
private val viewModel by activityViewModels<RegistrationViewModel>()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title))
|
||||
|
||||
val description = view.findViewById<TextView>(R.id.account_locked_description)
|
||||
|
||||
viewModel.lockedTimeRemaining.observe(
|
||||
viewLifecycleOwner
|
||||
) { t: Long? -> description.text = getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t!!)) }
|
||||
|
||||
view.findViewById<View>(R.id.account_locked_next).setOnClickListener { v: View? -> onNext() }
|
||||
view.findViewById<View>(R.id.account_locked_learn_more).setOnClickListener { v: View? -> learnMore() }
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun learnMore() {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun onNext() {
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
private fun durationToDays(duration: Long): Long {
|
||||
return if (duration != 0L) getLockoutDays(duration).toLong() else 7
|
||||
}
|
||||
|
||||
private fun getLockoutDays(timeRemainingMs: Long): Int {
|
||||
return timeRemainingMs.milliseconds.inWholeDays.toInt() + 1
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.captcha
|
||||
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
|
||||
/**
|
||||
* Screen that displays a captcha as part of the registration flow.
|
||||
* This subclass plugs in [RegistrationViewModel] to the shared super class.
|
||||
*
|
||||
* @see CaptchaFragment
|
||||
*/
|
||||
class RegistrationCaptchaFragment : CaptchaFragment() {
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
|
||||
override fun handleCaptchaToken(token: String) {
|
||||
sharedViewModel.setCaptchaResponse(token)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -1,432 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.entercode
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
|
||||
import org.thoughtcrime.securesms.registration.sms.ReceivedSmsEvent
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* The final screen of account registration, where the user enters their verification code.
|
||||
*/
|
||||
class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(EnterCodeFragment::class.java)
|
||||
|
||||
private const val BOTTOM_SHEET_TAG = "support_bottom_sheet"
|
||||
}
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val fragmentViewModel by viewModels<EnterCodeViewModel>()
|
||||
private val bottomSheet = ContactSupportBottomSheetFragment()
|
||||
private val binding: FragmentRegistrationEnterCodeBinding by ViewBinderDelegate(FragmentRegistrationEnterCodeBinding::bind)
|
||||
|
||||
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
|
||||
|
||||
private var autopilotCodeEntryActive = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||
|
||||
phoneStateListener = SignalStrengthPhoneStateListener(this, PhoneStateCallback())
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.wrongNumber.setOnClickListener {
|
||||
popBackStack()
|
||||
}
|
||||
|
||||
binding.code.setOnCompleteListener {
|
||||
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it)
|
||||
}
|
||||
|
||||
binding.havingTroubleButton.setOnClickListener {
|
||||
bottomSheet.showSafely(childFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
|
||||
binding.callMeCountDown.apply {
|
||||
setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in)
|
||||
setOnClickListener {
|
||||
sharedViewModel.requestVerificationCall(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
binding.resendSmsCountDown.apply {
|
||||
setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in)
|
||||
setOnClickListener {
|
||||
sharedViewModel.requestSmsCode(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
binding.keyboard.setOnKeyPressListener { key ->
|
||||
if (!autopilotCodeEntryActive) {
|
||||
if (key >= 0) {
|
||||
binding.code.append(key)
|
||||
} else {
|
||||
binding.code.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.incorrectCodeAttempts.observe(viewLifecycleOwner) { attempts: Int ->
|
||||
if (attempts >= 3) {
|
||||
binding.havingTroubleButton.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||
sharedState.sessionCreationError?.let { error ->
|
||||
handleSessionCreationError(error)
|
||||
sharedViewModel.sessionCreationErrorShown()
|
||||
}
|
||||
|
||||
sharedState.sessionStateError?.let { error ->
|
||||
handleSessionErrorResponse(error)
|
||||
sharedViewModel.sessionStateErrorShown()
|
||||
}
|
||||
|
||||
sharedState.registerAccountError?.let { error ->
|
||||
handleRegistrationErrorResponse(error)
|
||||
sharedViewModel.registerAccountErrorShown()
|
||||
}
|
||||
|
||||
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
|
||||
sharedViewModel.submitCaptchaToken(requireContext())
|
||||
} else if (sharedState.challengesRequested.isNotEmpty() && !sharedState.challengeInProgress) {
|
||||
handleChallenges(sharedState.challengesRequested)
|
||||
}
|
||||
|
||||
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)
|
||||
binding.callMeCountDown.startCountDownTo(sharedState.nextCallTimestamp)
|
||||
if (sharedState.inProgress) {
|
||||
binding.keyboard.displayProgress()
|
||||
} else {
|
||||
binding.keyboard.displayKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
fragmentViewModel.uiState.observe(viewLifecycleOwner) {
|
||||
if (it.resetRequiredAfterFailure) {
|
||||
binding.callMeCountDown.visibility = View.VISIBLE
|
||||
binding.resendSmsCountDown.visibility = View.VISIBLE
|
||||
binding.wrongNumber.visibility = View.VISIBLE
|
||||
binding.code.clear()
|
||||
binding.keyboard.displayKeyboard()
|
||||
fragmentViewModel.allViewsResetCompleted()
|
||||
} else if (it.showKeyboard) {
|
||||
binding.keyboard.displayKeyboard()
|
||||
fragmentViewModel.keyboardShown()
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.getDefault().registerForLifecycle(subscriber = this, lifecycleOwner = viewLifecycleOwner)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sharedViewModel.phoneNumber?.let {
|
||||
val formatted = PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
|
||||
binding.verificationSubheader.text = requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, formatted)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSessionCreationError(result: RegistrationSessionResult) {
|
||||
if (!result.isSuccess()) {
|
||||
Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause())
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is RegistrationSessionCheckResult.Success,
|
||||
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
|
||||
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
|
||||
|
||||
is RegistrationSessionCreationResult.RateLimited -> {
|
||||
val timeRemaining = result.timeRemaining?.milliseconds
|
||||
Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining")
|
||||
if (timeRemaining != null) {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
|
||||
} else {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
|
||||
}
|
||||
}
|
||||
|
||||
is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result)
|
||||
is RegistrationSessionCheckResult.SessionNotFound -> presentGenericError(result)
|
||||
is RegistrationSessionCheckResult.UnknownError,
|
||||
is RegistrationSessionCreationResult.UnknownError -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSessionErrorResponse(result: VerificationCodeRequestResult) {
|
||||
if (!result.isSuccess()) {
|
||||
Log.i(TAG, "[sessionError] Handling error response of ${result.javaClass.name}", result.getCause())
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
is VerificationCodeRequestResult.RateLimited -> {
|
||||
val timeRemaining = result.timeRemaining?.milliseconds
|
||||
Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining")
|
||||
if (timeRemaining != null) {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
|
||||
} else {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
|
||||
}
|
||||
}
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is VerificationCodeRequestResult.ExternalServiceFailure -> presentSmsGenericError(result)
|
||||
is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> {
|
||||
Log.i(TAG, result.log())
|
||||
handleRequestVerificationCodeRateLimited(result)
|
||||
}
|
||||
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited()
|
||||
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
|
||||
if (!result.isSuccess()) {
|
||||
Log.i(TAG, "[registrationError] Handling error response of ${result.javaClass.name}", result.getCause())
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
|
||||
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is RegisterAccountResult.AuthorizationFailed -> presentIncorrectCodeDialog()
|
||||
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
|
||||
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
|
||||
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChallenges(remainingChallenges: List<Challenge>) {
|
||||
when (remainingChallenges.first()) {
|
||||
Challenge.CAPTCHA -> moveToCaptcha()
|
||||
Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentAccountLocked() {
|
||||
binding.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
findNavController().safeNavigate(EnterCodeFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentRegistrationLocked(timeRemaining: Long) {
|
||||
binding.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining))
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentRateLimitedDialog() {
|
||||
binding.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean?>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
fragmentViewModel.resetAllViews()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentIncorrectCodeDialog() {
|
||||
sharedViewModel.incrementIncorrectCodeAttempts()
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show()
|
||||
|
||||
binding.keyboard.displayFailure().addListener(object : AssertedSuccessListener<Boolean?>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
fragmentViewModel.resetAllViews()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun presentSmsGenericError(requestResult: RegistrationResult) {
|
||||
binding.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
Log.w(TAG, "Encountered sms provider error!", requestResult.getCause())
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_sms_provider_error)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.showKeyboard() }
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(message)
|
||||
setPositiveButton(android.R.string.ok, positiveButtonListener)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentGenericError(requestResult: RegistrationResult) {
|
||||
binding.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
Log.w(TAG, "Encountered unexpected error!", requestResult.getCause())
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_error_connecting_to_service)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.showKeyboard() }
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) {
|
||||
if (result.willBeAbleToRequestAgain) {
|
||||
Log.i(TAG, "Attempted to request new code too soon, timers should be updated")
|
||||
} else {
|
||||
Log.w(TAG, "Request for new verification code impossible, need to restart registration")
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() }
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentSubmitVerificationCodeRateLimited() {
|
||||
binding.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
Log.w(TAG, "Submit verification code impossible, need to request a new code and restart registration")
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() }
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun popBackStack() {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PUSH_NETWORK_AUDITED)
|
||||
NavHostFragment.findNavController(this).popBackStack()
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun moveToCaptcha() {
|
||||
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequestCaptcha())
|
||||
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
|
||||
Log.i(TAG, "Received verification code via EventBus.")
|
||||
binding.code.clear()
|
||||
|
||||
if (event.code.isBlank() || event.code.length != ReceivedSmsEvent.CODE_LENGTH) {
|
||||
Log.i(TAG, "Received invalid code of length ${event.code.length}. Ignoring.")
|
||||
return
|
||||
}
|
||||
|
||||
val finalIndex = ReceivedSmsEvent.CODE_LENGTH - 1
|
||||
autopilotCodeEntryActive = true
|
||||
try {
|
||||
event.code
|
||||
.map { it.digitToInt() }
|
||||
.forEachIndexed { i, digit ->
|
||||
binding.code.postDelayed({
|
||||
binding.code.append(digit)
|
||||
if (i == finalIndex) {
|
||||
autopilotCodeEntryActive = false
|
||||
}
|
||||
}, i * 200L)
|
||||
}
|
||||
Log.i(TAG, "Finished auto-filling code.")
|
||||
} catch (notADigit: IllegalArgumentException) {
|
||||
Log.w(TAG, "Failed to convert code into digits.", notADigit)
|
||||
autopilotCodeEntryActive = false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
|
||||
override fun onNoCellSignalPresent() {
|
||||
if (isAdded) {
|
||||
bottomSheet.showSafely(childFragmentManager, BOTTOM_SHEET_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCellSignalPresent() {
|
||||
if (bottomSheet.isResumed) {
|
||||
bottomSheet.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.grantpermissions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
|
||||
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen in account registration that provides rationales for the suggested runtime permissions.
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
class GrantPermissionsFragment : ComposeFragment() {
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val args by navArgs<GrantPermissionsFragmentArgs>()
|
||||
private val isSearchingForBackup = mutableStateOf(false)
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions(),
|
||||
::onPermissionsGranted
|
||||
)
|
||||
|
||||
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
when (val resultCode = result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
sharedViewModel.onBackupSuccessfullyRestored()
|
||||
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
|
||||
}
|
||||
Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.")
|
||||
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var welcomeAction: WelcomeAction
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
welcomeAction = args.welcomeAction
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val isSearchingForBackup by this.isSearchingForBackup
|
||||
|
||||
GrantPermissionsScreen(
|
||||
deviceBuildVersion = Build.VERSION.SDK_INT,
|
||||
isSearchingForBackup = isSearchingForBackup,
|
||||
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
|
||||
onNextClicked = this::launchPermissionRequests,
|
||||
onNotNowClicked = this::proceedToNextScreen
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchPermissionRequests() {
|
||||
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
||||
|
||||
val neededPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).filterNot {
|
||||
ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (neededPermissions.isEmpty()) {
|
||||
proceedToNextScreen()
|
||||
} else {
|
||||
requestPermissionLauncher.launch(neededPermissions.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPermissionsGranted(permissions: Map<String, Boolean>) {
|
||||
permissions.forEach {
|
||||
Log.d(TAG, "${it.key} = ${it.value}")
|
||||
}
|
||||
sharedViewModel.maybePrefillE164(requireContext())
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
proceedToNextScreen()
|
||||
}
|
||||
|
||||
private fun proceedToNextScreen() {
|
||||
when (welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
|
||||
WelcomeAction.RESTORE_BACKUP -> {
|
||||
val restoreIntent = RestoreActivity.getRestoreIntent(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Which welcome action the user selected which prompted this
|
||||
* screen.
|
||||
*/
|
||||
enum class WelcomeAction {
|
||||
CONTINUE,
|
||||
RESTORE_BACKUP
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GrantPermissionsFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,711 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.i18n.phonenumbers.AsYouTypeFormatter
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberBinding
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationState
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeFragment
|
||||
import org.thoughtcrime.securesms.registration.ui.toE164
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Dialogs
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.SignalE164Util
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataObserverCallback
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Screen in registration where the user enters their phone number.
|
||||
*/
|
||||
class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number) {
|
||||
|
||||
private val TAG = Log.tag(EnterPhoneNumberFragment::class.java)
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val fragmentViewModel by viewModels<EnterPhoneNumberViewModel>()
|
||||
private val binding: FragmentRegistrationEnterPhoneNumberBinding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberBinding::bind)
|
||||
|
||||
private val skipToNextScreen: DialogInterface.OnClickListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> moveToVerificationEntryScreen() }
|
||||
|
||||
private lateinit var spinnerAdapter: ArrayAdapter<CountryPrefix>
|
||||
private lateinit var phoneNumberInputLayout: TextInputEditText
|
||||
private lateinit var spinnerView: TextInputEditText
|
||||
private lateinit var countryPickerView: View
|
||||
|
||||
private var currentPhoneNumberFormatter: AsYouTypeFormatter? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
phoneNumberInputLayout = binding.number.editText as TextInputEditText
|
||||
spinnerView = binding.countryCode.editText as TextInputEditText
|
||||
countryPickerView = binding.countryPicker
|
||||
|
||||
countryPickerView.setOnClickListener {
|
||||
moveToCountryPickerScreen()
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(
|
||||
CountryCodeFragment.REQUEST_KEY_COUNTRY,
|
||||
this
|
||||
) { _, bundle ->
|
||||
val country: Country = bundle.getParcelableCompat(CountryCodeFragment.RESULT_COUNTRY, Country::class.java)!!
|
||||
fragmentViewModel.setCountry(country.countryCode, country)
|
||||
}
|
||||
|
||||
spinnerAdapter = ArrayAdapter<CountryPrefix>(
|
||||
requireContext(),
|
||||
R.layout.registration_country_code_dropdown_item,
|
||||
fragmentViewModel.supportedCountryPrefixes
|
||||
)
|
||||
binding.registerButton.setOnClickListener { onRegistrationButtonClicked() }
|
||||
binding.cancelButton.setOnClickListener { popBackStack() }
|
||||
|
||||
binding.toolbar.title = ""
|
||||
val activity = requireActivity() as AppCompatActivity
|
||||
activity.setSupportActionBar(binding.toolbar)
|
||||
|
||||
requireActivity().addMenuProvider(UseProxyMenuProvider(), viewLifecycleOwner)
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||
presentRegisterButton(sharedState)
|
||||
updateEnabledControls(sharedState.inProgress, sharedState.isReRegister)
|
||||
|
||||
sharedState.networkError?.let {
|
||||
presentNetworkError(it)
|
||||
sharedViewModel.networkErrorShown()
|
||||
}
|
||||
|
||||
sharedState.sessionCreationError?.let {
|
||||
handleSessionCreationError(it)
|
||||
sharedViewModel.sessionCreationErrorShown()
|
||||
}
|
||||
|
||||
sharedState.sessionStateError?.let {
|
||||
handleSessionStateError(it)
|
||||
sharedViewModel.sessionStateErrorShown()
|
||||
}
|
||||
|
||||
sharedState.registerAccountError?.let {
|
||||
handleRegistrationErrorResponse(it)
|
||||
sharedViewModel.registerAccountErrorShown()
|
||||
}
|
||||
|
||||
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
|
||||
sharedViewModel.submitCaptchaToken(requireContext())
|
||||
} else if (sharedState.challengesRequested.isNotEmpty()) {
|
||||
if (!sharedState.challengeInProgress) {
|
||||
handleChallenges(sharedState.challengesRequested)
|
||||
}
|
||||
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
|
||||
moveToEnterPinScreen()
|
||||
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
|
||||
moveToVerificationEntryScreen()
|
||||
}
|
||||
}
|
||||
|
||||
fragmentViewModel
|
||||
.uiState
|
||||
.map { it.phoneNumberRegionCode }
|
||||
.distinctUntilChanged()
|
||||
.observe(viewLifecycleOwner) { regionCode ->
|
||||
if (regionCode.isNotNullOrBlank()) {
|
||||
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
|
||||
reformatText(phoneNumberInputLayout.text)
|
||||
phoneNumberInputLayout.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||
if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) {
|
||||
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
|
||||
sharedViewModel.nationalNumber = ""
|
||||
} else {
|
||||
sharedViewModel.setPhoneNumber(null)
|
||||
}
|
||||
|
||||
updateCountrySelection(fragmentState.country)
|
||||
|
||||
if (fragmentState.error != EnterPhoneNumberState.Error.NONE) {
|
||||
presentLocalError(fragmentState)
|
||||
}
|
||||
}
|
||||
|
||||
initializeInputFields()
|
||||
|
||||
val existingPhoneNumber = sharedViewModel.phoneNumber
|
||||
val existingNationalNumber = sharedViewModel.nationalNumber
|
||||
if (existingPhoneNumber != null) {
|
||||
fragmentViewModel.restoreState(existingPhoneNumber)
|
||||
spinnerView.setText(existingPhoneNumber.countryCode.toString())
|
||||
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
|
||||
} else if (spinnerView.text?.isEmpty() == true) {
|
||||
spinnerView.setText(fragmentViewModel.getDefaultCountryCode(requireContext()).toString())
|
||||
phoneNumberInputLayout.setText(existingNationalNumber)
|
||||
} else {
|
||||
phoneNumberInputLayout.setText(existingNationalNumber)
|
||||
}
|
||||
|
||||
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
|
||||
}
|
||||
|
||||
private fun updateCountrySelection(country: Country?) {
|
||||
if (country != null) {
|
||||
binding.countryEmoji.visible = true
|
||||
binding.countryEmoji.text = country.emoji
|
||||
binding.country.text = country.name
|
||||
if (spinnerView.text.toString() != country.countryCode.toString()) {
|
||||
spinnerView.setText(country.countryCode.toString())
|
||||
}
|
||||
} else {
|
||||
binding.countryEmoji.visible = false
|
||||
binding.country.text = getString(R.string.RegistrationActivity_select_a_country)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reformatText(text: Editable?) {
|
||||
if (text.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
currentPhoneNumberFormatter?.let { formatter ->
|
||||
formatter.clear()
|
||||
|
||||
var formattedNumber: String? = null
|
||||
text.forEach {
|
||||
if (it.isDigit()) {
|
||||
formattedNumber = formatter.inputDigit(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedNumber != null && text.toString() != formattedNumber) {
|
||||
text.replace(0, text.length, formattedNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChallenges(remainingChallenges: List<Challenge>) {
|
||||
when (remainingChallenges.first()) {
|
||||
Challenge.CAPTCHA -> moveToCaptcha()
|
||||
Challenge.PUSH -> performPushChallenge()
|
||||
}
|
||||
}
|
||||
|
||||
private fun performPushChallenge() {
|
||||
sharedViewModel.requestAndSubmitPushToken(requireContext())
|
||||
}
|
||||
|
||||
private fun initializeInputFields() {
|
||||
binding.countryCode.editText?.addTextChangedListener { s ->
|
||||
val sanitized = s.toString().filter { c -> c.isDigit() }
|
||||
if (sanitized.isNotNullOrBlank()) {
|
||||
val countryCode: Int = sanitized.toInt()
|
||||
fragmentViewModel.setCountry(countryCode)
|
||||
} else {
|
||||
binding.countryCode.editText?.setHint(R.string.RegistrationActivity_default_country_code)
|
||||
fragmentViewModel.clearCountry()
|
||||
}
|
||||
}
|
||||
|
||||
phoneNumberInputLayout.addTextChangedListener(
|
||||
afterTextChanged = {
|
||||
reformatText(it)
|
||||
fragmentViewModel.setPhoneNumber(it?.toString())
|
||||
sharedViewModel.nationalNumber = it?.toString() ?: ""
|
||||
}
|
||||
)
|
||||
|
||||
val scrollView = binding.scrollView
|
||||
val registerButton = binding.registerButton
|
||||
phoneNumberInputLayout.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||
if (hasFocus) {
|
||||
scrollView.postDelayed({
|
||||
scrollView.smoothScrollTo(0, registerButton.bottom)
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
|
||||
phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE && v != null) {
|
||||
onRegistrationButtonClicked()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentRegisterButton(sharedState: RegistrationState) {
|
||||
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isPossibleNumber(sharedState.phoneNumber)
|
||||
if (sharedState.inProgress) {
|
||||
binding.registerButton.setSpinning()
|
||||
} else {
|
||||
binding.registerButton.cancelSpinning()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentLocalError(state: EnterPhoneNumberState) {
|
||||
when (state.error) {
|
||||
EnterPhoneNumberState.Error.NONE -> Unit
|
||||
|
||||
EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER -> {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_invalid_number)
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid),
|
||||
state.phoneNumber
|
||||
)
|
||||
)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.clearError() }
|
||||
setOnCancelListener { fragmentViewModel.clearError() }
|
||||
setOnDismissListener { fragmentViewModel.clearError() }
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING -> {
|
||||
handlePromptForNoPlayServices()
|
||||
}
|
||||
|
||||
EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
|
||||
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show()
|
||||
}
|
||||
|
||||
EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT -> {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_play_services_error)
|
||||
setMessage(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.clearError() }
|
||||
setOnCancelListener { fragmentViewModel.clearError() }
|
||||
setOnDismissListener { fragmentViewModel.clearError() }
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentNetworkError(networkError: Throwable) {
|
||||
Log.i(TAG, "Unknown error during verification code request", networkError)
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_unable_to_connect_to_service)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSessionCreationError(result: RegistrationSessionResult) {
|
||||
if (!result.isSuccess()) {
|
||||
Log.i(TAG, "Handling error response of ${result.javaClass.name}", result.getCause())
|
||||
}
|
||||
when (result) {
|
||||
is RegistrationSessionCheckResult.Success,
|
||||
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
|
||||
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
|
||||
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
|
||||
|
||||
is RegistrationSessionCreationResult.RateLimited -> {
|
||||
val timeRemaining = result.timeRemaining?.milliseconds
|
||||
Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining")
|
||||
if (timeRemaining != null) {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
|
||||
} else {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
|
||||
}
|
||||
}
|
||||
|
||||
is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result)
|
||||
is RegistrationSessionCheckResult.SessionNotFound -> presentGenericError(result)
|
||||
is RegistrationSessionCheckResult.UnknownError,
|
||||
is RegistrationSessionCreationResult.UnknownError -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSessionStateError(result: VerificationCodeRequestResult) {
|
||||
if (!result.isSuccess()) {
|
||||
Log.i(TAG, "Handling error response.", result.getCause())
|
||||
}
|
||||
when (result) {
|
||||
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
is VerificationCodeRequestResult.ChallengeRequired -> handleChallenges(result.challenges)
|
||||
is VerificationCodeRequestResult.ExternalServiceFailure -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_sms_provider_error))
|
||||
is VerificationCodeRequestResult.ImpossibleNumber -> {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid, fragmentViewModel.phoneNumber?.toE164()))
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.InvalidTransportModeFailure -> {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_we_couldnt_send_you_a_verification_code)
|
||||
setPositiveButton(R.string.RegistrationActivity_voice_call) { _, _ ->
|
||||
sharedViewModel.requestVerificationCall(requireContext())
|
||||
}
|
||||
setNegativeButton(R.string.RegistrationActivity_cancel, null)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
|
||||
is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> {
|
||||
Log.i(TAG, result.log())
|
||||
handleRequestVerificationCodeRateLimited(result)
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result)
|
||||
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode)
|
||||
is VerificationCodeRequestResult.RateLimited -> {
|
||||
val timeRemaining = result.timeRemaining?.milliseconds
|
||||
Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining")
|
||||
if (timeRemaining != null) {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString()))
|
||||
} else {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
|
||||
}
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is VerificationCodeRequestResult.AlreadyVerified -> presentGenericError(result)
|
||||
is VerificationCodeRequestResult.NoSuchSession -> presentGenericError(result)
|
||||
is VerificationCodeRequestResult.UnknownError -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentGenericError(result: RegistrationResult) {
|
||||
Log.i(TAG, "Received unhandled response: ${result.javaClass.name}", result.getCause())
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service))
|
||||
}
|
||||
|
||||
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
|
||||
when (result) {
|
||||
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
|
||||
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
|
||||
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
|
||||
is RegisterAccountResult.SvrNoData -> presentAccountLocked()
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentRegistrationLocked(timeRemaining: Long) {
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionPhoneNumberRegistrationLock(timeRemaining))
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun presentRateLimitedDialog() {
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
|
||||
}
|
||||
|
||||
private fun presentAccountLocked() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionPhoneNumberAccountLocked())
|
||||
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
|
||||
}
|
||||
|
||||
private fun moveToCaptcha() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha())
|
||||
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
|
||||
}
|
||||
|
||||
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(message)
|
||||
setPositiveButton(android.R.string.ok, positiveButtonListener)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) {
|
||||
if (result.willBeAbleToRequestAgain) {
|
||||
Log.i(TAG, "New verification code cannot be requested yet but can soon, moving to enter code to show timers")
|
||||
moveToVerificationEntryScreen()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request new verification code, prompting to start new session")
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.RegistrationActivity_unable_to_connect_to_service)
|
||||
setPositiveButton(R.string.NetworkFailure__retry) { _, _ ->
|
||||
onRegistrationButtonClicked()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNonNormalizedNumberError(originalNumber: String, normalizedNumber: String, mode: RegistrationRepository.E164VerificationMode) {
|
||||
try {
|
||||
val phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_non_standard_number_format)
|
||||
setMessage(getString(R.string.RegistrationActivity_the_number_you_entered_appears_to_be_a_non_standard, originalNumber, normalizedNumber))
|
||||
setNegativeButton(android.R.string.no) { d: DialogInterface, i: Int -> d.dismiss() }
|
||||
setNeutralButton(R.string.RegistrationActivity_contact_signal_support) { dialogInterface, _ ->
|
||||
val subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format)
|
||||
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null)
|
||||
|
||||
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body)
|
||||
dialogInterface.dismiss()
|
||||
}
|
||||
setPositiveButton(R.string.yes) { dialogInterface, _ ->
|
||||
spinnerView.setText(phoneNumber.countryCode.toString())
|
||||
phoneNumberInputLayout.setText(phoneNumber.nationalNumber.toString())
|
||||
when (mode) {
|
||||
RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER,
|
||||
RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext())
|
||||
|
||||
RegistrationRepository.E164VerificationMode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext())
|
||||
}
|
||||
dialogInterface.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
} catch (e: NumberParseException) {
|
||||
Log.w(TAG, "Failed to parse number!", e)
|
||||
|
||||
Dialogs.showAlertDialog(
|
||||
requireContext(),
|
||||
getString(R.string.RegistrationActivity_invalid_number),
|
||||
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid, fragmentViewModel.phoneNumber?.toE164())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRegistrationButtonClicked() {
|
||||
ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout)
|
||||
sharedViewModel.setInProgress(true)
|
||||
val hasFcm = validateFcmStatus(requireContext())
|
||||
if (hasFcm) {
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner, FcmTokenRetrievedObserver())
|
||||
sharedViewModel.fetchFcmToken(requireContext())
|
||||
} else {
|
||||
sharedViewModel.uiState.value?.let { value ->
|
||||
val now = System.currentTimeMillis().milliseconds
|
||||
if (value.phoneNumber == null) {
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
|
||||
sharedViewModel.setInProgress(false)
|
||||
} else if (now < value.nextSmsTimestamp) {
|
||||
moveToVerificationEntryScreen()
|
||||
} else {
|
||||
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFcmTokenRetrieved(value: RegistrationState) {
|
||||
if (value.phoneNumber == null) {
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
|
||||
sharedViewModel.setInProgress(false)
|
||||
} else {
|
||||
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEnabledControls(showProgress: Boolean, isReRegister: Boolean) {
|
||||
binding.countryCode.isEnabled = !showProgress
|
||||
binding.number.isEnabled = !showProgress
|
||||
binding.cancelButton.visible = !showProgress && isReRegister
|
||||
}
|
||||
|
||||
private fun validateFcmStatus(context: Context): Boolean {
|
||||
val fcmStatus = PlayServicesUtil.getPlayServicesStatus(context)
|
||||
Log.d(TAG, "Got $fcmStatus for Play Services status.")
|
||||
when (fcmStatus) {
|
||||
PlayServicesUtil.PlayServicesStatus.SUCCESS -> {
|
||||
return true
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.MISSING -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
|
||||
return false
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT)
|
||||
return false
|
||||
}
|
||||
|
||||
null -> {
|
||||
Log.w(TAG, "Null result received from PlayServicesUtil, marking Play Services as missing.")
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfirmNumberDialogCanceled() {
|
||||
Log.d(TAG, "User canceled confirm number, returning to edit number.")
|
||||
sharedViewModel.setInProgress(false)
|
||||
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(phoneNumberInputLayout)
|
||||
}
|
||||
|
||||
private fun presentConfirmNumberDialog(phoneNumber: PhoneNumber, isReRegister: Boolean, canSkipSms: Boolean, missingFcmConsentRequired: Boolean) {
|
||||
val title = if (isReRegister) {
|
||||
R.string.RegistrationActivity_additional_verification_required
|
||||
} else {
|
||||
R.string.RegistrationActivity_phone_number_verification_dialog_title
|
||||
}
|
||||
|
||||
val message: CharSequence = SpannableStringBuilder().apply {
|
||||
append(SpanUtil.bold(SignalE164Util.prettyPrint(phoneNumber.toE164())))
|
||||
if (!canSkipSms) {
|
||||
append("\n\n")
|
||||
append(getString(R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number))
|
||||
}
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(title)
|
||||
setMessage(message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
Log.d(TAG, "User confirmed number.")
|
||||
if (missingFcmConsentRequired) {
|
||||
handlePromptForNoPlayServices()
|
||||
} else {
|
||||
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
|
||||
}
|
||||
}
|
||||
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> handleConfirmNumberDialogCanceled() }
|
||||
setOnCancelListener { _ -> handleConfirmNumberDialogCanceled() }
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun handlePromptForNoPlayServices() {
|
||||
val context = activity
|
||||
|
||||
if (context != null) {
|
||||
Log.d(TAG, "Device does not have Play Services, showing consent dialog.")
|
||||
MaterialAlertDialogBuilder(context).apply {
|
||||
setTitle(R.string.RegistrationActivity_missing_google_play_services)
|
||||
setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
|
||||
setPositiveButton(R.string.RegistrationActivity_i_understand) { _, _ ->
|
||||
Log.d(TAG, "User confirmed number.")
|
||||
sharedViewModel.onUserConfirmedPhoneNumber(AppDependencies.application)
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setOnCancelListener { fragmentViewModel.clearError() }
|
||||
setOnDismissListener { fragmentViewModel.clearError() }
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToEnterPinScreen() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment())
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun moveToVerificationEntryScreen() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode())
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun moveToCountryPickerScreen() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionCountryPicker(fragmentViewModel.country))
|
||||
}
|
||||
|
||||
private fun popBackStack() {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.INITIALIZATION)
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationState>(sharedViewModel.uiState) {
|
||||
override fun onValue(value: RegistrationState): Boolean {
|
||||
val fcmRetrieved = value.isFcmSupported
|
||||
if (fcmRetrieved) {
|
||||
onFcmTokenRetrieved(value)
|
||||
}
|
||||
return fcmRetrieved
|
||||
}
|
||||
}
|
||||
|
||||
private inner class UseProxyMenuProvider : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.enter_phone_number, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return if (menuItem.itemId == R.id.phone_menu_use_proxy) {
|
||||
NavHostFragment.findNavController(this@EnterPhoneNumberFragment).safeNavigate(EnterPhoneNumberFragmentDirections.actionEditProxy())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.E164Util
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
|
||||
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
/**
|
||||
* ViewModel for the phone number entry screen.
|
||||
*/
|
||||
class EnterPhoneNumberViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
|
||||
}
|
||||
|
||||
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
|
||||
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
|
||||
.sortedBy { it.digits }
|
||||
|
||||
private val store = MutableStateFlow(
|
||||
EnterPhoneNumberState(
|
||||
countryPrefixIndex = 0,
|
||||
phoneNumberRegionCode = supportedCountryPrefixes[0].regionCode
|
||||
)
|
||||
)
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
val phoneNumber: PhoneNumber?
|
||||
get() = try {
|
||||
parsePhoneNumber(store.value)
|
||||
} catch (ex: NumberParseException) {
|
||||
Log.w(TAG, "Could not parse phone number in current state.", ex)
|
||||
null
|
||||
}
|
||||
|
||||
var mode: RegistrationRepository.E164VerificationMode
|
||||
get() = store.value.mode
|
||||
set(value) = store.update {
|
||||
it.copy(mode = value)
|
||||
}
|
||||
|
||||
fun getDefaultCountryCode(context: Context): Int {
|
||||
val existingCountry = store.value.country
|
||||
val maybeRegionCode = Util.getNetworkCountryIso(context)
|
||||
val regionCode = if (maybeRegionCode != null && supportedCountryPrefixes.any { it.regionCode == maybeRegionCode }) {
|
||||
maybeRegionCode
|
||||
} else {
|
||||
Log.w(TAG, "Could not find region code")
|
||||
"US"
|
||||
}
|
||||
|
||||
val countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(regionCode)
|
||||
val prefixIndex = countryCodeToAdapterIndex(countryCode)
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
countryPrefixIndex = prefixIndex,
|
||||
phoneNumberRegionCode = regionCode,
|
||||
country = existingCountry ?: Country(
|
||||
name = E164Util.getRegionDisplayName(regionCode).orElse(""),
|
||||
emoji = CountryUtils.countryToEmoji(regionCode),
|
||||
countryCode = countryCode,
|
||||
regionCode = regionCode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return existingCountry?.countryCode ?: countryCode
|
||||
}
|
||||
|
||||
val country: Country?
|
||||
get() = store.value.country
|
||||
|
||||
fun setPhoneNumber(phoneNumber: String?) {
|
||||
store.update { it.copy(phoneNumber = phoneNumber ?: "") }
|
||||
}
|
||||
|
||||
fun clearCountry() {
|
||||
store.update {
|
||||
it.copy(
|
||||
country = null,
|
||||
phoneNumberRegionCode = "",
|
||||
countryPrefixIndex = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCountry(digits: Int, country: Country? = null) {
|
||||
if (country == null && digits == store.value.country?.countryCode) {
|
||||
return
|
||||
}
|
||||
|
||||
val matchingIndex = countryCodeToAdapterIndex(digits)
|
||||
if (matchingIndex == -1) {
|
||||
Log.d(TAG, "Invalid country code specified $digits")
|
||||
store.update {
|
||||
it.copy(
|
||||
country = null,
|
||||
phoneNumberRegionCode = "",
|
||||
countryPrefixIndex = 0
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val regionCode = supportedCountryPrefixes[matchingIndex].regionCode
|
||||
val matchedCountry = Country(
|
||||
name = E164Util.getRegionDisplayName(regionCode).orElse(""),
|
||||
emoji = CountryUtils.countryToEmoji(regionCode),
|
||||
countryCode = digits,
|
||||
regionCode = regionCode
|
||||
)
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
countryPrefixIndex = matchingIndex,
|
||||
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode,
|
||||
country = country ?: matchedCountry
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun parsePhoneNumber(state: EnterPhoneNumberState): PhoneNumber {
|
||||
return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode)
|
||||
}
|
||||
|
||||
fun isEnteredNumberPossible(state: EnterPhoneNumberState): Boolean {
|
||||
return try {
|
||||
state.country != null &&
|
||||
PhoneNumberUtil.getInstance().isPossibleNumber(parsePhoneNumber(state))
|
||||
} catch (ex: NumberParseException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreState(value: PhoneNumber) {
|
||||
val prefixIndex = countryCodeToAdapterIndex(value.countryCode)
|
||||
if (prefixIndex != -1) {
|
||||
store.update {
|
||||
it.copy(
|
||||
countryPrefixIndex = prefixIndex,
|
||||
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
|
||||
phoneNumber = value.nationalNumber.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun countryCodeToAdapterIndex(countryCode: Int): Int {
|
||||
return supportedCountryPrefixes.indexOfFirst { prefix -> prefix.digits == countryCode }
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
setError(EnterPhoneNumberState.Error.NONE)
|
||||
}
|
||||
|
||||
fun setError(error: EnterPhoneNumberState.Error) {
|
||||
store.update {
|
||||
it.copy(error = error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.registrationlock
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_lock) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationLockFragment::class.java)
|
||||
}
|
||||
|
||||
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind)
|
||||
|
||||
private val viewModel by activityViewModels<RegistrationViewModel>()
|
||||
|
||||
private var timeRemaining: Long = 0
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
|
||||
|
||||
val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
timeRemaining = args.getTimeRemaining()
|
||||
|
||||
binding.kbsLockForgotPin.visibility = View.GONE
|
||||
binding.kbsLockForgotPin.setOnClickListener { handleForgottenPin(timeRemaining) }
|
||||
|
||||
binding.kbsLockPinInput.setImeOptions(EditorInfo.IME_ACTION_DONE)
|
||||
binding.kbsLockPinInput.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v!!)
|
||||
handlePinEntry()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
enableAndFocusPinEntry()
|
||||
|
||||
binding.kbsLockPinConfirm.setOnClickListener {
|
||||
ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput)
|
||||
handlePinEntry()
|
||||
}
|
||||
|
||||
binding.kbsLockKeyboardToggle.setOnClickListener { viewModel.togglePinKeyboardType() }
|
||||
|
||||
viewModel.lockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> timeRemaining = t }
|
||||
|
||||
val triesRemaining: Int = viewModel.svrTriesRemaining
|
||||
|
||||
if (triesRemaining <= 3) {
|
||||
val daysRemaining = getLockoutDays(timeRemaining)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() }
|
||||
.show()
|
||||
}
|
||||
|
||||
if (triesRemaining < 5) {
|
||||
binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining)
|
||||
}
|
||||
|
||||
viewModel.uiState.observe(viewLifecycleOwner) {
|
||||
if (it.inProgress) {
|
||||
binding.kbsLockPinConfirm.setSpinning()
|
||||
} else {
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
}
|
||||
|
||||
it.sessionStateError?.let { error ->
|
||||
handleSessionErrorResponse(error)
|
||||
viewModel.sessionStateErrorShown()
|
||||
}
|
||||
|
||||
it.registerAccountError?.let { error ->
|
||||
handleRegistrationErrorResponse(error)
|
||||
viewModel.registerAccountErrorShown()
|
||||
}
|
||||
|
||||
it.pinKeyboardType.applyTo(
|
||||
pinEditText = binding.kbsLockPinInput,
|
||||
toggleTypeButton = binding.kbsLockKeyboardToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePinEntry() {
|
||||
binding.kbsLockPinInput.setEnabled(false)
|
||||
|
||||
val pin: String = binding.kbsLockPinInput.getText().toString()
|
||||
|
||||
val trimmedLength = pin.replace(" ", "").length
|
||||
if (trimmedLength == 0) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
|
||||
enableAndFocusPinEntry()
|
||||
return
|
||||
}
|
||||
|
||||
if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
|
||||
enableAndFocusPinEntry()
|
||||
return
|
||||
}
|
||||
|
||||
binding.kbsLockPinConfirm.setSpinning()
|
||||
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin)
|
||||
}
|
||||
|
||||
private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) {
|
||||
when (requestResult) {
|
||||
is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
|
||||
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> {
|
||||
Log.i(TAG, "Registration locked response to verify account!")
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
enableAndFocusPinEntry()
|
||||
Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause())
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRegistrationErrorResponse(result: RegisterAccountResult) {
|
||||
when (result) {
|
||||
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
|
||||
is RegisterAccountResult.RateLimited -> onRateLimited()
|
||||
is RegisterAccountResult.AttemptsExhausted -> {
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
|
||||
is RegisterAccountResult.RegistrationLocked -> {
|
||||
Log.i(TAG, "Registration locked response to register account!")
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
enableAndFocusPinEntry()
|
||||
Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
is RegisterAccountResult.SvrWrongPin -> onIncorrectKbsRegistrationLockPin(result.triesRemaining)
|
||||
is RegisterAccountResult.SvrNoData -> {
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unable to register account with registration lock", result.getCause())
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIncorrectKbsRegistrationLockPin(svrTriesRemaining: Int) {
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
binding.kbsLockPinInput.getText()?.clear()
|
||||
enableAndFocusPinEntry()
|
||||
|
||||
if (svrTriesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.")
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
return
|
||||
}
|
||||
|
||||
if (svrTriesRemaining == 3) {
|
||||
val daysRemaining = getLockoutDays(timeRemaining)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
if (svrTriesRemaining > 5) {
|
||||
binding.kbsLockPinInputLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again)
|
||||
} else {
|
||||
binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining)
|
||||
binding.kbsLockForgotPin.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRateLimited() {
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
enableAndFocusPinEntry()
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun onError() {
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
enableAndFocusPinEntry()
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun handleForgottenPin(timeRemainingMs: Long) {
|
||||
val lockoutDays = getLockoutDays(timeRemainingMs)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
|
||||
.setMessage(requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun getLockoutDays(timeRemainingMs: Long): Int {
|
||||
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1
|
||||
}
|
||||
|
||||
private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String {
|
||||
val resources = requireContext().resources
|
||||
val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining)
|
||||
val days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining)
|
||||
|
||||
return "$tries $days"
|
||||
}
|
||||
|
||||
private fun enableAndFocusPinEntry() {
|
||||
binding.kbsLockPinInput.setEnabled(true)
|
||||
binding.kbsLockPinInput.setFocusable(true)
|
||||
ViewUtil.focusAndShowKeyboard(binding.kbsLockPinInput)
|
||||
}
|
||||
|
||||
private fun sendEmailToSupport() {
|
||||
val subject = R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin
|
||||
|
||||
val body = SupportEmailUtil.generateSupportEmailBody(
|
||||
requireContext(),
|
||||
subject,
|
||||
null,
|
||||
null
|
||||
)
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(subject),
|
||||
body
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationPinRestoreEntryV2Binding
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationState
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
|
||||
}
|
||||
|
||||
private val registrationViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val reRegisterViewModel by viewModels<ReRegisterWithPinViewModel>()
|
||||
|
||||
private val binding: FragmentRegistrationPinRestoreEntryV2Binding by ViewBinderDelegate(FragmentRegistrationPinRestoreEntryV2Binding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.pinRestorePinTitle)
|
||||
binding.pinRestorePinDescription.setText(R.string.RegistrationLockFragment__enter_the_pin_you_created_for_your_account)
|
||||
|
||||
binding.pinRestoreForgotPin.visibility = View.GONE
|
||||
binding.pinRestoreForgotPin.setOnClickListener { onNeedHelpClicked() }
|
||||
|
||||
binding.pinRestoreSkipButton.setOnClickListener { onSkipClicked() }
|
||||
|
||||
binding.pinRestorePinInput.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
binding.pinRestorePinInput.setOnEditorActionListener { v, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v!!)
|
||||
handlePinEntry()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
enableAndFocusPinEntry()
|
||||
|
||||
binding.pinRestorePinContinue.setOnClickListener {
|
||||
handlePinEntry()
|
||||
}
|
||||
|
||||
binding.pinRestoreKeyboardToggle.setOnClickListener { reRegisterViewModel.toggleKeyboardType() }
|
||||
|
||||
LiveDataUtil
|
||||
.combineLatest(registrationViewModel.uiState, reRegisterViewModel.uiState) { reg, rereg -> reg to rereg }
|
||||
.observe(viewLifecycleOwner) { (registrationState, reRegisterState) -> updateViewState(registrationState, reRegisterState) }
|
||||
}
|
||||
|
||||
private fun updateViewState(state: RegistrationState, reRegisterState: ReRegisterWithPinState) {
|
||||
if (state.networkError != null) {
|
||||
genericErrorDialog()
|
||||
registrationViewModel.networkErrorShown()
|
||||
} else if (!state.canSkipSms) {
|
||||
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberFragment())
|
||||
registrationViewModel.setInProgress(false)
|
||||
} else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) {
|
||||
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
|
||||
onAccountLocked()
|
||||
} else {
|
||||
presentProgress(state.inProgress)
|
||||
presentTriesRemaining(reRegisterState, state.svrTriesRemaining)
|
||||
}
|
||||
|
||||
reRegisterState.pinKeyboardType.applyTo(
|
||||
pinEditText = binding.pinRestorePinInput,
|
||||
toggleTypeButton = binding.pinRestoreKeyboardToggle
|
||||
)
|
||||
|
||||
state.registerAccountError?.let { error ->
|
||||
registrationErrorHandler(error)
|
||||
registrationViewModel.registerAccountErrorShown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentProgress(inProgress: Boolean) {
|
||||
if (inProgress) {
|
||||
ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput)
|
||||
binding.pinRestorePinInput.isEnabled = false
|
||||
binding.pinRestorePinContinue.setSpinning()
|
||||
} else {
|
||||
binding.pinRestorePinInput.isEnabled = true
|
||||
binding.pinRestorePinContinue.cancelSpinning()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePinEntry() {
|
||||
val pin: String? = binding.pinRestorePinInput.text?.toString()
|
||||
|
||||
if (pin.isNullOrBlank()) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
|
||||
enableAndFocusPinEntry()
|
||||
return
|
||||
}
|
||||
|
||||
if (pin.trim().length < SvrConstants.MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
|
||||
enableAndFocusPinEntry()
|
||||
return
|
||||
}
|
||||
|
||||
registrationViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PIN_CONFIRMED)
|
||||
|
||||
registrationViewModel.verifyReRegisterWithPin(
|
||||
context = requireContext(),
|
||||
pin = pin,
|
||||
wrongPinHandler = {
|
||||
registrationViewModel.setInProgress(false)
|
||||
reRegisterViewModel.markIncorrectGuess()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentTriesRemaining(reRegisterState: ReRegisterWithPinState, triesRemaining: Int) {
|
||||
if (reRegisterState.hasIncorrectGuess) {
|
||||
if (triesRemaining == 1 && !reRegisterState.isLocalVerification) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
} else {
|
||||
binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)
|
||||
}
|
||||
binding.pinRestoreForgotPin.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (triesRemaining == 1) {
|
||||
binding.pinRestoreForgotPin.visibility = View.VISIBLE
|
||||
if (!reRegisterState.isLocalVerification) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.")
|
||||
onAccountLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccountLocked() {
|
||||
Log.d(TAG, "Showing Incorrect PIN dialog. Is local verification: ${reRegisterViewModel.isLocalVerification}")
|
||||
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() }
|
||||
.setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun enableAndFocusPinEntry() {
|
||||
binding.pinRestorePinInput.isEnabled = true
|
||||
binding.pinRestorePinInput.isFocusable = true
|
||||
ViewUtil.focusAndShowKeyboard(binding.pinRestorePinInput)
|
||||
}
|
||||
|
||||
private fun onNeedHelpClicked() {
|
||||
Log.i(TAG, "User clicked need help dialog.")
|
||||
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_need_help)
|
||||
.setMessage(getString(message, SvrConstants.MINIMUM_PIN_LENGTH))
|
||||
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ ->
|
||||
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null)
|
||||
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(R.string.ReRegisterWithPinFragment_support_email_subject),
|
||||
body
|
||||
)
|
||||
}
|
||||
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onSkipClicked() {
|
||||
Log.i(TAG, "User clicked the skip PIN button.")
|
||||
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
|
||||
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onSkipPinEntry() {
|
||||
Log.d(TAG, "User skipping PIN entry.")
|
||||
registrationViewModel.setUserSkippedReRegisterFlow(true)
|
||||
}
|
||||
|
||||
private fun presentRateLimitedDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun genericErrorDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.RegistrationActivity_error_connecting_to_service)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun registrationErrorHandler(result: RegisterAccountResult) {
|
||||
when (result) {
|
||||
is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!")
|
||||
is RegisterAccountResult.AuthorizationFailed,
|
||||
is RegisterAccountResult.MalformedRequest,
|
||||
is RegisterAccountResult.UnknownError,
|
||||
is RegisterAccountResult.ValidationError,
|
||||
is RegisterAccountResult.RegistrationLocked -> {
|
||||
Log.i(TAG, "Registration failed.", result.getCause())
|
||||
genericErrorDialog()
|
||||
}
|
||||
|
||||
is RegisterAccountResult.IncorrectRecoveryPassword -> {
|
||||
registrationViewModel.setUserSkippedReRegisterFlow(true)
|
||||
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberFragment())
|
||||
}
|
||||
|
||||
is RegisterAccountResult.AttemptsExhausted,
|
||||
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
|
||||
|
||||
is RegisterAccountResult.SvrNoData -> onAccountLocked()
|
||||
is RegisterAccountResult.SvrWrongPin -> {
|
||||
reRegisterViewModel.markIncorrectGuess()
|
||||
reRegisterViewModel.markAsRemoteVerification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.welcome
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeBinding
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.ui.grantpermissions.GrantPermissionsFragment
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* First screen that is displayed on the very first app launch.
|
||||
*/
|
||||
class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome) {
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val binding: FragmentRegistrationWelcomeBinding by ViewBinderDelegate(FragmentRegistrationWelcomeBinding::bind)
|
||||
|
||||
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
when (val resultCode = result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
sharedViewModel.onBackupSuccessfullyRestored()
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionGoToRegistration())
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
Log.w(TAG, "Backup restoration canceled.")
|
||||
}
|
||||
else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setDebugLogSubmitMultiTapView(binding.image)
|
||||
setDebugLogSubmitMultiTapView(binding.title)
|
||||
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
|
||||
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
|
||||
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
|
||||
}
|
||||
|
||||
private fun onContinueClicked() {
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.CONTINUE))
|
||||
} else {
|
||||
sharedViewModel.maybePrefillE164(requireContext())
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionSkipRestore())
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPermissions(): Boolean {
|
||||
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
||||
return WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
private fun onTermsClicked() {
|
||||
CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL)
|
||||
}
|
||||
|
||||
private fun onTransferOrRestoreClicked() {
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP))
|
||||
} else {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
|
||||
val restoreIntent = RestoreActivity.getRestoreIntent(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(WelcomeFragment::class.java)
|
||||
private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.transferorrestore
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreV2Binding
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* This presents a list of options for the user to restore (or skip) a backup.
|
||||
*/
|
||||
class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_restore_v2) {
|
||||
private val sharedViewModel by activityViewModels<RestoreViewModel>()
|
||||
private val binding: FragmentTransferRestoreV2Binding by ViewBinderDelegate(FragmentTransferRestoreV2Binding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.transferOrRestoreTitle)
|
||||
binding.transferOrRestoreFragmentTransfer.setOnClickListener { sharedViewModel.onTransferFromAndroidDeviceSelected() }
|
||||
binding.transferOrRestoreFragmentRestore.setOnClickListener { sharedViewModel.onRestoreFromLocalBackupSelected() }
|
||||
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(sharedViewModel.getBackupRestorationType()) }
|
||||
|
||||
val description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device)
|
||||
val toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device)
|
||||
|
||||
binding.transferOrRestoreFragmentTransferDescription.text = SpanUtil.boldSubstring(description, toBold)
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||
updateSelection(state.restorationType)
|
||||
}
|
||||
|
||||
// TODO [regv2]: port backup file detection to here
|
||||
}
|
||||
|
||||
private fun updateSelection(restorationType: BackupRestorationType) {
|
||||
binding.transferOrRestoreFragmentTransferCard.isSelected = restorationType == BackupRestorationType.DEVICE_TRANSFER
|
||||
binding.transferOrRestoreFragmentRestoreCard.isSelected = restorationType == BackupRestorationType.LOCAL_BACKUP
|
||||
}
|
||||
|
||||
private fun launchSelection(restorationType: BackupRestorationType) {
|
||||
when (restorationType) {
|
||||
BackupRestorationType.DEVICE_TRANSFER -> {
|
||||
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionNewDeviceTransferInstructions())
|
||||
}
|
||||
BackupRestorationType.LOCAL_BACKUP -> {
|
||||
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToLocalRestore())
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(TransferOrRestoreV2Fragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user