mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Delete registration V1.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user