mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 00:29:11 +01:00
Delete registration V1.
This commit is contained in:
@@ -16,7 +16,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberLockV2Activity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
@@ -28,8 +27,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.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -181,7 +179,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_TRANSFER_ONGOING;
|
||||
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
return STATE_TRANSFER_LOCKED;
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class && getClass() != ChangeNumberLockV2Activity.class) {
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
|
||||
return STATE_CHANGE_NUMBER_LOCK;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
@@ -222,11 +220,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
if (RemoteConfig.registrationV2()) {
|
||||
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
|
||||
} else {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
|
||||
private Intent getEnterSignalPinIntent() {
|
||||
@@ -269,11 +263,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getChangeNumberLockIntent() {
|
||||
if (RemoteConfig.registrationV2()) {
|
||||
return ChangeNumberLockV2Activity.createIntent(this);
|
||||
} else {
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||
import org.thoughtcrime.securesms.restore.restorelocalbackup.PassphraseAsYouTypeFormatter;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -170,7 +170,7 @@ public class BackupDialog {
|
||||
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButton.setEnabled(false);
|
||||
|
||||
RestoreBackupFragment.PassphraseAsYouTypeFormatter formatter = new RestoreBackupFragment.PassphraseAsYouTypeFormatter();
|
||||
PassphraseAsYouTypeFormatter formatter = new PassphraseAsYouTypeFormatter();
|
||||
|
||||
prompt.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
formatter.afterTextChanged(editable);
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class UnauthorizedReminder extends Reminder {
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val START_LOCATION = "app.settings.start.location"
|
||||
@@ -40,8 +39,7 @@ class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
||||
val navGraphResId = if (RemoteConfig.registrationV2) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings
|
||||
intent?.putExtra(ARG_NAV_GRAPH, navGraphResId)
|
||||
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
@@ -197,9 +195,8 @@ class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent {
|
||||
fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, StartLocation.RECOVER_USERNAME)
|
||||
|
||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||
val navGraphResId = if (RemoteConfig.registrationV2) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings
|
||||
return Intent(context, AppSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, navGraphResId)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number)
|
||||
.putExtra(START_LOCATION, startLocation.code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.thoughtcrime.securesms.events.ReminderUpdateEvent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
@@ -114,7 +114,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
|
||||
}
|
||||
R.id.reminder_action_re_register -> {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,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.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -151,7 +151,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_account_reregister),
|
||||
onClick = {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,74 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
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.registration.fragments.BaseAccountLockedFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ChangeNumberAccountLockedFragment : BaseAccountLockedFragment(R.layout.fragment_change_number_account_locked) {
|
||||
/**
|
||||
* Screen visible to the user when they are registration locked and have no SVR data.
|
||||
*/
|
||||
class ChangeNumberAccountLockedFragment : LoggingFragment(R.layout.fragment_change_number_account_locked) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberAccountLockedFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title))
|
||||
|
||||
val description = view.findViewById<TextView>(R.id.account_locked_description)
|
||||
|
||||
viewModel.liveLockedTimeRemaining.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 { onNext() }
|
||||
view.findViewById<View>(R.id.account_locked_learn_more).setOnClickListener { learnMore() }
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseRegistrationViewModel {
|
||||
return ChangeNumberUtil.getViewModel(this)
|
||||
private fun learnMore() {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onNext() {
|
||||
private fun durationToDays(duration: Long): Long {
|
||||
return if (duration != 0L) getLockoutDays(duration).toLong() else 7
|
||||
}
|
||||
|
||||
private fun getLockoutDays(timeRemainingMs: Long): Int {
|
||||
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1
|
||||
}
|
||||
|
||||
fun onNext() {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.captcha.CaptchaV2Fragment
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.ui.captcha.CaptchaFragment
|
||||
|
||||
/**
|
||||
* Screen visible to the user when they are to solve a captcha. @see [CaptchaV2Fragment]
|
||||
* Screen visible to the user when they are to solve a captcha. @see [CaptchaFragment]
|
||||
*/
|
||||
class ChangeNumberCaptchaV2Fragment : CaptchaV2Fragment() {
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
class ChangeNumberCaptchaFragment : CaptchaFragment() {
|
||||
private val viewModel by activityViewModels<ChangeNumberViewModel>()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.addPresentedChallenge(Challenge.CAPTCHA)
|
||||
@@ -1,24 +1,33 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil.PlayServicesStatus
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen visible to the user for them to confirm their new phone number was entered correctly.
|
||||
*/
|
||||
class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberConfirmFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel = ChangeNumberUtil.getViewModel(this)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
@@ -33,35 +42,14 @@ class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_num
|
||||
editNumber.setOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number)
|
||||
changeNumber.setOnClickListener { onConfirm() }
|
||||
}
|
||||
|
||||
private fun onConfirm() {
|
||||
val playServicesAvailable = PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesStatus.SUCCESS
|
||||
|
||||
if (playServicesAvailable) {
|
||||
val client = SmsRetriever.getClient(requireContext())
|
||||
val task = client.startSmsRetriever()
|
||||
|
||||
task.addOnSuccessListener {
|
||||
Log.i(TAG, "Successfully registered SMS listener.")
|
||||
navigateToVerify(smsListenerEnabled = true)
|
||||
changeNumber.setOnClickListener {
|
||||
viewModel.registerSmsListenerWithCompletionListener(requireContext()) {
|
||||
navigateToVerify(it)
|
||||
}
|
||||
|
||||
task.addOnFailureListener { e ->
|
||||
Log.w(TAG, "Failed to register SMS listener.", e)
|
||||
navigateToVerify()
|
||||
}
|
||||
} else {
|
||||
navigateToVerify()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToVerify(smsListenerEnabled: Boolean = false) {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment, ChangeNumberVerifyFragmentArgs.Builder().setSmsListenerEnabled(smsListenerEnabled).build().toBundle())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberConfirmFragment::class.java)
|
||||
findNavController().safeNavigate(ChangeNumberConfirmFragmentDirections.actionChangePhoneNumberConfirmFragmentToChangePhoneNumberVerifyFragment(smsListenerEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
@@ -24,12 +24,12 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
|
||||
import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterCodeBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent
|
||||
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.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -38,13 +38,13 @@ import org.thoughtcrime.securesms.util.visible
|
||||
/**
|
||||
* Screen used to enter the registration code provided by the service.
|
||||
*/
|
||||
class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change_number_enter_code) {
|
||||
class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_number_enter_code) {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterCodeV2Fragment::class.java)
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterCodeFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
private val viewModel by activityViewModels<ChangeNumberViewModel>()
|
||||
private val binding: FragmentChangeNumberEnterCodeBinding by ViewBinderDelegate(FragmentChangeNumberEnterCodeBinding::bind)
|
||||
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
|
||||
|
||||
@@ -154,7 +154,7 @@ class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc.isChangeNumberLocked) {
|
||||
Log.d(TAG, "Change number locked, navigateUp")
|
||||
startActivity(ChangeNumberLockV2Activity.createIntent(requireContext()))
|
||||
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
|
||||
} else {
|
||||
Log.d(TAG, "navigateUp")
|
||||
findNavController().navigateUp()
|
||||
@@ -187,7 +187,7 @@ class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change
|
||||
binding.codeEntryLayout.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
findNavController().safeNavigate(ChangeNumberEnterCodeV2FragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
findNavController().safeNavigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -198,7 +198,7 @@ class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
Log.i(TAG, "Account is registration locked, cannot register.")
|
||||
findNavController().safeNavigate(ChangeNumberEnterCodeV2FragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
findNavController().safeNavigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,86 +1,71 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.LabeledEditText
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel.ContinueStatus
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterPhoneNumberBinding
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs
|
||||
import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController
|
||||
import org.thoughtcrime.securesms.util.Dialogs
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
|
||||
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
|
||||
|
||||
/**
|
||||
* Screen for the user to enter their old and new phone numbers.
|
||||
*/
|
||||
class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number) {
|
||||
|
||||
private var binding: FragmentChangeNumberEnterPhoneNumberBinding? = null
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterPhoneNumberFragment::class.java)
|
||||
|
||||
private val scrollView: ScrollView
|
||||
get() = binding!!.changeNumberEnterPhoneNumberScroll
|
||||
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
|
||||
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
|
||||
}
|
||||
|
||||
private val oldNumberCountrySpinner: Spinner
|
||||
get() = binding!!.changeNumberEnterPhoneNumberOldNumberSpinner
|
||||
private val oldNumberCountryCode: LabeledEditText
|
||||
get() = binding!!.changeNumberEnterPhoneNumberOldNumberCountryCode
|
||||
private val oldNumber: LabeledEditText
|
||||
get() = binding!!.changeNumberEnterPhoneNumberOldNumberNumber
|
||||
|
||||
private val newNumberCountrySpinner: Spinner
|
||||
get() = binding!!.changeNumberEnterPhoneNumberNewNumberSpinner
|
||||
private val newNumberCountryCode: LabeledEditText
|
||||
get() = binding!!.changeNumberEnterPhoneNumberNewNumberCountryCode
|
||||
private val newNumber: LabeledEditText
|
||||
get() = binding!!.changeNumberEnterPhoneNumberNewNumberNumber
|
||||
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
private val binding: FragmentChangeNumberEnterPhoneNumberBinding by ViewBinderDelegate(FragmentChangeNumberEnterPhoneNumberBinding::bind)
|
||||
private val viewModel by activityViewModels<ChangeNumberViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding = FragmentChangeNumberEnterPhoneNumberBinding.bind(view)
|
||||
|
||||
viewModel = getViewModel(this)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_number_enter_phone_number_continue).setOnClickListener {
|
||||
binding.changeNumberEnterPhoneNumberContinue.setOnClickListener {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
val oldController = ChangeNumberInputController(
|
||||
requireContext(),
|
||||
oldNumberCountryCode,
|
||||
oldNumber,
|
||||
oldNumberCountrySpinner,
|
||||
binding.changeNumberEnterPhoneNumberOldNumberCountryCode,
|
||||
binding.changeNumberEnterPhoneNumberOldNumberNumber,
|
||||
binding.changeNumberEnterPhoneNumberOldNumberSpinner,
|
||||
false,
|
||||
object : ChangeNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
scrollView.postDelayed({ scrollView.smoothScrollTo(0, oldNumber.bottom) }, 250)
|
||||
binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberOldNumberNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) {
|
||||
newNumberCountryCode.requestFocus()
|
||||
binding.changeNumberEnterPhoneNumberNewNumberCountryCode.requestFocus()
|
||||
}
|
||||
|
||||
override fun onNumberInputDone(view: View) = Unit
|
||||
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(OLD_NUMBER_COUNTRY_SELECT))
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
@@ -95,13 +80,13 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
|
||||
val newController = ChangeNumberInputController(
|
||||
requireContext(),
|
||||
newNumberCountryCode,
|
||||
newNumber,
|
||||
newNumberCountrySpinner,
|
||||
binding.changeNumberEnterPhoneNumberNewNumberCountryCode,
|
||||
binding.changeNumberEnterPhoneNumberNewNumberNumber,
|
||||
binding.changeNumberEnterPhoneNumberNewNumberSpinner,
|
||||
true,
|
||||
object : ChangeNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
scrollView.postDelayed({ scrollView.smoothScrollTo(0, newNumber.bottom) }, 250)
|
||||
binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberNewNumberNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) = Unit
|
||||
@@ -111,9 +96,7 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
}
|
||||
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(NEW_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(NEW_NUMBER_COUNTRY_SELECT))
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
@@ -130,50 +113,45 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
|
||||
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle ->
|
||||
viewModel.setNewCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
viewModel.getLiveOldNumber().observe(viewLifecycleOwner, oldController::updateNumber)
|
||||
viewModel.getLiveNewNumber().observe(viewLifecycleOwner, newController::updateNumber)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
viewModel.liveOldNumberState.observe(viewLifecycleOwner, oldController::updateNumber)
|
||||
viewModel.liveNewNumberState.observe(viewLifecycleOwner, newController::updateNumber)
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
if (TextUtils.isEmpty(oldNumberCountryCode.text)) {
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(oldNumber.text)) {
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(newNumberCountryCode.text)) {
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(newNumber.text)) {
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
when (viewModel.canContinue()) {
|
||||
ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment)
|
||||
ContinueStatus.INVALID_NUMBER -> {
|
||||
ChangeNumberViewModel.ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToChangePhoneNumberConfirmFragment())
|
||||
ChangeNumberViewModel.ContinueStatus.INVALID_NUMBER -> {
|
||||
Dialogs.showAlertDialog(
|
||||
context,
|
||||
getString(R.string.RegistrationActivity_invalid_number),
|
||||
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.number.e164Number)
|
||||
)
|
||||
}
|
||||
ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> {
|
||||
ChangeNumberViewModel.ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberEnterPhoneNumberFragment__the_phone_number_you_entered_doesnt_match_your_accounts)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseEnterSmsCodeFragment
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterSmsCodeFragment::class.java)
|
||||
|
||||
class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.title = viewModel.number.fullFormattedNumber
|
||||
toolbar.setNavigationOnClickListener {
|
||||
Log.d(TAG, "Toolbar navigation clicked.")
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.verify_header).setOnClickListener(null)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
Log.d(TAG, "onBackPressed")
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc.isChangeNumberLocked) {
|
||||
Log.d(TAG, "Change number locked, navigateUp")
|
||||
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
|
||||
} else {
|
||||
Log.d(TAG, "navigateUp")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewModel(): ChangeNumberViewModel {
|
||||
return getViewModel(this)
|
||||
}
|
||||
|
||||
override fun handleSuccessfulVerify() {
|
||||
Log.d(TAG, "handleSuccessfulVerify")
|
||||
displaySuccess { changeNumberSuccess() }
|
||||
}
|
||||
|
||||
override fun navigateToCaptcha() {
|
||||
Log.d(TAG, "navigateToCaptcha")
|
||||
findNavController().safeNavigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
|
||||
}
|
||||
|
||||
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
||||
Log.d(TAG, "navigateToRegistrationLock")
|
||||
findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
|
||||
override fun navigateToKbsAccountLocked() {
|
||||
Log.d(TAG, "navigateToKbsAccountLocked")
|
||||
findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.FragmentChangePhoneNumberBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen used to educate the user about what they're about to do (change their phone number)
|
||||
*/
|
||||
class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_number) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberFragment::class.java)
|
||||
}
|
||||
|
||||
private val binding: FragmentChangePhoneNumberBinding by ViewBinderDelegate(FragmentChangePhoneNumberBinding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_phone_number_continue).setOnClickListener {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
|
||||
binding.changePhoneNumberContinue.setOnClickListener {
|
||||
findNavController().safeNavigate(ChangeNumberFragmentDirections.actionChangePhoneNumberFragmentToEnterPhoneNumberChangeFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
@@ -18,10 +20,6 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.Objects
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
|
||||
/**
|
||||
* A captive activity that can determine if an interrupted/erred change number request
|
||||
@@ -29,17 +27,34 @@ private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
*/
|
||||
class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, ChangeNumberLockActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: ChangeNumberViewModel by viewModels()
|
||||
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
private lateinit var changeNumberRepository: ChangeNumberRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
dynamicTheme.onCreate(this)
|
||||
disposables.bindTo(lifecycle)
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
Log.d(TAG, "Back button press swallowed.")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
setContentView(R.layout.activity_change_number_lock)
|
||||
|
||||
changeNumberRepository = ChangeNumberRepository()
|
||||
checkWhoAmI()
|
||||
}
|
||||
|
||||
@@ -48,31 +63,11 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onBackPressed() = Unit
|
||||
|
||||
private fun checkWhoAmI() {
|
||||
disposables += changeNumberRepository
|
||||
.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, SignalStore.account.e164)) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account.e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
Single
|
||||
.just(true)
|
||||
.flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) }
|
||||
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
|
||||
.map { true }
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
|
||||
viewModel.checkWhoAmI(::onChangeStatusConfirmed, ::onFailedToGetChangeNumberStatus)
|
||||
}
|
||||
|
||||
private fun onChangeStatusConfirmed() {
|
||||
SignalStore.misc.unlockChangeNumber()
|
||||
SignalStore.misc.clearPendingChangeNumberMetadata()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
@@ -101,13 +96,4 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, ChangeNumberLockActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
@@ -5,13 +10,31 @@ import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
|
||||
/**
|
||||
* A screen to educate the user if their PIN differs from old number to new number.
|
||||
*/
|
||||
class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberPinDiffersFragment::class.java)
|
||||
}
|
||||
|
||||
private val confirmCancelDialog = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
view.findViewById<View>(R.id.change_number_pin_differs_keep_old_pin).setOnClickListener {
|
||||
changeNumberSuccess()
|
||||
@@ -27,17 +50,6 @@ class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_
|
||||
changePin.launch(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext()))
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, confirmCancelDialog)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,60 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
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
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) {
|
||||
/**
|
||||
* Screen presented to the user if the new account is registration locked, and allows them to enter their PIN.
|
||||
*/
|
||||
class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_change_number_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<ChangeNumberViewModel>()
|
||||
|
||||
private var timeRemaining: Long = 0
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
|
||||
@@ -31,20 +66,242 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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 {
|
||||
val keyboardType: PinKeyboardType = getPinEntryKeyboardType()
|
||||
updateKeyboard(keyboardType.other)
|
||||
binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource)
|
||||
}
|
||||
|
||||
val keyboardType: PinKeyboardType = getPinEntryKeyboardType().getOther()
|
||||
binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource)
|
||||
|
||||
viewModel.liveLockedTimeRemaining.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, ::onStateUpdate)
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseRegistrationViewModel {
|
||||
return ChangeNumberUtil.getViewModel(this)
|
||||
private fun onStateUpdate(state: ChangeNumberState) {
|
||||
if (state.changeNumberOutcome == ChangeNumberOutcome.VerificationCodeWorked) {
|
||||
handleSuccessfulPinEntry(state.enteredPin)
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToAccountLocked() {
|
||||
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
|
||||
}
|
||||
|
||||
viewModel.setEnteredPin(pin)
|
||||
|
||||
binding.kbsLockPinConfirm.setSpinning()
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleChangeNumberErrorResponse)
|
||||
}
|
||||
|
||||
private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) {
|
||||
when (requestResult) {
|
||||
is VerificationCodeRequestResult.Success -> Unit
|
||||
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
|
||||
is VerificationCodeRequestResult.AttemptsExhausted,
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> {
|
||||
navigateToAccountLocked()
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.AlreadyVerified,
|
||||
is VerificationCodeRequestResult.ChallengeRequired,
|
||||
is VerificationCodeRequestResult.ExternalServiceFailure,
|
||||
is VerificationCodeRequestResult.ImpossibleNumber,
|
||||
is VerificationCodeRequestResult.InvalidTransportModeFailure,
|
||||
is VerificationCodeRequestResult.MalformedRequest,
|
||||
is VerificationCodeRequestResult.MustRetry,
|
||||
is VerificationCodeRequestResult.NoSuchSession,
|
||||
is VerificationCodeRequestResult.NonNormalizedNumber,
|
||||
is VerificationCodeRequestResult.TokenNotAccepted,
|
||||
is VerificationCodeRequestResult.UnknownError -> {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause())
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeNumberErrorResponse(result: ChangeNumberResult) {
|
||||
when (result) {
|
||||
is ChangeNumberResult.Success -> Unit
|
||||
is ChangeNumberResult.RateLimited -> onRateLimited()
|
||||
is ChangeNumberResult.AttemptsExhausted -> navigateToAccountLocked()
|
||||
|
||||
is ChangeNumberResult.SvrWrongPin -> {
|
||||
Log.i(TAG, "SVR returned a WrongPinException.")
|
||||
onIncorrectKbsRegistrationLockPin(result.triesRemaining)
|
||||
}
|
||||
|
||||
is ChangeNumberResult.SvrNoData -> {
|
||||
Log.i(TAG, "SVR returned a NoDataException.")
|
||||
navigateToAccountLocked()
|
||||
}
|
||||
|
||||
is ChangeNumberResult.AuthorizationFailed,
|
||||
is ChangeNumberResult.IncorrectRecoveryPassword,
|
||||
is ChangeNumberResult.MalformedRequest,
|
||||
is ChangeNumberResult.RegistrationLocked,
|
||||
is ChangeNumberResult.UnknownError,
|
||||
is ChangeNumberResult.ValidationError -> {
|
||||
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.")
|
||||
navigateToAccountLocked()
|
||||
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) { _, which -> 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 getPinEntryKeyboardType(): PinKeyboardType {
|
||||
val isNumeric = (binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER
|
||||
|
||||
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
|
||||
}
|
||||
|
||||
private fun updateKeyboard(keyboard: PinKeyboardType) {
|
||||
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
|
||||
|
||||
binding.kbsLockPinInput.setInputType(
|
||||
if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
)
|
||||
|
||||
binding.kbsLockPinInput.getText().clear()
|
||||
}
|
||||
|
||||
private fun navigateToAccountLocked() {
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
|
||||
}
|
||||
|
||||
override fun handleSuccessfulPinEntry(pin: String) {
|
||||
private fun handleSuccessfulPinEntry(pin: String) {
|
||||
val pinsDiffer: Boolean = SignalStore.svr.localPinHash?.let { !PinHashUtil.verifyLocalPinHash(it, pin) } ?: false
|
||||
|
||||
pinButton.cancelSpinning()
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
|
||||
if (pinsDiffer) {
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
@@ -53,22 +310,12 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendEmailToSupport() {
|
||||
private fun sendEmailToSupport() {
|
||||
val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin
|
||||
|
||||
val body: String = SupportEmailUtil.generateSupportEmailBody(
|
||||
requireContext(),
|
||||
subject,
|
||||
null,
|
||||
null
|
||||
)
|
||||
val body: String = SupportEmailUtil.generateSupportEmailBody(requireContext(), subject, null, null)
|
||||
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(subject),
|
||||
body
|
||||
)
|
||||
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), getString(subject), body)
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
@@ -25,23 +31,21 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
@@ -49,14 +53,14 @@ import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevic
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Provides various change number operations. All operations must run on [Schedulers.single] to support
|
||||
* the global "I am changing the number" lock exclusivity.
|
||||
* Repository to perform data operations during change number.
|
||||
*
|
||||
* @see [org.thoughtcrime.securesms.registration.data.RegistrationRepository]
|
||||
*/
|
||||
class ChangeNumberRepository(
|
||||
private val accountManager: SignalServiceAccountManager = AppDependencies.signalServiceAccountManager,
|
||||
@@ -64,157 +68,42 @@ class ChangeNumberRepository(
|
||||
) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* This lock should be held by anyone who is performing a change number operation, so that two different parties cannot change the user's number
|
||||
* at the same time.
|
||||
*/
|
||||
val CHANGE_NUMBER_LOCK = ReentrantLock()
|
||||
private val TAG = Log.tag(ChangeNumberRepository::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Rx operators to chain to acquire and release the [CHANGE_NUMBER_LOCK] on subscribe and on finish.
|
||||
*/
|
||||
fun <T : Any> acquireReleaseChangeNumberLock(upstream: Single<T>): Single<T> {
|
||||
return upstream.doOnSubscribe {
|
||||
CHANGE_NUMBER_LOCK.lock()
|
||||
SignalStore.misc.lockChangeNumber()
|
||||
}
|
||||
.subscribeOn(Schedulers.single())
|
||||
.observeOn(Schedulers.single())
|
||||
.doFinally {
|
||||
if (CHANGE_NUMBER_LOCK.isHeldByCurrentThread) {
|
||||
CHANGE_NUMBER_LOCK.unlock()
|
||||
fun whoAmI(): WhoAmIResponse {
|
||||
return accountManager.whoAmI
|
||||
}
|
||||
|
||||
suspend fun ensureDecryptionsDrained(timeout: Duration = 15.seconds) =
|
||||
withTimeoutOrNull(timeout) {
|
||||
suspendCancellableCoroutine {
|
||||
val drainedListener = object : Runnable {
|
||||
override fun run() {
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.removeDecryptionDrainedListener(this)
|
||||
Log.d(TAG, "Decryptions drained.")
|
||||
it.resume(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val drainedListener = object : Runnable {
|
||||
override fun run() {
|
||||
emitter.onComplete()
|
||||
it.invokeOnCancellation { cancellationCause ->
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.removeDecryptionDrainedListener(this)
|
||||
.removeDecryptionDrainedListener(drainedListener)
|
||||
Log.d(TAG, "Decryptions draining canceled.", cancellationCause)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.setCancellable {
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.removeDecryptionDrainedListener(drainedListener)
|
||||
.addDecryptionDrainedListener(drainedListener)
|
||||
Log.d(TAG, "Waiting for decryption drain.")
|
||||
}
|
||||
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.addDecryptionDrainedListener(drainedListener)
|
||||
}.subscribeOn(Schedulers.single())
|
||||
.timeout(15, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
fun changeNumber(sessionId: String? = null, recoveryPassword: String? = null, newE164: String): Single<ServiceResponse<VerifyResponse>> {
|
||||
check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null))
|
||||
|
||||
return Single.fromCallable {
|
||||
var completed = false
|
||||
var attempts = 0
|
||||
lateinit var changeNumberResponse: ServiceResponse<VerifyAccountResponse>
|
||||
|
||||
while (!completed && attempts < 5) {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
|
||||
sessionId = sessionId,
|
||||
recoveryPassword = recoveryPassword,
|
||||
newE164 = newE164
|
||||
)
|
||||
|
||||
SignalStore.misc.setPendingChangeNumberMetadata(metadata)
|
||||
|
||||
changeNumberResponse = accountManager.changeNumber(request)
|
||||
|
||||
val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
|
||||
if (possibleError is MismatchedDevicesException) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
|
||||
VerifyResponse.from(
|
||||
response = changeNumberResponse,
|
||||
masterKey = null,
|
||||
pin = null,
|
||||
aciPreKeyCollection = null,
|
||||
pniPreKeyCollection = null
|
||||
)
|
||||
}.subscribeOn(Schedulers.single())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
fun changeNumber(
|
||||
sessionId: String,
|
||||
newE164: String,
|
||||
pin: String,
|
||||
svrAuthCredentials: SvrAuthCredentialSet
|
||||
): Single<ServiceResponse<VerifyResponse>> {
|
||||
return Single.fromCallable {
|
||||
val masterKey: MasterKey
|
||||
val registrationLock: String
|
||||
|
||||
try {
|
||||
masterKey = SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin)
|
||||
registrationLock = masterKey.deriveRegistrationLock()
|
||||
} catch (e: SvrWrongPinException) {
|
||||
return@fromCallable ServiceResponse.forExecutionError(e)
|
||||
} catch (e: SvrNoDataException) {
|
||||
return@fromCallable ServiceResponse.forExecutionError(e)
|
||||
} catch (e: IOException) {
|
||||
return@fromCallable ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
|
||||
var completed = false
|
||||
var attempts = 0
|
||||
lateinit var changeNumberResponse: ServiceResponse<VerifyAccountResponse>
|
||||
|
||||
while (!completed && attempts < 5) {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
|
||||
sessionId = sessionId,
|
||||
newE164 = newE164,
|
||||
registrationLock = registrationLock
|
||||
)
|
||||
|
||||
SignalStore.misc.setPendingChangeNumberMetadata(metadata)
|
||||
|
||||
changeNumberResponse = accountManager.changeNumber(request)
|
||||
|
||||
val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
|
||||
if (possibleError is MismatchedDevicesException) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
|
||||
VerifyResponse.from(
|
||||
response = changeNumberResponse,
|
||||
masterKey = masterKey,
|
||||
pin = pin,
|
||||
aciPreKeyCollection = null,
|
||||
pniPreKeyCollection = null
|
||||
)
|
||||
}.subscribeOn(Schedulers.single())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
fun whoAmI(): Single<WhoAmIResponse> {
|
||||
return Single.fromCallable { AppDependencies.signalServiceAccountManager.getWhoAmI() }
|
||||
.subscribeOn(Schedulers.single())
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> {
|
||||
fun changeLocalNumber(e164: String, pni: ServiceId.PNI) {
|
||||
val oldStorageId: ByteArray? = Recipient.self().storageId
|
||||
SignalDatabase.recipients.updateSelfE164(e164, pni)
|
||||
val newStorageId: ByteArray? = Recipient.self().storageId
|
||||
@@ -243,7 +132,7 @@ class ChangeNumberRepository(
|
||||
throw AssertionError("No change number metadata")
|
||||
}
|
||||
|
||||
val originalPni = PNI.parseOrThrow(metadata.previousPni)
|
||||
val originalPni = ServiceId.PNI.parseOrThrow(metadata.previousPni)
|
||||
|
||||
if (originalPni == pni) {
|
||||
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
|
||||
@@ -269,6 +158,8 @@ class ChangeNumberRepository(
|
||||
}
|
||||
|
||||
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
|
||||
Log.i(TAG, "Submitting prekeys with PNI identity key: ${pniIdentityKeyPair.publicKey.fingerprint}")
|
||||
|
||||
accountManager.setPreKeys(
|
||||
PreKeyUpload(
|
||||
serviceIdType = ServiceIdType.PNI,
|
||||
@@ -303,28 +194,100 @@ class ChangeNumberRepository(
|
||||
|
||||
AppDependencies.jobManager.add(RefreshAttributesJob())
|
||||
|
||||
return rotateCertificates()
|
||||
rotateCertificates()
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
private fun rotateCertificates(): Single<Unit> {
|
||||
@WorkerThread
|
||||
private fun rotateCertificates() {
|
||||
val certificateTypes = SignalStore.phoneNumberPrivacy.allCertificateTypes
|
||||
|
||||
Log.i(TAG, "Rotating these certificates $certificateTypes")
|
||||
|
||||
return Single.fromCallable {
|
||||
for (certificateType in certificateTypes) {
|
||||
val certificate: ByteArray? = when (certificateType) {
|
||||
CertificateType.ACI_AND_E164 -> accountManager.getSenderCertificate()
|
||||
CertificateType.ACI_ONLY -> accountManager.getSenderCertificateForPhoneNumberPrivacy()
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully got $certificateType certificate")
|
||||
|
||||
SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
for (certificateType in certificateTypes) {
|
||||
val certificate: ByteArray? = when (certificateType) {
|
||||
CertificateType.ACI_AND_E164 -> accountManager.senderCertificate
|
||||
CertificateType.ACI_ONLY -> accountManager.senderCertificateForPhoneNumberPrivacy
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}.subscribeOn(Schedulers.single())
|
||||
|
||||
Log.i(TAG, "Successfully got $certificateType certificate")
|
||||
|
||||
SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changeNumberWithRecoveryPassword(recoveryPassword: String, newE164: String): ChangeNumberResult {
|
||||
return changeNumberInternal(recoveryPassword = recoveryPassword, newE164 = newE164)
|
||||
}
|
||||
|
||||
suspend fun changeNumberWithoutRegistrationLock(sessionId: String, newE164: String): ChangeNumberResult {
|
||||
return changeNumberInternal(sessionId = sessionId, newE164 = newE164)
|
||||
}
|
||||
|
||||
suspend fun changeNumberWithRegistrationLock(
|
||||
sessionId: String,
|
||||
newE164: String,
|
||||
pin: String,
|
||||
svrAuthCredentials: SvrAuthCredentialSet
|
||||
): ChangeNumberResult {
|
||||
val masterKey: MasterKey
|
||||
|
||||
try {
|
||||
masterKey = SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin)
|
||||
} catch (e: SvrWrongPinException) {
|
||||
return ChangeNumberResult.SvrWrongPin(e)
|
||||
} catch (e: SvrNoDataException) {
|
||||
return ChangeNumberResult.SvrNoData(e)
|
||||
} catch (e: IOException) {
|
||||
return ChangeNumberResult.UnknownError(e)
|
||||
}
|
||||
|
||||
val registrationLock = masterKey.deriveRegistrationLock()
|
||||
return changeNumberInternal(sessionId = sessionId, registrationLock = registrationLock, newE164 = newE164)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the service to change the phone number associated with this account.
|
||||
*/
|
||||
private suspend fun changeNumberInternal(sessionId: String? = null, recoveryPassword: String? = null, registrationLock: String? = null, newE164: String): ChangeNumberResult {
|
||||
check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null))
|
||||
var completed = false
|
||||
var attempts = 0
|
||||
lateinit var result: NetworkResult<VerifyAccountResponse>
|
||||
|
||||
while (!completed && attempts < 5) {
|
||||
Log.i(TAG, "Attempt #$attempts")
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
|
||||
sessionId = sessionId,
|
||||
recoveryPassword = recoveryPassword,
|
||||
newE164 = newE164,
|
||||
registrationLock = registrationLock
|
||||
)
|
||||
|
||||
SignalStore.misc.setPendingChangeNumberMetadata(metadata)
|
||||
withContext(Dispatchers.IO) {
|
||||
result = accountManager.registrationApi.changeNumber(request)
|
||||
}
|
||||
|
||||
val possibleError = result.getCause() as? MismatchedDevicesException
|
||||
if (possibleError != null) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Returning change number network result.")
|
||||
return ChangeNumberResult.from(
|
||||
result.map { accountRegistrationResponse: VerifyAccountResponse ->
|
||||
NumberChangeResult(
|
||||
uuid = accountRegistrationResponse.uuid,
|
||||
pni = accountRegistrationResponse.pni,
|
||||
storageCapable = accountRegistrationResponse.storageCapable,
|
||||
number = accountRegistrationResponse.number
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -410,11 +373,12 @@ class ChangeNumberRepository(
|
||||
return ChangeNumberRequestData(request, metadata)
|
||||
}
|
||||
|
||||
fun verifyAccount(sessionId: String, code: String): Single<ServiceResponse<RegistrationSessionMetadataResponse>> {
|
||||
return Single.fromCallable {
|
||||
accountManager.verifyAccount(code, sessionId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
private data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
|
||||
|
||||
data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
|
||||
data class NumberChangeResult(
|
||||
val uuid: String,
|
||||
val pni: String,
|
||||
val storageCapable: Boolean,
|
||||
val number: String
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
@@ -23,7 +23,7 @@ import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
*/
|
||||
sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
companion object {
|
||||
fun from(networkResult: NetworkResult<ChangeNumberV2Repository.NumberChangeResult>): ChangeNumberResult {
|
||||
fun from(networkResult: NetworkResult<ChangeNumberRepository.NumberChangeResult>): ChangeNumberResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> Success(networkResult.result)
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
@@ -56,7 +56,7 @@ sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
}
|
||||
}
|
||||
|
||||
class Success(val numberChangeResult: ChangeNumberV2Repository.NumberChangeResult) : ChangeNumberResult(null)
|
||||
class Success(val numberChangeResult: ChangeNumberRepository.NumberChangeResult) : ChangeNumberResult(null)
|
||||
class IncorrectRecoveryPassword(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class AuthorizationFailed(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class MalformedRequest(cause: Throwable) : ChangeNumberResult(cause)
|
||||
@@ -3,16 +3,16 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
|
||||
/**
|
||||
* State holder for [ChangeNumberV2ViewModel]
|
||||
* State holder for [ChangeNumberViewModel]
|
||||
*/
|
||||
data class ChangeNumberState(
|
||||
val number: NumberViewState = NumberViewState.INITIAL,
|
||||
@@ -1,37 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.registration.fragments.CaptchaFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
|
||||
/**
|
||||
* Helpers for various aspects of the change number flow.
|
||||
*/
|
||||
object ChangeNumberUtil {
|
||||
@JvmStatic
|
||||
fun getViewModel(fragment: Fragment): ChangeNumberViewModel {
|
||||
val navController = NavHostFragment.findNavController(fragment)
|
||||
return ViewModelProvider(
|
||||
navController.getViewModelStoreOwner(R.id.app_settings_change_number),
|
||||
ChangeNumberViewModel.Factory(navController.getBackStackEntry(R.id.app_settings_change_number))
|
||||
).get(ChangeNumberViewModel::class.java)
|
||||
}
|
||||
|
||||
fun getCaptchaArguments(): Bundle {
|
||||
return Bundle().apply {
|
||||
putSerializable(
|
||||
CaptchaFragment.EXTRA_VIEW_MODEL_PROVIDER,
|
||||
object : CaptchaFragment.CaptchaViewModelProvider {
|
||||
override fun get(fragment: CaptchaFragment): BaseRegistrationViewModel = getViewModel(fragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.changeNumberSuccess() {
|
||||
requireActivity().finish()
|
||||
|
||||
@@ -1,109 +1,148 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
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.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
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.util.navigation.safeNavigate
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
|
||||
|
||||
/**
|
||||
* Screen to show while the change number is in-progress.
|
||||
*/
|
||||
class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phone_number_verify) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
private var requestingCaptcha: Boolean = false
|
||||
|
||||
private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleDisposable.bindTo(lifecycle)
|
||||
viewModel = getViewModel(this)
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberViewModel>()
|
||||
private var dialogVisible: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberVerifyFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
toolbar.setNavigationOnClickListener {
|
||||
findNavController().navigateUp()
|
||||
viewModel.resetLocalSessionState()
|
||||
}
|
||||
|
||||
val status: TextView = view.findViewById(R.id.change_phone_number_verify_status)
|
||||
status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber)
|
||||
|
||||
if (!requestingCaptcha || viewModel.hasCaptchaToken()) {
|
||||
requestCode()
|
||||
} else {
|
||||
Log.d(TAG, "Captcha required.")
|
||||
Toast.makeText(requireContext(), R.string.ChangeNumberVerifyFragment__captcha_required, Toast.LENGTH_SHORT).show()
|
||||
findNavController().navigateUp()
|
||||
viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate)
|
||||
|
||||
requestCode()
|
||||
}
|
||||
|
||||
private fun onStateUpdate(state: ChangeNumberState) {
|
||||
if (state.challengesRequested.contains(Challenge.CAPTCHA) && state.captchaToken.isNotNullOrBlank()) {
|
||||
viewModel.submitCaptchaToken(requireContext())
|
||||
} else if (state.challengesRemaining.isNotEmpty()) {
|
||||
handleChallenges(state.challengesRemaining)
|
||||
} else if (state.changeNumberOutcome != null) {
|
||||
handleRequestCodeResult(state.changeNumberOutcome)
|
||||
} else if (!state.inProgress) {
|
||||
Log.d(TAG, "Not in progress, navigating up.")
|
||||
if (state.allowedToRequestCode) {
|
||||
requestCode()
|
||||
} else if (!dialogVisible) {
|
||||
showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
val mode = if (ChangeNumberVerifyFragmentArgs.fromBundle(requireArguments()).smsListenerEnabled) VerifyAccountRepository.Mode.SMS_WITH_LISTENER else VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER
|
||||
val mccMncProducer = MccMncProducer(requireContext())
|
||||
lifecycleDisposable += viewModel
|
||||
.ensureDecryptionsDrained()
|
||||
.onErrorComplete()
|
||||
.andThen(viewModel.changeNumberWithRecoveryPassword())
|
||||
.flatMap { changed ->
|
||||
if (changed) {
|
||||
Log.d(TAG, "Successfully changed number using recovery password.")
|
||||
Single.just(RequestCodeResult.RecoveryPasswordWorked)
|
||||
} else {
|
||||
viewModel.requestVerificationCode(mode, mccMncProducer.mcc, mccMncProducer.mnc)
|
||||
.map { p -> RequestCodeResult.RequestedVerificationCode(p) }
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
if (result is RequestCodeResult.RecoveryPasswordWorked) {
|
||||
changeNumberSuccess()
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
val processor: RegistrationSessionProcessor = (result as RequestCodeResult.RequestedVerificationCode).processor
|
||||
|
||||
if (processor.verificationCodeRequestSuccess()) {
|
||||
Log.i(TAG, "Successfully requested SMS code.")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired(viewModel.excludedChallenges)) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
showErrorDialog(requireContext(), R.string.RegistrationActivity_rate_limited_to_service) { _, _ -> findNavController().navigateUp() }
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
showErrorDialog(requireContext(), R.string.RegistrationActivity_unable_to_request_verification_code) { _, _ -> findNavController().navigateUp() }
|
||||
}
|
||||
}
|
||||
val mode = if (ChangeNumberVerifyFragmentArgs.fromBundle(requireArguments()).smsListenerEnabled) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER
|
||||
viewModel.initiateChangeNumberSession(requireContext(), mode)
|
||||
}
|
||||
|
||||
private fun showErrorDialog(context: Context, @StringRes message: Int, onPositiveButtonClickListener: OnClickListener?) {
|
||||
MaterialAlertDialogBuilder(context).setMessage(message).setPositiveButton(android.R.string.ok, onPositiveButtonClickListener).show()
|
||||
private fun handleRequestCodeResult(changeNumberOutcome: ChangeNumberOutcome) {
|
||||
Log.d(TAG, "Handling request code result: ${changeNumberOutcome.javaClass.name}")
|
||||
when (changeNumberOutcome) {
|
||||
is ChangeNumberOutcome.RecoveryPasswordWorked -> {
|
||||
Log.i(TAG, "Successfully changed number with recovery password.")
|
||||
changeNumberSuccess()
|
||||
}
|
||||
|
||||
is ChangeNumberOutcome.ChangeNumberRequestOutcome -> {
|
||||
when (val castResult = changeNumberOutcome.result) {
|
||||
is VerificationCodeRequestResult.Success -> {
|
||||
Log.i(TAG, "Successfully requested SMS code.")
|
||||
findNavController().safeNavigate(ChangeNumberVerifyFragmentDirections.actionChangePhoneNumberVerifyFragmentToChangeNumberEnterCodeFragment())
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.ChallengeRequired -> {
|
||||
Log.i(TAG, "Unable to request sms code due to challenges required: ${castResult.challenges.joinToString { it.key }}")
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.RateLimited -> {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
showErrorDialog(R.string.RegistrationActivity_rate_limited_to_service)
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.TokenNotAccepted -> {
|
||||
Log.i(TAG, "Token was not accepted.")
|
||||
showErrorDialog(R.string.RegistrationActivity_additional_verification_required)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unable to request sms code", castResult.getCause())
|
||||
showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ChangeNumberOutcome.VerificationCodeWorked -> {
|
||||
Log.i(TAG, "Successfully changed number with verification code.")
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface RequestCodeResult {
|
||||
object RecoveryPasswordWorked : RequestCodeResult
|
||||
class RequestedVerificationCode(val processor: RegistrationSessionProcessor) : RequestCodeResult
|
||||
private fun handleChallenges(remainingChallenges: List<Challenge>) {
|
||||
Log.i(TAG, "Handling challenge(s): ${remainingChallenges.joinToString { it.key }}")
|
||||
when (remainingChallenges.first()) {
|
||||
Challenge.CAPTCHA -> {
|
||||
findNavController().safeNavigate(ChangeNumberVerifyFragmentDirections.actionChangePhoneNumberVerifyFragmentToCaptchaFragment())
|
||||
}
|
||||
|
||||
Challenge.PUSH -> {
|
||||
viewModel.requestAndSubmitPushToken(requireContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog(@StringRes message: Int) {
|
||||
if (dialogVisible) {
|
||||
Log.i(TAG, "Dialog already being shown, failed to display dialog with message ${getString(message)}")
|
||||
return
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
findNavController().navigateUp()
|
||||
viewModel.resetLocalSessionState()
|
||||
}
|
||||
show()
|
||||
dialogVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,82 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.app.Application
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
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.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Objects
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberViewModel::class.java)
|
||||
/**
|
||||
* [ViewModel] for the change number flow.
|
||||
*
|
||||
* @see [RegistrationViewModel], from which this is derived.
|
||||
*/
|
||||
class ChangeNumberViewModel : ViewModel() {
|
||||
|
||||
class ChangeNumberViewModel(
|
||||
private val localNumber: String,
|
||||
private val changeNumberRepository: ChangeNumberRepository,
|
||||
savedState: SavedStateHandle,
|
||||
password: String,
|
||||
verifyAccountRepository: VerifyAccountRepository,
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberViewModel::class.java)
|
||||
|
||||
val CHANGE_NUMBER_LOCK = ReentrantLock()
|
||||
}
|
||||
|
||||
private val repository = ChangeNumberRepository()
|
||||
private val store = MutableStateFlow(ChangeNumberState())
|
||||
private val serialContext = SignalExecutors.SERIAL.asCoroutineDispatcher()
|
||||
private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(AppDependencies.application)
|
||||
) : BaseRegistrationViewModel(savedState, verifyAccountRepository, password) {
|
||||
|
||||
var oldNumberState: NumberViewState = NumberViewState.Builder().build()
|
||||
private set
|
||||
private val initialLocalNumber = SignalStore.account.e164
|
||||
private val password = SignalStore.account.servicePassword!!
|
||||
|
||||
private val liveOldNumberState = DefaultValueLiveData(oldNumberState)
|
||||
private val liveNewNumberState = DefaultValueLiveData(number)
|
||||
val uiState = store.asLiveData()
|
||||
val liveOldNumberState = store.map { it.oldPhoneNumber }.asLiveData()
|
||||
val liveNewNumberState = store.map { it.number }.asLiveData()
|
||||
val liveLockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData()
|
||||
val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData()
|
||||
|
||||
init {
|
||||
try {
|
||||
val countryCode: Int = PhoneNumberUtil.getInstance()
|
||||
.parse(localNumber, null)
|
||||
.parse(SignalStore.account.e164!!, null)
|
||||
.countryCode
|
||||
|
||||
setOldCountry(countryCode)
|
||||
setNewCountry(countryCode)
|
||||
store.update {
|
||||
it.copy(
|
||||
number = it.number.toBuilder().countryCode(countryCode).build(),
|
||||
oldPhoneNumber = it.oldPhoneNumber.toBuilder().countryCode(countryCode).build()
|
||||
)
|
||||
}
|
||||
} catch (e: NumberParseException) {
|
||||
Log.i(TAG, "Unable to parse number for default country code")
|
||||
}
|
||||
@@ -69,45 +89,83 @@ class ChangeNumberViewModel(
|
||||
smsRetrieverReceiver.unregisterReceiver()
|
||||
}
|
||||
|
||||
fun getLiveOldNumber(): LiveData<NumberViewState> {
|
||||
return liveOldNumberState
|
||||
}
|
||||
// region Public Getters and Setters
|
||||
|
||||
fun getLiveNewNumber(): LiveData<NumberViewState> {
|
||||
return liveNewNumberState
|
||||
}
|
||||
val number: NumberViewState
|
||||
get() = store.value.number
|
||||
|
||||
fun setOldNationalNumber(number: String) {
|
||||
oldNumberState = oldNumberState.toBuilder()
|
||||
.nationalNumber(number)
|
||||
.build()
|
||||
val oldNumberState: NumberViewState
|
||||
get() = store.value.oldPhoneNumber
|
||||
|
||||
liveOldNumberState.value = oldNumberState
|
||||
val svrTriesRemaining: Int
|
||||
get() = store.value.svrTriesRemaining
|
||||
|
||||
fun setOldNationalNumber(updatedNumber: String) {
|
||||
store.update {
|
||||
it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setOldCountry(countryCode: Int, country: String? = null) {
|
||||
oldNumberState = oldNumberState.toBuilder()
|
||||
.selectedCountryDisplayName(country)
|
||||
.countryCode(countryCode)
|
||||
.build()
|
||||
|
||||
liveOldNumberState.value = oldNumberState
|
||||
store.update {
|
||||
it.copy(oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setNewNationalNumber(number: String) {
|
||||
setNationalNumber(number)
|
||||
|
||||
liveNewNumberState.value = this.number
|
||||
fun setNewNationalNumber(updatedNumber: String) {
|
||||
store.update {
|
||||
it.copy(number = number.toBuilder().nationalNumber(updatedNumber).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setNewCountry(countryCode: Int, country: String? = null) {
|
||||
onCountrySelected(country, countryCode)
|
||||
store.update {
|
||||
it.copy(number = number.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build())
|
||||
}
|
||||
}
|
||||
|
||||
liveNewNumberState.value = this.number
|
||||
fun setCaptchaResponse(token: String) {
|
||||
Log.v(TAG, "setCaptchaResponse()")
|
||||
store.update {
|
||||
it.copy(captchaToken = token)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnteredPin(pin: String) {
|
||||
store.update {
|
||||
it.copy(enteredPin = pin)
|
||||
}
|
||||
}
|
||||
|
||||
fun incrementIncorrectCodeAttempts() {
|
||||
store.update {
|
||||
it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPresentedChallenge(challenge: Challenge) {
|
||||
Log.v(TAG, "addPresentedChallenge()")
|
||||
store.update {
|
||||
it.copy(challengesPresented = it.challengesPresented.plus(challenge))
|
||||
}
|
||||
}
|
||||
|
||||
fun removePresentedChallenge(challenge: Challenge) {
|
||||
Log.v(TAG, "addPresentedChallenge()")
|
||||
store.update {
|
||||
it.copy(challengesPresented = it.challengesPresented.minus(challenge))
|
||||
}
|
||||
}
|
||||
|
||||
fun resetLocalSessionState() {
|
||||
Log.v(TAG, "resetLocalSessionState()")
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = null, captchaToken = null, challengesRequested = emptyList(), allowedToRequestCode = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun canContinue(): ContinueStatus {
|
||||
return if (oldNumberState.e164Number == localNumber) {
|
||||
return if (oldNumberState.e164Number == initialLocalNumber) {
|
||||
if (number.isValid) {
|
||||
ContinueStatus.CAN_CONTINUE
|
||||
} else {
|
||||
@@ -118,129 +176,378 @@ class ChangeNumberViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return changeNumberRepository.ensureDecryptionsDrained()
|
||||
}
|
||||
// endregion
|
||||
|
||||
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyResponseProcessor> {
|
||||
return super.verifyCodeWithoutRegistrationLock(code)
|
||||
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
|
||||
.flatMap(this::attemptToUnlockChangeNumber)
|
||||
}
|
||||
// region Public actions
|
||||
|
||||
override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyResponseWithRegistrationLockProcessor> {
|
||||
return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
|
||||
.flatMap(this::attemptToUnlockChangeNumber)
|
||||
}
|
||||
fun checkWhoAmI(onSuccess: () -> Unit, onError: (Throwable) -> Unit) {
|
||||
Log.v(TAG, "checkWhoAmI()")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val whoAmI = repository.whoAmI()
|
||||
|
||||
private fun <T : VerifyResponseProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
|
||||
return if (processor.hasResult() || processor.isServerSentError()) {
|
||||
SignalStore.misc.unlockChangeNumber()
|
||||
SignalStore.misc.clearPendingChangeNumberMetadata()
|
||||
Single.just(processor)
|
||||
} else {
|
||||
changeNumberRepository.whoAmI()
|
||||
.map { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, localNumber)) {
|
||||
Log.i(TAG, "Local and remote numbers match, we can unlock.")
|
||||
SignalStore.misc.unlockChangeNumber()
|
||||
SignalStore.misc.clearPendingChangeNumberMetadata()
|
||||
}
|
||||
processor
|
||||
if (whoAmI.number == SignalStore.account.e164) {
|
||||
return@launch bail { Log.i(TAG, "Local and remote numbers match, nothing needs to be done.") }
|
||||
}
|
||||
.onErrorReturn { processor }
|
||||
|
||||
Log.i(TAG, "Local (${SignalStore.account.e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
|
||||
withLockOnSerialExecutor {
|
||||
repository.changeLocalNumber(whoAmI.number, ServiceId.PNI.parseOrThrow(whoAmI.pni))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onSuccess()
|
||||
}
|
||||
} catch (ioException: IOException) {
|
||||
Log.w(TAG, "Encountered an exception when requesting whoAmI()", ioException)
|
||||
withContext(Dispatchers.Main) {
|
||||
onError(ioException)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyAccountWithoutRegistrationLock(): Single<ServiceResponse<VerifyResponse>> {
|
||||
val sessionId = sessionId ?: throw IllegalStateException("No valid registration session")
|
||||
|
||||
return changeNumberRepository.verifyAccount(sessionId, textCodeEntered)
|
||||
.map { RegistrationSessionProcessor.RegistrationSessionProcessorForVerification(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess {
|
||||
if (it.hasResult()) {
|
||||
setCanSmsAtTime(it.getNextCodeViaSmsAttempt())
|
||||
setCanCallAtTime(it.getNextCodeViaCallAttempt())
|
||||
}
|
||||
}
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { processor ->
|
||||
if (processor.isAlreadyVerified() || processor.hasResult() && processor.isVerified()) {
|
||||
changeNumberRepository.changeNumber(sessionId = sessionId, newE164 = number.e164Number)
|
||||
} else if (processor.error == null) {
|
||||
Single.just<ServiceResponse<VerifyResponse>>(ServiceResponse.forApplicationError(IncorrectCodeException(), 403, null))
|
||||
} else {
|
||||
Single.just<ServiceResponse<VerifyResponse>>(ServiceResponse.coerceError(processor.response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyAccountWithRegistrationLock(pin: String, svrAuthCredentials: SvrAuthCredentialSet): Single<ServiceResponse<VerifyResponse>> {
|
||||
val sessionId = sessionId ?: throw IllegalStateException("No valid registration session")
|
||||
return changeNumberRepository.changeNumber(sessionId, number.e164Number, pin, svrAuthCredentials)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onVerifySuccess(processor: VerifyResponseProcessor): Single<VerifyResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni))
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
VerifyResponseWithoutKbs(ServiceResponse.forUnknownError(t))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVerifySuccessWithRegistrationLock(processor: VerifyResponseWithRegistrationLockProcessor, pin: String): Single<VerifyResponseWithRegistrationLockProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni))
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
VerifyResponseWithRegistrationLockProcessor(ServiceResponse.forUnknownError(t), processor.svrAuthCredentials)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeNumberWithRecoveryPassword(): Single<Boolean> {
|
||||
val recoveryPassword = SignalStore.svr.recoveryPassword
|
||||
|
||||
return if (SignalStore.svr.hasPin() && recoveryPassword != null) {
|
||||
changeNumberRepository.changeNumber(recoveryPassword = recoveryPassword, newE164 = number.e164Number)
|
||||
.map { r -> VerifyResponseWithoutKbs(r) }
|
||||
.flatMap { p ->
|
||||
if (p.hasResult()) {
|
||||
onVerifySuccess(p).map { true }
|
||||
} else {
|
||||
Single.just(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Single.just(false)
|
||||
fun registerSmsListenerWithCompletionListener(context: Context, onComplete: (Boolean) -> Unit) {
|
||||
Log.v(TAG, "registerSmsListenerWithCompletionListener()")
|
||||
viewModelScope.launch {
|
||||
val listenerRegistered = RegistrationRepository.registerSmsListener(context)
|
||||
onComplete(listenerRegistered)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(owner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(owner, null) {
|
||||
|
||||
override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
||||
val context: Application = AppDependencies.application
|
||||
val localNumber: String = SignalStore.account.e164!!
|
||||
val password: String = SignalStore.account.servicePassword!!
|
||||
|
||||
val viewModel = ChangeNumberViewModel(
|
||||
localNumber = localNumber,
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = handle,
|
||||
password = password,
|
||||
verifyAccountRepository = VerifyAccountRepository(context)
|
||||
fun verifyCodeWithoutRegistrationLock(context: Context, code: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
Log.v(TAG, "verifyCodeWithoutRegistrationLock()")
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = true,
|
||||
enteredCode = code
|
||||
)
|
||||
}
|
||||
|
||||
return requireNotNull(modelClass.cast(viewModel))
|
||||
viewModelScope.launch {
|
||||
verifyCodeInternal(context = context, pin = null, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()")
|
||||
store.update { it.copy(inProgress = true) }
|
||||
|
||||
viewModelScope.launch {
|
||||
verifyCodeInternal(context = context, pin = pin, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
val sessionId = getOrCreateValidSession(context)?.body?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") }
|
||||
val registrationData = getRegistrationData(context)
|
||||
|
||||
val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
|
||||
|
||||
if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) {
|
||||
handleVerificationError(verificationResponse, verificationErrorHandler)
|
||||
return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") }
|
||||
}
|
||||
|
||||
val result: ChangeNumberResult = if (pin == null) {
|
||||
repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number)
|
||||
} else {
|
||||
repository.changeNumberWithRegistrationLock(
|
||||
sessionId = sessionId,
|
||||
newE164 = number.e164Number,
|
||||
pin = pin,
|
||||
svrAuthCredentials = SvrAuthCredentialSet(
|
||||
svr2Credentials = store.value.svr2Credentials,
|
||||
svr3Credentials = store.value.svr3Credentials
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (result is ChangeNumberResult.Success) {
|
||||
handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked)
|
||||
} else {
|
||||
handleChangeNumberError(result, numberChangeErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
fun submitCaptchaToken(context: Context) {
|
||||
Log.v(TAG, "submitCaptchaToken()")
|
||||
val e164 = number.e164Number
|
||||
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
|
||||
store.update {
|
||||
it.copy(
|
||||
captchaToken = null,
|
||||
inProgress = true,
|
||||
changeNumberOutcome = null
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Getting session in order to submit captcha token…")
|
||||
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") }
|
||||
if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.CAPTCHA)) {
|
||||
Log.d(TAG, "Captcha submission no longer necessary, bailing.")
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = false,
|
||||
changeNumberOutcome = null
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
Log.d(TAG, "Submitting captcha token…")
|
||||
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken)
|
||||
Log.d(TAG, "Captcha token submitted.")
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAndSubmitPushToken(context: Context) {
|
||||
Log.v(TAG, "validatePushToken()")
|
||||
|
||||
addPresentedChallenge(Challenge.PUSH)
|
||||
|
||||
val e164 = number.e164Number
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Getting session in order to perform push token verification…")
|
||||
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") }
|
||||
|
||||
if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) {
|
||||
Log.d(TAG, "Push submission no longer necessary, bailing.")
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = false,
|
||||
changeNumberOutcome = null
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.d(TAG, "Requesting push challenge token…")
|
||||
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password)
|
||||
Log.d(TAG, "Push challenge token submitted.")
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initiateChangeNumberSession(context: Context, mode: RegistrationRepository.Mode) {
|
||||
Log.v(TAG, "changeNumber()")
|
||||
store.update { it.copy(inProgress = true) }
|
||||
viewModelScope.launch {
|
||||
val encryptionDrained = repository.ensureDecryptionsDrained() ?: false
|
||||
|
||||
if (!encryptionDrained) {
|
||||
return@launch bail { Log.i(TAG, "Failed to drain encryption.") }
|
||||
}
|
||||
|
||||
val changed = changeNumberWithRecoveryPassword()
|
||||
|
||||
if (changed) {
|
||||
Log.d(TAG, "Successfully changed number using recovery password, which cleaned up after itself.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
requestVerificationCode(context, mode)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Private actions
|
||||
|
||||
private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) {
|
||||
Log.v(TAG, "updateLocalStateFromSession()")
|
||||
store.update {
|
||||
it.copy(sessionId = response.body.id, challengesRequested = Challenge.parse(response.body.requestedInformation), allowedToRequestCode = response.body.allowedToRequestCode)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? {
|
||||
Log.v(TAG, "getOrCreateValidSession()")
|
||||
val e164 = number.e164Number
|
||||
val mccMncProducer = MccMncProducer(context)
|
||||
val existingSessionId = store.value.sessionId
|
||||
return RegistrationViewModel.getOrCreateValidSession(context = context, existingSessionId = existingSessionId, e164 = e164, password = password, mcc = mccMncProducer.mcc, mnc = mccMncProducer.mnc, successListener = { freshMetadata ->
|
||||
Log.v(TAG, "Valid session received, updating local state.")
|
||||
updateLocalStateFromSession(freshMetadata)
|
||||
}, errorHandler = { result ->
|
||||
val requestCode: VerificationCodeRequestResult = when (result) {
|
||||
is RegistrationSessionCreationResult.RateLimited -> VerificationCodeRequestResult.RateLimited(result.getCause(), result.timeRemaining)
|
||||
is RegistrationSessionCreationResult.MalformedRequest -> VerificationCodeRequestResult.MalformedRequest(result.getCause())
|
||||
else -> VerificationCodeRequestResult.UnknownError(result.getCause())
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(requestCode))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private suspend fun changeNumberWithRecoveryPassword(): Boolean {
|
||||
Log.v(TAG, "changeNumberWithRecoveryPassword()")
|
||||
SignalStore.svr.recoveryPassword?.let { recoveryPassword ->
|
||||
if (SignalStore.svr.hasPin()) {
|
||||
val result = repository.changeNumberWithRecoveryPassword(recoveryPassword = recoveryPassword, newE164 = number.e164Number)
|
||||
|
||||
if (result is ChangeNumberResult.Success) {
|
||||
handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked)
|
||||
return true
|
||||
}
|
||||
|
||||
Log.d(TAG, "Encountered error while trying to change number with recovery password.", result.getCause())
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun handleSuccessfulChangedRemoteNumber(e164: String, pni: ServiceId.PNI, changeNumberOutcome: ChangeNumberOutcome) {
|
||||
var result = changeNumberOutcome
|
||||
Log.v(TAG, "handleSuccessfulChangedRemoteNumber(${result.javaClass.simpleName}")
|
||||
try {
|
||||
withLockOnSerialExecutor {
|
||||
repository.changeLocalNumber(e164, pni)
|
||||
}
|
||||
} catch (ioException: IOException) {
|
||||
Log.w(TAG, "Failed to change local number!", ioException)
|
||||
result = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(ioException))
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerificationError(result: VerificationCodeRequestResult, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit) {
|
||||
Log.v(TAG, "handleVerificationError(${result.javaClass.simpleName}")
|
||||
when (result) {
|
||||
is VerificationCodeRequestResult.Success -> Unit
|
||||
is VerificationCodeRequestResult.RegistrationLocked ->
|
||||
store.update {
|
||||
it.copy(
|
||||
svr2Credentials = result.svr2Credentials,
|
||||
svr3Credentials = result.svr3Credentials
|
||||
)
|
||||
}
|
||||
else -> Log.i(TAG, "Received exception during verification.", result.getCause())
|
||||
}
|
||||
|
||||
verificationErrorHandler(result)
|
||||
}
|
||||
|
||||
private fun handleChangeNumberError(result: ChangeNumberResult, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
Log.v(TAG, "handleChangeNumberError(${result.javaClass.simpleName}")
|
||||
when (result) {
|
||||
is ChangeNumberResult.Success -> Unit
|
||||
is ChangeNumberResult.RegistrationLocked ->
|
||||
store.update {
|
||||
it.copy(
|
||||
svr2Credentials = result.svr2Credentials,
|
||||
svr3Credentials = result.svr3Credentials
|
||||
)
|
||||
}
|
||||
is ChangeNumberResult.SvrWrongPin -> {
|
||||
store.update {
|
||||
it.copy(
|
||||
svrTriesRemaining = result.triesRemaining
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Log.i(TAG, "Received exception during change number.", result.getCause())
|
||||
}
|
||||
|
||||
numberChangeErrorHandler(result)
|
||||
}
|
||||
|
||||
private suspend fun requestVerificationCode(context: Context, mode: RegistrationRepository.Mode) {
|
||||
Log.v(TAG, "requestVerificationCode()")
|
||||
val e164 = number.e164Number
|
||||
|
||||
val validSession = getOrCreateValidSession(context)
|
||||
|
||||
if (validSession == null) {
|
||||
Log.w(TAG, "Bailing on requesting verification code because could not create a session!")
|
||||
resetLocalSessionState()
|
||||
return
|
||||
}
|
||||
|
||||
val result = if (!validSession.body.allowedToRequestCode) {
|
||||
val challenges = validSession.body.requestedInformation.joinToString()
|
||||
Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
|
||||
VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.body.requestedInformation))
|
||||
} else {
|
||||
store.update {
|
||||
it.copy(changeNumberOutcome = null, challengesRequested = emptyList())
|
||||
}
|
||||
val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.body.id, e164 = e164, password = password, mode = mode)
|
||||
Log.d(TAG, "SMS code request submitted")
|
||||
response
|
||||
}
|
||||
|
||||
val challengesRequested = if (result is VerificationCodeRequestResult.ChallengeRequired) {
|
||||
result.challenges
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received result: ${result.javaClass.canonicalName}\nwith challenges: ${challengesRequested.joinToString { it.key }}")
|
||||
|
||||
store.update {
|
||||
it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result), challengesRequested = challengesRequested, inProgress = false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getRegistrationData(context: Context): RegistrationData {
|
||||
val currentState = store.value
|
||||
val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!")
|
||||
val e164: String = number.e164Number ?: throw IllegalStateException("Can't construct registration data without E164!")
|
||||
val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr.getRecoveryPassword() else null
|
||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||
return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Utility Functions
|
||||
|
||||
/**
|
||||
* Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened.
|
||||
*
|
||||
* @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages.
|
||||
*/
|
||||
private fun bail(logMessage: () -> Unit) {
|
||||
logMessage()
|
||||
store.update {
|
||||
it.copy(inProgress = false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything that runs through this will be run serially, with locks.
|
||||
*/
|
||||
private suspend fun <T> withLockOnSerialExecutor(action: () -> T): T = withContext(serialContext) {
|
||||
Log.v(TAG, "withLock()")
|
||||
val result = CHANGE_NUMBER_LOCK.withLock {
|
||||
SignalStore.misc.lockChangeNumber()
|
||||
Log.v(TAG, "Change number lock acquired.")
|
||||
try {
|
||||
action()
|
||||
} finally {
|
||||
SignalStore.misc.unlockChangeNumber()
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "Change number lock released.")
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
enum class ContinueStatus {
|
||||
CAN_CONTINUE,
|
||||
INVALID_NUMBER,
|
||||
OLD_NUMBER_DOESNT_MATCH
|
||||
CAN_CONTINUE, INVALID_NUMBER, OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
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 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.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Screen visible to the user when they are registration locked and have no SVR data.
|
||||
*/
|
||||
class ChangeNumberAccountLockedV2Fragment : LoggingFragment(R.layout.fragment_change_number_account_locked) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberAccountLockedV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
|
||||
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.liveLockedTimeRemaining.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 { onNext() }
|
||||
view.findViewById<View>(R.id.account_locked_learn_more).setOnClickListener { 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)
|
||||
}
|
||||
|
||||
private fun durationToDays(duration: Long): Long {
|
||||
return if (duration != 0L) getLockoutDays(duration).toLong() else 7
|
||||
}
|
||||
|
||||
private fun getLockoutDays(timeRemainingMs: Long): Int {
|
||||
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1
|
||||
}
|
||||
|
||||
fun onNext() {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen visible to the user for them to confirm their new phone number was entered correctly.
|
||||
*/
|
||||
class ChangeNumberConfirmV2Fragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberConfirmV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
val confirmMessage: TextView = view.findViewById(R.id.change_number_confirm_new_number_message)
|
||||
confirmMessage.text = getString(R.string.ChangeNumberConfirmFragment__you_are_about_to_change_your_phone_number_from_s_to_s, viewModel.oldNumberState.fullFormattedNumber, viewModel.number.fullFormattedNumber)
|
||||
|
||||
val newNumber: TextView = view.findViewById(R.id.change_number_confirm_new_number)
|
||||
newNumber.text = viewModel.number.fullFormattedNumber
|
||||
|
||||
val editNumber: View = view.findViewById(R.id.change_number_confirm_edit_number)
|
||||
editNumber.setOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number)
|
||||
changeNumber.setOnClickListener {
|
||||
viewModel.registerSmsListenerWithCompletionListener(requireContext()) {
|
||||
navigateToVerify(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToVerify(smsListenerEnabled: Boolean = false) {
|
||||
findNavController().safeNavigate(
|
||||
R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment,
|
||||
ChangeNumberVerifyV2FragmentArgs.Builder()
|
||||
.setSmsListenerEnabled(smsListenerEnabled)
|
||||
.build()
|
||||
.toBundle()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.FragmentChangeNumberEnterPhoneNumberV2Binding
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs
|
||||
import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController
|
||||
import org.thoughtcrime.securesms.util.Dialogs
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen for the user to enter their old and new phone numbers.
|
||||
*/
|
||||
class ChangeNumberEnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number_v2) {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterPhoneNumberV2Fragment::class.java)
|
||||
|
||||
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
|
||||
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
|
||||
}
|
||||
|
||||
private val binding: FragmentChangeNumberEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentChangeNumberEnterPhoneNumberV2Binding::bind)
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
binding.changeNumberEnterPhoneNumberContinue.setOnClickListener {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
val oldController = ChangeNumberInputController(
|
||||
requireContext(),
|
||||
binding.changeNumberEnterPhoneNumberOldNumberCountryCode,
|
||||
binding.changeNumberEnterPhoneNumberOldNumberNumber,
|
||||
binding.changeNumberEnterPhoneNumberOldNumberSpinner,
|
||||
false,
|
||||
object : ChangeNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberOldNumberNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) {
|
||||
binding.changeNumberEnterPhoneNumberNewNumberCountryCode.requestFocus()
|
||||
}
|
||||
|
||||
override fun onNumberInputDone(view: View) = Unit
|
||||
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
viewModel.setOldNationalNumber(number)
|
||||
}
|
||||
|
||||
override fun setCountry(countryCode: Int) {
|
||||
viewModel.setOldCountry(countryCode)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val newController = ChangeNumberInputController(
|
||||
requireContext(),
|
||||
binding.changeNumberEnterPhoneNumberNewNumberCountryCode,
|
||||
binding.changeNumberEnterPhoneNumberNewNumberNumber,
|
||||
binding.changeNumberEnterPhoneNumberNewNumberSpinner,
|
||||
true,
|
||||
object : ChangeNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberNewNumberNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) = Unit
|
||||
|
||||
override fun onNumberInputDone(view: View) {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(NEW_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
viewModel.setNewNationalNumber(number)
|
||||
}
|
||||
|
||||
override fun setCountry(countryCode: Int) {
|
||||
viewModel.setNewCountry(countryCode)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle ->
|
||||
viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle ->
|
||||
viewModel.setNewCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
viewModel.liveOldNumberState.observe(viewLifecycleOwner, oldController::updateNumber)
|
||||
viewModel.liveNewNumberState.observe(viewLifecycleOwner, newController::updateNumber)
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
when (viewModel.canContinue()) {
|
||||
ChangeNumberV2ViewModel.ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment)
|
||||
ChangeNumberV2ViewModel.ContinueStatus.INVALID_NUMBER -> {
|
||||
Dialogs.showAlertDialog(
|
||||
context,
|
||||
getString(R.string.RegistrationActivity_invalid_number),
|
||||
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.number.e164Number)
|
||||
)
|
||||
}
|
||||
ChangeNumberV2ViewModel.ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberEnterPhoneNumberFragment__the_phone_number_you_entered_doesnt_match_your_accounts)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
/**
|
||||
* A captive activity that can determine if an interrupted/erred change number request
|
||||
* caused a disparity between the server and our locally stored number.
|
||||
*/
|
||||
class ChangeNumberLockV2Activity : PassphraseRequiredActivity() {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberLockV2Activity::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, ChangeNumberLockV2Activity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: ChangeNumberV2ViewModel by viewModels()
|
||||
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
dynamicTheme.onCreate(this)
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
Log.d(TAG, "Back button press swallowed.")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
setContentView(R.layout.activity_change_number_lock)
|
||||
|
||||
checkWhoAmI()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
private fun checkWhoAmI() {
|
||||
viewModel.checkWhoAmI(::onChangeStatusConfirmed, ::onFailedToGetChangeNumberStatus)
|
||||
}
|
||||
|
||||
private fun onChangeStatusConfirmed() {
|
||||
SignalStore.misc.clearPendingChangeNumberMetadata()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
||||
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(SignalStore.account.e164!!)))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
finish()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onFailedToGetChangeNumberStatus(error: Throwable) {
|
||||
Log.w(TAG, "Unable to determine status of change number", error)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_unconfirmed)
|
||||
.setMessage(getString(R.string.ChangeNumberLockActivity__we_could_not_determine_the_status_of_your_change_number_request, error.javaClass.simpleName))
|
||||
.setPositiveButton(R.string.ChangeNumberLockActivity__retry) { _, _ -> checkWhoAmI() }
|
||||
.setNegativeButton(R.string.ChangeNumberLockActivity__leave) { _, _ -> finish() }
|
||||
.setNeutralButton(R.string.ChangeNumberLockActivity__submit_debug_log) { _, _ ->
|
||||
startActivity(Intent(this, SubmitDebugLogActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
|
||||
/**
|
||||
* A screen to educate the user if their PIN differs from old number to new number.
|
||||
*/
|
||||
class ChangeNumberPinDiffersV2Fragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberPinDiffersV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val confirmCancelDialog = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
view.findViewById<View>(R.id.change_number_pin_differs_keep_old_pin).setOnClickListener {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
|
||||
val changePin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == CreateSvrPinActivity.RESULT_OK) {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.change_number_pin_differs_update_pin).setOnClickListener {
|
||||
changePin.launch(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext()))
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, confirmCancelDialog)
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.registrationlock.RegistrationLockV2Fragment
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.registrationlock.RegistrationLockV2FragmentArgs
|
||||
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 org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Screen presented to the user if the new account is registration locked, and allows them to enter their PIN.
|
||||
*/
|
||||
class ChangeNumberRegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_change_number_registration_lock) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationLockV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind)
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
|
||||
private var timeRemaining: Long = 0
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val args: RegistrationLockV2FragmentArgs = RegistrationLockV2FragmentArgs.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 {
|
||||
val keyboardType: PinKeyboardType = getPinEntryKeyboardType()
|
||||
updateKeyboard(keyboardType.other)
|
||||
binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource)
|
||||
}
|
||||
|
||||
val keyboardType: PinKeyboardType = getPinEntryKeyboardType().getOther()
|
||||
binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource)
|
||||
|
||||
viewModel.liveLockedTimeRemaining.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, ::onStateUpdate)
|
||||
}
|
||||
|
||||
private fun onStateUpdate(state: ChangeNumberState) {
|
||||
if (state.changeNumberOutcome == ChangeNumberOutcome.VerificationCodeWorked) {
|
||||
handleSuccessfulPinEntry(state.enteredPin)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
viewModel.setEnteredPin(pin)
|
||||
|
||||
binding.kbsLockPinConfirm.setSpinning()
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleChangeNumberErrorResponse)
|
||||
}
|
||||
|
||||
private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) {
|
||||
when (requestResult) {
|
||||
is VerificationCodeRequestResult.Success -> Unit
|
||||
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
|
||||
is VerificationCodeRequestResult.AttemptsExhausted,
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> {
|
||||
navigateToAccountLocked()
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.AlreadyVerified,
|
||||
is VerificationCodeRequestResult.ChallengeRequired,
|
||||
is VerificationCodeRequestResult.ExternalServiceFailure,
|
||||
is VerificationCodeRequestResult.ImpossibleNumber,
|
||||
is VerificationCodeRequestResult.InvalidTransportModeFailure,
|
||||
is VerificationCodeRequestResult.MalformedRequest,
|
||||
is VerificationCodeRequestResult.MustRetry,
|
||||
is VerificationCodeRequestResult.NoSuchSession,
|
||||
is VerificationCodeRequestResult.NonNormalizedNumber,
|
||||
is VerificationCodeRequestResult.TokenNotAccepted,
|
||||
is VerificationCodeRequestResult.UnknownError -> {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause())
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeNumberErrorResponse(result: ChangeNumberResult) {
|
||||
when (result) {
|
||||
is ChangeNumberResult.Success -> Unit
|
||||
is ChangeNumberResult.RateLimited -> onRateLimited()
|
||||
is ChangeNumberResult.AttemptsExhausted -> navigateToAccountLocked()
|
||||
|
||||
is ChangeNumberResult.SvrWrongPin -> {
|
||||
Log.i(TAG, "SVR returned a WrongPinException.")
|
||||
onIncorrectKbsRegistrationLockPin(result.triesRemaining)
|
||||
}
|
||||
|
||||
is ChangeNumberResult.SvrNoData -> {
|
||||
Log.i(TAG, "SVR returned a NoDataException.")
|
||||
navigateToAccountLocked()
|
||||
}
|
||||
|
||||
is ChangeNumberResult.AuthorizationFailed,
|
||||
is ChangeNumberResult.IncorrectRecoveryPassword,
|
||||
is ChangeNumberResult.MalformedRequest,
|
||||
is ChangeNumberResult.RegistrationLocked,
|
||||
is ChangeNumberResult.UnknownError,
|
||||
is ChangeNumberResult.ValidationError -> {
|
||||
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.")
|
||||
navigateToAccountLocked()
|
||||
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) { _, which -> 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 getPinEntryKeyboardType(): PinKeyboardType {
|
||||
val isNumeric = (binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER
|
||||
|
||||
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
|
||||
}
|
||||
|
||||
private fun updateKeyboard(keyboard: PinKeyboardType) {
|
||||
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
|
||||
|
||||
binding.kbsLockPinInput.setInputType(
|
||||
if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
)
|
||||
|
||||
binding.kbsLockPinInput.getText().clear()
|
||||
}
|
||||
|
||||
private fun navigateToAccountLocked() {
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockV2FragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
|
||||
}
|
||||
|
||||
private fun handleSuccessfulPinEntry(pin: String) {
|
||||
val pinsDiffer: Boolean = SignalStore.svr.localPinHash?.let { !PinHashUtil.verifyLocalPinHash(it, pin) } ?: false
|
||||
|
||||
binding.kbsLockPinConfirm.cancelSpinning()
|
||||
|
||||
if (pinsDiffer) {
|
||||
findNavController().safeNavigate(ChangeNumberRegistrationLockV2FragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
} else {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEmailToSupport() {
|
||||
val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin
|
||||
|
||||
val body: String = SupportEmailUtil.generateSupportEmailBody(requireContext(), subject, null, null)
|
||||
|
||||
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), getString(subject), body)
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc.isChangeNumberLocked) {
|
||||
startActivity(ChangeNumberLockV2Activity.createIntent(requireContext()))
|
||||
} else {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.FragmentChangePhoneNumberV2Binding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen used to educate the user about what they're about to do (change their phone number)
|
||||
*/
|
||||
class ChangeNumberV2Fragment : LoggingFragment(R.layout.fragment_change_phone_number_v2) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val binding: FragmentChangePhoneNumberV2Binding by ViewBinderDelegate(FragmentChangePhoneNumberV2Binding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
binding.changePhoneNumberContinue.setOnClickListener {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignalProtocolStore
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Repository to perform data operations during change number.
|
||||
*
|
||||
* @see [org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository]
|
||||
*/
|
||||
class ChangeNumberV2Repository(
|
||||
private val accountManager: SignalServiceAccountManager = AppDependencies.signalServiceAccountManager,
|
||||
private val messageSender: SignalServiceMessageSender = AppDependencies.signalServiceMessageSender
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberV2Repository::class.java)
|
||||
}
|
||||
|
||||
fun whoAmI(): WhoAmIResponse {
|
||||
return accountManager.whoAmI
|
||||
}
|
||||
|
||||
suspend fun ensureDecryptionsDrained(timeout: Duration = 15.seconds) =
|
||||
withTimeoutOrNull(timeout) {
|
||||
suspendCancellableCoroutine {
|
||||
val drainedListener = object : Runnable {
|
||||
override fun run() {
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.removeDecryptionDrainedListener(this)
|
||||
Log.d(TAG, "Decryptions drained.")
|
||||
it.resume(true)
|
||||
}
|
||||
}
|
||||
|
||||
it.invokeOnCancellation { cancellationCause ->
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.removeDecryptionDrainedListener(drainedListener)
|
||||
Log.d(TAG, "Decryptions draining canceled.", cancellationCause)
|
||||
}
|
||||
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.addDecryptionDrainedListener(drainedListener)
|
||||
Log.d(TAG, "Waiting for decryption drain.")
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String, pni: ServiceId.PNI) {
|
||||
val oldStorageId: ByteArray? = Recipient.self().storageId
|
||||
SignalDatabase.recipients.updateSelfE164(e164, pni)
|
||||
val newStorageId: ByteArray? = Recipient.self().storageId
|
||||
|
||||
if (e164 != SignalStore.account.requireE164() && MessageDigest.isEqual(oldStorageId, newStorageId)) {
|
||||
Log.w(TAG, "Self storage id was not rotated, attempting to rotate again")
|
||||
SignalDatabase.recipients.rotateStorageId(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
val secondAttemptStorageId: ByteArray? = Recipient.self().storageId
|
||||
if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) {
|
||||
Log.w(TAG, "Second attempt also failed to rotate storage id")
|
||||
}
|
||||
}
|
||||
|
||||
AppDependencies.recipientCache.clear()
|
||||
|
||||
SignalStore.account.setE164(e164)
|
||||
SignalStore.account.setPni(pni)
|
||||
AppDependencies.resetProtocolStores()
|
||||
|
||||
AppDependencies.groupsV2Authorization.clear()
|
||||
|
||||
val metadata: PendingChangeNumberMetadata? = SignalStore.misc.pendingChangeNumberMetadata
|
||||
if (metadata == null) {
|
||||
Log.w(TAG, "No change number metadata, this shouldn't happen")
|
||||
throw AssertionError("No change number metadata")
|
||||
}
|
||||
|
||||
val originalPni = ServiceId.PNI.parseOrThrow(metadata.previousPni)
|
||||
|
||||
if (originalPni == pni) {
|
||||
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
|
||||
} else {
|
||||
val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray())
|
||||
val pniRegistrationId = metadata.pniRegistrationId
|
||||
val pniSignedPreyKeyId = metadata.pniSignedPreKeyId
|
||||
val pniLastResortKyberPreKeyId = metadata.pniLastResortKyberPreKeyId
|
||||
|
||||
val pniProtocolStore = AppDependencies.protocolStore.pni()
|
||||
val pniMetadataStore = SignalStore.account.pniPreKeys
|
||||
|
||||
SignalStore.account.pniRegistrationId = pniRegistrationId
|
||||
SignalStore.account.setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair)
|
||||
|
||||
val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId)
|
||||
val oneTimeEcPreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(pniProtocolStore, pniMetadataStore)
|
||||
val lastResortKyberPreKey = pniProtocolStore.loadLastResortKyberPreKeys().firstOrNull { it.id == pniLastResortKyberPreKeyId }
|
||||
val oneTimeKyberPreKeys = PreKeyUtil.generateAndStoreOneTimeKyberPreKeys(pniProtocolStore, pniMetadataStore)
|
||||
|
||||
if (lastResortKyberPreKey == null) {
|
||||
Log.w(TAG, "Last-resort kyber prekey is missing!")
|
||||
}
|
||||
|
||||
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
|
||||
Log.i(TAG, "Submitting prekeys with PNI identity key: ${pniIdentityKeyPair.publicKey.fingerprint}")
|
||||
|
||||
accountManager.setPreKeys(
|
||||
PreKeyUpload(
|
||||
serviceIdType = ServiceIdType.PNI,
|
||||
signedPreKey = signedPreKey,
|
||||
oneTimeEcPreKeys = oneTimeEcPreKeys,
|
||||
lastResortKyberPreKey = lastResortKyberPreKey,
|
||||
oneTimeKyberPreKeys = oneTimeKyberPreKeys
|
||||
)
|
||||
)
|
||||
pniMetadataStore.isSignedPreKeyRegistered = true
|
||||
pniMetadataStore.lastResortKyberPreKeyId = pniLastResortKyberPreKeyId
|
||||
|
||||
pniProtocolStore.identities().saveIdentityWithoutSideEffects(
|
||||
Recipient.self().id,
|
||||
pni,
|
||||
pniProtocolStore.identityKeyPair.publicKey,
|
||||
IdentityTable.VerifiedStatus.VERIFIED,
|
||||
true,
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
|
||||
SignalStore.misc.hasPniInitializedDevices = true
|
||||
AppDependencies.groupsV2Authorization.clear()
|
||||
}
|
||||
|
||||
Recipient.self().live().refresh()
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
|
||||
AppDependencies.resetNetwork()
|
||||
AppDependencies.incomingMessageObserver
|
||||
|
||||
AppDependencies.jobManager.add(RefreshAttributesJob())
|
||||
|
||||
rotateCertificates()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun rotateCertificates() {
|
||||
val certificateTypes = SignalStore.phoneNumberPrivacy.allCertificateTypes
|
||||
|
||||
Log.i(TAG, "Rotating these certificates $certificateTypes")
|
||||
|
||||
for (certificateType in certificateTypes) {
|
||||
val certificate: ByteArray? = when (certificateType) {
|
||||
CertificateType.ACI_AND_E164 -> accountManager.senderCertificate
|
||||
CertificateType.ACI_ONLY -> accountManager.senderCertificateForPhoneNumberPrivacy
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully got $certificateType certificate")
|
||||
|
||||
SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changeNumberWithRecoveryPassword(recoveryPassword: String, newE164: String): ChangeNumberResult {
|
||||
return changeNumberInternal(recoveryPassword = recoveryPassword, newE164 = newE164)
|
||||
}
|
||||
|
||||
suspend fun changeNumberWithoutRegistrationLock(sessionId: String, newE164: String): ChangeNumberResult {
|
||||
return changeNumberInternal(sessionId = sessionId, newE164 = newE164)
|
||||
}
|
||||
|
||||
suspend fun changeNumberWithRegistrationLock(
|
||||
sessionId: String,
|
||||
newE164: String,
|
||||
pin: String,
|
||||
svrAuthCredentials: SvrAuthCredentialSet
|
||||
): ChangeNumberResult {
|
||||
val masterKey: MasterKey
|
||||
|
||||
try {
|
||||
masterKey = SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin)
|
||||
} catch (e: SvrWrongPinException) {
|
||||
return ChangeNumberResult.SvrWrongPin(e)
|
||||
} catch (e: SvrNoDataException) {
|
||||
return ChangeNumberResult.SvrNoData(e)
|
||||
} catch (e: IOException) {
|
||||
return ChangeNumberResult.UnknownError(e)
|
||||
}
|
||||
|
||||
val registrationLock = masterKey.deriveRegistrationLock()
|
||||
return changeNumberInternal(sessionId = sessionId, registrationLock = registrationLock, newE164 = newE164)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the service to change the phone number associated with this account.
|
||||
*/
|
||||
private suspend fun changeNumberInternal(sessionId: String? = null, recoveryPassword: String? = null, registrationLock: String? = null, newE164: String): ChangeNumberResult {
|
||||
check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null))
|
||||
var completed = false
|
||||
var attempts = 0
|
||||
lateinit var result: NetworkResult<VerifyAccountResponse>
|
||||
|
||||
while (!completed && attempts < 5) {
|
||||
Log.i(TAG, "Attempt #$attempts")
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
|
||||
sessionId = sessionId,
|
||||
recoveryPassword = recoveryPassword,
|
||||
newE164 = newE164,
|
||||
registrationLock = registrationLock
|
||||
)
|
||||
|
||||
SignalStore.misc.setPendingChangeNumberMetadata(metadata)
|
||||
withContext(Dispatchers.IO) {
|
||||
result = accountManager.registrationApi.changeNumber(request)
|
||||
}
|
||||
|
||||
val possibleError = result.getCause() as? MismatchedDevicesException
|
||||
if (possibleError != null) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Returning change number network result.")
|
||||
return ChangeNumberResult.from(
|
||||
result.map { accountRegistrationResponse: VerifyAccountResponse ->
|
||||
NumberChangeResult(
|
||||
uuid = accountRegistrationResponse.uuid,
|
||||
pni = accountRegistrationResponse.pni,
|
||||
storageCapable = accountRegistrationResponse.storageCapable,
|
||||
number = accountRegistrationResponse.number
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun createChangeNumberRequest(
|
||||
sessionId: String? = null,
|
||||
recoveryPassword: String? = null,
|
||||
newE164: String,
|
||||
registrationLock: String? = null
|
||||
): ChangeNumberRequestData {
|
||||
val selfIdentifier: String = SignalStore.account.requireAci().toString()
|
||||
val aciProtocolStore: SignalProtocolStore = AppDependencies.protocolStore.aci()
|
||||
|
||||
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
val deviceMessages = mutableListOf<OutgoingPushMessage>()
|
||||
val devicePniSignedPreKeys = mutableMapOf<Int, SignedPreKeyEntity>()
|
||||
val devicePniLastResortKyberPreKeys = mutableMapOf<Int, KyberPreKeyEntity>()
|
||||
val pniRegistrationIds = mutableMapOf<Int, Int>()
|
||||
val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID
|
||||
|
||||
val devices: List<Int> = listOf(primaryDeviceId) + aciProtocolStore.getSubDeviceSessions(selfIdentifier)
|
||||
|
||||
devices
|
||||
.filter { it == primaryDeviceId || aciProtocolStore.containsSession(SignalProtocolAddress(selfIdentifier, it)) }
|
||||
.forEach { deviceId ->
|
||||
// Signed Prekeys
|
||||
val signedPreKeyRecord: SignedPreKeyRecord = if (deviceId == primaryDeviceId) {
|
||||
PreKeyUtil.generateAndStoreSignedPreKey(AppDependencies.protocolStore.pni(), SignalStore.account.pniPreKeys, pniIdentity.privateKey)
|
||||
} else {
|
||||
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
|
||||
}
|
||||
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
|
||||
// Last-resort kyber prekeys
|
||||
val lastResortKyberPreKeyRecord: KyberPreKeyRecord = if (deviceId == primaryDeviceId) {
|
||||
PreKeyUtil.generateAndStoreLastResortKyberPreKey(AppDependencies.protocolStore.pni(), SignalStore.account.pniPreKeys, pniIdentity.privateKey)
|
||||
} else {
|
||||
PreKeyUtil.generateLastResortKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
|
||||
}
|
||||
devicePniLastResortKyberPreKeys[deviceId] = KyberPreKeyEntity(lastResortKyberPreKeyRecord.id, lastResortKyberPreKeyRecord.keyPair.publicKey, lastResortKyberPreKeyRecord.signature)
|
||||
|
||||
// Registration Ids
|
||||
var pniRegistrationId = -1
|
||||
|
||||
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
}
|
||||
pniRegistrationIds[deviceId] = pniRegistrationId
|
||||
|
||||
// Device Messages
|
||||
if (deviceId != primaryDeviceId) {
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber(
|
||||
identityKeyPair = pniIdentity.serialize().toByteString(),
|
||||
signedPreKey = signedPreKeyRecord.serialize().toByteString(),
|
||||
lastResortKyberPreKey = lastResortKyberPreKeyRecord.serialize().toByteString(),
|
||||
registrationId = pniRegistrationId,
|
||||
newE164 = newE164
|
||||
)
|
||||
|
||||
deviceMessages += messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId, pniChangeNumber)
|
||||
}
|
||||
}
|
||||
|
||||
val request = ChangePhoneNumberRequest(
|
||||
sessionId,
|
||||
recoveryPassword,
|
||||
newE164,
|
||||
registrationLock,
|
||||
pniIdentity.publicKey,
|
||||
deviceMessages,
|
||||
devicePniSignedPreKeys.mapKeys { it.key.toString() },
|
||||
devicePniLastResortKyberPreKeys.mapKeys { it.key.toString() },
|
||||
pniRegistrationIds.mapKeys { it.key.toString() }
|
||||
)
|
||||
|
||||
val metadata = PendingChangeNumberMetadata(
|
||||
previousPni = SignalStore.account.pni!!.toByteString(),
|
||||
pniIdentityKeyPair = pniIdentity.serialize().toByteString(),
|
||||
pniRegistrationId = pniRegistrationIds[primaryDeviceId]!!,
|
||||
pniSignedPreKeyId = devicePniSignedPreKeys[primaryDeviceId]!!.keyId,
|
||||
pniLastResortKyberPreKeyId = devicePniLastResortKyberPreKeys[primaryDeviceId]!!.keyId
|
||||
)
|
||||
|
||||
return ChangeNumberRequestData(request, metadata)
|
||||
}
|
||||
|
||||
private data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
|
||||
|
||||
data class NumberChangeResult(
|
||||
val uuid: String,
|
||||
val pni: String,
|
||||
val storageCapable: Boolean,
|
||||
val number: String
|
||||
)
|
||||
}
|
||||
@@ -1,553 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/**
|
||||
* [ViewModel] for the change number flow.
|
||||
*
|
||||
* @see [RegistrationV2ViewModel], from which this is derived.
|
||||
*/
|
||||
class ChangeNumberV2ViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChangeNumberV2ViewModel::class.java)
|
||||
|
||||
val CHANGE_NUMBER_LOCK = ReentrantLock()
|
||||
}
|
||||
|
||||
private val repository = ChangeNumberV2Repository()
|
||||
private val store = MutableStateFlow(ChangeNumberState())
|
||||
private val serialContext = SignalExecutors.SERIAL.asCoroutineDispatcher()
|
||||
private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(AppDependencies.application)
|
||||
|
||||
private val initialLocalNumber = SignalStore.account.e164
|
||||
private val password = SignalStore.account.servicePassword!!
|
||||
|
||||
val uiState = store.asLiveData()
|
||||
val liveOldNumberState = store.map { it.oldPhoneNumber }.asLiveData()
|
||||
val liveNewNumberState = store.map { it.number }.asLiveData()
|
||||
val liveLockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData()
|
||||
val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData()
|
||||
|
||||
init {
|
||||
try {
|
||||
val countryCode: Int = PhoneNumberUtil.getInstance()
|
||||
.parse(SignalStore.account.e164!!, null)
|
||||
.countryCode
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
number = it.number.toBuilder().countryCode(countryCode).build(),
|
||||
oldPhoneNumber = it.oldPhoneNumber.toBuilder().countryCode(countryCode).build()
|
||||
)
|
||||
}
|
||||
} catch (e: NumberParseException) {
|
||||
Log.i(TAG, "Unable to parse number for default country code")
|
||||
}
|
||||
|
||||
smsRetrieverReceiver.registerReceiver()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
smsRetrieverReceiver.unregisterReceiver()
|
||||
}
|
||||
|
||||
// region Public Getters and Setters
|
||||
|
||||
val number: NumberViewState
|
||||
get() = store.value.number
|
||||
|
||||
val oldNumberState: NumberViewState
|
||||
get() = store.value.oldPhoneNumber
|
||||
|
||||
val svrTriesRemaining: Int
|
||||
get() = store.value.svrTriesRemaining
|
||||
|
||||
fun setOldNationalNumber(updatedNumber: String) {
|
||||
store.update {
|
||||
it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setOldCountry(countryCode: Int, country: String? = null) {
|
||||
store.update {
|
||||
it.copy(oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setNewNationalNumber(updatedNumber: String) {
|
||||
store.update {
|
||||
it.copy(number = number.toBuilder().nationalNumber(updatedNumber).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setNewCountry(countryCode: Int, country: String? = null) {
|
||||
store.update {
|
||||
it.copy(number = number.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setCaptchaResponse(token: String) {
|
||||
Log.v(TAG, "setCaptchaResponse()")
|
||||
store.update {
|
||||
it.copy(captchaToken = token)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnteredPin(pin: String) {
|
||||
store.update {
|
||||
it.copy(enteredPin = pin)
|
||||
}
|
||||
}
|
||||
|
||||
fun incrementIncorrectCodeAttempts() {
|
||||
store.update {
|
||||
it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPresentedChallenge(challenge: Challenge) {
|
||||
Log.v(TAG, "addPresentedChallenge()")
|
||||
store.update {
|
||||
it.copy(challengesPresented = it.challengesPresented.plus(challenge))
|
||||
}
|
||||
}
|
||||
|
||||
fun removePresentedChallenge(challenge: Challenge) {
|
||||
Log.v(TAG, "addPresentedChallenge()")
|
||||
store.update {
|
||||
it.copy(challengesPresented = it.challengesPresented.minus(challenge))
|
||||
}
|
||||
}
|
||||
|
||||
fun resetLocalSessionState() {
|
||||
Log.v(TAG, "resetLocalSessionState()")
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = null, captchaToken = null, challengesRequested = emptyList(), allowedToRequestCode = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun canContinue(): ContinueStatus {
|
||||
return if (oldNumberState.e164Number == initialLocalNumber) {
|
||||
if (number.isValid) {
|
||||
ContinueStatus.CAN_CONTINUE
|
||||
} else {
|
||||
ContinueStatus.INVALID_NUMBER
|
||||
}
|
||||
} else {
|
||||
ContinueStatus.OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Public actions
|
||||
|
||||
fun checkWhoAmI(onSuccess: () -> Unit, onError: (Throwable) -> Unit) {
|
||||
Log.v(TAG, "checkWhoAmI()")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val whoAmI = repository.whoAmI()
|
||||
|
||||
if (whoAmI.number == SignalStore.account.e164) {
|
||||
return@launch bail { Log.i(TAG, "Local and remote numbers match, nothing needs to be done.") }
|
||||
}
|
||||
|
||||
Log.i(TAG, "Local (${SignalStore.account.e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
|
||||
withLockOnSerialExecutor {
|
||||
repository.changeLocalNumber(whoAmI.number, ServiceId.PNI.parseOrThrow(whoAmI.pni))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onSuccess()
|
||||
}
|
||||
} catch (ioException: IOException) {
|
||||
Log.w(TAG, "Encountered an exception when requesting whoAmI()", ioException)
|
||||
withContext(Dispatchers.Main) {
|
||||
onError(ioException)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerSmsListenerWithCompletionListener(context: Context, onComplete: (Boolean) -> Unit) {
|
||||
Log.v(TAG, "registerSmsListenerWithCompletionListener()")
|
||||
viewModelScope.launch {
|
||||
val listenerRegistered = RegistrationRepository.registerSmsListener(context)
|
||||
onComplete(listenerRegistered)
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyCodeWithoutRegistrationLock(context: Context, code: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
Log.v(TAG, "verifyCodeWithoutRegistrationLock()")
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = true,
|
||||
enteredCode = code
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
verifyCodeInternal(context = context, pin = null, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()")
|
||||
store.update { it.copy(inProgress = true) }
|
||||
|
||||
viewModelScope.launch {
|
||||
verifyCodeInternal(context = context, pin = pin, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
val sessionId = getOrCreateValidSession(context)?.body?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") }
|
||||
val registrationData = getRegistrationData(context)
|
||||
|
||||
val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
|
||||
|
||||
if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) {
|
||||
handleVerificationError(verificationResponse, verificationErrorHandler)
|
||||
return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") }
|
||||
}
|
||||
|
||||
val result: ChangeNumberResult = if (pin == null) {
|
||||
repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number)
|
||||
} else {
|
||||
repository.changeNumberWithRegistrationLock(
|
||||
sessionId = sessionId,
|
||||
newE164 = number.e164Number,
|
||||
pin = pin,
|
||||
svrAuthCredentials = SvrAuthCredentialSet(
|
||||
svr2Credentials = store.value.svr2Credentials,
|
||||
svr3Credentials = store.value.svr3Credentials
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (result is ChangeNumberResult.Success) {
|
||||
handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked)
|
||||
} else {
|
||||
handleChangeNumberError(result, numberChangeErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
fun submitCaptchaToken(context: Context) {
|
||||
Log.v(TAG, "submitCaptchaToken()")
|
||||
val e164 = number.e164Number
|
||||
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
|
||||
store.update {
|
||||
it.copy(
|
||||
captchaToken = null,
|
||||
inProgress = true,
|
||||
changeNumberOutcome = null
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Getting session in order to submit captcha token…")
|
||||
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") }
|
||||
if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.CAPTCHA)) {
|
||||
Log.d(TAG, "Captcha submission no longer necessary, bailing.")
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = false,
|
||||
changeNumberOutcome = null
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
Log.d(TAG, "Submitting captcha token…")
|
||||
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken)
|
||||
Log.d(TAG, "Captcha token submitted.")
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAndSubmitPushToken(context: Context) {
|
||||
Log.v(TAG, "validatePushToken()")
|
||||
|
||||
addPresentedChallenge(Challenge.PUSH)
|
||||
|
||||
val e164 = number.e164Number
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Getting session in order to perform push token verification…")
|
||||
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") }
|
||||
|
||||
if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) {
|
||||
Log.d(TAG, "Push submission no longer necessary, bailing.")
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = false,
|
||||
changeNumberOutcome = null
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.d(TAG, "Requesting push challenge token…")
|
||||
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password)
|
||||
Log.d(TAG, "Push challenge token submitted.")
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initiateChangeNumberSession(context: Context, mode: RegistrationRepository.Mode) {
|
||||
Log.v(TAG, "changeNumber()")
|
||||
store.update { it.copy(inProgress = true) }
|
||||
viewModelScope.launch {
|
||||
val encryptionDrained = repository.ensureDecryptionsDrained() ?: false
|
||||
|
||||
if (!encryptionDrained) {
|
||||
return@launch bail { Log.i(TAG, "Failed to drain encryption.") }
|
||||
}
|
||||
|
||||
val changed = changeNumberWithRecoveryPassword()
|
||||
|
||||
if (changed) {
|
||||
Log.d(TAG, "Successfully changed number using recovery password, which cleaned up after itself.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
requestVerificationCode(context, mode)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Private actions
|
||||
|
||||
private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) {
|
||||
Log.v(TAG, "updateLocalStateFromSession()")
|
||||
store.update {
|
||||
it.copy(sessionId = response.body.id, challengesRequested = Challenge.parse(response.body.requestedInformation), allowedToRequestCode = response.body.allowedToRequestCode)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? {
|
||||
Log.v(TAG, "getOrCreateValidSession()")
|
||||
val e164 = number.e164Number
|
||||
val mccMncProducer = MccMncProducer(context)
|
||||
val existingSessionId = store.value.sessionId
|
||||
return RegistrationV2ViewModel.getOrCreateValidSession(context = context, existingSessionId = existingSessionId, e164 = e164, password = password, mcc = mccMncProducer.mcc, mnc = mccMncProducer.mnc, successListener = { freshMetadata ->
|
||||
Log.v(TAG, "Valid session received, updating local state.")
|
||||
updateLocalStateFromSession(freshMetadata)
|
||||
}, errorHandler = { result ->
|
||||
val requestCode: VerificationCodeRequestResult = when (result) {
|
||||
is RegistrationSessionCreationResult.RateLimited -> VerificationCodeRequestResult.RateLimited(result.getCause(), result.timeRemaining)
|
||||
is RegistrationSessionCreationResult.MalformedRequest -> VerificationCodeRequestResult.MalformedRequest(result.getCause())
|
||||
else -> VerificationCodeRequestResult.UnknownError(result.getCause())
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(requestCode))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private suspend fun changeNumberWithRecoveryPassword(): Boolean {
|
||||
Log.v(TAG, "changeNumberWithRecoveryPassword()")
|
||||
SignalStore.svr.recoveryPassword?.let { recoveryPassword ->
|
||||
if (SignalStore.svr.hasPin()) {
|
||||
val result = repository.changeNumberWithRecoveryPassword(recoveryPassword = recoveryPassword, newE164 = number.e164Number)
|
||||
|
||||
if (result is ChangeNumberResult.Success) {
|
||||
handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked)
|
||||
return true
|
||||
}
|
||||
|
||||
Log.d(TAG, "Encountered error while trying to change number with recovery password.", result.getCause())
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun handleSuccessfulChangedRemoteNumber(e164: String, pni: ServiceId.PNI, changeNumberOutcome: ChangeNumberOutcome) {
|
||||
var result = changeNumberOutcome
|
||||
Log.v(TAG, "handleSuccessfulChangedRemoteNumber(${result.javaClass.simpleName}")
|
||||
try {
|
||||
withLockOnSerialExecutor {
|
||||
repository.changeLocalNumber(e164, pni)
|
||||
}
|
||||
} catch (ioException: IOException) {
|
||||
Log.w(TAG, "Failed to change local number!", ioException)
|
||||
result = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(ioException))
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(inProgress = false, changeNumberOutcome = result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerificationError(result: VerificationCodeRequestResult, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit) {
|
||||
Log.v(TAG, "handleVerificationError(${result.javaClass.simpleName}")
|
||||
when (result) {
|
||||
is VerificationCodeRequestResult.Success -> Unit
|
||||
is VerificationCodeRequestResult.RegistrationLocked ->
|
||||
store.update {
|
||||
it.copy(
|
||||
svr2Credentials = result.svr2Credentials,
|
||||
svr3Credentials = result.svr3Credentials
|
||||
)
|
||||
}
|
||||
else -> Log.i(TAG, "Received exception during verification.", result.getCause())
|
||||
}
|
||||
|
||||
verificationErrorHandler(result)
|
||||
}
|
||||
|
||||
private fun handleChangeNumberError(result: ChangeNumberResult, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
|
||||
Log.v(TAG, "handleChangeNumberError(${result.javaClass.simpleName}")
|
||||
when (result) {
|
||||
is ChangeNumberResult.Success -> Unit
|
||||
is ChangeNumberResult.RegistrationLocked ->
|
||||
store.update {
|
||||
it.copy(
|
||||
svr2Credentials = result.svr2Credentials,
|
||||
svr3Credentials = result.svr3Credentials
|
||||
)
|
||||
}
|
||||
is ChangeNumberResult.SvrWrongPin -> {
|
||||
store.update {
|
||||
it.copy(
|
||||
svrTriesRemaining = result.triesRemaining
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Log.i(TAG, "Received exception during change number.", result.getCause())
|
||||
}
|
||||
|
||||
numberChangeErrorHandler(result)
|
||||
}
|
||||
|
||||
private suspend fun requestVerificationCode(context: Context, mode: RegistrationRepository.Mode) {
|
||||
Log.v(TAG, "requestVerificationCode()")
|
||||
val e164 = number.e164Number
|
||||
|
||||
val validSession = getOrCreateValidSession(context)
|
||||
|
||||
if (validSession == null) {
|
||||
Log.w(TAG, "Bailing on requesting verification code because could not create a session!")
|
||||
resetLocalSessionState()
|
||||
return
|
||||
}
|
||||
|
||||
val result = if (!validSession.body.allowedToRequestCode) {
|
||||
val challenges = validSession.body.requestedInformation.joinToString()
|
||||
Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
|
||||
VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.body.requestedInformation))
|
||||
} else {
|
||||
store.update {
|
||||
it.copy(changeNumberOutcome = null, challengesRequested = emptyList())
|
||||
}
|
||||
val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.body.id, e164 = e164, password = password, mode = mode)
|
||||
Log.d(TAG, "SMS code request submitted")
|
||||
response
|
||||
}
|
||||
|
||||
val challengesRequested = if (result is VerificationCodeRequestResult.ChallengeRequired) {
|
||||
result.challenges
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received result: ${result.javaClass.canonicalName}\nwith challenges: ${challengesRequested.joinToString { it.key }}")
|
||||
|
||||
store.update {
|
||||
it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result), challengesRequested = challengesRequested, inProgress = false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getRegistrationData(context: Context): RegistrationData {
|
||||
val currentState = store.value
|
||||
val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!")
|
||||
val e164: String = number.e164Number ?: throw IllegalStateException("Can't construct registration data without E164!")
|
||||
val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr.getRecoveryPassword() else null
|
||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||
return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Utility Functions
|
||||
|
||||
/**
|
||||
* Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened.
|
||||
*
|
||||
* @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages.
|
||||
*/
|
||||
private fun bail(logMessage: () -> Unit) {
|
||||
logMessage()
|
||||
store.update {
|
||||
it.copy(inProgress = false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything that runs through this will be run serially, with locks.
|
||||
*/
|
||||
private suspend fun <T> withLockOnSerialExecutor(action: () -> T): T = withContext(serialContext) {
|
||||
Log.v(TAG, "withLock()")
|
||||
val result = CHANGE_NUMBER_LOCK.withLock {
|
||||
SignalStore.misc.lockChangeNumber()
|
||||
Log.v(TAG, "Change number lock acquired.")
|
||||
try {
|
||||
action()
|
||||
} finally {
|
||||
SignalStore.misc.unlockChangeNumber()
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "Change number lock released.")
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
enum class ContinueStatus {
|
||||
CAN_CONTINUE, INVALID_NUMBER, OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberVerifyFragmentArgs
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen to show while the change number is in-progress.
|
||||
*/
|
||||
class ChangeNumberVerifyV2Fragment : LoggingFragment(R.layout.fragment_change_phone_number_verify) {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
private var dialogVisible: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberVerifyFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener {
|
||||
findNavController().navigateUp()
|
||||
viewModel.resetLocalSessionState()
|
||||
}
|
||||
|
||||
val status: TextView = view.findViewById(R.id.change_phone_number_verify_status)
|
||||
status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber)
|
||||
|
||||
viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate)
|
||||
|
||||
requestCode()
|
||||
}
|
||||
|
||||
private fun onStateUpdate(state: ChangeNumberState) {
|
||||
if (state.challengesRequested.contains(Challenge.CAPTCHA) && state.captchaToken.isNotNullOrBlank()) {
|
||||
viewModel.submitCaptchaToken(requireContext())
|
||||
} else if (state.challengesRemaining.isNotEmpty()) {
|
||||
handleChallenges(state.challengesRemaining)
|
||||
} else if (state.changeNumberOutcome != null) {
|
||||
handleRequestCodeResult(state.changeNumberOutcome)
|
||||
} else if (!state.inProgress) {
|
||||
Log.d(TAG, "Not in progress, navigating up.")
|
||||
if (state.allowedToRequestCode) {
|
||||
requestCode()
|
||||
} else if (!dialogVisible) {
|
||||
showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
val mode = if (ChangeNumberVerifyFragmentArgs.fromBundle(requireArguments()).smsListenerEnabled) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER
|
||||
viewModel.initiateChangeNumberSession(requireContext(), mode)
|
||||
}
|
||||
|
||||
private fun handleRequestCodeResult(changeNumberOutcome: ChangeNumberOutcome) {
|
||||
Log.d(TAG, "Handling request code result: ${changeNumberOutcome.javaClass.name}")
|
||||
when (changeNumberOutcome) {
|
||||
is ChangeNumberOutcome.RecoveryPasswordWorked -> {
|
||||
Log.i(TAG, "Successfully changed number with recovery password.")
|
||||
changeNumberSuccess()
|
||||
}
|
||||
|
||||
is ChangeNumberOutcome.ChangeNumberRequestOutcome -> {
|
||||
when (val castResult = changeNumberOutcome.result) {
|
||||
is VerificationCodeRequestResult.Success -> {
|
||||
Log.i(TAG, "Successfully requested SMS code.")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.ChallengeRequired -> {
|
||||
Log.i(TAG, "Unable to request sms code due to challenges required: ${castResult.challenges.joinToString { it.key }}")
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.RateLimited -> {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
showErrorDialog(R.string.RegistrationActivity_rate_limited_to_service)
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.TokenNotAccepted -> {
|
||||
Log.i(TAG, "Token was not accepted.")
|
||||
showErrorDialog(R.string.RegistrationActivity_additional_verification_required)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unable to request sms code", castResult.getCause())
|
||||
showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ChangeNumberOutcome.VerificationCodeWorked -> {
|
||||
Log.i(TAG, "Successfully changed number with verification code.")
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChallenges(remainingChallenges: List<Challenge>) {
|
||||
Log.i(TAG, "Handling challenge(s): ${remainingChallenges.joinToString { it.key }}")
|
||||
when (remainingChallenges.first()) {
|
||||
Challenge.CAPTCHA -> {
|
||||
findNavController().safeNavigate(ChangeNumberVerifyV2FragmentDirections.actionChangePhoneNumberVerifyFragmentToCaptchaFragment())
|
||||
}
|
||||
|
||||
Challenge.PUSH -> {
|
||||
viewModel.requestAndSubmitPushToken(requireContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog(@StringRes message: Int) {
|
||||
if (dialogVisible) {
|
||||
Log.i(TAG, "Dialog already being shown, failed to display dialog with message ${getString(message)}")
|
||||
return
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
findNavController().navigateUp()
|
||||
viewModel.resetLocalSessionState()
|
||||
}
|
||||
show()
|
||||
dialogVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,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.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
@@ -3706,7 +3706,7 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun reRegisterAction() {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
|
||||
override fun reviewJoinRequestsAction() {
|
||||
@@ -3781,7 +3781,7 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onReRegisterClicked() {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
|
||||
override fun onCancelGroupRequestClicked() {
|
||||
|
||||
@@ -158,7 +158,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.search.MessageResult;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -794,7 +794,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
} else if (reminderActionId == R.id.reminder_action_fix_username_link) {
|
||||
startActivity(AppSettingsActivity.usernameLinkSettings(requireContext()));
|
||||
} else if (reminderActionId == R.id.reminder_action_re_register) {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()));
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class DeviceTransferFragment extends LoggingFragment {
|
||||
protected boolean transferFinished;
|
||||
|
||||
public DeviceTransferFragment() {
|
||||
super(R.layout.device_transfer_fragment);
|
||||
super(R.layout.fragment_device_transfer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,18 +37,18 @@ public final class NewDeviceTransferFragment extends DeviceTransferFragment {
|
||||
|
||||
@Override
|
||||
protected void navigateToRestartTransfer() {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_newDeviceTransfer_to_newDeviceTransferInstructions);
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferInstructions());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigateAwayFromTransfer() {
|
||||
EventBus.getDefault().unregister(serverTaskListener);
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_restart_to_welcomeFragment);
|
||||
requireActivity().finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigateToTransferComplete() {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_newDeviceTransfer_to_newDeviceTransferComplete);
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferComplete());
|
||||
}
|
||||
|
||||
private class ServerTaskListener {
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class TransferOrRestoreFragment extends LoggingFragment {
|
||||
private void launchSelection(BackupRestorationType restorationType) {
|
||||
switch (restorationType) {
|
||||
case DEVICE_TRANSFER -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_new_device_transfer_instructions);
|
||||
case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_choose_backup);
|
||||
case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transfer_or_restore_to_local_restore);
|
||||
case REMOTE_BACKUP -> {}
|
||||
default -> throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.signal.libsignal.protocol.state.SignalProtocolStore
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
@@ -87,7 +87,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
|
||||
return
|
||||
}
|
||||
|
||||
ChangeNumberRepository.CHANGE_NUMBER_LOCK.lock()
|
||||
ChangeNumberViewModel.CHANGE_NUMBER_LOCK.lock()
|
||||
try {
|
||||
if (SignalStore.misc.hasPniInitializedDevices) {
|
||||
Log.w(TAG, "We found out that things have been initialized after we got the lock! No need to do anything else.")
|
||||
@@ -112,7 +112,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
|
||||
|
||||
SignalStore.misc.hasPniInitializedDevices = true
|
||||
} finally {
|
||||
ChangeNumberRepository.CHANGE_NUMBER_LOCK.unlock()
|
||||
ChangeNumberViewModel.CHANGE_NUMBER_LOCK.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.payments.backup.RecoveryPhraseStates;
|
||||
import org.thoughtcrime.securesms.payments.backup.confirm.PaymentsRecoveryPhraseConfirmFragment;
|
||||
import org.thoughtcrime.securesms.payments.preferences.model.InfoCard;
|
||||
import org.thoughtcrime.securesms.payments.preferences.model.PaymentItem;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
@@ -262,7 +262,7 @@ public class PaymentsHomeFragment extends LoggingFragment {
|
||||
if (actionId == R.id.reminder_action_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||
} else if (actionId == R.id.reminder_action_re_register) {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()));
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -37,7 +37,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.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity
|
||||
import org.thoughtcrime.securesms.util.NameUtil.getAbbreviation
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
@@ -389,7 +389,7 @@ class EditProfileFragment : LoggingFragment() {
|
||||
.setMessage(R.string.EditProfileFragment_unregistered_dialog_body)
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(R.string.EditProfileFragment_unregistered_dialog_reregister_button) { d, _ ->
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
startActivity(RegistrationActivity.newIntentForReRegistration(requireContext()))
|
||||
d.dismiss()
|
||||
}
|
||||
.show()
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
|
||||
public final class RegistrationNavigationActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationNavigationActivity.class);
|
||||
|
||||
public static final String RE_REGISTRATION_EXTRA = "re_registration";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private SmsRetrieverReceiver smsRetrieverReceiver;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
public static Intent newIntentForNewRegistration(@NonNull Context context, @Nullable Intent originalIntent) {
|
||||
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
|
||||
intent.putExtra(RE_REGISTRATION_EXTRA, false);
|
||||
|
||||
if (originalIntent != null) {
|
||||
intent.setData(originalIntent.getData());
|
||||
}
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent newIntentForReRegistration(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
|
||||
intent.putExtra(RE_REGISTRATION_EXTRA, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class);
|
||||
|
||||
setContentView(R.layout.activity_registration_navigation);
|
||||
initializeChallengeListener();
|
||||
|
||||
if (getIntent() != null && getIntent().getData() != null) {
|
||||
CommunicationActions.handlePotentialProxyLinkUrl(this, getIntent().getDataString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (intent.getData() != null) {
|
||||
CommunicationActions.handlePotentialProxyLinkUrl(this, intent.getDataString());
|
||||
}
|
||||
|
||||
viewModel.setIsReregister(isReregister(intent));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
shutdownChallengeListener();
|
||||
}
|
||||
|
||||
private boolean isReregister(@NonNull Intent intent) {
|
||||
return intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false);
|
||||
}
|
||||
|
||||
private void initializeChallengeListener() {
|
||||
smsRetrieverReceiver = new SmsRetrieverReceiver(getApplication());
|
||||
smsRetrieverReceiver.registerReceiver();
|
||||
}
|
||||
|
||||
private void shutdownChallengeListener() {
|
||||
if (smsRetrieverReceiver != null) {
|
||||
smsRetrieverReceiver.unregisterReceiver();
|
||||
smsRetrieverReceiver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data
|
||||
package org.thoughtcrime.securesms.registration.data
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
@@ -45,12 +45,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
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.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
/**
|
||||
* This is a merging of the NetworkResult pattern and the Processor pattern of registration v1.
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
@@ -3,11 +3,11 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data.network
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import okio.IOException
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
public class AccountLockedFragment extends BaseAccountLockedFragment {
|
||||
|
||||
public AccountLockedFragment() {
|
||||
super(R.layout.account_locked_fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BaseRegistrationViewModel getViewModel() {
|
||||
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNext() {
|
||||
requireActivity().finish();
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
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.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
|
||||
/**
|
||||
* Base fragment used by registration and change number flow to show an account as locked.
|
||||
*/
|
||||
public abstract class BaseAccountLockedFragment extends LoggingFragment {
|
||||
|
||||
public BaseAccountLockedFragment(int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title));
|
||||
|
||||
TextView description = view.findViewById(R.id.account_locked_description);
|
||||
|
||||
BaseRegistrationViewModel viewModel = getViewModel();
|
||||
viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(),
|
||||
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
|
||||
);
|
||||
|
||||
view.findViewById(R.id.account_locked_next).setOnClickListener(v -> onNext());
|
||||
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(v -> learnMore());
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void learnMore() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private static long durationToDays(long duration) {
|
||||
return duration != 0L ? getLockoutDays(duration) : 7;
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
protected abstract BaseRegistrationViewModel getViewModel();
|
||||
|
||||
protected abstract void onNext();
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.registration.ActionCountDownButton;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
|
||||
|
||||
/**
|
||||
* Base fragment used by registration and change number flow to input an SMS verification code or request a
|
||||
* phone code after requesting SMS.
|
||||
*
|
||||
* @param <ViewModel> - The concrete view model used by the subclasses, for ease of access in said subclass
|
||||
*/
|
||||
public abstract class BaseEnterSmsCodeFragment<ViewModel extends BaseRegistrationViewModel> extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(BaseEnterSmsCodeFragment.class);
|
||||
|
||||
private ScrollView scrollView;
|
||||
private TextView subheader;
|
||||
private VerificationCodeView verificationCodeView;
|
||||
private VerificationPinKeyboard keyboard;
|
||||
private ActionCountDownButton callMeCountDown;
|
||||
private ActionCountDownButton resendSmsCountDown;
|
||||
private MaterialButton wrongNumber;
|
||||
private MaterialButton bottomSheetButton;
|
||||
private boolean autoCompleting;
|
||||
|
||||
private ViewModel viewModel;
|
||||
|
||||
protected final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
public BaseEnterSmsCodeFragment(@LayoutRes int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
|
||||
|
||||
scrollView = view.findViewById(R.id.scroll_view);
|
||||
subheader = view.findViewById(R.id.verification_subheader);
|
||||
verificationCodeView = view.findViewById(R.id.code);
|
||||
keyboard = view.findViewById(R.id.keyboard);
|
||||
callMeCountDown = view.findViewById(R.id.call_me_count_down);
|
||||
resendSmsCountDown = view.findViewById(R.id.resend_sms_count_down);
|
||||
wrongNumber = view.findViewById(R.id.wrong_number);
|
||||
bottomSheetButton = view.findViewById(R.id.having_trouble_button);
|
||||
|
||||
new SignalStrengthPhoneStateListener(this, this);
|
||||
|
||||
connectKeyboard(verificationCodeView, keyboard);
|
||||
ViewUtil.hideKeyboard(requireContext(), view);
|
||||
|
||||
setOnCodeFullyEnteredListener(verificationCodeView);
|
||||
|
||||
wrongNumber.setOnClickListener(v -> returnToPhoneEntryScreen());
|
||||
bottomSheetButton.setOnClickListener( v -> showBottomSheet());
|
||||
|
||||
callMeCountDown.setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in);
|
||||
resendSmsCountDown.setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in);
|
||||
|
||||
callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest());
|
||||
resendSmsCountDown.setOnClickListener(v -> handleSmsRequest());
|
||||
|
||||
callMeCountDown.setListener((v, remaining) -> {
|
||||
if (remaining <= 30) {
|
||||
scrollView.smoothScrollTo(0, v.getBottom());
|
||||
callMeCountDown.setListener(null);
|
||||
}
|
||||
});
|
||||
|
||||
resendSmsCountDown.setListener((v, remaining) -> {
|
||||
if (remaining <= 30) {
|
||||
scrollView.smoothScrollTo(0, v.getBottom());
|
||||
resendSmsCountDown.setListener(null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = getViewModel();
|
||||
viewModel.getIncorrectCodeAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
|
||||
if (attempts >= 3) {
|
||||
bottomSheetButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
viewModel.resetSession();
|
||||
this.remove();
|
||||
requireActivity().getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract ViewModel getViewModel();
|
||||
|
||||
protected abstract void handleSuccessfulVerify();
|
||||
|
||||
protected abstract void navigateToCaptcha();
|
||||
|
||||
protected abstract void navigateToRegistrationLock(long timeRemaining);
|
||||
|
||||
protected abstract void navigateToKbsAccountLocked();
|
||||
|
||||
private void returnToPhoneEntryScreen() {
|
||||
viewModel.resetSession();
|
||||
Navigation.findNavController(requireView()).navigateUp();
|
||||
}
|
||||
|
||||
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
|
||||
verificationCodeView.setOnCompleteListener(code -> {
|
||||
|
||||
callMeCountDown.setVisibility(View.INVISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.INVISIBLE);
|
||||
wrongNumber.setVisibility(View.INVISIBLE);
|
||||
keyboard.displayProgress();
|
||||
|
||||
Disposable verify = viewModel.verifyCodeWithoutRegistrationLock(code)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((VerifyResponseProcessor processor) -> {
|
||||
if (!processor.hasResult()) {
|
||||
Log.w(TAG, "post verify: ", processor.getError());
|
||||
}
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulVerify();
|
||||
} else if (processor.rateLimit()) {
|
||||
handleRateLimited();
|
||||
} else if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) {
|
||||
LockedException lockedException = processor.getLockedException();
|
||||
handleRegistrationLock(lockedException.getTimeRemaining());
|
||||
} else if (processor.authorizationFailed()) {
|
||||
handleIncorrectCodeError();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code", processor.getError());
|
||||
handleGeneralError();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(verify);
|
||||
});
|
||||
}
|
||||
|
||||
protected void displaySuccess(@NonNull Runnable runAfterAnimation) {
|
||||
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
runAfterAnimation.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleRateLimited() {
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
|
||||
|
||||
builder.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, (dialog, which) -> {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleRegistrationLock(long timeRemaining) {
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
navigateToRegistrationLock(timeRemaining);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleSvrAccountLocked() {
|
||||
navigateToKbsAccountLocked();
|
||||
}
|
||||
|
||||
protected void handleIncorrectCodeError() {
|
||||
viewModel.incrementIncorrectCodeAttempts();
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleGeneralError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
resendSmsCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onVerificationCodeReceived(@NonNull ReceivedSmsEvent event) {
|
||||
verificationCodeView.clear();
|
||||
|
||||
List<Integer> parsedCode = convertVerificationCodeToDigits(event.getCode());
|
||||
|
||||
autoCompleting = true;
|
||||
|
||||
final int size = parsedCode.size();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
final int index = i;
|
||||
verificationCodeView.postDelayed(() -> {
|
||||
verificationCodeView.append(parsedCode.get(index));
|
||||
if (index == size - 1) {
|
||||
autoCompleting = false;
|
||||
}
|
||||
}, i * 200L);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Integer> convertVerificationCodeToDigits(@Nullable String code) {
|
||||
if (code == null || code.length() != 6) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Integer> result = new ArrayList<>(code.length());
|
||||
|
||||
try {
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
result.add(Integer.parseInt(Character.toString(code.charAt(i))));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Failed to convert code into digits.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequest() {
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_phone_number_verification_dialog_title,
|
||||
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
|
||||
viewModel.getNumber().getE164Number(),
|
||||
() -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.PHONE_CALL),
|
||||
this::returnToPhoneEntryScreen);
|
||||
}
|
||||
|
||||
private void handleSmsRequest() {
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_phone_number_verification_dialog_title,
|
||||
R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number,
|
||||
viewModel.getNumber().getE164Number(),
|
||||
() -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.SMS_WITH_LISTENER),
|
||||
this::returnToPhoneEntryScreen);
|
||||
}
|
||||
|
||||
private void handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode mode) {
|
||||
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
|
||||
Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
Toast.makeText(requireContext(), getCodeRequestedToastText(mode), Toast.LENGTH_LONG).show();
|
||||
} else if (processor.captchaRequired(viewModel.getExcludedChallenges())) {
|
||||
navigateToCaptcha();
|
||||
} else if (processor.rateLimit()) {
|
||||
handleRateLimited();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request phone code", processor.getError());
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private int getCodeRequestedToastText(VerifyAccountRepository.Mode mode) {
|
||||
switch (mode) {
|
||||
case PHONE_CALL:
|
||||
return R.string.RegistrationActivity_call_requested;
|
||||
case SMS_WITH_LISTENER:
|
||||
case SMS_WITHOUT_LISTENER:
|
||||
return R.string.RegistrationActivity_sms_requested;
|
||||
default:
|
||||
return R.string.RegistrationActivity_code_requested;
|
||||
}
|
||||
}
|
||||
|
||||
private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) {
|
||||
keyboard.setOnKeyPressListener(key -> {
|
||||
if (!autoCompleting) {
|
||||
if (key >= 0) {
|
||||
verificationCodeView.append(key);
|
||||
} else {
|
||||
verificationCodeView.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
String sessionE164 = viewModel.getSessionE164();
|
||||
if (sessionE164 == null) {
|
||||
returnToPhoneEntryScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
subheader.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
|
||||
|
||||
Disposable request = viewModel.validateSession(sessionE164)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (!processor.hasResult()) {
|
||||
Log.d(TAG, "Network error.");
|
||||
returnToPhoneEntryScreen();
|
||||
} else if (processor.isInvalidSession()) {
|
||||
Log.d(TAG, "Registration session is invalid.");
|
||||
returnToPhoneEntryScreen();
|
||||
} else if (processor.cannotSubmitVerificationAttempt()) {
|
||||
Log.d(TAG, "Cannot submit any more verification attempts.");
|
||||
returnToPhoneEntryScreen();
|
||||
} else if (processor.mustWaitToSubmitProof()) {
|
||||
Log.d(TAG, "Blocked from submitting proof at this time.");
|
||||
handleRateLimited();
|
||||
}
|
||||
// else session state is valid and server is ready to accept code
|
||||
});
|
||||
|
||||
disposables.add(request);
|
||||
|
||||
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> {
|
||||
if (callAtTime > 0) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
callMeCountDown.startCountDownTo(callAtTime);
|
||||
} else {
|
||||
callMeCountDown.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
viewModel.getCanSmsAtTime().observe(getViewLifecycleOwner(), smsAtTime -> {
|
||||
if (smsAtTime > 0) {
|
||||
resendSmsCountDown.setVisibility(View.VISIBLE);
|
||||
resendSmsCountDown.startCountDownTo(smsAtTime);
|
||||
} else {
|
||||
resendSmsCountDown.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void showBottomSheet() {
|
||||
ContactSupportBottomSheetFragment bottomSheet = new ContactSupportBottomSheetFragment();
|
||||
bottomSheet.show(getChildFragmentManager(), "support_bottom_sheet");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoCellSignalPresent() {
|
||||
// TODO animate in bottom sheet
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCellSignalPresent() {
|
||||
// TODO animate away bottom sheet
|
||||
}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
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.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
|
||||
/**
|
||||
* Base fragment used by registration and change number flow to deal with a registration locked account.
|
||||
*/
|
||||
public abstract class BaseRegistrationLockFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(BaseRegistrationLockFragment.class);
|
||||
|
||||
/**
|
||||
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
|
||||
*/
|
||||
public static final int MINIMUM_PIN_LENGTH = 4;
|
||||
|
||||
private EditText pinEntry;
|
||||
private View forgotPin;
|
||||
protected CircularProgressMaterialButton pinButton;
|
||||
private TextView errorLabel;
|
||||
private MaterialButton keyboardToggle;
|
||||
private long timeRemaining;
|
||||
|
||||
private BaseRegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
public BaseRegistrationLockFragment(int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
|
||||
|
||||
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
|
||||
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
|
||||
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
|
||||
forgotPin = view.findViewById(R.id.kbs_lock_forgot_pin);
|
||||
|
||||
RegistrationLockFragmentArgs args = RegistrationLockFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
timeRemaining = args.getTimeRemaining();
|
||||
|
||||
forgotPin.setVisibility(View.GONE);
|
||||
forgotPin.setOnClickListener(v -> handleForgottenPin(timeRemaining));
|
||||
|
||||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
handlePinEntry();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
pinButton.setOnClickListener((v) -> {
|
||||
ViewUtil.hideKeyboard(requireContext(), pinEntry);
|
||||
handlePinEntry();
|
||||
});
|
||||
|
||||
keyboardToggle.setOnClickListener((v) -> {
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType();
|
||||
|
||||
updateKeyboard(keyboardType.getOther());
|
||||
keyboardToggle.setIconResource(keyboardType.getIconResource());
|
||||
});
|
||||
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
|
||||
keyboardToggle.setIconResource(keyboardType.getIconResource());
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = getViewModel();
|
||||
|
||||
viewModel.getLockedTimeRemaining()
|
||||
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
|
||||
|
||||
Integer triesRemaining = viewModel.getSvrTriesRemaining();
|
||||
|
||||
if (triesRemaining != null) {
|
||||
if (triesRemaining <= 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new 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, (dialog, which) -> sendEmailToSupport())
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining < 5) {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract BaseRegistrationViewModel getViewModel();
|
||||
|
||||
private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) {
|
||||
Resources resources = requireContext().getResources();
|
||||
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
|
||||
String 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;
|
||||
}
|
||||
|
||||
protected PinKeyboardType getPinEntryKeyboardType() {
|
||||
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
|
||||
|
||||
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
|
||||
}
|
||||
|
||||
private void handlePinEntry() {
|
||||
pinEntry.setEnabled(false);
|
||||
|
||||
final String pin = pinEntry.getText().toString();
|
||||
|
||||
int 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 < MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
pinButton.setSpinning();
|
||||
|
||||
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulPinEntry(pin);
|
||||
} else if (processor.wrongPin()) {
|
||||
onIncorrectKbsRegistrationLockPin(Objects.requireNonNull(processor.getSvrTriesRemaining()));
|
||||
} else if (processor.isRegistrationLockPresentAndSvrExhausted() || processor.registrationLock()) {
|
||||
onKbsAccountLocked();
|
||||
} else if (processor.rateLimit()) {
|
||||
onRateLimited();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", processor.getError());
|
||||
onError();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(verify);
|
||||
}
|
||||
|
||||
public void onIncorrectKbsRegistrationLockPin(int svrTriesRemaining) {
|
||||
pinButton.cancelSpinning();
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
viewModel.setSvrTriesRemaining(svrTriesRemaining);
|
||||
|
||||
if (svrTriesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
return;
|
||||
}
|
||||
|
||||
if (svrTriesRemaining == 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (svrTriesRemaining > 5) {
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining));
|
||||
forgotPin.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onRateLimited() {
|
||||
pinButton.cancelSpinning();
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
new 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();
|
||||
}
|
||||
|
||||
public void onKbsAccountLocked() {
|
||||
onAccountLocked();
|
||||
}
|
||||
|
||||
public void onError() {
|
||||
pinButton.cancelSpinning();
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void handleForgottenPin(long timeRemainingMs) {
|
||||
int lockoutDays = getLockoutDays(timeRemainingMs);
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
|
||||
.setMessage(requireContext().getResources().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, (dialog, which) -> sendEmailToSupport())
|
||||
.show();
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void onAccountLocked() {
|
||||
navigateToAccountLocked();
|
||||
}
|
||||
|
||||
protected abstract void navigateToAccountLocked();
|
||||
|
||||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
|
||||
pinEntry.getText().clear();
|
||||
}
|
||||
|
||||
private void enableAndFocusPinEntry() {
|
||||
pinEntry.setEnabled(true);
|
||||
pinEntry.setFocusable(true);
|
||||
ViewUtil.focusAndShowKeyboard(pinEntry);
|
||||
}
|
||||
|
||||
protected abstract void handleSuccessfulPinEntry(@NonNull String pin);
|
||||
|
||||
protected abstract void sendEmailToSupport();
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Fragment that displays a Captcha in a WebView.
|
||||
*/
|
||||
public final class CaptchaFragment extends LoggingFragment {
|
||||
|
||||
public static final String EXTRA_VIEW_MODEL_PROVIDER = "view_model_provider";
|
||||
|
||||
private BaseRegistrationViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_captcha, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
WebView webView = view.findViewById(R.id.registration_captcha_web_view);
|
||||
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.clearCache(true);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url != null && url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
|
||||
handleToken(url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
webView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL);
|
||||
|
||||
CaptchaViewModelProvider provider = null;
|
||||
if (getArguments() != null) {
|
||||
provider = (CaptchaViewModelProvider) requireArguments().getSerializable(EXTRA_VIEW_MODEL_PROVIDER);
|
||||
}
|
||||
|
||||
if (provider == null) {
|
||||
viewModel = new ViewModelProvider(
|
||||
requireActivity()).get(RegistrationViewModel.class);
|
||||
} else {
|
||||
viewModel = provider.get(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleToken(@NonNull String token) {
|
||||
viewModel.setCaptchaResponse(token);
|
||||
|
||||
NavHostFragment.findNavController(this).navigateUp();
|
||||
}
|
||||
|
||||
public interface CaptchaViewModelProvider extends Serializable {
|
||||
@NonNull BaseRegistrationViewModel get(@NonNull CaptchaFragment fragment);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
public class ChooseBackupFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ChooseBackupFragment.class);
|
||||
|
||||
private static final short OPEN_FILE_REQUEST_CODE = 3862;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.fragment_registration_choose_backup, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
View chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
|
||||
|
||||
TextView learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
|
||||
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
|
||||
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
ChooseBackupFragmentDirections.ActionRestore restore = ChooseBackupFragmentDirections.actionRestore();
|
||||
|
||||
restore.setUri(data.getData());
|
||||
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), restore);
|
||||
}
|
||||
}
|
||||
|
||||
private void onChooseBackupSelected(@NonNull View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
|
||||
intent.setType("application/octet-stream");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory());
|
||||
}
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, OPEN_FILE_REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(requireContext(), R.string.ChooseBackupFragment__no_file_browser_available, Toast.LENGTH_LONG).show();
|
||||
Log.w(TAG, "No matching activity!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
@@ -20,8 +25,8 @@ import androidx.loader.content.Loader;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel;
|
||||
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
@@ -32,7 +37,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
||||
public static final String KEY_COUNTRY_CODE = "country_code";
|
||||
|
||||
private EditText countryFilter;
|
||||
private RegistrationViewModel model;
|
||||
private ChangeNumberViewModel model;
|
||||
private String resultKey;
|
||||
|
||||
@Override
|
||||
@@ -50,7 +55,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
||||
}
|
||||
|
||||
if (resultKey == null) {
|
||||
model = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
model = new ViewModelProvider(requireActivity()).get(ChangeNumberViewModel.class);
|
||||
}
|
||||
|
||||
countryFilter = view.findViewById(R.id.country_search);
|
||||
@@ -67,7 +72,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
||||
String countryName = item.get("country_name");
|
||||
|
||||
if (resultKey == null) {
|
||||
model.onCountrySelected(countryName, countryCode);
|
||||
model.setNewCountry(countryCode, countryName);
|
||||
} else {
|
||||
Bundle result = new Bundle();
|
||||
result.putString(KEY_COUNTRY, countryName);
|
||||
|
||||
@@ -1,494 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever;
|
||||
import com.google.android.gms.auth.api.phone.SmsRetrieverClient;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
|
||||
|
||||
public final class EnterPhoneNumberFragment extends LoggingFragment implements RegistrationNumberInputController.Callbacks {
|
||||
|
||||
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
|
||||
|
||||
private TextInputLayout countryCode;
|
||||
private TextInputLayout number;
|
||||
private CircularProgressMaterialButton register;
|
||||
private View cancel;
|
||||
private ScrollView scrollView;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
|
||||
|
||||
countryCode = view.findViewById(R.id.country_code);
|
||||
number = view.findViewById(R.id.number);
|
||||
cancel = view.findViewById(R.id.cancel_button);
|
||||
scrollView = view.findViewById(R.id.scroll_view);
|
||||
register = view.findViewById(R.id.registerButton);
|
||||
|
||||
RegistrationNumberInputController controller = new RegistrationNumberInputController(requireContext(),
|
||||
this,
|
||||
Objects.requireNonNull(number.getEditText()),
|
||||
countryCode);
|
||||
register.setOnClickListener(v -> handleRegister(requireContext()));
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
if (viewModel.isReregister()) {
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
cancel.setOnClickListener(v -> requireActivity().finish());
|
||||
} else {
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
viewModel.getLiveNumber().observe(getViewLifecycleOwner(), controller::updateNumberFormatter);
|
||||
|
||||
if (viewModel.hasCaptchaToken()) {
|
||||
ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250);
|
||||
}
|
||||
|
||||
Toolbar toolbar = view.findViewById(R.id.toolbar);
|
||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||
final ActionBar supportActionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setTitle(null);
|
||||
}
|
||||
|
||||
final NumberViewState viewModelNumber = viewModel.getNumber();
|
||||
if (viewModelNumber.getCountryCode() == 0) {
|
||||
controller.prepopulateCountryCode();
|
||||
}
|
||||
controller.setNumberAndCountryCode(viewModelNumber);
|
||||
|
||||
ViewUtil.focusAndShowKeyboard(number.getEditText());
|
||||
|
||||
if (viewModel.hasUserSkippedReRegisterFlow() && viewModel.shouldAutoShowSmsConfirmDialog()) {
|
||||
viewModel.setAutoShowSmsConfirmDialog(false);
|
||||
ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.enter_phone_number, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.phone_menu_use_proxy) {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), EnterPhoneNumberFragmentDirections.actionEditProxy());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRegister(@NonNull Context context) {
|
||||
if (viewModel.getNumber().getCountryCode() == 0) {
|
||||
showErrorDialog(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code));
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(viewModel.getNumber().getNationalNumber())) {
|
||||
showErrorDialog(context, getString(R.string.RegistrationActivity_please_enter_a_valid_phone_number_to_register));
|
||||
return;
|
||||
}
|
||||
|
||||
final NumberViewState number = viewModel.getNumber();
|
||||
final String e164number = number.getE164Number();
|
||||
|
||||
if (!number.isValid()) {
|
||||
Dialogs.showAlertDialog(context,
|
||||
getString(R.string.RegistrationActivity_invalid_number),
|
||||
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number));
|
||||
return;
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context);
|
||||
|
||||
if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||
confirmNumberPrompt(context, e164number, () -> onE164EnteredSuccessfully(context, true));
|
||||
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
|
||||
confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context));
|
||||
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) {
|
||||
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show();
|
||||
} else {
|
||||
Dialogs.showAlertDialog(context,
|
||||
getString(R.string.RegistrationActivity_play_services_error),
|
||||
getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
|
||||
}
|
||||
}
|
||||
|
||||
private void onE164EnteredSuccessfully(@NonNull Context context, boolean fcmSupported) {
|
||||
enterInProgressUiState();
|
||||
Log.d(TAG, "E164 entered successfully.");
|
||||
Disposable disposable = viewModel.canEnterSkipSmsFlow()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.onErrorReturnItem(false)
|
||||
.subscribe(canEnter -> {
|
||||
if (canEnter) {
|
||||
Log.i(TAG, "Entering skip flow.");
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment());
|
||||
} else {
|
||||
Log.i(TAG, "Unable to collect necessary data to enter skip flow, returning to normal");
|
||||
handleRequestVerification(context, fcmSupported);
|
||||
}
|
||||
});
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) {
|
||||
if (fcmSupported) {
|
||||
SmsRetrieverClient client = SmsRetriever.getClient(context);
|
||||
Task<Void> task = client.startSmsRetriever();
|
||||
AtomicBoolean handled = new AtomicBoolean(false);
|
||||
|
||||
Debouncer debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(5));
|
||||
debouncer.publish(() -> {
|
||||
if (!handled.getAndSet(true)) {
|
||||
Log.w(TAG, "Timed out waiting for SMS listener!");
|
||||
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
|
||||
}
|
||||
});
|
||||
|
||||
task.addOnSuccessListener(none -> {
|
||||
if (!handled.getAndSet(true)) {
|
||||
Log.i(TAG, "Successfully registered SMS listener.");
|
||||
requestVerificationCode(Mode.SMS_WITH_LISTENER);
|
||||
} else {
|
||||
Log.w(TAG, "Successfully registered listener after timeout.");
|
||||
}
|
||||
debouncer.clear();
|
||||
});
|
||||
|
||||
task.addOnFailureListener(e -> {
|
||||
if (!handled.getAndSet(true)) {
|
||||
Log.w(TAG, "Failed to register SMS listener.", e);
|
||||
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
|
||||
} else {
|
||||
Log.w(TAG, "Failed to register listener after timeout.");
|
||||
}
|
||||
debouncer.clear();
|
||||
});
|
||||
|
||||
task.addOnCanceledListener(() -> {
|
||||
if (!handled.getAndSet(true)) {
|
||||
Log.w(TAG, "SMS listener registration canceled.");
|
||||
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
|
||||
} else {
|
||||
Log.w(TAG, "SMS listener registration canceled after timeout.");
|
||||
}
|
||||
debouncer.clear();
|
||||
});
|
||||
|
||||
} else {
|
||||
Log.i(TAG, "FCM is not supported, using no SMS listener");
|
||||
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterInProgressUiState() {
|
||||
register.setSpinning();
|
||||
countryCode.setEnabled(false);
|
||||
number.setEnabled(false);
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void exitInProgressUiState() {
|
||||
register.cancelSpinning();
|
||||
countryCode.setEnabled(true);
|
||||
number.setEnabled(true);
|
||||
if (viewModel.isReregister()) {
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestVerificationCode(@NonNull Mode mode) {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
MccMncProducer mccMncProducer = new MccMncProducer(requireContext());
|
||||
final DialogInterface.OnClickListener proceedToNextScreen = (dialog, which) -> SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc())
|
||||
.doOnSubscribe(unused -> SignalStore.account().setRegistered(false))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((RegistrationSessionProcessor processor) -> {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.w(TAG, "[requestVerificationCode] Invalid context! Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (processor.verificationCodeRequestSuccess()) {
|
||||
disposables.add(updateFcmTokenValue());
|
||||
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} else if (processor.captchaRequired(viewModel.getExcludedChallenges())) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required");
|
||||
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
|
||||
} else if (processor.exhaustedVerificationCodeAttempts()) {
|
||||
Log.i(TAG, "Unable to request sms code due to exhausting attempts");
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_rate_limited_to_service));
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit");
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_rate_limited_to_try_again, formatMillisecondsToString(processor.getRateLimit())));
|
||||
} else if (processor.isImpossibleNumber()) {
|
||||
Log.w(TAG, "Impossible number", processor.getError());
|
||||
Dialogs.showAlertDialog(requireContext(),
|
||||
context.getString(R.string.RegistrationActivity_invalid_number),
|
||||
String.format(context.getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber()));
|
||||
} else if (processor.isNonNormalizedNumber()) {
|
||||
handleNonNormalizedNumberError(processor.getOriginalNumber(), processor.getNormalizedNumber(), mode);
|
||||
} else if (processor.isTokenRejected()) {
|
||||
Log.i(TAG, "The server did not accept the information.", processor.getError());
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human));
|
||||
} else if (processor.externalServiceFailure()) {
|
||||
Log.w(TAG, "The server reported a failure with an external service.", processor.getError());
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen);
|
||||
} else if (processor.invalidTransportModeFailure()) {
|
||||
Log.w(TAG, "The server reported an invalid transport mode failure.", processor.getError());
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.RegistrationActivity_we_couldnt_send_you_a_verification_code)
|
||||
.setPositiveButton(R.string.RegistrationActivity_voice_call, (dialog, which) -> requestVerificationCode(Mode.PHONE_CALL))
|
||||
.setNegativeButton(R.string.RegistrationActivity_cancel, null)
|
||||
.show();
|
||||
} else if ( processor.isMalformedRequest()){
|
||||
Log.w(TAG, "The server reported a malformed request.", processor.getError());
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen);
|
||||
|
||||
} else if (processor.isRetryException()) {
|
||||
Log.w(TAG, "The server reported a failure that is retryable.", processor.getError());
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen);
|
||||
} else {
|
||||
Log.i(TAG, "Unknown error during verification code request", processor.getError());
|
||||
showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service));
|
||||
}
|
||||
|
||||
exitInProgressUiState();
|
||||
});
|
||||
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
private Disposable updateFcmTokenValue() {
|
||||
return viewModel.updateFcmTokenValue().subscribe();
|
||||
}
|
||||
|
||||
private String formatMillisecondsToString(long milliseconds) {
|
||||
long totalSeconds = milliseconds / 1000;
|
||||
long HH = totalSeconds / 3600;
|
||||
long MM = (totalSeconds % 3600) / 60;
|
||||
long SS = totalSeconds % 60;
|
||||
return String.format(Locale.getDefault(), "%02d:%02d:%02d", HH, MM, SS);
|
||||
}
|
||||
|
||||
public void showErrorDialog(Context context, String msg) {
|
||||
showErrorDialog(context, msg, null);
|
||||
}
|
||||
|
||||
public void showErrorDialog(Context context, String msg, DialogInterface.OnClickListener positiveButtonListener) {
|
||||
new MaterialAlertDialogBuilder(context).setMessage(msg).setPositiveButton(android.R.string.ok, positiveButtonListener).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNumberFocused() {
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNumberInputDone(@NonNull View view) {
|
||||
ViewUtil.hideKeyboard(requireContext(), view);
|
||||
handleRegister(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNationalNumber(@NonNull String number) {
|
||||
viewModel.setNationalNumber(number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCountry(int countryCode) {
|
||||
viewModel.onCountrySelected(null, countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
String sessionE164 = viewModel.getSessionE164();
|
||||
if (sessionE164 != null && viewModel.getSessionId() != null && viewModel.getCaptchaToken() == null) {
|
||||
checkIfSessionIsInProgressAndAdvance(sessionE164);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIfSessionIsInProgressAndAdvance(@NonNull String sessionE164) {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
Disposable request = viewModel.validateSession(sessionE164)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult() && processor.canSubmitProofImmediately()) {
|
||||
try {
|
||||
viewModel.restorePhoneNumberStateFromE164(sessionE164);
|
||||
SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
|
||||
} catch (NumberParseException numberParseException) {
|
||||
viewModel.resetSession();
|
||||
}
|
||||
} else {
|
||||
viewModel.resetSession();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
private void handleNonNormalizedNumberError(@NonNull String originalNumber, @NonNull String normalizedNumber, @NonNull Mode mode) {
|
||||
try {
|
||||
Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null);
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.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, i) -> d.dismiss())
|
||||
.setNeutralButton(R.string.RegistrationActivity_contact_signal_support, (d, i) -> {
|
||||
String subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format);
|
||||
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null);
|
||||
|
||||
CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body);
|
||||
d.dismiss();
|
||||
})
|
||||
.setPositiveButton(R.string.yes, (d, i) -> {
|
||||
countryCode.getEditText().setText(String.valueOf(phoneNumber.getCountryCode()));
|
||||
number.getEditText().setText(String.valueOf(phoneNumber.getNationalNumber()));
|
||||
requestVerificationCode(mode);
|
||||
d.dismiss();
|
||||
})
|
||||
.show();
|
||||
} catch (NumberParseException e) {
|
||||
Log.w(TAG, "Failed to parse number!", e);
|
||||
|
||||
Dialogs.showAlertDialog(requireContext(),
|
||||
getString(R.string.RegistrationActivity_invalid_number),
|
||||
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePromptForNoPlayServices(@NonNull Context context) {
|
||||
Log.d(TAG, "Device does not have Play Services, showing consent dialog.");
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.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, (dialog1, which) -> onE164EnteredSuccessfully(context, false))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmNumberPrompt(@NonNull Context context,
|
||||
@NonNull String e164number,
|
||||
@NonNull Runnable onConfirmed)
|
||||
{
|
||||
enterInProgressUiState();
|
||||
|
||||
disposables.add(
|
||||
viewModel.canEnterSkipSmsFlow()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(canSkipSms -> {
|
||||
Log.d(TAG, "Showing confirm number dialog. canSkipSms = " + canSkipSms + " hasUserSkipped = " + viewModel.hasUserSkippedReRegisterFlow());
|
||||
final EditText editText = this.number.getEditText();
|
||||
showConfirmNumberDialogIfTranslated(context,
|
||||
viewModel.hasUserSkippedReRegisterFlow() ? R.string.RegistrationActivity_additional_verification_required
|
||||
: R.string.RegistrationActivity_phone_number_verification_dialog_title,
|
||||
canSkipSms ? null
|
||||
: R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number,
|
||||
e164number,
|
||||
() -> {
|
||||
Log.d(TAG, "User confirmed number.");
|
||||
if (editText != null) {
|
||||
ViewUtil.hideKeyboard(context, editText);
|
||||
}
|
||||
onConfirmed.run();
|
||||
},
|
||||
() -> {
|
||||
Log.d(TAG, "User canceled confirm number, returning to edit number.");
|
||||
exitInProgressUiState();
|
||||
if (editText != null) {
|
||||
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(editText);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class EnterSmsCodeFragment extends BaseEnterSmsCodeFragment<RegistrationViewModel> implements SignalStrengthPhoneStateListener.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(EnterSmsCodeFragment.class);
|
||||
|
||||
public EnterSmsCodeFragment() {
|
||||
super(R.layout.fragment_registration_enter_code);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull RegistrationViewModel getViewModel() {
|
||||
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleSuccessfulVerify() {
|
||||
SimpleTask.run(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
RemoteConfig.refreshSync();
|
||||
Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags.");
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e);
|
||||
}
|
||||
return null;
|
||||
}, none -> displaySuccess(() -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), EnterSmsCodeFragmentDirections.actionSuccessfulRegistration())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigateToRegistrationLock(long timeRemaining) {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
|
||||
EnterSmsCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigateToCaptcha() {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterSmsCodeFragmentDirections.actionRequestCaptcha());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigateToKbsAccountLocked() {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.fragments
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
|
||||
/**
|
||||
* Fragment displayed during registration which allows a user to read through
|
||||
* what permissions are granted to Signal and why, and a means to either skip
|
||||
* granting those permissions or continue to grant via system dialogs.
|
||||
*/
|
||||
class GrantPermissionsFragment : ComposeFragment() {
|
||||
|
||||
private val args by navArgs<GrantPermissionsFragmentArgs>()
|
||||
private val viewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val isSearchingForBackup = mutableStateOf(false)
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val isSearchingForBackup by this.isSearchingForBackup
|
||||
|
||||
GrantPermissionsScreen(
|
||||
deviceBuildVersion = Build.VERSION.SDK_INT,
|
||||
isSearchingForBackup = isSearchingForBackup,
|
||||
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
|
||||
onNextClicked = this::onNextClicked,
|
||||
onNotNowClicked = this::onNotNowClicked
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
private fun onNextClicked() {
|
||||
when (args.welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> {
|
||||
WelcomeFragment.continueClicked(
|
||||
this,
|
||||
viewModel,
|
||||
{ isSearchingForBackup.value = true },
|
||||
{ isSearchingForBackup.value = false },
|
||||
GrantPermissionsFragmentDirections.actionSkipRestore(),
|
||||
GrantPermissionsFragmentDirections.actionRestore()
|
||||
)
|
||||
}
|
||||
|
||||
WelcomeAction.RESTORE_BACKUP -> {
|
||||
WelcomeFragment.restoreFromBackupClicked(
|
||||
this,
|
||||
viewModel,
|
||||
GrantPermissionsFragmentDirections.actionTransferOrRestore()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNotNowClicked() {
|
||||
when (args.welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> {
|
||||
WelcomeFragment.gatherInformationAndContinue(
|
||||
this,
|
||||
viewModel,
|
||||
{ isSearchingForBackup.value = true },
|
||||
{ isSearchingForBackup.value = false },
|
||||
GrantPermissionsFragmentDirections.actionSkipRestore(),
|
||||
GrantPermissionsFragmentDirections.actionRestore()
|
||||
)
|
||||
}
|
||||
|
||||
WelcomeAction.RESTORE_BACKUP -> {
|
||||
WelcomeFragment.gatherInformationAndChooseBackup(
|
||||
this,
|
||||
viewModel,
|
||||
GrantPermissionsFragmentDirections.actionTransferOrRestore()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Which welcome action the user selected which prompted this
|
||||
* screen.
|
||||
*/
|
||||
enum class WelcomeAction {
|
||||
CONTINUE,
|
||||
RESTORE_BACKUP
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.PinRestoreEntryFragmentBinding
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.ReRegisterWithPinViewModel
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.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
|
||||
|
||||
/**
|
||||
* Using a recovery password or restored KBS token attempt to register in the skip flow.
|
||||
*/
|
||||
class ReRegisterWithPinFragment : LoggingFragment(R.layout.pin_restore_entry_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
|
||||
}
|
||||
|
||||
private var _binding: PinRestoreEntryFragmentBinding? = null
|
||||
private val binding: PinRestoreEntryFragmentBinding
|
||||
get() = _binding!!
|
||||
|
||||
private val registrationViewModel: RegistrationViewModel by activityViewModels()
|
||||
private val reRegisterViewModel: ReRegisterWithPinViewModel by viewModels()
|
||||
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
_binding = PinRestoreEntryFragmentBinding.bind(view)
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
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.pinRestorePinConfirm.setOnClickListener {
|
||||
handlePinEntry()
|
||||
}
|
||||
|
||||
binding.pinRestoreKeyboardToggle.setOnClickListener {
|
||||
val currentKeyboardType: PinKeyboardType = getPinEntryKeyboardType()
|
||||
updateKeyboard(currentKeyboardType.other)
|
||||
binding.pinRestoreKeyboardToggle.setIconResource(currentKeyboardType.iconResource)
|
||||
}
|
||||
|
||||
binding.pinRestoreKeyboardToggle.setIconResource(getPinEntryKeyboardType().other.iconResource)
|
||||
|
||||
reRegisterViewModel.updateSvrTriesRemaining(registrationViewModel.svrTriesRemaining)
|
||||
|
||||
disposables += reRegisterViewModel.triesRemaining.subscribe(this::updateTriesRemaining)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun handlePinEntry() {
|
||||
val pin: String? = binding.pinRestorePinInput.text?.toString()
|
||||
|
||||
val trimmedLength = pin?.trim()?.length ?: 0
|
||||
if (trimmedLength == 0) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
|
||||
enableAndFocusPinEntry()
|
||||
return
|
||||
}
|
||||
|
||||
if (trimmedLength < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
|
||||
enableAndFocusPinEntry()
|
||||
return
|
||||
}
|
||||
|
||||
disposables += registrationViewModel.verifyReRegisterWithPin(pin!!)
|
||||
.doOnSubscribe {
|
||||
ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput)
|
||||
binding.pinRestorePinInput.isEnabled = false
|
||||
binding.pinRestorePinConfirm.setSpinning()
|
||||
}
|
||||
.doAfterTerminate {
|
||||
binding.pinRestorePinInput.isEnabled = true
|
||||
binding.pinRestorePinConfirm.cancelSpinning()
|
||||
}
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
Log.i(TAG, "Successfully re-registered via skip flow")
|
||||
try {
|
||||
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment)
|
||||
return@subscribe
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.w(TAG, "Could not get parent activity fragment manager!")
|
||||
}
|
||||
|
||||
try {
|
||||
val hostActivity = activity
|
||||
if (hostActivity != null) {
|
||||
Navigation.findNavController(hostActivity, R.id.nav_host_fragment).safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment)
|
||||
return@subscribe
|
||||
} else {
|
||||
Log.w(TAG, "Could not get parent activity!")
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.w(TAG, "Could not find navigation host fragment!")
|
||||
}
|
||||
|
||||
activity?.let {
|
||||
Log.w(TAG, "Could not navigate to registration complete. Finishing activity gracefully.")
|
||||
it.finish()
|
||||
}
|
||||
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
reRegisterViewModel.hasIncorrectGuess = true
|
||||
|
||||
if (processor is VerifyResponseWithRegistrationLockProcessor && processor.wrongPin()) {
|
||||
reRegisterViewModel.updateSvrTriesRemaining(processor.svrTriesRemaining)
|
||||
if (processor.svrTriesRemaining != null) {
|
||||
registrationViewModel.svrTriesRemaining = processor.svrTriesRemaining
|
||||
}
|
||||
return@subscribe
|
||||
} else if (processor.isRegistrationLockPresentAndSvrExhausted()) {
|
||||
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
|
||||
onAccountLocked()
|
||||
} else if (processor.isIncorrectRegistrationRecoveryPassword()) {
|
||||
Log.w(TAG, "Registration recovery password was incorrect. Moving to SMS verification.")
|
||||
onSkipPinEntry()
|
||||
} else if (processor.isServerSentError()) {
|
||||
Log.i(TAG, "Error from server, not likely recoverable", processor.error)
|
||||
genericErrorDialog()
|
||||
} else {
|
||||
Log.i(TAG, "Unexpected error occurred", processor.error)
|
||||
genericErrorDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTriesRemaining(triesRemaining: Int) {
|
||||
if (reRegisterViewModel.hasIncorrectGuess) {
|
||||
if (triesRemaining == 1 && !reRegisterViewModel.isLocalVerification) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
} else {
|
||||
binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)
|
||||
}
|
||||
binding.pinRestoreForgotPin.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (triesRemaining == 1) {
|
||||
binding.pinRestoreForgotPin.visibility = View.VISIBLE
|
||||
if (!reRegisterViewModel.isLocalVerification) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.")
|
||||
onAccountLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccountLocked() {
|
||||
Log.d(TAG, "Showing Incorrect PIN dialog. Is local verification: ${reRegisterViewModel.isLocalVerification}")
|
||||
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() }
|
||||
.setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun enableAndFocusPinEntry() {
|
||||
binding.pinRestorePinInput.isEnabled = true
|
||||
binding.pinRestorePinInput.isFocusable = true
|
||||
ViewUtil.focusAndShowKeyboard(binding.pinRestorePinInput)
|
||||
}
|
||||
|
||||
private fun getPinEntryKeyboardType(): PinKeyboardType {
|
||||
val isNumeric = binding.pinRestorePinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER
|
||||
return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC
|
||||
}
|
||||
|
||||
private fun updateKeyboard(keyboard: PinKeyboardType) {
|
||||
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
|
||||
binding.pinRestorePinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
binding.pinRestorePinInput.text?.clear()
|
||||
}
|
||||
|
||||
private fun onNeedHelpClicked() {
|
||||
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_need_help)
|
||||
.setMessage(getString(message, SvrConstants.MINIMUM_PIN_LENGTH))
|
||||
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ ->
|
||||
val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null)
|
||||
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(R.string.ReRegisterWithPinFragment_support_email_subject),
|
||||
body
|
||||
)
|
||||
}
|
||||
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onSkipClicked() {
|
||||
val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() }
|
||||
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onSkipPinEntry() {
|
||||
Log.d(TAG, "User skipping PIN entry.")
|
||||
registrationViewModel.setUserSkippedReRegisterFlow(true)
|
||||
findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_enterPhoneNumberFragment)
|
||||
}
|
||||
|
||||
private fun genericErrorDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.RegistrationActivity_error_connecting_to_service)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
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.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
|
||||
|
||||
/**
|
||||
* [RegistrationCompleteFragment] is not visible to the user, but functions as basically a redirect towards one of:
|
||||
* - [PIN Restore flow activity](org.thoughtcrime.securesms.pin.PinRestoreActivity)
|
||||
* - [Profile](org.thoughtcrime.securesms.profiles.edit.EditProfileActivity) / [PIN creation](org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity) flow activities (this class chains the necessary activities together as an intent)
|
||||
* - Exit registration flow and progress to conversation list
|
||||
*/
|
||||
class RegistrationCompleteFragment : LoggingFragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_registration_blank, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = requireActivity()
|
||||
val viewModel: RegistrationViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
|
||||
if (SignalStore.misc.hasLinkedDevices) {
|
||||
SignalStore.misc.shouldShowLinkedDevicesReminder = viewModel.isReregister
|
||||
}
|
||||
|
||||
if (SignalStore.storageService.needsAccountRestore()) {
|
||||
Log.i(TAG, "Performing pin restore.")
|
||||
activity.startActivity(Intent(activity, PinRestoreActivity::class.java))
|
||||
} else {
|
||||
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
|
||||
val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id)
|
||||
val needsProfile = isProfileNameEmpty || isAvatarEmpty
|
||||
val needsPin = !SignalStore.svr.hasPin() && !viewModel.isReregister
|
||||
|
||||
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
|
||||
|
||||
if (!needsProfile && !needsPin) {
|
||||
AppDependencies.jobManager
|
||||
.startChain(ProfileUploadJob())
|
||||
.then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob()))
|
||||
.enqueue()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
}
|
||||
|
||||
var startIntent = MainActivity.clearTop(activity)
|
||||
|
||||
if (needsPin) {
|
||||
startIntent = chainIntents(CreateSvrPinActivity.getIntentForPinCreate(activity), startIntent)
|
||||
}
|
||||
|
||||
if (needsProfile) {
|
||||
startIntent = chainIntents(CreateProfileActivity.getIntentForUserProfile(activity), startIntent)
|
||||
}
|
||||
|
||||
activity.startActivity(startIntent)
|
||||
}
|
||||
|
||||
activity.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
|
||||
}
|
||||
|
||||
private fun chainIntents(sourceIntent: Intent, nextIntent: Intent): Intent {
|
||||
sourceIntent.putExtra("next_intent", nextIntent)
|
||||
return sourceIntent
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationCompleteFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
public final class RegistrationConstants {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RegistrationLockFragment extends BaseRegistrationLockFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationLockFragment.class);
|
||||
|
||||
public RegistrationLockFragment() {
|
||||
super(R.layout.fragment_registration_lock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BaseRegistrationViewModel getViewModel() {
|
||||
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigateToAccountLocked() {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleSuccessfulPinEntry(@NonNull String pin) {
|
||||
SignalStore.pin().setKeyboardType(getPinEntryKeyboardType());
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
SignalStore.onboarding().clearAll();
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("RegistrationLockRestore");
|
||||
|
||||
AppDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
||||
stopwatch.split("AccountRestore");
|
||||
|
||||
AppDependencies
|
||||
.getJobManager()
|
||||
.startChain(new StorageSyncJob())
|
||||
.then(new ReclaimUsernameAndLinkJob())
|
||||
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
|
||||
stopwatch.split("ContactRestore");
|
||||
|
||||
try {
|
||||
RemoteConfig.refreshSync();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh flags.", e);
|
||||
}
|
||||
stopwatch.split("RemoteConfig");
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
return null;
|
||||
}, none -> {
|
||||
pinButton.cancelSpinning();
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionSuccessfulRegistration());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendEmailToSupport() {
|
||||
int subject = R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin;
|
||||
|
||||
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
|
||||
subject,
|
||||
null,
|
||||
null);
|
||||
CommunicationActions.openEmail(requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(subject),
|
||||
body);
|
||||
}
|
||||
}
|
||||
@@ -1,494 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
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.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.AppInitialization;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent;
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
|
||||
public final class RestoreBackupFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RestoreBackupFragment.class);
|
||||
private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782;
|
||||
|
||||
private TextView restoreBackupSize;
|
||||
private TextView restoreBackupTime;
|
||||
private TextView restoreBackupProgress;
|
||||
private CircularProgressMaterialButton restoreButton;
|
||||
private View skipRestoreButton;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_restore_backup, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
|
||||
|
||||
Log.i(TAG, "Backup restore.");
|
||||
|
||||
restoreBackupSize = view.findViewById(R.id.backup_size_text);
|
||||
restoreBackupTime = view.findViewById(R.id.backup_created_text);
|
||||
restoreBackupProgress = view.findViewById(R.id.backup_progress_text);
|
||||
restoreButton = view.findViewById(R.id.restore_button);
|
||||
skipRestoreButton = view.findViewById(R.id.skip_restore_button);
|
||||
|
||||
skipRestoreButton.setOnClickListener((v) -> {
|
||||
Log.i(TAG, "User skipped backup restore.");
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(view),
|
||||
RestoreBackupFragmentDirections.actionSkip());
|
||||
});
|
||||
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
if (viewModel.isReregister()) {
|
||||
Log.i(TAG, "Skipping backup restore during re-register.");
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(view),
|
||||
RestoreBackupFragmentDirections.actionSkipNoReturn());
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewModel.hasBackupCompleted()) {
|
||||
onBackupComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (SignalStore.settings().isBackupEnabled()) {
|
||||
Log.i(TAG, "Backups enabled, so a backup must have been previously restored.");
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(view),
|
||||
RestoreBackupFragmentDirections.actionSkipNoReturn());
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreBackupFragmentArgs args = RestoreBackupFragmentArgs.fromBundle(requireArguments());
|
||||
if ((Build.VERSION.SDK_INT < 29 || BackupUtil.isUserSelectionRequired(requireContext())) && args.getUri() != null) {
|
||||
Log.i(TAG, "Restoring backup from passed uri");
|
||||
initializeBackupForUri(view, args.getUri());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
initializeBackupDetection(view);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping backup detection. We don't have the permission.");
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(view),
|
||||
RestoreBackupFragmentDirections.actionSkipNoReturn());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_DOCUMENT_TREE_RESULT_CODE && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
Uri backupDirectoryUri = data.getData();
|
||||
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||
requireContext().getContentResolver()
|
||||
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||
|
||||
enableBackups(requireContext());
|
||||
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
|
||||
RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBackupForUri(@NonNull View view, @NonNull Uri uri) {
|
||||
getFromUri(requireContext(), uri, backup -> handleBackupInfo(view, backup));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeBackupDetection(@NonNull View view) {
|
||||
searchForBackup(backup -> handleBackupInfo(view, backup));
|
||||
}
|
||||
|
||||
private void handleBackupInfo(@NonNull View view, @Nullable BackupUtil.BackupInfo backup) {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.i(TAG, "No context on fragment, must have navigated away.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (backup == null) {
|
||||
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(view),
|
||||
RestoreBackupFragmentDirections.actionNoBackupFound());
|
||||
} else {
|
||||
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
|
||||
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp())));
|
||||
|
||||
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
|
||||
}
|
||||
}
|
||||
|
||||
interface OnBackupSearchResultListener {
|
||||
|
||||
@MainThread
|
||||
void run(@Nullable BackupUtil.BackupInfo backup);
|
||||
}
|
||||
|
||||
static void searchForBackup(@NonNull OnBackupSearchResultListener listener) {
|
||||
new AsyncTask<Void, Void, BackupUtil.BackupInfo>() {
|
||||
@Override
|
||||
protected @Nullable
|
||||
BackupUtil.BackupInfo doInBackground(Void... voids) {
|
||||
try {
|
||||
return BackupUtil.getLatestBackup();
|
||||
} catch (NoExternalStorageException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable BackupUtil.BackupInfo backup) {
|
||||
listener.run(backup);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
static void getFromUri(@NonNull Context context,
|
||||
@NonNull Uri backupUri,
|
||||
@NonNull OnBackupSearchResultListener listener)
|
||||
{
|
||||
SimpleTask.run(() -> {
|
||||
try {
|
||||
return BackupUtil.getBackupInfoFromSingleUri(context, backupUri);
|
||||
} catch (BackupUtil.BackupFileException e) {
|
||||
Log.w(TAG, "Could not restore backup.", e);
|
||||
postToastForBackupRestorationFailure(context, e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
listener::run);
|
||||
}
|
||||
|
||||
private static void postToastForBackupRestorationFailure(@NonNull Context context, @NonNull BackupUtil.BackupFileException exception) {
|
||||
final @StringRes int errorResId;
|
||||
switch (exception.getState()) {
|
||||
case READABLE:
|
||||
throw new AssertionError("Unexpected error state.");
|
||||
case NOT_FOUND:
|
||||
errorResId = R.string.RestoreBackupFragment__backup_not_found;
|
||||
break;
|
||||
case UNSUPPORTED_FILE_EXTENSION:
|
||||
errorResId = R.string.RestoreBackupFragment__backup_has_a_bad_extension;
|
||||
break;
|
||||
default:
|
||||
errorResId = R.string.RestoreBackupFragment__backup_could_not_be_read;
|
||||
}
|
||||
|
||||
ThreadUtil.postToMain(() -> Toast.makeText(context, errorResId, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||
|
||||
prompt.addTextChangedListener(new PassphraseAsYouTypeFormatter());
|
||||
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.RegistrationActivity_enter_backup_passphrase)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.RegistrationActivity_restore, (dialog, which) -> {
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
inputMethodManager.hideSoftInputFromWindow(prompt.getWindowToken(), 0);
|
||||
|
||||
restoreButton.setSpinning();
|
||||
skipRestoreButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
String passphrase = prompt.getText().toString();
|
||||
|
||||
restoreAsynchronously(context, backup, passphrase);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
Log.i(TAG, "Prompt for backup passphrase shown to user.");
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void restoreAsynchronously(@NonNull Context context,
|
||||
@NonNull BackupUtil.BackupInfo backup,
|
||||
@NonNull String passphrase)
|
||||
{
|
||||
new AsyncTask<Void, Void, BackupImportResult>() {
|
||||
@Override
|
||||
protected BackupImportResult doInBackground(Void... voids) {
|
||||
try {
|
||||
Log.i(TAG, "Starting backup restore.");
|
||||
DataRestoreConstraint.setRestoringData(true);
|
||||
|
||||
SQLiteDatabase database = SignalDatabase.getBackupDatabase();
|
||||
|
||||
BackupPassphrase.set(context, passphrase);
|
||||
|
||||
if (!FullBackupImporter.validatePassphrase(context, backup.getUri(), passphrase)) {
|
||||
return BackupImportResult.FAILURE_UNKNOWN;
|
||||
}
|
||||
|
||||
FullBackupImporter.importFile(context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
database,
|
||||
backup.getUri(),
|
||||
passphrase);
|
||||
|
||||
SignalDatabase.runPostBackupRestoreTasks(database);
|
||||
NotificationChannels.getInstance().restoreContactNotificationChannels();
|
||||
|
||||
enableBackups(context);
|
||||
|
||||
AppInitialization.onPostBackupRestore(context);
|
||||
|
||||
Log.i(TAG, "Backup restore complete.");
|
||||
return BackupImportResult.SUCCESS;
|
||||
} catch (FullBackupImporter.DatabaseDowngradeException e) {
|
||||
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
|
||||
return BackupImportResult.FAILURE_VERSION_DOWNGRADE;
|
||||
} catch (FullBackupImporter.ForeignKeyViolationException e) {
|
||||
Log.w(TAG, "Failed due to foreign key constraint violations.", e);
|
||||
return BackupImportResult.FAILURE_FOREIGN_KEY;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return BackupImportResult.FAILURE_UNKNOWN;
|
||||
} finally {
|
||||
DataRestoreConstraint.setRestoringData(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@NonNull BackupImportResult result) {
|
||||
viewModel.markBackupCompleted();
|
||||
restoreButton.cancelSpinning();
|
||||
skipRestoreButton.setVisibility(View.VISIBLE);
|
||||
|
||||
restoreBackupProgress.setText("");
|
||||
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
Log.i(TAG, "Successful backup restore.");
|
||||
break;
|
||||
case FAILURE_VERSION_DOWNGRADE:
|
||||
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case FAILURE_FOREIGN_KEY:
|
||||
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case FAILURE_UNKNOWN:
|
||||
Toast.makeText(context, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (viewModel != null && viewModel.hasBackupCompleted()) {
|
||||
onBackupComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(@NonNull BackupEvent event) {
|
||||
long count = event.getCount();
|
||||
|
||||
if (count == 0) {
|
||||
restoreBackupProgress.setText(R.string.RegistrationActivity_checking);
|
||||
} else {
|
||||
restoreBackupProgress.setText(getString(R.string.RegistrationActivity_d_messages_so_far, count));
|
||||
}
|
||||
|
||||
restoreButton.setSpinning();
|
||||
skipRestoreButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
if (event.getType() == BackupEvent.Type.FINISHED) {
|
||||
onBackupComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private void onBackupComplete() {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext()) && !BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
displayConfirmationDialog(requireContext());
|
||||
} else {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
|
||||
RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
}
|
||||
}
|
||||
|
||||
private void enableBackups(@NonNull Context context) {
|
||||
if (BackupUtil.canUserAccessBackupDirectory(context)) {
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
|
||||
SignalStore.settings().setBackupEnabled(true);
|
||||
LocalBackupListener.schedule(context);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void displayConfirmationDialog(@NonNull Context context) {
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.RestoreBackupFragment__restore_complete)
|
||||
.setMessage(R.string.RestoreBackupFragment__to_continue_using_backups_please_choose_a_folder)
|
||||
.setPositiveButton(R.string.RestoreBackupFragment__choose_folder, (dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
startActivityForResult(intent, OPEN_DOCUMENT_TREE_RESULT_CODE);
|
||||
})
|
||||
.setNegativeButton(R.string.RestoreBackupFragment__not_now, (dialog, which) -> {
|
||||
BackupPassphrase.set(context, null);
|
||||
dialog.dismiss();
|
||||
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()),
|
||||
RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
private enum BackupImportResult {
|
||||
SUCCESS,
|
||||
FAILURE_VERSION_DOWNGRADE,
|
||||
FAILURE_FOREIGN_KEY,
|
||||
FAILURE_UNKNOWN
|
||||
}
|
||||
|
||||
public static class PassphraseAsYouTypeFormatter implements TextWatcher {
|
||||
|
||||
private static final int GROUP_SIZE = 5;
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
removeSpans(editable);
|
||||
|
||||
addSpans(editable);
|
||||
}
|
||||
|
||||
private static void removeSpans(Editable editable) {
|
||||
SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class);
|
||||
|
||||
for (SpaceSpan span : paddingSpans) {
|
||||
editable.removeSpan(span);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addSpans(Editable editable) {
|
||||
final int length = editable.length();
|
||||
|
||||
for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) {
|
||||
editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
if (editable.length() > BackupUtil.PASSPHRASE_LENGTH) {
|
||||
editable.delete(BackupUtil.PASSPHRASE_LENGTH, editable.length());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ReplacementSpan} adds a small space after a single character.
|
||||
* Based on https://stackoverflow.com/a/51949578
|
||||
*/
|
||||
private static class SpaceSpan extends ReplacementSpan {
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
|
||||
return (int) (paint.measureText(text, start, end) * 1.7f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
canvas.drawText(text.subSequence(start, end).toString(), x, y, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
import androidx.navigation.NavDirections;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.devicetransfer.DeviceToDeviceTransferService;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
|
||||
public final class WelcomeFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(WelcomeFragment.class);
|
||||
|
||||
private CircularProgressMaterialButton continueButton;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_welcome, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
if (viewModel.isReregister()) {
|
||||
if (viewModel.hasRestoreFlowBeenShown()) {
|
||||
Log.i(TAG, "We've come back to the home fragment on a restore, user must be backing out");
|
||||
if (!Navigation.findNavController(view).popBackStack()) {
|
||||
FragmentActivity activity = requireActivity();
|
||||
activity.finish();
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
initializeNumber(requireContext(), viewModel);
|
||||
|
||||
Log.i(TAG, "Skipping restore because this is a reregistration.");
|
||||
viewModel.setWelcomeSkippedOnRestore();
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
|
||||
WelcomeFragmentDirections.actionSkipRestore());
|
||||
} else {
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.image));
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.title));
|
||||
|
||||
continueButton = view.findViewById(R.id.welcome_continue_button);
|
||||
continueButton.setOnClickListener(v -> onContinueClicked());
|
||||
|
||||
Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
|
||||
restoreFromBackup.setOnClickListener(v -> onRestoreFromBackupClicked());
|
||||
|
||||
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
|
||||
welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
|
||||
|
||||
if (!canUserSelectBackup()) {
|
||||
restoreFromBackup.setText(R.string.registration_activity__transfer_account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null) {
|
||||
Log.i(TAG, "Found existing transferStatus, redirect to transfer flow");
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_welcomeFragment_to_deviceTransferSetup);
|
||||
} else {
|
||||
DeviceToDeviceTransferService.stop(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private void onContinueClicked() {
|
||||
if (Permissions.isRuntimePermissionsRequired()) {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
|
||||
WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.CONTINUE));
|
||||
} else {
|
||||
gatherInformationAndContinue(
|
||||
this,
|
||||
viewModel,
|
||||
() -> continueButton.setSpinning(),
|
||||
() -> continueButton.cancelSpinning(),
|
||||
WelcomeFragmentDirections.actionSkipRestore(),
|
||||
WelcomeFragmentDirections.actionRestore()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void onRestoreFromBackupClicked() {
|
||||
if (Permissions.isRuntimePermissionsRequired()) {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
|
||||
WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP));
|
||||
} else {
|
||||
gatherInformationAndChooseBackup(this, viewModel, WelcomeFragmentDirections.actionTransferOrRestore());
|
||||
}
|
||||
}
|
||||
|
||||
static void continueClicked(@NonNull Fragment fragment,
|
||||
@NonNull RegistrationViewModel viewModel,
|
||||
@NonNull Runnable onSearchForBackupStarted,
|
||||
@NonNull Runnable onSearchForBackupFinished,
|
||||
@NonNull NavDirections actionSkipRestore,
|
||||
@NonNull NavDirections actionRestore)
|
||||
{
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
|
||||
|
||||
Permissions.with(fragment)
|
||||
.request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
|
||||
.ifNecessary()
|
||||
.onAnyResult(() -> gatherInformationAndContinue(fragment,
|
||||
viewModel,
|
||||
onSearchForBackupStarted,
|
||||
onSearchForBackupFinished,
|
||||
actionSkipRestore,
|
||||
actionRestore))
|
||||
.execute();
|
||||
}
|
||||
|
||||
static void restoreFromBackupClicked(@NonNull Fragment fragment,
|
||||
@NonNull RegistrationViewModel viewModel,
|
||||
@NonNull NavDirections actionTransferOrRestore)
|
||||
{
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
|
||||
|
||||
Permissions.with(fragment)
|
||||
.request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
|
||||
.ifNecessary()
|
||||
.onAnyResult(() -> gatherInformationAndChooseBackup(fragment, viewModel, actionTransferOrRestore))
|
||||
.execute();
|
||||
}
|
||||
|
||||
static void gatherInformationAndContinue(
|
||||
@NonNull Fragment fragment,
|
||||
@NonNull RegistrationViewModel viewModel,
|
||||
@NonNull Runnable onSearchForBackupStarted,
|
||||
@NonNull Runnable onSearchForBackupFinished,
|
||||
@NonNull NavDirections actionSkipRestore,
|
||||
@NonNull NavDirections actionRestore
|
||||
) {
|
||||
onSearchForBackupStarted.run();
|
||||
|
||||
RestoreBackupFragment.searchForBackup(backup -> {
|
||||
Context context = fragment.getContext();
|
||||
if (context == null) {
|
||||
Log.i(TAG, "No context on fragment, must have navigated away.");
|
||||
return;
|
||||
}
|
||||
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
|
||||
|
||||
initializeNumber(fragment.requireContext(), viewModel);
|
||||
|
||||
onSearchForBackupFinished.run();
|
||||
|
||||
if (backup == null) {
|
||||
Log.i(TAG, "Skipping backup. No backup found, or no permission to look.");
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
|
||||
actionSkipRestore);
|
||||
} else {
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
|
||||
actionRestore);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void gatherInformationAndChooseBackup(@NonNull Fragment fragment,
|
||||
@NonNull RegistrationViewModel viewModel,
|
||||
@NonNull NavDirections actionTransferOrRestore) {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
|
||||
|
||||
initializeNumber(fragment.requireContext(), viewModel);
|
||||
|
||||
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
|
||||
actionTransferOrRestore);
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private static void initializeNumber(@NonNull Context context, @NonNull RegistrationViewModel viewModel) {
|
||||
Optional<Phonenumber.PhoneNumber> localNumber = Optional.empty();
|
||||
|
||||
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
|
||||
localNumber = Util.getDeviceNumber(context);
|
||||
} else {
|
||||
Log.i(TAG, "No phone permission");
|
||||
}
|
||||
|
||||
if (localNumber.isPresent()) {
|
||||
Log.i(TAG, "Phone number detected");
|
||||
Phonenumber.PhoneNumber phoneNumber = localNumber.get();
|
||||
String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
|
||||
|
||||
viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
|
||||
} else {
|
||||
Log.i(TAG, "No number detected");
|
||||
Optional<String> simCountryIso = Util.getSimCountryIso(context);
|
||||
|
||||
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
|
||||
viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onTermsClicked() {
|
||||
CommunicationActions.openBrowserLink(requireContext(), RegistrationConstants.TERMS_AND_CONDITIONS_URL);
|
||||
}
|
||||
|
||||
private boolean canUserSelectBackup() {
|
||||
return BackupUtil.isUserSelectionRequired(requireContext()) &&
|
||||
!viewModel.isReregister() &&
|
||||
!SignalStore.settings().isBackupEnabled();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
package org.thoughtcrime.securesms.registration.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -23,19 +23,19 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
/**
|
||||
* Activity to hold the entire registration process.
|
||||
*/
|
||||
class RegistrationV2Activity : BaseActivity() {
|
||||
class RegistrationActivity : BaseActivity() {
|
||||
|
||||
private val TAG = Log.tag(RegistrationV2Activity::class.java)
|
||||
private val TAG = Log.tag(RegistrationActivity::class.java)
|
||||
|
||||
private val dynamicTheme = DynamicNoActionBarTheme()
|
||||
val sharedViewModel: RegistrationV2ViewModel by viewModels()
|
||||
val sharedViewModel: RegistrationViewModel by viewModels()
|
||||
|
||||
private var smsRetrieverReceiver: SmsRetrieverReceiver? = null
|
||||
|
||||
@@ -49,6 +49,8 @@ class RegistrationV2Activity : BaseActivity() {
|
||||
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()
|
||||
@@ -85,11 +87,11 @@ class RegistrationV2Activity : BaseActivity() {
|
||||
|
||||
val startIntent = MainActivity.clearTop(this).apply {
|
||||
if (needsPin) {
|
||||
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity))
|
||||
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationActivity))
|
||||
} else if (!SignalStore.registration.hasSkippedTransferOrRestore() && RemoteConfig.messageBackups) {
|
||||
putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationV2Activity))
|
||||
putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationActivity))
|
||||
} else if (needsProfile) {
|
||||
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity))
|
||||
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationActivity))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +115,21 @@ class RegistrationV2Activity : BaseActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RE_REGISTRATION_EXTRA: String = "re_registration"
|
||||
|
||||
@JvmStatic
|
||||
fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent {
|
||||
return Intent(context, RegistrationV2Activity::class.java).apply {
|
||||
return Intent(context, RegistrationActivity::class.java).apply {
|
||||
putExtra(RE_REGISTRATION_EXTRA, false)
|
||||
setData(originalIntent.data)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newIntentForReRegistration(context: Context): Intent {
|
||||
return Intent(context, RegistrationActivity::class.java).apply {
|
||||
putExtra(RE_REGISTRATION_EXTRA, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
package org.thoughtcrime.securesms.registration.ui
|
||||
|
||||
/**
|
||||
* An ordered list of checkpoints of the registration process.
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
package org.thoughtcrime.securesms.registration.ui
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
@@ -3,21 +3,21 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
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.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
|
||||
/**
|
||||
* State holder shared across all of registration.
|
||||
*/
|
||||
data class RegistrationV2State(
|
||||
data class RegistrationState(
|
||||
val sessionId: String? = null,
|
||||
val enteredCode: String = "",
|
||||
val phoneNumber: Phonenumber.PhoneNumber? = fetchExistingE164FromValues(),
|
||||
@@ -50,7 +50,7 @@ data class RegistrationV2State(
|
||||
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationV2State::class)
|
||||
private val TAG = Log.tag(RegistrationState::class)
|
||||
|
||||
private fun fetchExistingE164FromValues(): Phonenumber.PhoneNumber? {
|
||||
val existingE164 = SignalStore.registration.sessionE164
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
package org.thoughtcrime.securesms.registration.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
@@ -36,29 +36,29 @@ import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AlreadyVerified
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ExternalServiceFailure
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ImpossibleNumber
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MalformedRequest
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NoSuchSession
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NonNormalizedNumber
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RateLimited
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RegistrationLocked
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.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.AlreadyVerified
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AttemptsExhausted
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ExternalServiceFailure
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ImpossibleNumber
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MalformedRequest
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MustRetry
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NoSuchSession
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NonNormalizedNumber
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RateLimited
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RegistrationLocked
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
@@ -74,9 +74,9 @@ import kotlin.time.Duration.Companion.minutes
|
||||
/**
|
||||
* ViewModel shared across all of registration.
|
||||
*/
|
||||
class RegistrationV2ViewModel : ViewModel() {
|
||||
class RegistrationViewModel : ViewModel() {
|
||||
|
||||
private val store = MutableStateFlow(RegistrationV2State())
|
||||
private val store = MutableStateFlow(RegistrationState())
|
||||
private val password = Util.getSecret(18)
|
||||
|
||||
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
|
||||
@@ -102,8 +102,13 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
val svrTriesRemaining: Int
|
||||
get() = store.value.svrTriesRemaining
|
||||
|
||||
val isReregister: Boolean
|
||||
var isReregister: Boolean
|
||||
get() = store.value.isReRegister
|
||||
set(value) {
|
||||
store.update {
|
||||
it.copy(isReRegister = value)
|
||||
}
|
||||
}
|
||||
|
||||
val phoneNumber: Phonenumber.PhoneNumber?
|
||||
get() = store.value.phoneNumber
|
||||
@@ -857,7 +862,7 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationV2ViewModel::class.java)
|
||||
private val TAG = Log.tag(RegistrationViewModel::class.java)
|
||||
|
||||
private suspend fun restoreBackupTier() = withContext(Dispatchers.IO) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.accountlocked
|
||||
package org.thoughtcrime.securesms.registration.ui.accountlocked
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -15,14 +15,14 @@ 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.v2.ui.RegistrationV2ViewModel
|
||||
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 AccountLockedV2Fragment : LoggingFragment(R.layout.account_locked_fragment) {
|
||||
private val viewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
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)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.captcha
|
||||
package org.thoughtcrime.securesms.registration.ui.captcha
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
@@ -17,12 +17,12 @@ 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.FragmentRegistrationCaptchaV2Binding
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaBinding
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationConstants
|
||||
|
||||
abstract class CaptchaV2Fragment : LoggingFragment(R.layout.fragment_registration_captcha_v2) {
|
||||
abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_captcha) {
|
||||
|
||||
private val binding: FragmentRegistrationCaptchaV2Binding by ViewBinderDelegate(FragmentRegistrationCaptchaV2Binding::bind)
|
||||
private val binding: FragmentRegistrationCaptchaBinding by ViewBinderDelegate(FragmentRegistrationCaptchaBinding::bind)
|
||||
|
||||
private val backListener = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
@@ -3,22 +3,22 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.captcha
|
||||
package org.thoughtcrime.securesms.registration.ui.captcha
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
|
||||
|
||||
/**
|
||||
* Screen that displays a captcha as part of the registration flow.
|
||||
* This subclass plugs in [RegistrationV2ViewModel] to the shared super class.
|
||||
* This subclass plugs in [RegistrationViewModel] to the shared super class.
|
||||
*
|
||||
* @see CaptchaV2Fragment
|
||||
* @see CaptchaFragment
|
||||
*/
|
||||
class RegistrationCaptchaV2Fragment : CaptchaV2Fragment() {
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
class RegistrationCaptchaFragment : CaptchaFragment() {
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedViewModel.addPresentedChallenge(Challenge.CAPTCHA)
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.entercode
|
||||
package org.thoughtcrime.securesms.registration.ui.entercode
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
@@ -21,16 +21,16 @@ 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.FragmentRegistrationEnterCodeV2Binding
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
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.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
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
|
||||
@@ -38,16 +38,16 @@ import org.thoughtcrime.securesms.util.visible
|
||||
/**
|
||||
* The final screen of account registration, where the user enters their verification code.
|
||||
*/
|
||||
class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_code_v2) {
|
||||
class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) {
|
||||
|
||||
companion object {
|
||||
private const val BOTTOM_SHEET_TAG = "support_bottom_sheet"
|
||||
}
|
||||
|
||||
private val TAG = Log.tag(EnterCodeV2Fragment::class.java)
|
||||
private val TAG = Log.tag(EnterCodeFragment::class.java)
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val binding: FragmentRegistrationEnterCodeV2Binding by ViewBinderDelegate(FragmentRegistrationEnterCodeV2Binding::bind)
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val binding: FragmentRegistrationEnterCodeBinding by ViewBinderDelegate(FragmentRegistrationEnterCodeBinding::bind)
|
||||
|
||||
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
|
||||
|
||||
@@ -158,7 +158,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
|
||||
binding.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionAccountLocked())
|
||||
findNavController().safeNavigate(EnterCodeFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -168,7 +168,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
|
||||
binding.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionRequireKbsLockPin(timeRemaining))
|
||||
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
|
||||
package org.thoughtcrime.securesms.registration.ui.grantpermissions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
@@ -25,8 +25,8 @@ 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.v2.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
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
|
||||
@@ -35,10 +35,10 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
* Screen in account registration that provides rationales for the suggested runtime permissions.
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
class GrantPermissionsFragment : ComposeFragment() {
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val args by navArgs<GrantPermissionsFragmentArgs>()
|
||||
private val isSearchingForBackup = mutableStateOf(false)
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
@@ -50,7 +50,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
when (val resultCode = result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
sharedViewModel.onBackupSuccessfullyRestored()
|
||||
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
|
||||
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")
|
||||
@@ -102,7 +102,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
|
||||
private fun proceedToNextScreen() {
|
||||
when (welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
|
||||
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
|
||||
WelcomeAction.RESTORE_BACKUP -> {
|
||||
val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
@@ -120,6 +120,6 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
|
||||
private val TAG = Log.tag(GrantPermissionsFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
@@ -40,19 +40,19 @@ 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.FragmentRegistrationEnterPhoneNumberV2Binding
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberBinding
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
|
||||
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.toE164
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.toE164
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Dialogs
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
@@ -67,12 +67,12 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
/**
|
||||
* Screen in registration where the user enters their phone number.
|
||||
*/
|
||||
class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number_v2) {
|
||||
class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number) {
|
||||
|
||||
private val TAG = Log.tag(EnterPhoneNumberV2Fragment::class.java)
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val fragmentViewModel by viewModels<EnterPhoneNumberV2ViewModel>()
|
||||
private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind)
|
||||
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() }
|
||||
|
||||
@@ -139,7 +139,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
sharedViewModel.setPhoneNumber(null)
|
||||
}
|
||||
|
||||
if (fragmentState.error != EnterPhoneNumberV2State.Error.NONE) {
|
||||
if (fragmentState.error != EnterPhoneNumberState.Error.NONE) {
|
||||
presentLocalError(fragmentState)
|
||||
}
|
||||
}
|
||||
@@ -237,7 +237,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentRegisterButton(sharedState: RegistrationV2State) {
|
||||
private fun presentRegisterButton(sharedState: RegistrationState) {
|
||||
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber)
|
||||
if (sharedState.inProgress) {
|
||||
binding.registerButton.setSpinning()
|
||||
@@ -246,11 +246,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentLocalError(state: EnterPhoneNumberV2State) {
|
||||
private fun presentLocalError(state: EnterPhoneNumberState) {
|
||||
when (state.error) {
|
||||
EnterPhoneNumberV2State.Error.NONE -> Unit
|
||||
EnterPhoneNumberState.Error.NONE -> Unit
|
||||
|
||||
EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER -> {
|
||||
EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER -> {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_invalid_number)
|
||||
setMessage(
|
||||
@@ -266,15 +266,15 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING -> {
|
||||
EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING -> {
|
||||
handlePromptForNoPlayServices()
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
|
||||
EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
|
||||
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show()
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT -> {
|
||||
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)
|
||||
@@ -340,7 +340,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
|
||||
private fun moveToCaptcha() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionRequestCaptcha())
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha())
|
||||
}
|
||||
|
||||
private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) {
|
||||
@@ -400,7 +400,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
sharedViewModel.uiState.value?.let { value ->
|
||||
val now = System.currentTimeMillis()
|
||||
if (value.phoneNumber == null) {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
|
||||
sharedViewModel.setInProgress(false)
|
||||
} else if (now < value.nextSmsTimestamp) {
|
||||
moveToVerificationEntryScreen()
|
||||
@@ -411,9 +411,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFcmTokenRetrieved(value: RegistrationV2State) {
|
||||
private fun onFcmTokenRetrieved(value: RegistrationState) {
|
||||
if (value.phoneNumber == null) {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
|
||||
sharedViewModel.setInProgress(false)
|
||||
} else {
|
||||
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = false)
|
||||
@@ -440,23 +440,23 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.MISSING -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
|
||||
return false
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE)
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT)
|
||||
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(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -516,12 +516,12 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
}
|
||||
|
||||
private fun moveToEnterPinScreen() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment())
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionReRegisterWithPinV2Fragment())
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun moveToVerificationEntryScreen() {
|
||||
findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
|
||||
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode())
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
@@ -530,8 +530,8 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationV2State>(sharedViewModel.uiState) {
|
||||
override fun onValue(value: RegistrationV2State): Boolean {
|
||||
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationState>(sharedViewModel.uiState) {
|
||||
override fun onValue(value: RegistrationState): Boolean {
|
||||
val fcmRetrieved = value.isFcmSupported
|
||||
if (fcmRetrieved) {
|
||||
onFcmTokenRetrieved(value)
|
||||
@@ -547,7 +547,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return if (menuItem.itemId == R.id.phone_menu_use_proxy) {
|
||||
NavHostFragment.findNavController(this@EnterPhoneNumberV2Fragment).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEditProxy())
|
||||
NavHostFragment.findNavController(this@EnterPhoneNumberFragment).safeNavigate(EnterPhoneNumberFragmentDirections.actionEditProxy())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.text.TextWatcher
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
|
||||
/**
|
||||
* State holder for the phone number entry screen, including phone number and Play Services errors.
|
||||
*/
|
||||
data class EnterPhoneNumberState(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) {
|
||||
enum class Error {
|
||||
NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.telephony.PhoneNumberFormattingTextWatcher
|
||||
import android.text.TextWatcher
|
||||
@@ -19,17 +19,17 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
|
||||
/**
|
||||
* ViewModel for the phone number entry screen.
|
||||
*/
|
||||
class EnterPhoneNumberV2ViewModel : ViewModel() {
|
||||
class EnterPhoneNumberViewModel : ViewModel() {
|
||||
|
||||
private val TAG = Log.tag(EnterPhoneNumberV2ViewModel::class.java)
|
||||
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
|
||||
|
||||
private val store = MutableStateFlow(EnterPhoneNumberV2State())
|
||||
private val store = MutableStateFlow(EnterPhoneNumberState())
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
val formatter: TextWatcher?
|
||||
@@ -85,11 +85,11 @@ class EnterPhoneNumberV2ViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun parsePhoneNumber(state: EnterPhoneNumberV2State): PhoneNumber {
|
||||
fun parsePhoneNumber(state: EnterPhoneNumberState): PhoneNumber {
|
||||
return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode)
|
||||
}
|
||||
|
||||
fun isEnteredNumberValid(state: EnterPhoneNumberV2State): Boolean {
|
||||
fun isEnteredNumberValid(state: EnterPhoneNumberState): Boolean {
|
||||
return try {
|
||||
PhoneNumberUtil.getInstance().isValidNumber(parsePhoneNumber(state))
|
||||
} catch (ex: NumberParseException) {
|
||||
@@ -114,10 +114,10 @@ class EnterPhoneNumberV2ViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
setError(EnterPhoneNumberV2State.Error.NONE)
|
||||
setError(EnterPhoneNumberState.Error.NONE)
|
||||
}
|
||||
|
||||
fun setError(error: EnterPhoneNumberV2State.Error) {
|
||||
fun setError(error: EnterPhoneNumberState.Error) {
|
||||
store.update {
|
||||
it.copy(error = error)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.registrationlock
|
||||
package org.thoughtcrime.securesms.registration.ui.registrationlock
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
@@ -23,25 +23,25 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
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 RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registration_lock) {
|
||||
class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_lock) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationLockV2Fragment::class.java)
|
||||
private val TAG = Log.tag(RegistrationLockFragment::class.java)
|
||||
}
|
||||
|
||||
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind)
|
||||
|
||||
private val viewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val viewModel by activityViewModels<RegistrationViewModel>()
|
||||
|
||||
private var timeRemaining: Long = 0
|
||||
|
||||
@@ -49,7 +49,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
|
||||
|
||||
val args: RegistrationLockV2FragmentArgs = RegistrationLockV2FragmentArgs.fromBundle(requireArguments())
|
||||
val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
timeRemaining = args.getTimeRemaining()
|
||||
|
||||
@@ -140,7 +140,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
is VerificationCodeRequestResult.Success -> Unit
|
||||
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
|
||||
is VerificationCodeRequestResult.AttemptsExhausted -> {
|
||||
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> {
|
||||
@@ -162,7 +162,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
is RegisterAccountResult.Success -> Unit
|
||||
is RegisterAccountResult.RateLimited -> onRateLimited()
|
||||
is RegisterAccountResult.AttemptsExhausted -> {
|
||||
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
|
||||
is RegisterAccountResult.RegistrationLocked -> {
|
||||
@@ -174,7 +174,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
|
||||
is RegisterAccountResult.SvrWrongPin -> onIncorrectKbsRegistrationLockPin(result.triesRemaining)
|
||||
is RegisterAccountResult.SvrNoData -> {
|
||||
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -191,7 +191,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
|
||||
|
||||
if (svrTriesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.")
|
||||
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked())
|
||||
findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
|
||||
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
@@ -21,23 +21,23 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationPinRestoreEntryV2Binding
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.registration.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.navigation.safeNavigate
|
||||
|
||||
class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
|
||||
class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(ReRegisterWithPinV2Fragment::class.java)
|
||||
private val TAG = Log.tag(ReRegisterWithPinFragment::class.java)
|
||||
}
|
||||
|
||||
private val registrationViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val reRegisterViewModel by viewModels<ReRegisterWithPinV2ViewModel>()
|
||||
private val registrationViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val reRegisterViewModel by viewModels<ReRegisterWithPinViewModel>()
|
||||
|
||||
private val binding: FragmentRegistrationPinRestoreEntryV2Binding by ViewBinderDelegate(FragmentRegistrationPinRestoreEntryV2Binding::bind)
|
||||
|
||||
@@ -79,11 +79,11 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati
|
||||
registrationViewModel.uiState.observe(viewLifecycleOwner, ::updateViewState)
|
||||
}
|
||||
|
||||
private fun updateViewState(state: RegistrationV2State) {
|
||||
private fun updateViewState(state: RegistrationState) {
|
||||
if (state.networkError != null) {
|
||||
genericErrorDialog()
|
||||
} else if (!state.canSkipSms) {
|
||||
findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
|
||||
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
|
||||
} else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) {
|
||||
Log.w(TAG, "Unable to continue skip flow, KBS is locked")
|
||||
onAccountLocked()
|
||||
@@ -263,7 +263,7 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati
|
||||
|
||||
is RegisterAccountResult.IncorrectRecoveryPassword -> {
|
||||
registrationViewModel.setUserSkippedReRegisterFlow(true)
|
||||
findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
|
||||
findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment())
|
||||
}
|
||||
|
||||
is RegisterAccountResult.AttemptsExhausted,
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
|
||||
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
|
||||
|
||||
data class ReRegisterWithPinV2State(
|
||||
data class ReRegisterWithPinState(
|
||||
val isLocalVerification: Boolean = false,
|
||||
val hasIncorrectGuess: Boolean = false,
|
||||
val localPinMatches: Boolean = false
|
||||
@@ -3,19 +3,19 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin
|
||||
package org.thoughtcrime.securesms.registration.ui.reregisterwithpin
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
class ReRegisterWithPinV2ViewModel : ViewModel() {
|
||||
class ReRegisterWithPinViewModel : ViewModel() {
|
||||
companion object {
|
||||
private val TAG = Log.tag(ReRegisterWithPinV2ViewModel::class.java)
|
||||
private val TAG = Log.tag(ReRegisterWithPinViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = MutableStateFlow(ReRegisterWithPinV2State())
|
||||
private val store = MutableStateFlow(ReRegisterWithPinState())
|
||||
|
||||
val isLocalVerification: Boolean
|
||||
get() = store.value.isLocalVerification
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.restore
|
||||
package org.thoughtcrime.securesms.registration.ui.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.welcome
|
||||
package org.thoughtcrime.securesms.registration.ui.welcome
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
@@ -18,13 +18,13 @@ 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.FragmentRegistrationWelcomeV2Binding
|
||||
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.v2.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
|
||||
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
|
||||
@@ -36,15 +36,15 @@ import org.thoughtcrime.securesms.util.visible
|
||||
/**
|
||||
* First screen that is displayed on the very first app launch.
|
||||
*/
|
||||
class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome_v2) {
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
|
||||
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(WelcomeV2FragmentDirections.actionGoToRegistration())
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionGoToRegistration())
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
Log.w(TAG, "Backup restoration canceled.")
|
||||
@@ -67,10 +67,10 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
private fun onContinueClicked() {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsFragment.WelcomeAction.CONTINUE))
|
||||
} else {
|
||||
sharedViewModel.maybePrefillE164(requireContext())
|
||||
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionSkipRestore())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
|
||||
private fun onTransferOrRestoreClicked() {
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.RESTORE_BACKUP))
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP))
|
||||
} else {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
|
||||
@@ -95,7 +95,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(WelcomeV2Fragment::class.java)
|
||||
private val TAG = Log.tag(WelcomeFragment::class.java)
|
||||
private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.util
|
||||
|
||||
data class CountryPrefix(val digits: Int, val regionCode: String) {
|
||||
override fun toString(): String {
|
||||
return "+$digits"
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.util
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnFocusChangeListener
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.i18n.phonenumbers.AsYouTypeFormatter
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
|
||||
/**
|
||||
* Handle the logic and formatting of phone number input specifically for registration number the flow.
|
||||
*/
|
||||
class RegistrationNumberInputController(
|
||||
val context: Context,
|
||||
val callbacks: Callbacks,
|
||||
private val phoneNumberInputLayout: EditText,
|
||||
countryCodeInputLayout: TextInputLayout
|
||||
) {
|
||||
private val spinnerView: MaterialAutoCompleteTextView = countryCodeInputLayout.editText as MaterialAutoCompleteTextView
|
||||
private val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
|
||||
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
|
||||
.sortedBy { it.digits.toString() }
|
||||
private val spinnerAdapter: ArrayAdapter<CountryPrefix> = ArrayAdapter<CountryPrefix>(context, R.layout.registration_country_code_dropdown_item, supportedCountryPrefixes)
|
||||
private val countryCodeEntryListener = CountryCodeEntryListener()
|
||||
|
||||
private var countryFormatter: AsYouTypeFormatter? = null
|
||||
private var isUpdating = true
|
||||
|
||||
init {
|
||||
setUpNumberInput()
|
||||
|
||||
spinnerView.threshold = 100
|
||||
spinnerView.setAdapter(spinnerAdapter)
|
||||
spinnerView.addTextChangedListener(countryCodeEntryListener)
|
||||
}
|
||||
|
||||
fun prepopulateCountryCode() {
|
||||
if (spinnerView.editableText.isBlank()) {
|
||||
spinnerView.setText(supportedCountryPrefixes[0].toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun advanceToPhoneNumberInput() {
|
||||
if (!isUpdating) {
|
||||
phoneNumberInputLayout.requestFocus()
|
||||
}
|
||||
val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0
|
||||
phoneNumberInputLayout.setSelection(numberLength, numberLength)
|
||||
}
|
||||
|
||||
private fun setUpNumberInput() {
|
||||
phoneNumberInputLayout.addTextChangedListener(NumberChangedListener())
|
||||
phoneNumberInputLayout.onFocusChangeListener = OnFocusChangeListener { v: View?, hasFocus: Boolean ->
|
||||
if (hasFocus) {
|
||||
callbacks.onNumberFocused()
|
||||
}
|
||||
}
|
||||
phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
callbacks.onNumberInputDone(v!!)
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun setNumberAndCountryCode(numberViewState: NumberViewState) {
|
||||
val countryCode = numberViewState.countryCode
|
||||
|
||||
isUpdating = true
|
||||
phoneNumberInputLayout.setText(numberViewState.nationalNumber)
|
||||
if (numberViewState.countryCode != 0) {
|
||||
spinnerView.setText(supportedCountryPrefixes.first { it.digits == numberViewState.countryCode }.toString())
|
||||
}
|
||||
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode)
|
||||
setCountryFormatter(regionCode)
|
||||
|
||||
isUpdating = false
|
||||
}
|
||||
|
||||
fun updateNumberFormatter(numberViewState: NumberViewState) {
|
||||
val countryCode = numberViewState.countryCode
|
||||
|
||||
isUpdating = true
|
||||
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode)
|
||||
setCountryFormatter(regionCode)
|
||||
|
||||
isUpdating = false
|
||||
}
|
||||
|
||||
private fun setCountryFormatter(regionCode: String?) {
|
||||
val util = PhoneNumberUtil.getInstance()
|
||||
countryFormatter = if (regionCode != null) util.getAsYouTypeFormatter(regionCode) else null
|
||||
reformatText(phoneNumberInputLayout.text)
|
||||
}
|
||||
|
||||
private fun reformatText(editable: Editable): String? {
|
||||
if (TextUtils.isEmpty(editable)) {
|
||||
return null
|
||||
}
|
||||
val countryFormatter: AsYouTypeFormatter = countryFormatter ?: return null
|
||||
countryFormatter.clear()
|
||||
var formattedNumber: String? = null
|
||||
val justDigits = StringBuilder()
|
||||
for (character in editable) {
|
||||
if (Character.isDigit(character)) {
|
||||
formattedNumber = countryFormatter.inputDigit(character)
|
||||
justDigits.append(character)
|
||||
}
|
||||
}
|
||||
if (formattedNumber != null && editable.toString() != formattedNumber) {
|
||||
editable.replace(0, editable.length, formattedNumber)
|
||||
}
|
||||
return if (justDigits.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
justDigits.toString()
|
||||
}
|
||||
}
|
||||
|
||||
inner class NumberChangedListener : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
val number: String = reformatText(s) ?: return
|
||||
if (!isUpdating) {
|
||||
callbacks.setNationalNumber(number)
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
}
|
||||
|
||||
inner class CountryCodeEntryListener : TextWatcher {
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (s.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (s[0] != '+') {
|
||||
s.insert(0, "+")
|
||||
}
|
||||
|
||||
supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let {
|
||||
setCountryFormatter(it.regionCode)
|
||||
callbacks.setCountry(it.digits)
|
||||
advanceToPhoneNumberInput()
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
fun onNumberFocused()
|
||||
fun onNumberInputDone(view: View)
|
||||
fun setNationalNumber(number: String)
|
||||
fun setCountry(countryCode: Int)
|
||||
}
|
||||
}
|
||||
|
||||
data class CountryPrefix(val digits: Int, val regionCode: String) {
|
||||
override fun toString(): String {
|
||||
return "+$digits"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
|
||||
|
||||
import android.text.TextWatcher
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
|
||||
/**
|
||||
* State holder for the phone number entry screen, including phone number and Play Services errors.
|
||||
*/
|
||||
data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) {
|
||||
enum class Error {
|
||||
NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
public final class BaseEnterCodeViewModelDelegate {
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
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.registration.RegistrationSessionProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseHitRegistrationLock;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
* Base view model used in registration and change number flow. Handles the storage of all data
|
||||
* shared between the two flows, orchestrating verification, and calling to subclasses to perform
|
||||
* the specific verify operations for each flow.
|
||||
*/
|
||||
public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(BaseRegistrationViewModel.class);
|
||||
|
||||
private static final String STATE_NUMBER = "NUMBER";
|
||||
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
|
||||
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
|
||||
private static final String STATE_CAPTCHA = "CAPTCHA";
|
||||
private static final String STATE_PUSH_TIMED_OUT = "PUSH_TIMED_OUT";
|
||||
private static final String STATE_INCORRECT_CODE_ATTEMPTS = "STATE_INCORRECT_CODE_ATTEMPTS";
|
||||
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
|
||||
private static final String STATE_SVR_AUTH = "SVR_AUTH";
|
||||
private static final String STATE_SVR_TRIES_REMAINING = "SVR_TRIES_REMAINING";
|
||||
private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
|
||||
private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME";
|
||||
private static final String STATE_CAN_SMS_AT_TIME = "CAN_SMS_AT_TIME";
|
||||
private static final String STATE_RECOVERY_PASSWORD = "RECOVERY_PASSWORD";
|
||||
|
||||
protected final SavedStateHandle savedState;
|
||||
protected final VerifyAccountRepository verifyAccountRepository;
|
||||
|
||||
public BaseRegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
|
||||
@NonNull VerifyAccountRepository verifyAccountRepository,
|
||||
@NonNull String password)
|
||||
{
|
||||
this.savedState = savedStateHandle;
|
||||
|
||||
this.verifyAccountRepository = verifyAccountRepository;
|
||||
|
||||
setInitialDefaultValue(STATE_NUMBER, NumberViewState.INITIAL);
|
||||
setInitialDefaultValue(STATE_REGISTRATION_SECRET, password);
|
||||
setInitialDefaultValue(STATE_VERIFICATION_CODE, "");
|
||||
setInitialDefaultValue(STATE_INCORRECT_CODE_ATTEMPTS, 0);
|
||||
setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
|
||||
setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.svr().getRecoveryPassword());
|
||||
setInitialDefaultValue(STATE_PUSH_TIMED_OUT, false);
|
||||
}
|
||||
|
||||
protected <T> void setInitialDefaultValue(@NonNull String key, @Nullable T initialValue) {
|
||||
if (!savedState.contains(key) || savedState.get(key) == null) {
|
||||
savedState.set(key, initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String getSessionId() {
|
||||
return SignalStore.registration().getSessionId();
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
SignalStore.registration().setSessionId(sessionId);
|
||||
}
|
||||
|
||||
public @Nullable String getSessionE164() {
|
||||
return SignalStore.registration().getSessionE164();
|
||||
}
|
||||
|
||||
public void setSessionE164(String sessionE164) {
|
||||
SignalStore.registration().setSessionE164(sessionE164);
|
||||
}
|
||||
|
||||
public void resetSession() {
|
||||
setSessionE164(null);
|
||||
setSessionId(null);
|
||||
}
|
||||
|
||||
public @NonNull NumberViewState getNumber() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_NUMBER);
|
||||
}
|
||||
|
||||
public @NonNull LiveData<NumberViewState> getLiveNumber() {
|
||||
return savedState.getLiveData(STATE_NUMBER);
|
||||
}
|
||||
|
||||
public void restorePhoneNumberStateFromE164(String e164) throws NumberParseException {
|
||||
Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(e164, null);
|
||||
onCountrySelected(null, phoneNumber.getCountryCode());
|
||||
setNationalNumber(String.valueOf(phoneNumber.getNationalNumber()));
|
||||
}
|
||||
|
||||
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
|
||||
setViewState(getNumber().toBuilder()
|
||||
.selectedCountryDisplayName(selectedCountryName)
|
||||
.countryCode(countryCode)
|
||||
.build());
|
||||
}
|
||||
|
||||
public void setNationalNumber(String number) {
|
||||
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
|
||||
setViewState(numberViewState);
|
||||
}
|
||||
|
||||
protected void setViewState(NumberViewState numberViewState) {
|
||||
if (!numberViewState.equals(getNumber())) {
|
||||
savedState.set(STATE_NUMBER, numberViewState);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull String getRegistrationSecret() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_REGISTRATION_SECRET);
|
||||
}
|
||||
|
||||
public @NonNull String getTextCodeEntered() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_VERIFICATION_CODE);
|
||||
}
|
||||
|
||||
public @Nullable String getCaptchaToken() {
|
||||
return savedState.get(STATE_CAPTCHA);
|
||||
}
|
||||
|
||||
public boolean hasCaptchaToken() {
|
||||
return getCaptchaToken() != null;
|
||||
}
|
||||
|
||||
public void setCaptchaResponse(@Nullable String captchaToken) {
|
||||
savedState.set(STATE_CAPTCHA, captchaToken);
|
||||
}
|
||||
|
||||
public void clearCaptchaResponse() {
|
||||
setCaptchaResponse(null);
|
||||
}
|
||||
|
||||
public void onVerificationCodeEntered(String code) {
|
||||
savedState.set(STATE_VERIFICATION_CODE, code);
|
||||
}
|
||||
|
||||
public void incrementIncorrectCodeAttempts() {
|
||||
//noinspection ConstantConditions
|
||||
savedState.set(STATE_INCORRECT_CODE_ATTEMPTS, (Integer) savedState.get(STATE_INCORRECT_CODE_ATTEMPTS) + 1);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getIncorrectCodeAttempts() {
|
||||
return savedState.getLiveData(STATE_INCORRECT_CODE_ATTEMPTS, 0);
|
||||
}
|
||||
|
||||
public void markPushChallengeTimedOut() {
|
||||
savedState.set(STATE_PUSH_TIMED_OUT, true);
|
||||
}
|
||||
|
||||
public List<String> getExcludedChallenges() {
|
||||
ArrayList<String> challengeKeys = new ArrayList<>();
|
||||
if (Boolean.TRUE.equals(savedState.get(STATE_PUSH_TIMED_OUT))) {
|
||||
challengeKeys.add(RegistrationSessionProcessor.PUSH_CHALLENGE_KEY);
|
||||
}
|
||||
return challengeKeys;
|
||||
}
|
||||
|
||||
protected void setSvrAuthCredentials(SvrAuthCredentialSet credentials) {
|
||||
savedState.set(STATE_SVR_AUTH, credentials);
|
||||
}
|
||||
|
||||
protected @Nullable SvrAuthCredentialSet getSvrAuthCredentials() {
|
||||
return savedState.get(STATE_SVR_AUTH);
|
||||
}
|
||||
|
||||
public @Nullable Integer getSvrTriesRemaining() {
|
||||
return savedState.get(STATE_SVR_TRIES_REMAINING);
|
||||
}
|
||||
|
||||
public void setSvrTriesRemaining(@Nullable Integer triesRemaining) {
|
||||
savedState.set(STATE_SVR_TRIES_REMAINING, triesRemaining);
|
||||
}
|
||||
|
||||
public void setRecoveryPassword(@Nullable String recoveryPassword) {
|
||||
savedState.set(STATE_RECOVERY_PASSWORD, recoveryPassword);
|
||||
}
|
||||
|
||||
public @Nullable String getRecoveryPassword() {
|
||||
return savedState.get(STATE_RECOVERY_PASSWORD);
|
||||
}
|
||||
|
||||
public LiveData<Long> getLockedTimeRemaining() {
|
||||
return savedState.getLiveData(STATE_TIME_REMAINING, 0L);
|
||||
}
|
||||
|
||||
public LiveData<Long> getCanCallAtTime() {
|
||||
return savedState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L);
|
||||
}
|
||||
|
||||
public LiveData<Long> getCanSmsAtTime() {
|
||||
return savedState.getLiveData(STATE_CAN_SMS_AT_TIME, 0L);
|
||||
}
|
||||
|
||||
public void setLockedTimeRemaining(long lockedTimeRemaining) {
|
||||
savedState.set(STATE_TIME_REMAINING, lockedTimeRemaining);
|
||||
}
|
||||
|
||||
public void setCanCallAtTime(long callingTimestamp) {
|
||||
savedState.getLiveData(STATE_CAN_CALL_AT_TIME).postValue(callingTimestamp);
|
||||
}
|
||||
|
||||
public void setCanSmsAtTime(long smsTimestamp) {
|
||||
savedState.getLiveData(STATE_CAN_SMS_AT_TIME).postValue(smsTimestamp);
|
||||
}
|
||||
|
||||
public Single<RegistrationSessionProcessor> requestVerificationCode(@NonNull Mode mode, @Nullable String mcc, @Nullable String mnc) {
|
||||
|
||||
final String e164 = getNumber().getE164Number();
|
||||
|
||||
return getValidSession(e164, mcc, mnc)
|
||||
.flatMap(processor -> {
|
||||
if (!processor.hasResult()) {
|
||||
return Single.just(processor);
|
||||
}
|
||||
|
||||
String sessionId = processor.getSessionId();
|
||||
setSessionId(sessionId);
|
||||
setSessionE164(e164);
|
||||
|
||||
return handleRequiredChallenges(processor, e164);
|
||||
})
|
||||
.flatMap(processor -> {
|
||||
if (!processor.hasResult()) {
|
||||
return Single.just(processor);
|
||||
}
|
||||
|
||||
if (!processor.isAllowedToRequestCode()) {
|
||||
return Single.just(processor);
|
||||
}
|
||||
|
||||
String sessionId = processor.getSessionId();
|
||||
clearCaptchaResponse();
|
||||
return verifyAccountRepository.requestVerificationCode(sessionId,
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
mode)
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess((RegistrationSessionProcessor processor) -> {
|
||||
if (processor.hasResult() && processor.isAllowedToRequestCode()) {
|
||||
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
|
||||
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<RegistrationSessionProcessor.RegistrationSessionProcessorForSession> validateSession(String e164) {
|
||||
String storedSessionId = null;
|
||||
if (e164.equals(getSessionE164())) {
|
||||
storedSessionId = getSessionId();
|
||||
}
|
||||
return verifyAccountRepository.validateSession(storedSessionId, e164, getRegistrationSecret())
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
|
||||
}
|
||||
|
||||
public Single<RegistrationSessionProcessor.RegistrationSessionProcessorForSession> getValidSession(String e164, @Nullable String mcc, @Nullable String mnc) {
|
||||
return validateSession(e164)
|
||||
.flatMap(processor -> {
|
||||
if (processor.isInvalidSession()) {
|
||||
return verifyAccountRepository.requestValidSession(e164, getRegistrationSecret(), mcc, mnc)
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new)
|
||||
.doOnSuccess(createSessionProcessor -> {
|
||||
if (createSessionProcessor.pushChallengeTimedOut()) {
|
||||
Log.w(TAG, "Registration push challenge timed out.");
|
||||
markPushChallengeTimedOut();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Single.just(processor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<RegistrationSessionProcessor> handleRequiredChallenges(RegistrationSessionProcessor processor, String e164) {
|
||||
final String sessionId = processor.getSessionId();
|
||||
|
||||
if (processor.isAllowedToRequestCode()) {
|
||||
Log.d(TAG, "All challenges satisfied.");
|
||||
return Single.just(processor);
|
||||
}
|
||||
|
||||
if (hasCaptchaToken() && processor.captchaRequired(getExcludedChallenges())) {
|
||||
Log.d(TAG, "Submitting completed captcha challenge");
|
||||
final String captcha = Objects.requireNonNull(getCaptchaToken());
|
||||
clearCaptchaResponse();
|
||||
return verifyAccountRepository.verifyCaptcha(sessionId, captcha, e164, getRegistrationSecret())
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
|
||||
} else {
|
||||
String challenge = processor.getChallenge(getExcludedChallenges());
|
||||
Log.d(TAG, "Handling challenge of type " + challenge);
|
||||
if (challenge != null) {
|
||||
switch (challenge) {
|
||||
case RegistrationSessionProcessor.PUSH_CHALLENGE_KEY:
|
||||
return verifyAccountRepository.requestAndVerifyPushToken(sessionId,
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret())
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new);
|
||||
|
||||
case RegistrationSessionProcessor.CAPTCHA_KEY:
|
||||
// fall through to passing the processor back so that the eventual subscriber will check captchaRequired() and handle accordingly
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Single.just(processor);
|
||||
}
|
||||
|
||||
public Single<VerifyResponseProcessor> verifyCodeWithoutRegistrationLock(@NonNull String code) {
|
||||
onVerificationCodeEntered(code);
|
||||
|
||||
return verifyAccountWithoutRegistrationLock()
|
||||
.flatMap(response -> {
|
||||
if (response.getResult().isPresent() && response.getResult().get().getMasterKey() != null) {
|
||||
return onVerifySuccessWithRegistrationLock(new VerifyResponseWithRegistrationLockProcessor(response, null), response.getResult().get().getPin());
|
||||
}
|
||||
|
||||
VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(response);
|
||||
if (processor.hasResult()) {
|
||||
return onVerifySuccess(processor);
|
||||
} else if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) {
|
||||
return Single.just(new VerifyResponseHitRegistrationLock(processor.getResponse()));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
setSvrTriesRemaining(processor.getSvrTriesRemaining());
|
||||
setSvrAuthCredentials(processor.getSvrAuthCredentials());
|
||||
} else if (processor.isRegistrationLockPresentAndSvrExhausted()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<VerifyResponseWithRegistrationLockProcessor> verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) {
|
||||
SvrAuthCredentialSet authCredentials = Objects.requireNonNull(getSvrAuthCredentials());
|
||||
|
||||
return verifyAccountWithRegistrationLock(pin, authCredentials)
|
||||
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, authCredentials))
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return onVerifySuccessWithRegistrationLock(processor, pin);
|
||||
} else if (processor.wrongPin()) {
|
||||
return Single.just(new VerifyResponseWithRegistrationLockProcessor(processor.getResponse(), authCredentials));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.wrongPin()) {
|
||||
setSvrTriesRemaining(processor.getSvrTriesRemaining());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract Single<ServiceResponse<VerifyResponse>> verifyAccountWithoutRegistrationLock();
|
||||
|
||||
protected abstract Single<ServiceResponse<VerifyResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull SvrAuthCredentialSet svrAuthCredentials);
|
||||
|
||||
protected abstract Single<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor);
|
||||
|
||||
protected abstract Single<VerifyResponseWithRegistrationLockProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin);
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class LocalCodeRequestRateLimiter implements Parcelable {
|
||||
|
||||
private final long timePeriod;
|
||||
private final Map<Mode, Data> dataMap;
|
||||
|
||||
public LocalCodeRequestRateLimiter(long timePeriod) {
|
||||
this.timePeriod = timePeriod;
|
||||
this.dataMap = new HashMap<>();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public boolean canRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
|
||||
Data data = dataMap.get(mode);
|
||||
|
||||
return data == null || !data.limited(e164Number, currentTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
|
||||
*/
|
||||
@MainThread
|
||||
public void onSuccessfulRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
|
||||
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this if a mode was unsuccessful in sending.
|
||||
*/
|
||||
@MainThread
|
||||
public void onUnsuccessfulRequest() {
|
||||
dataMap.clear();
|
||||
}
|
||||
|
||||
static class Data {
|
||||
|
||||
final String e164Number;
|
||||
final long limitedUntil;
|
||||
|
||||
Data(@NonNull String e164Number, long limitedUntil) {
|
||||
this.e164Number = e164Number;
|
||||
this.limitedUntil = limitedUntil;
|
||||
}
|
||||
|
||||
boolean limited(String e164Number, long currentTime) {
|
||||
return this.e164Number.equals(e164Number) && currentTime < limitedUntil;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Creator<LocalCodeRequestRateLimiter> CREATOR = new Creator<LocalCodeRequestRateLimiter>() {
|
||||
@Override
|
||||
public LocalCodeRequestRateLimiter createFromParcel(Parcel in) {
|
||||
long timePeriod = in.readLong();
|
||||
int numberOfMapEntries = in.readInt();
|
||||
|
||||
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
|
||||
|
||||
for (int i = 0; i < numberOfMapEntries; i++) {
|
||||
Mode mode = Mode.values()[in.readInt()];
|
||||
String e164Number = in.readString();
|
||||
long limitedUntil = in.readLong();
|
||||
|
||||
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
|
||||
}
|
||||
return localCodeRequestRateLimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalCodeRequestRateLimiter[] newArray(int size) {
|
||||
return new LocalCodeRequestRateLimiter[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(timePeriod);
|
||||
dest.writeInt(dataMap.size());
|
||||
|
||||
for (Map.Entry<Mode, Data> a : dataMap.entrySet()) {
|
||||
dest.writeInt(a.getKey().ordinal());
|
||||
dest.writeString(a.getValue().e164Number);
|
||||
dest.writeLong(a.getValue().limitedUntil);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
|
||||
/**
|
||||
* Used during re-registration flow when pin entry is required to skip SMS verification. Mostly tracks
|
||||
* guesses remaining in both the local and remote check flows.
|
||||
*/
|
||||
class ReRegisterWithPinViewModel : ViewModel() {
|
||||
var isLocalVerification: Boolean = false
|
||||
private set
|
||||
|
||||
var hasIncorrectGuess: Boolean = false
|
||||
|
||||
private val _triesRemaining: BehaviorSubject<Int> = BehaviorSubject.createDefault(10)
|
||||
val triesRemaining: Observable<Int> = _triesRemaining.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
fun updateSvrTriesRemaining(triesRemaining: Int?) {
|
||||
if (triesRemaining == null) {
|
||||
isLocalVerification = true
|
||||
if (hasIncorrectGuess) {
|
||||
_triesRemaining.onNext((_triesRemaining.value!! - 1).coerceAtLeast(0))
|
||||
}
|
||||
} else {
|
||||
_triesRemaining.onNext(triesRemaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.savedstate.SavedStateRegistryOwner;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException;
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationRepository;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
|
||||
import org.signal.core.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class RegistrationViewModel extends BaseRegistrationViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationViewModel.class);
|
||||
|
||||
private static final String STATE_FCM_TOKEN = "FCM_TOKEN";
|
||||
private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN";
|
||||
private static final String STATE_IS_REREGISTER = "IS_REREGISTER";
|
||||
private static final String STATE_BACKUP_COMPLETED = "BACKUP_COMPLETED";
|
||||
|
||||
private final RegistrationRepository registrationRepository;
|
||||
|
||||
private boolean userSkippedReRegisterFlow = false;
|
||||
private boolean autoShowSmsConfirmDialog = false;
|
||||
|
||||
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
|
||||
boolean isReregister,
|
||||
@NonNull VerifyAccountRepository verifyAccountRepository,
|
||||
@NonNull RegistrationRepository registrationRepository)
|
||||
{
|
||||
super(savedStateHandle, verifyAccountRepository, Util.getSecret(18));
|
||||
|
||||
this.registrationRepository = registrationRepository;
|
||||
|
||||
setInitialDefaultValue(STATE_RESTORE_FLOW_SHOWN, false);
|
||||
setInitialDefaultValue(STATE_BACKUP_COMPLETED, false);
|
||||
|
||||
this.savedState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
|
||||
public boolean isReregister() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_IS_REREGISTER);
|
||||
}
|
||||
|
||||
public void onNumberDetected(int countryCode, String nationalNumber) {
|
||||
setViewState(getNumber().toBuilder()
|
||||
.countryCode(countryCode)
|
||||
.nationalNumber(nationalNumber)
|
||||
.build());
|
||||
}
|
||||
|
||||
public @Nullable String getFcmToken() {
|
||||
String token = savedState.get(STATE_FCM_TOKEN);
|
||||
if (token == null || token.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setFcmToken(@Nullable String fcmToken) {
|
||||
savedState.set(STATE_FCM_TOKEN, fcmToken);
|
||||
}
|
||||
|
||||
public void setWelcomeSkippedOnRestore() {
|
||||
savedState.set(STATE_RESTORE_FLOW_SHOWN, true);
|
||||
}
|
||||
|
||||
public boolean hasRestoreFlowBeenShown() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_RESTORE_FLOW_SHOWN);
|
||||
}
|
||||
|
||||
public void setIsReregister(boolean isReregister) {
|
||||
savedState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
|
||||
public void markBackupCompleted() {
|
||||
savedState.set(STATE_BACKUP_COMPLETED, true);
|
||||
}
|
||||
|
||||
public boolean hasBackupCompleted() {
|
||||
Boolean completed = savedState.get(STATE_BACKUP_COMPLETED);
|
||||
return completed != null ? completed : false;
|
||||
}
|
||||
|
||||
public boolean hasUserSkippedReRegisterFlow() {
|
||||
return userSkippedReRegisterFlow;
|
||||
}
|
||||
|
||||
public void setUserSkippedReRegisterFlow(boolean userSkippedReRegisterFlow) {
|
||||
Log.i(TAG, "User skipped re-register flow.");
|
||||
this.userSkippedReRegisterFlow = userSkippedReRegisterFlow;
|
||||
if (userSkippedReRegisterFlow) {
|
||||
setAutoShowSmsConfirmDialog(true);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldAutoShowSmsConfirmDialog() {
|
||||
return autoShowSmsConfirmDialog;
|
||||
}
|
||||
|
||||
public void setAutoShowSmsConfirmDialog(boolean autoShowSmsConfirmDialog) {
|
||||
this.autoShowSmsConfirmDialog = autoShowSmsConfirmDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithoutRegistrationLock() {
|
||||
final String sessionId = getSessionId();
|
||||
if (sessionId == null) {
|
||||
throw new IllegalStateException("No valid registration session");
|
||||
}
|
||||
return verifyAccountRepository.verifyAccount(sessionId, getRegistrationData())
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
|
||||
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
|
||||
}
|
||||
})
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(processor -> {
|
||||
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
|
||||
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), null, null);
|
||||
} else if (processor.getError() == null) {
|
||||
return Single.just(ServiceResponse.<VerifyResponse>forApplicationError(new IncorrectCodeException(), 403, null));
|
||||
} else {
|
||||
return Single.just(ServiceResponse.<VerifyResponse, RegistrationSessionMetadataResponse>coerceError(processor.getResponse()));
|
||||
}
|
||||
})
|
||||
.flatMap(verifyAccountWithoutKbsResponse -> {
|
||||
VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(verifyAccountWithoutKbsResponse);
|
||||
String pin = SignalStore.svr().getPin();
|
||||
|
||||
if ((processor.isRegistrationLockPresentAndSvrExhausted() || processor.registrationLock()) && SignalStore.svr().getRegistrationLockToken() != null && pin != null) {
|
||||
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> SignalStore.svr().getOrCreateMasterKey())
|
||||
.map(verifyAccountWithPinResponse -> {
|
||||
if (verifyAccountWithPinResponse.getResult().isPresent() && verifyAccountWithPinResponse.getResult().get().getMasterKey() != null) {
|
||||
return verifyAccountWithPinResponse;
|
||||
} else {
|
||||
return verifyAccountWithoutKbsResponse;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Single.just(verifyAccountWithoutKbsResponse);
|
||||
}
|
||||
})
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ServiceResponse<VerifyResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull SvrAuthCredentialSet svrAuthCredentials) {
|
||||
final String sessionId = getSessionId();
|
||||
if (sessionId == null) {
|
||||
throw new IllegalStateException("No valid registration session");
|
||||
}
|
||||
return verifyAccountRepository.verifyAccount(sessionId, getRegistrationData())
|
||||
.map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new)
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
setCanSmsAtTime(processor.getNextCodeViaSmsAttempt());
|
||||
setCanCallAtTime(processor.getNextCodeViaCallAttempt());
|
||||
}
|
||||
})
|
||||
.<ServiceResponse<VerifyResponse>>flatMap(processor -> {
|
||||
if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) {
|
||||
return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin));
|
||||
} else {
|
||||
return Single.just(ServiceResponse.coerceError(processor.getResponse()));
|
||||
}
|
||||
})
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor) {
|
||||
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), false)
|
||||
.map(VerifyResponseWithoutKbs::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<VerifyResponseWithRegistrationLockProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin) {
|
||||
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), true)
|
||||
.map(processor::updatedIfRegistrationFailed);
|
||||
}
|
||||
|
||||
private RegistrationData getRegistrationData() {
|
||||
return new RegistrationData(getTextCodeEntered(),
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken(),
|
||||
registrationRepository.getPniRegistrationId(),
|
||||
getSessionId() != null ? null : getRecoveryPassword());
|
||||
}
|
||||
|
||||
public @NonNull Single<VerifyResponseProcessor> verifyReRegisterWithPin(@NonNull String pin) {
|
||||
return Single.fromCallable(() -> verifyReRegisterWithPinInternal(pin))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(data -> {
|
||||
if (data.canProceed) {
|
||||
return updateFcmTokenValue().subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.onErrorReturnItem("")
|
||||
.flatMap(s -> verifyReRegisterWithRecoveryPassword(pin, data.masterKey));
|
||||
} else {
|
||||
throw new IncorrectRegistrationRecoveryPasswordException();
|
||||
}
|
||||
})
|
||||
.onErrorReturn(t -> new VerifyResponseWithRegistrationLockProcessor(ServiceResponse.forUnknownError(t), getSvrAuthCredentials()))
|
||||
.map(p -> {
|
||||
if (p instanceof VerifyResponseWithRegistrationLockProcessor) {
|
||||
VerifyResponseWithRegistrationLockProcessor lockProcessor = (VerifyResponseWithRegistrationLockProcessor) p;
|
||||
if (lockProcessor.wrongPin() && lockProcessor.getSvrTriesRemaining() != null) {
|
||||
return new VerifyResponseWithRegistrationLockProcessor(lockProcessor.getResponse(), lockProcessor.getSvrAuthCredentials());
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
})
|
||||
.doOnSuccess(p -> {
|
||||
if (p.hasResult()) {
|
||||
restoreFromStorageService();
|
||||
}
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull ReRegistrationData verifyReRegisterWithPinInternal(@NonNull String pin)
|
||||
throws SvrWrongPinException, IOException, SvrNoDataException
|
||||
{
|
||||
String localPinHash = SignalStore.svr().getLocalPinHash();
|
||||
|
||||
if (hasRecoveryPassword() && localPinHash != null) {
|
||||
if (PinHashUtil.verifyLocalPinHash(localPinHash, pin)) {
|
||||
Log.i(TAG, "Local pin matches input, attempting registration");
|
||||
return ReRegistrationData.canProceed(SignalStore.svr().getOrCreateMasterKey());
|
||||
} else {
|
||||
throw new SvrWrongPinException(0);
|
||||
}
|
||||
} else {
|
||||
SvrAuthCredentialSet authCredentials = getSvrAuthCredentials();
|
||||
if (authCredentials == null) {
|
||||
Log.w(TAG, "No SVR auth credentials, abort skip flow");
|
||||
return ReRegistrationData.cannotProceed();
|
||||
}
|
||||
|
||||
MasterKey masterKey = SvrRepository.restoreMasterKeyPreRegistration(authCredentials, pin);
|
||||
|
||||
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword());
|
||||
setSvrTriesRemaining(10);
|
||||
return ReRegistrationData.canProceed(masterKey);
|
||||
}
|
||||
}
|
||||
|
||||
private Single<VerifyResponseProcessor> verifyReRegisterWithRecoveryPassword(@NonNull String pin, @NonNull MasterKey masterKey) {
|
||||
RegistrationData registrationData = getRegistrationData();
|
||||
if (registrationData.getRecoveryPassword() == null) {
|
||||
throw new IllegalStateException("No valid recovery password");
|
||||
}
|
||||
|
||||
return verifyAccountRepository.registerAccount(null, registrationData, null, null)
|
||||
.observeOn(Schedulers.io())
|
||||
.onErrorReturn(ServiceResponse::forUnknownError)
|
||||
.map(VerifyResponseWithoutKbs::new)
|
||||
.flatMap(processor -> {
|
||||
if (processor.registrationLock()) {
|
||||
setSvrAuthCredentials(processor.getSvrAuthCredentials());
|
||||
return verifyAccountRepository.registerAccount(null, registrationData, pin, () -> masterKey)
|
||||
.onErrorReturn(ServiceResponse::forUnknownError)
|
||||
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, processor.getSvrAuthCredentials()));
|
||||
} else {
|
||||
return Single.just(processor);
|
||||
}
|
||||
})
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
VerifyResponse verifyResponse = processor.getResult();
|
||||
boolean setRegistrationLockEnabled = verifyResponse.getMasterKey() != null;
|
||||
|
||||
if (!setRegistrationLockEnabled) {
|
||||
verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), masterKey, pin, verifyResponse.getAciPreKeyCollection(), verifyResponse.getPniPreKeyCollection());
|
||||
}
|
||||
|
||||
return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled)
|
||||
.map(r -> new VerifyResponseWithRegistrationLockProcessor(r, getSvrAuthCredentials()));
|
||||
} else {
|
||||
return Single.just(processor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public @NonNull Single<Boolean> canEnterSkipSmsFlow() {
|
||||
if (userSkippedReRegisterFlow) {
|
||||
Log.d(TAG, "User skipped re-register flow.");
|
||||
return Single.just(false);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Querying if user can enter skip SMS flow.");
|
||||
return Single.just(hasRecoveryPassword())
|
||||
.flatMap(hasRecoveryPassword -> {
|
||||
Log.i(TAG, "Checking if user has existing recovery password: " + hasRecoveryPassword);
|
||||
if (hasRecoveryPassword) {
|
||||
return Single.just(true);
|
||||
} else {
|
||||
return checkForValidSvrAuthCredentials();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Single<Boolean> checkForValidSvrAuthCredentials() {
|
||||
final List<String> svrAuthTokenList = SignalStore.svr().getSvr2AuthTokens();
|
||||
List<String> usernamePasswords = svrAuthTokenList
|
||||
.stream()
|
||||
.limit(10)
|
||||
.map(t -> {
|
||||
try {
|
||||
return new String(Base64.decode(t.replace("Basic ", "").trim()), StandardCharsets.ISO_8859_1);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (usernamePasswords.isEmpty()) {
|
||||
Log.d(TAG, "No valid SVR tokens in local store.");
|
||||
return Single.just(false);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Valid tokens in local store, validating with SVR.");
|
||||
return registrationRepository.getSvrAuthCredential(getRegistrationData(), usernamePasswords)
|
||||
.flatMap(p -> {
|
||||
if (p.hasValidSvr2AuthCredential()) {
|
||||
Log.d(TAG, "Saving valid SVR2 auth credential.");
|
||||
setSvrAuthCredentials(new SvrAuthCredentialSet(p.requireSvr2AuthCredential(), null));
|
||||
return Single.just(true);
|
||||
} else {
|
||||
Log.d(TAG, "SVR2 response contained no valid SVR2 auth credentials.");
|
||||
return Single.just(false);
|
||||
}
|
||||
})
|
||||
.onErrorReturnItem(false)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public Single<String> updateFcmTokenValue() {
|
||||
return verifyAccountRepository.getFcmToken().observeOn(AndroidSchedulers.mainThread()).doOnSuccess(this::setFcmToken);
|
||||
}
|
||||
|
||||
private void restoreFromStorageService() {
|
||||
SignalStore.onboarding().clearAll();
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("ReRegisterRestore");
|
||||
|
||||
AppDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
||||
stopwatch.split("AccountRestore");
|
||||
|
||||
AppDependencies
|
||||
.getJobManager()
|
||||
.startChain(new StorageSyncJob())
|
||||
.then(new ReclaimUsernameAndLinkJob())
|
||||
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
|
||||
stopwatch.split("ContactRestore");
|
||||
|
||||
try {
|
||||
RemoteConfig.refreshSync();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh flags.", e);
|
||||
}
|
||||
stopwatch.split("RemoteConfig");
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
}
|
||||
|
||||
private boolean hasRecoveryPassword() {
|
||||
return getRecoveryPassword() != null && Objects.equals(getRegistrationData().getE164(), SignalStore.account().getE164());
|
||||
}
|
||||
|
||||
private static class ReRegistrationData {
|
||||
public boolean canProceed;
|
||||
public MasterKey masterKey;
|
||||
|
||||
private ReRegistrationData(boolean canProceed, @Nullable MasterKey masterKey) {
|
||||
this.canProceed = canProceed;
|
||||
this.masterKey = masterKey;
|
||||
}
|
||||
|
||||
public static ReRegistrationData cannotProceed() {
|
||||
return new ReRegistrationData(false, null);
|
||||
}
|
||||
|
||||
public static ReRegistrationData canProceed(@NonNull MasterKey masterKey) {
|
||||
return new ReRegistrationData(true, masterKey);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory extends AbstractSavedStateViewModelFactory {
|
||||
private final boolean isReregister;
|
||||
|
||||
public Factory(@NonNull SavedStateRegistryOwner owner, boolean isReregister) {
|
||||
super(owner, null);
|
||||
this.isReregister = isReregister;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new RegistrationViewModel(handle,
|
||||
isReregister,
|
||||
new VerifyAccountRepository(AppDependencies.getApplication()),
|
||||
new RegistrationRepository(AppDependencies.getApplication())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ 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.FragmentChooseBackupV2Binding
|
||||
import org.thoughtcrime.securesms.databinding.FragmentChooseBackupBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
@@ -30,9 +30,9 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
/**
|
||||
* This fragment presents a button to the user to browse their local file system for a legacy backup file.
|
||||
*/
|
||||
class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v2) {
|
||||
class ChooseBackupFragment : LoggingFragment(R.layout.fragment_choose_backup) {
|
||||
private val sharedViewModel by activityViewModels<RestoreViewModel>()
|
||||
private val binding: FragmentChooseBackupV2Binding by ViewBinderDelegate(FragmentChooseBackupV2Binding::bind)
|
||||
private val binding: FragmentChooseBackupBinding by ViewBinderDelegate(FragmentChooseBackupBinding::bind)
|
||||
|
||||
private val pickMedia = registerForActivityResult(BackupFileContract()) {
|
||||
if (it != null) {
|
||||
@@ -57,7 +57,7 @@ class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v
|
||||
|
||||
private fun onUserChoseBackupFile(backupFileUri: Uri) {
|
||||
sharedViewModel.setBackupFileUri(backupFileUri)
|
||||
NavHostFragment.findNavController(this).safeNavigate(ChooseBackupV2FragmentDirections.actionChooseLocalBackupFragmentToRestoreLocalBackupFragment())
|
||||
NavHostFragment.findNavController(this).safeNavigate(ChooseBackupFragmentDirections.actionChooseLocalBackupFragmentToRestoreLocalBackupFragment())
|
||||
}
|
||||
|
||||
private class BackupFileContract : ActivityResultContracts.GetContent() {
|
||||
@@ -72,6 +72,6 @@ class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChooseBackupV2Fragment::class.java)
|
||||
private val TAG = Log.tag(ChooseBackupFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,15 @@ import org.signal.devicetransfer.TransferStatus
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentDeviceTransferV2Binding
|
||||
import org.thoughtcrime.securesms.databinding.FragmentDeviceTransferBinding
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
sealed class DeviceTransferV2Fragment : LoggingFragment(R.layout.fragment_device_transfer_v2) {
|
||||
sealed class DeviceTransferFragment : LoggingFragment(R.layout.fragment_device_transfer) {
|
||||
private val onBackPressed = OnBackPressed()
|
||||
private val transferModeListener = TransferModeListener()
|
||||
protected val navigationViewModel: RestoreViewModel by activityViewModels()
|
||||
protected val binding: FragmentDeviceTransferV2Binding by ViewBinderDelegate(FragmentDeviceTransferV2Binding::bind)
|
||||
protected val binding: FragmentDeviceTransferBinding by ViewBinderDelegate(FragmentDeviceTransferBinding::bind)
|
||||
|
||||
protected var transferFinished: Boolean = false
|
||||
|
||||
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
* This approximates that by taking the place of [EnterPhoneNumberFragment],
|
||||
* then bridging us back to [RegistrationV2Activity] by immediately closing the [RestoreActivity].
|
||||
*/
|
||||
class RestoreCompleteV2Fragment : LoggingFragment(R.layout.fragment_registration_blank) {
|
||||
class RestoreCompleteFragment : LoggingFragment(R.layout.fragment_registration_blank) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -33,6 +33,6 @@ class RestoreCompleteV2Fragment : LoggingFragment(R.layout.fragment_registration
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RestoreCompleteV2Fragment::class.java)
|
||||
private val TAG = Log.tag(RestoreCompleteFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.restorelocalbackup;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
||||
public class PassphraseAsYouTypeFormatter implements TextWatcher {
|
||||
|
||||
private static final int GROUP_SIZE = 5;
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
removeSpans(editable);
|
||||
|
||||
addSpans(editable);
|
||||
}
|
||||
|
||||
private static void removeSpans(Editable editable) {
|
||||
SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class);
|
||||
|
||||
for (SpaceSpan span : paddingSpans) {
|
||||
editable.removeSpan(span);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addSpans(Editable editable) {
|
||||
final int length = editable.length();
|
||||
|
||||
for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) {
|
||||
editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
if (editable.length() > BackupUtil.PASSPHRASE_LENGTH) {
|
||||
editable.delete(BackupUtil.PASSPHRASE_LENGTH, editable.length());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ReplacementSpan} adds a small space after a single character.
|
||||
* Based on https://stackoverflow.com/a/51949578
|
||||
*/
|
||||
private static class SpaceSpan extends ReplacementSpan {
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
|
||||
return (int) (paint.measureText(text, start, end) * 1.7f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
canvas.drawText(text.subSequence(start, end).toString(), x, y, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,9 @@ import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Binding
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.restore.RestoreRepository
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
@@ -41,7 +40,7 @@ import java.util.Locale
|
||||
/**
|
||||
* This fragment is used to monitor and manage an in-progress backup restore.
|
||||
*/
|
||||
class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_local_backup_v2) {
|
||||
class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_local_backup) {
|
||||
private val navigationViewModel: RestoreViewModel by activityViewModels()
|
||||
private val restoreLocalBackupViewModel: RestoreLocalBackupViewModel by viewModels(
|
||||
factoryProducer = ViewModelFactory.factoryProducer {
|
||||
@@ -49,7 +48,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
RestoreLocalBackupViewModel(fileBackupUri)
|
||||
}
|
||||
)
|
||||
private val binding: FragmentRestoreLocalBackupV2Binding by ViewBinderDelegate(FragmentRestoreLocalBackupV2Binding::bind)
|
||||
private val binding: FragmentRestoreLocalBackupBinding by ViewBinderDelegate(FragmentRestoreLocalBackupBinding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -68,8 +67,6 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
|
||||
// TODO [regv2]: check for re-register and skip ahead to phone number entry
|
||||
|
||||
if (SignalStore.settings.isBackupEnabled) {
|
||||
Log.i(TAG, "Backups enabled, so a backup must have been previously restored.")
|
||||
onBackupCompletedSuccessfully()
|
||||
@@ -158,7 +155,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
private fun presentProgressEnded() {
|
||||
binding.restoreButton.cancelSpinning()
|
||||
binding.cancelLocalRestoreButton.visible = true
|
||||
binding.backupProgressText.text = ""
|
||||
binding.backupProgressText.text = null
|
||||
}
|
||||
|
||||
private fun presentRestoreProgress(backupProgressCount: Long) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.databinding.TransferOrRestoreOptionsBottomShee
|
||||
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user