mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Change Number V2.
This commit is contained in:
committed by
Cody Henthorne
parent
b99c2165fa
commit
1e35403c87
@@ -783,6 +783,12 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".components.settings.app.changenumber.v2.ChangeNumberLockV2Activity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberLockV2Activity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
@@ -180,7 +181,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_TRANSFER_ONGOING;
|
||||
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
return STATE_TRANSFER_LOCKED;
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class && getClass() != ChangeNumberLockV2Activity.class) {
|
||||
return STATE_CHANGE_NUMBER_LOCK;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
@@ -264,7 +265,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getChangeNumberLockIntent() {
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
if (FeatureFlags.registrationV2()) {
|
||||
return ChangeNumberLockV2Activity.createIntent(this);
|
||||
} else {
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
|
||||
|
||||
@@ -22,6 +22,7 @@ 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.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private const val START_LOCATION = "app.settings.start.location"
|
||||
@@ -39,7 +40,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
||||
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||
val navGraphResId = if (FeatureFlags.registrationV2()) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings
|
||||
intent?.putExtra(ARG_NAV_GRAPH, navGraphResId)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
@@ -195,8 +197,9 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, StartLocation.RECOVER_USERNAME)
|
||||
|
||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||
val navGraphResId = if (FeatureFlags.registrationV2()) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings
|
||||
return Intent(context, AppSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||
.putExtra(ARG_NAV_GRAPH, navGraphResId)
|
||||
.putExtra(START_LOCATION, startLocation.code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c
|
||||
}
|
||||
)
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
|
||||
parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle ->
|
||||
viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.fragment.app.activityViewModels
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.captcha.CaptchaV2Fragment
|
||||
|
||||
/**
|
||||
* Screen visible to the user when they are to solve a captcha. @see [CaptchaV2Fragment]
|
||||
*/
|
||||
class ChangeNumberCaptchaV2Fragment : CaptchaV2Fragment() {
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.addPresentedChallenge(Challenge.CAPTCHA)
|
||||
}
|
||||
|
||||
override fun handleCaptchaToken(token: String) {
|
||||
viewModel.setCaptchaResponse(token)
|
||||
}
|
||||
|
||||
override fun handleUserExit() {
|
||||
viewModel.removePresentedChallenge(Challenge.CAPTCHA)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
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.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterCodeBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent
|
||||
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
|
||||
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) {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterCodeV2Fragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by activityViewModels<ChangeNumberV2ViewModel>()
|
||||
private val binding: FragmentChangeNumberEnterCodeBinding by ViewBinderDelegate(FragmentChangeNumberEnterCodeBinding::bind)
|
||||
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
|
||||
|
||||
private var autopilotCodeEntryActive = false
|
||||
|
||||
private val bottomSheet = ContactSupportBottomSheetFragment()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
binding.codeEntryLayout.verifyHeader.setOnClickListener(null)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
Log.d(TAG, "onBackPressed")
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.codeEntryLayout.verifyHeader)
|
||||
|
||||
phoneStateListener = SignalStrengthPhoneStateListener(this, PhoneStateCallback())
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.codeEntryLayout.wrongNumber.setOnClickListener {
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
binding.codeEntryLayout.code.setOnCompleteListener {
|
||||
viewModel.verifyCodeWithoutRegistrationLock(requireContext(), it, ::handleSessionErrorResponse, ::handleChangeNumberErrorResponse)
|
||||
}
|
||||
|
||||
binding.codeEntryLayout.havingTroubleButton.setOnClickListener {
|
||||
bottomSheet.show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
binding.codeEntryLayout.callMeCountDown.apply {
|
||||
setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in)
|
||||
setOnClickListener {
|
||||
viewModel.initiateChangeNumberSession(requireContext(), RegistrationRepository.Mode.PHONE_CALL)
|
||||
}
|
||||
}
|
||||
|
||||
binding.codeEntryLayout.resendSmsCountDown.apply {
|
||||
setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in)
|
||||
setOnClickListener {
|
||||
viewModel.initiateChangeNumberSession(requireContext(), RegistrationRepository.Mode.SMS_WITHOUT_LISTENER)
|
||||
}
|
||||
}
|
||||
|
||||
binding.codeEntryLayout.keyboard.setOnKeyPressListener { key ->
|
||||
if (!autopilotCodeEntryActive) {
|
||||
if (key >= 0) {
|
||||
binding.codeEntryLayout.code.append(key)
|
||||
} else {
|
||||
binding.codeEntryLayout.code.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.incorrectCodeAttempts.observe(viewLifecycleOwner) { attempts: Int ->
|
||||
if (attempts >= 3) {
|
||||
binding.codeEntryLayout.havingTroubleButton.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate)
|
||||
}
|
||||
|
||||
private fun onStateUpdate(state: ChangeNumberState) {
|
||||
binding.codeEntryLayout.resendSmsCountDown.startCountDownTo(state.nextSmsTimestamp)
|
||||
binding.codeEntryLayout.callMeCountDown.startCountDownTo(state.nextCallTimestamp)
|
||||
when (val outcome = state.changeNumberOutcome) {
|
||||
is ChangeNumberOutcome.RecoveryPasswordWorked,
|
||||
is ChangeNumberOutcome.VerificationCodeWorked -> changeNumberSuccess()
|
||||
|
||||
is ChangeNumberOutcome.ChangeNumberRequestOutcome -> if (!state.inProgress && !outcome.result.isSuccess()) {
|
||||
presentGenericError(outcome.result)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
if (state.inProgress) {
|
||||
binding.codeEntryLayout.keyboard.displayProgress()
|
||||
} else {
|
||||
binding.codeEntryLayout.keyboard.displayKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc().isChangeNumberLocked) {
|
||||
Log.d(TAG, "Change number locked, navigateUp")
|
||||
startActivity(ChangeNumberLockV2Activity.createIntent(requireContext()))
|
||||
} else {
|
||||
Log.d(TAG, "navigateUp")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSessionErrorResponse(result: VerificationCodeRequestResult) {
|
||||
when (result) {
|
||||
is VerificationCodeRequestResult.Success -> binding.codeEntryLayout.keyboard.displaySuccess()
|
||||
is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog()
|
||||
is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked()
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChangeNumberErrorResponse(result: ChangeNumberResult) {
|
||||
when (result) {
|
||||
is ChangeNumberResult.Success -> binding.codeEntryLayout.keyboard.displaySuccess()
|
||||
is ChangeNumberResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is ChangeNumberResult.AuthorizationFailed -> presentIncorrectCodeDialog()
|
||||
is ChangeNumberResult.AttemptsExhausted -> presentAccountLocked()
|
||||
is ChangeNumberResult.RateLimited -> presentRateLimitedDialog()
|
||||
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentAccountLocked() {
|
||||
binding.codeEntryLayout.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
findNavController().safeNavigate(ChangeNumberEnterCodeV2FragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentRegistrationLocked(timeRemaining: Long) {
|
||||
binding.codeEntryLayout.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
Log.i(TAG, "Account is registration locked, cannot register.")
|
||||
findNavController().safeNavigate(ChangeNumberEnterCodeV2FragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentRateLimitedDialog() {
|
||||
binding.codeEntryLayout.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean?>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
binding.codeEntryLayout.callMeCountDown.visibility = View.VISIBLE
|
||||
binding.codeEntryLayout.resendSmsCountDown.visibility = View.VISIBLE
|
||||
binding.codeEntryLayout.wrongNumber.visibility = View.VISIBLE
|
||||
binding.codeEntryLayout.code.clear()
|
||||
binding.codeEntryLayout.keyboard.displayKeyboard()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentIncorrectCodeDialog() {
|
||||
viewModel.incrementIncorrectCodeAttempts()
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show()
|
||||
binding.codeEntryLayout.keyboard.displayFailure().addListener(object : AssertedSuccessListener<Boolean?>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
binding.codeEntryLayout.callMeCountDown.setVisibility(View.VISIBLE)
|
||||
binding.codeEntryLayout.resendSmsCountDown.setVisibility(View.VISIBLE)
|
||||
binding.codeEntryLayout.wrongNumber.setVisibility(View.VISIBLE)
|
||||
binding.codeEntryLayout.code.clear()
|
||||
binding.codeEntryLayout.keyboard.displayKeyboard()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun presentGenericError(requestResult: RegistrationResult) {
|
||||
binding.codeEntryLayout.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
Log.w(TAG, "Encountered unexpected error!", requestResult.getCause())
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
null?.let<String, MaterialAlertDialogBuilder> {
|
||||
setTitle(it)
|
||||
}
|
||||
setMessage(getString(R.string.RegistrationActivity_error_connecting_to_service))
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
navigateUp()
|
||||
viewModel.resetLocalSessionState()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
|
||||
binding.codeEntryLayout.code.clear()
|
||||
|
||||
if (event.code.isBlank() || event.code.length != ReceivedSmsEvent.CODE_LENGTH) {
|
||||
Log.i(TAG, "Received invalid code of length ${event.code.length}. Ignoring.")
|
||||
return
|
||||
}
|
||||
|
||||
val finalIndex = ReceivedSmsEvent.CODE_LENGTH - 1
|
||||
autopilotCodeEntryActive = true
|
||||
try {
|
||||
event.code
|
||||
.map { it.digitToInt() }
|
||||
.forEachIndexed { i, digit ->
|
||||
binding.codeEntryLayout.code.postDelayed({
|
||||
binding.codeEntryLayout.code.append(digit)
|
||||
if (i == finalIndex) {
|
||||
autopilotCodeEntryActive = false
|
||||
}
|
||||
}, i * 200L)
|
||||
}
|
||||
} catch (notADigit: IllegalArgumentException) {
|
||||
Log.w(TAG, "Failed to convert code into digits.", notADigit)
|
||||
autopilotCodeEntryActive = false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
|
||||
override fun onNoCellSignalPresent() {
|
||||
bottomSheet.show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
override fun onCellSignalPresent() {
|
||||
if (bottomSheet.isResumed) {
|
||||
bottomSheet.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.LockedException
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
|
||||
/**
|
||||
* This is a processor to map a [VerifyAccountResponse] to all the known outcomes.
|
||||
*/
|
||||
sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
companion object {
|
||||
fun from(networkResult: NetworkResult<ChangeNumberV2Repository.NumberChangeResult>): ChangeNumberResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> Success(networkResult.result)
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
is NetworkResult.NetworkError -> UnknownError(networkResult.exception)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (val cause = networkResult.exception) {
|
||||
is IncorrectRegistrationRecoveryPasswordException -> IncorrectRecoveryPassword(cause)
|
||||
is AuthorizationFailedException -> AuthorizationFailed(cause)
|
||||
is MalformedRequestException -> MalformedRequest(cause)
|
||||
is RateLimitException -> createRateLimitProcessor(cause)
|
||||
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials)
|
||||
else -> {
|
||||
if (networkResult.code == 422) {
|
||||
ValidationError(cause)
|
||||
} else {
|
||||
UnknownError(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRateLimitProcessor(exception: RateLimitException): ChangeNumberResult {
|
||||
return if (exception.retryAfterMilliseconds.isPresent) {
|
||||
RateLimited(exception, exception.retryAfterMilliseconds.get())
|
||||
} else {
|
||||
AttemptsExhausted(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Success(val numberChangeResult: ChangeNumberV2Repository.NumberChangeResult) : ChangeNumberResult(null)
|
||||
class IncorrectRecoveryPassword(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class AuthorizationFailed(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class MalformedRequest(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class ValidationError(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class RateLimited(cause: Throwable, val timeRemaining: Long) : ChangeNumberResult(cause)
|
||||
class AttemptsExhausted(cause: Throwable) : ChangeNumberResult(cause)
|
||||
class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials?) : ChangeNumberResult(cause)
|
||||
class UnknownError(cause: Throwable) : ChangeNumberResult(cause)
|
||||
|
||||
class SvrNoData(cause: SvrNoDataException) : ChangeNumberResult(cause)
|
||||
class SvrWrongPin(cause: SvrWrongPinException) : ChangeNumberResult(cause) {
|
||||
val triesRemaining = cause.triesRemaining
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber.v2
|
||||
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
|
||||
/**
|
||||
* State holder for [ChangeNumberV2ViewModel]
|
||||
*/
|
||||
data class ChangeNumberState(
|
||||
val number: NumberViewState = NumberViewState.INITIAL,
|
||||
val enteredCode: String? = null,
|
||||
val enteredPin: String = "",
|
||||
val oldPhoneNumber: NumberViewState = NumberViewState.INITIAL,
|
||||
val sessionId: String? = null,
|
||||
val changeNumberOutcome: ChangeNumberOutcome? = null,
|
||||
val lockedTimeRemaining: Long = 0L,
|
||||
val svrCredentials: AuthCredentials? = null,
|
||||
val svrTriesRemaining: Int = 10,
|
||||
val incorrectCodeAttempts: Int = 0,
|
||||
val nextSmsTimestamp: Long = 0L,
|
||||
val nextCallTimestamp: Long = 0L,
|
||||
val inProgress: Boolean = false,
|
||||
val captchaToken: String? = null,
|
||||
val challengesRequested: List<Challenge> = emptyList(),
|
||||
val challengesPresented: Set<Challenge> = emptySet(),
|
||||
val allowedToRequestCode: Boolean = false
|
||||
) {
|
||||
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
|
||||
}
|
||||
|
||||
sealed interface ChangeNumberOutcome {
|
||||
data object RecoveryPasswordWorked : ChangeNumberOutcome
|
||||
data object VerificationCodeWorked : ChangeNumberOutcome
|
||||
class ChangeNumberRequestOutcome(val result: VerificationCodeRequestResult) : ChangeNumberOutcome
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* 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 {
|
||||
AppDependencies
|
||||
.incomingMessageObserver
|
||||
.removeDecryptionDrainedListener(drainedListener)
|
||||
Log.d(TAG, "Decryptions draining canceled.")
|
||||
}
|
||||
|
||||
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.certificateValues().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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
/*
|
||||
* 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, SvrAuthCredentialSet(null, store.value.svrCredentials))
|
||||
}
|
||||
|
||||
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!")
|
||||
|
||||
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(
|
||||
captchaToken = null,
|
||||
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(captchaToken = null, 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(
|
||||
svrCredentials = result.svr2Credentials
|
||||
)
|
||||
}
|
||||
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(
|
||||
svrCredentials = result.svr2Credentials
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -9,7 +7,8 @@
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<include
|
||||
layout="@layout/fragment_registration_enter_code"
|
||||
layout="@layout/fragment_registration_enter_code_v2"
|
||||
android:id="@+id/code_entry_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/change_number_enter_phone_number_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/change_number_enter_phone_number_continue"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_number_enter_phone_number_old_number_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/ChangeNumberEnterPhoneNumberFragment__your_old_number"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="@color/signal_text_primary_dialog"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/change_number_enter_phone_number_old_number_spinner_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/labeled_edit_text_background_inactive"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/change_number_enter_phone_number_old_number_label">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/change_number_enter_phone_number_old_number_spinner"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/change_number_enter_phone_number_old_number_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_enter_phone_number_old_number_spinner_frame">
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledEditText
|
||||
android:id="@+id/change_number_enter_phone_number_old_number_country_code"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_weight="1"
|
||||
app:labeledEditText_background="@color/signal_background_primary"
|
||||
app:labeledEditText_textLayout="@layout/country_code_text" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledEditText
|
||||
android:id="@+id/change_number_enter_phone_number_old_number_number"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="3"
|
||||
app:labeledEditText_background="@color/signal_background_primary"
|
||||
app:labeledEditText_label="@string/ChangeNumberEnterPhoneNumberFragment__old_phone_number"
|
||||
app:labeledEditText_textLayout="@layout/phone_text" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_number_enter_phone_number_new_number_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/ChangeNumberEnterPhoneNumberFragment__your_new_number"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="@color/signal_text_primary_dialog"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_enter_phone_number_old_number_input_layout" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/change_number_enter_phone_number_new_number_spinner_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/labeled_edit_text_background_inactive"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/change_number_enter_phone_number_new_number_label">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/change_number_enter_phone_number_new_number_spinner"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/change_number_enter_phone_number_new_number_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_enter_phone_number_new_number_spinner_frame">
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledEditText
|
||||
android:id="@+id/change_number_enter_phone_number_new_number_country_code"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_weight="1"
|
||||
app:labeledEditText_background="@color/signal_background_primary"
|
||||
app:labeledEditText_textLayout="@layout/country_code_text" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledEditText
|
||||
android:id="@+id/change_number_enter_phone_number_new_number_number"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="3"
|
||||
app:labeledEditText_background="@color/signal_background_primary"
|
||||
app:labeledEditText_label="@string/ChangeNumberEnterPhoneNumberFragment__new_phone_number"
|
||||
app:labeledEditText_textLayout="@layout/phone_text" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/change_number_enter_phone_number_continue"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/ChangeNumberFragment__continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_enter_phone_number_scroll" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
84
app/src/main/res/layout/fragment_change_phone_number_v2.xml
Normal file
84
app/src/main/res/layout/fragment_change_phone_number_v2.xml
Normal file
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/change_phone_number_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/change_phone_number_continue"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/change_phone_number_hero"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/change_phone_number_header"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside"
|
||||
app:srcCompat="@drawable/change_number_hero_image" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_phone_number_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/AccountSettingsFragment__change_phone_number"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/change_phone_number_body"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_phone_number_hero" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_phone_number_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/ChangeNumberFragment__use_this_to_change_your_current_phone_number_to_a_new_phone_number"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_phone_number_header" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/change_phone_number_continue"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/ChangeNumberFragment__continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_phone_number_scroll" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
184
app/src/main/res/navigation/app_settings_change_number_v2.xml
Normal file
184
app/src/main/res/navigation/app_settings_change_number_v2.xml
Normal file
@@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/app_settings_change_number"
|
||||
app:startDestination="@id/changePhoneNumberFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changePhoneNumberFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberV2Fragment"
|
||||
tools:layout="@layout/fragment_change_phone_number">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment"
|
||||
app:destination="@id/enterPhoneNumberChangeFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/enterPhoneNumberChangeFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberEnterPhoneNumberV2Fragment"
|
||||
tools:layout="@layout/fragment_change_number_enter_phone_number">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment"
|
||||
app:destination="@id/changePhoneNumberConfirmFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_enterPhoneNumberChangeFragment_to_countryPickerFragment"
|
||||
app:destination="@id/countryPickerFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changePhoneNumberConfirmFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberConfirmV2Fragment"
|
||||
tools:layout="@layout/fragment_change_number_confirm">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment"
|
||||
app:destination="@id/changePhoneNumberVerifyFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@+id/enterPhoneNumberChangeFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/countryPickerFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
|
||||
tools:layout="@layout/fragment_registration_country_picker">
|
||||
|
||||
<argument
|
||||
android:name="result_key"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changePhoneNumberVerifyFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberVerifyV2Fragment"
|
||||
tools:layout="@layout/fragment_change_phone_number_verify">
|
||||
|
||||
<argument android:name="sms_listener_enabled"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changePhoneNumberVerifyFragment_to_captchaFragment"
|
||||
app:destination="@id/captchaFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment"
|
||||
app:destination="@id/changeNumberEnterCodeFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@+id/enterPhoneNumberChangeFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/captchaFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberCaptchaV2Fragment"
|
||||
tools:layout="@layout/fragment_registration_captcha" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changeNumberEnterCodeFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberEnterCodeV2Fragment"
|
||||
tools:layout="@layout/fragment_change_number_enter_code">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changeNumberEnterCodeFragment_to_captchaFragment"
|
||||
app:destination="@id/captchaFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changeNumberEnterCodeFragment_to_changeNumberRegistrationLock"
|
||||
app:destination="@id/changeNumberRegistrationLock"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/enterPhoneNumberChangeFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changeNumberEnterCodeFragment_to_changeNumberAccountLocked"
|
||||
app:destination="@id/changeNumberAccountLocked"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/enterPhoneNumberChangeFragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changeNumberRegistrationLock"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberRegistrationLockV2Fragment"
|
||||
tools:layout="@layout/fragment_change_number_registration_lock">
|
||||
|
||||
<argument
|
||||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changeNumberRegistrationLock_to_changeNumberAccountLocked"
|
||||
app:destination="@id/changeNumberAccountLocked"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/enterPhoneNumberChangeFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changeNumberRegistrationLock_to_changeNumberPinDiffers"
|
||||
app:destination="@id/changeNumberPinDiffers"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/enterPhoneNumberChangeFragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changeNumberAccountLocked"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberAccountLockedV2Fragment"
|
||||
tools:layout="@layout/fragment_change_number_account_locked" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changeNumberPinDiffers"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberPinDiffersV2Fragment"
|
||||
tools:layout="@layout/fragment_change_number_pin_differs" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_pop_app_settings_change_number"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/changePhoneNumberFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</navigation>
|
||||
@@ -0,0 +1,951 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/app_settings"
|
||||
app:startDestination="@id/appSettingsFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/appSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.AppSettingsFragment"
|
||||
android:label="app_settings_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_usernameLinkSettingsFragment"
|
||||
app:destination="@id/username_link_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_usernameEducationFragment"
|
||||
app:destination="@id/manageProfileActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
<argument
|
||||
android:name="start_at_username"
|
||||
android:defaultValue="true"
|
||||
app:argType="boolean" />
|
||||
</action>
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_accountSettingsFragment"
|
||||
app:destination="@id/accountSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_deviceActivity"
|
||||
app:destination="@id/deviceActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_paymentsActivity"
|
||||
app:destination="@id/paymentsActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_appearanceSettingsFragment"
|
||||
app:destination="@id/appearanceSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_chatsSettingsFragment"
|
||||
app:destination="@id/chatsSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_notificationsSettingsFragment"
|
||||
app:destination="@id/notificationsSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_privacySettingsFragment"
|
||||
app:destination="@id/privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_dataAndStorageSettingsFragment"
|
||||
app:destination="@id/dataAndStorageSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_manageProfileActivity"
|
||||
app:destination="@id/manageProfileActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_helpSettingsFragment"
|
||||
app:destination="@id/helpSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_inviteActivity"
|
||||
app:destination="@id/inviteActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_appUpdatesSettingsFragment"
|
||||
app:destination="@id/appUpdatesFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_internalSettingsFragment"
|
||||
app:destination="@id/internalSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_manageDonationsFragment"
|
||||
app:destination="@id/manageDonationsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_storyPrivacySettings"
|
||||
app:destination="@+id/story_privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="title_id"
|
||||
app:argType="integer"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/manageProfileActivity"
|
||||
android:name="org.thoughtcrime.securesms.profiles.manage.EditProfileActivity"
|
||||
android:label="manage_profile_activity" />
|
||||
|
||||
<!-- region Account Settings and subpages -->
|
||||
<fragment
|
||||
android:id="@+id/accountSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.account.AccountSettingsFragment"
|
||||
android:label="account_settings_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_accountSettingsFragment_to_changePhoneNumberFragment"
|
||||
app:destination="@id/app_settings_change_number"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_accountSettingsFragment_to_deleteAccountFragment"
|
||||
app:destination="@id/deleteAccountFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_accountSettingsFragment_to_advancedPinSettingsActivity"
|
||||
app:destination="@id/advancedPinSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_accountSettingsFragment_to_oldDeviceTransferActivity"
|
||||
app:destination="@id/oldDeviceTransferActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_accountSettingsFragment_to_exportAccountFragment"
|
||||
app:destination="@id/exportAccountDataFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/app_settings_change_number_v2" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/advancedPinSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedAdvancedPinPreferenceFragment"
|
||||
android:label="advanced_pin_settings_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/deleteAccountFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedDeleteAccountFragment"
|
||||
android:label="delete_account_fragment"
|
||||
tools:layout="@layout/delete_account_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/exportAccountDataFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.account.export.ExportAccountDataFragment"
|
||||
android:label="export_account_data_fragment" />
|
||||
|
||||
<activity
|
||||
android:id="@+id/oldDeviceTransferActivity"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity"
|
||||
android:label="old_device_transfer_Activity"
|
||||
tools:layout="@layout/old_device_transfer_activity" />
|
||||
|
||||
<!-- CreateKbsPinActivity -->
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Linked Devices -->
|
||||
<activity
|
||||
android:id="@+id/deviceActivity"
|
||||
android:name="org.thoughtcrime.securesms.DeviceActivity"
|
||||
android:label="device_activity" />
|
||||
|
||||
<!-- Payments -->
|
||||
<activity
|
||||
android:id="@+id/paymentsActivity"
|
||||
android:name="org.thoughtcrime.securesms.payments.preferences.PaymentsActivity"
|
||||
android:label="payments_activity" />
|
||||
|
||||
<!-- region Appearance settings and Subpages -->
|
||||
<fragment
|
||||
android:id="@+id/appearanceSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.appearance.AppearanceSettingsFragment"
|
||||
android:label="appearance_settings_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_appearanceSettings_to_wallpaperActivity"
|
||||
app:destination="@id/wallpaperActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_appearanceSettings_to_appIconActivity"
|
||||
app:destination="@id/appIconSelectionFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
android:id="@+id/wallpaperActivity"
|
||||
android:name="org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity"
|
||||
android:label="wallpaper_activity" />
|
||||
|
||||
<!-- App Icon settings and Subpages -->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/appIconSelectionFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.appearance.appicon.AppIconSelectionFragment"
|
||||
android:label="@string/preferences__app_icon">
|
||||
<action
|
||||
android:id="@+id/action_appIconSelectionFragment_to_appIconTutorialFragment"
|
||||
app:destination="@id/appIconTutorialFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/appIconTutorialFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.appearance.appicon.AppIconTutorialFragment"
|
||||
android:label="@string/preferences__app_icon" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- region Chats settings and subpages -->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/chatsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.ChatsSettingsFragment"
|
||||
android:label="chats_settings_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_chatsSettingsFragment_to_backupsPreferenceFragment"
|
||||
app:destination="@id/backupsPreferenceFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_chatsSettingsFragment_to_editReactionsFragment"
|
||||
app:destination="@id/editReactionsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_chatsSettingsFragment_to_remoteBackupsSettingsFragment"
|
||||
app:destination="@id/remoteBackupsSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/backupsPreferenceFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedBackupsPreferenceFragment"
|
||||
android:label="backups_preference_fragment"
|
||||
tools:layout="@layout/fragment_backups" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/editReactionsFragment"
|
||||
android:name="org.thoughtcrime.securesms.reactions.edit.EditReactionsFragment"
|
||||
android:label="edit_reactions_fragment"
|
||||
tools:layout="@layout/edit_reactions_fragment" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Notifications -->
|
||||
<fragment
|
||||
android:id="@+id/notificationsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.NotificationsSettingsFragment"
|
||||
android:label="notifications_settings_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationsSettingsFragment_to_notificationProfilesFragment"
|
||||
app:destination="@id/notificationProfilesFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<!-- region Privacy -->
|
||||
|
||||
<include app:graph="@navigation/privacy_settings" />
|
||||
|
||||
<!-- region Data and Storage -->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/dataAndStorageSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.data.DataAndStorageSettingsFragment"
|
||||
android:label="data_and_storage_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_dataAndStorageSettingsFragment_to_storagePreferenceFragment"
|
||||
app:destination="@id/storagePreferenceFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_dataAndStorageSettingsFragment_to_editProxyFragment"
|
||||
app:destination="@id/editProxyFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/storagePreferenceFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.storage.ManageStorageSettingsFragment"
|
||||
android:label="storage_preference_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/editProxyFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedEditProxyFragment"
|
||||
android:label="edit_proxy_fragment" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- region Help -->
|
||||
<fragment
|
||||
android:id="@+id/helpSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.help.HelpSettingsFragment"
|
||||
android:label="help_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_helpSettingsFragment_to_helpFragment"
|
||||
app:destination="@id/helpFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_helpSettingsFragment_to_submitDebugLogActivity"
|
||||
app:destination="@id/submitDebugLogActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_helpSettingsFragment_to_licenseFragment"
|
||||
app:destination="@id/licenseFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/helpFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedHelpFragment"
|
||||
android:label="help_fragment">
|
||||
|
||||
<argument
|
||||
android:name="start_category_index"
|
||||
android:defaultValue="0"
|
||||
app:argType="integer"
|
||||
app:nullable="false" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/licenseFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.help.LicenseFragment"
|
||||
android:label="license_fragment" />
|
||||
|
||||
<activity
|
||||
android:id="@+id/submitDebugLogActivity"
|
||||
android:name="org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity"
|
||||
android:label="submit_debug_log_activity" />
|
||||
<!-- endregion -->
|
||||
|
||||
<activity
|
||||
android:id="@+id/inviteActivity"
|
||||
android:name="org.thoughtcrime.securesms.InviteActivity"
|
||||
android:label="invite_activity"
|
||||
tools:layout="@layout/invite_activity" />
|
||||
|
||||
<!-- region Direct-To-Page actions -->
|
||||
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_backupsPreferenceFragment"
|
||||
app:destination="@id/backupsPreferenceFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_helpFragment"
|
||||
app:destination="@id/helpFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_editProxyFragment"
|
||||
app:destination="@id/editProxyFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_notificationsSettingsFragment"
|
||||
app:destination="@id/notificationsSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_changeNumberFragment"
|
||||
app:destination="@id/app_settings_change_number"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_manageDonations"
|
||||
app:destination="@id/manageDonationsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_donateToSignal"
|
||||
app:destination="@id/donate_to_signal"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true">
|
||||
|
||||
<argument
|
||||
android:name="start_type"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$Type"
|
||||
app:nullable="false" />
|
||||
|
||||
</action>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_notificationProfiles"
|
||||
app:destination="@id/notificationProfilesFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_createNotificationProfiles"
|
||||
app:destination="@id/editNotificationProfileFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_notificationProfileDetails"
|
||||
app:destination="@id/notificationProfileDetailsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_privacy"
|
||||
app:destination="@id/privacy_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_devices"
|
||||
app:destination="@id/deviceActivity"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_usernameLinkSettings"
|
||||
app:destination="@id/username_link_settings"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_usernameRecovery"
|
||||
app:destination="@id/createUsernameFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true">
|
||||
<argument
|
||||
android:name="mode"
|
||||
android:defaultValue="RECOVERY"
|
||||
app:argType="org.thoughtcrime.securesms.profiles.manage.UsernameEditMode" />
|
||||
</action>
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Internal Settings -->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalSettingsFragment"
|
||||
android:label="internal_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_donorErrorConfigurationFragment"
|
||||
app:destination="@id/donorErrorConfigurationFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_storyDialogsLauncherFragment"
|
||||
app:destination="@id/storyDialogsLauncherFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalSearchFragment"
|
||||
app:destination="@id/internalSearchFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalSvrPlaygroundFragment"
|
||||
app:destination="@id/internalSvrPlaygroundFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalConversationSpringboardFragment"
|
||||
app:destination="@id/internalConversationSpringboardFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_oneTimeDonationConfigurationFragment"
|
||||
app:destination="@id/oneTimeDonationConfigurationFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_terminalDonationConfigurationFragment"
|
||||
app:destination="@id/terminalDonationConfigurationFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalBackupPlaygroundFragment"
|
||||
app:destination="@id/internalBackupPlaygroundFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/terminalDonationConfigurationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalTerminalDonationConfigurationFragment"
|
||||
android:label="terminal_donation_configuration_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/oneTimeDonationConfigurationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalPendingOneTimeDonationConfigurationFragment"
|
||||
android:label="one_time_donation_configuration_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donorErrorConfigurationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.donor.InternalDonorErrorConfigurationFragment"
|
||||
android:label="donor_error_configuration_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/storyDialogsLauncherFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalStoryDialogLauncherFragment"
|
||||
android:label="story_dialogs_launcher_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalSearchFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.search.InternalSearchFragment"
|
||||
android:label="internal_search_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalSvrPlaygroundFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.svr.InternalSvrPlaygroundFragment"
|
||||
android:label="internal_svr_playground_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalConversationSpringboardFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.conversation.springboard.InternalConversationSpringboardFragment"
|
||||
android:label="internal_conversation_springboard_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_internalConversationSpringboardFragment_to_internalConversationTestFragment"
|
||||
app:destination="@id/internalConversationTestFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalConversationTestFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.conversation.test.InternalConversationTestFragment"
|
||||
android:label="internal_conversation_test_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalBackupPlaygroundFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundFragment"
|
||||
android:label="internal_backup_playground_fragment" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- App updates -->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/appUpdatesFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.updates.AppUpdatesSettingsFragment"
|
||||
android:label="app_update_fragment" />
|
||||
|
||||
<!-- Subscriptions -->
|
||||
<fragment
|
||||
android:id="@+id/manageDonationsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.manage.ManageDonationsFragment"
|
||||
android:label="manage_donations_fragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_manage_badges"
|
||||
app:destination="@id/manage_badges"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_donationReceiptListFragment"
|
||||
app:destination="@id/donationReceiptListFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_subscribeLearnMoreBottomSheetDialog"
|
||||
app:destination="@id/subscribeLearnMoreBottomSheetDialog" />
|
||||
<action
|
||||
android:id="@+id/action_manageDonationsFragment_to_donateToSignalFragment"
|
||||
app:destination="@id/donate_to_signal"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="start_type"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$Type"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donationReceiptListFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list.DonationReceiptListFragment"
|
||||
android:label="donation_receipt_list_fragment"
|
||||
tools:layout="@layout/donation_receipt_list_fragment">
|
||||
<action
|
||||
android:id="@+id/action_donationReceiptListFragment_to_donationReceiptDetailFragment"
|
||||
app:destination="@id/donationReceiptDetailFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/donationReceiptDetailFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail.DonationReceiptDetailFragment"
|
||||
android:label="donation_receipt_detail_fragment"
|
||||
tools:layout="@layout/donation_receipt_detail_fragment">
|
||||
|
||||
<argument
|
||||
android:name="id"
|
||||
app:argType="long" />
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/donate_to_signal" />
|
||||
|
||||
<dialog
|
||||
android:id="@+id/subscribeLearnMoreBottomSheetDialog"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.subscribe.SubscribeLearnMoreBottomSheetDialogFragment"
|
||||
android:label="subscribe_learn_more_bottom_sheet_dialog"
|
||||
tools:layout="@layout/subscribe_learn_more_bottom_sheet_dialog_fragment" />
|
||||
|
||||
<include app:graph="@navigation/manage_badges" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Notification Profiles -->
|
||||
<fragment
|
||||
android:id="@+id/notificationProfilesFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesFragment"
|
||||
tools:layout="@layout/notification_profiles_empty">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationProfilesFragment_to_editNotificationProfileFragment"
|
||||
app:destination="@id/editNotificationProfileFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationProfilesFragment_to_notificationProfileDetailsFragment"
|
||||
app:destination="@id/notificationProfileDetailsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/editNotificationProfileFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileFragment"
|
||||
tools:layout="@layout/fragment_edit_notification_profile">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
android:defaultValue="-1L"
|
||||
app:argType="long" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_editNotificationProfileFragment_to_addAllowedMembersFragment"
|
||||
app:destination="@id/addAllowedMembersFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/editNotificationProfileFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/addAllowedMembersFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersFragment"
|
||||
tools:layout="@layout/fragment_add_allowed_members">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
app:argType="long" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_addAllowedMembersFragment_to_selectRecipientsFragment"
|
||||
app:destination="@id/selectRecipientsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_addAllowedMembersFragment_to_editNotificationProfileScheduleFragment"
|
||||
app:destination="@id/editNotificationProfileScheduleFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/selectRecipientsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.SelectRecipientsFragment"
|
||||
tools:layout="@layout/fragment_select_recipients_fragment">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
app:argType="long" />
|
||||
|
||||
<argument
|
||||
android:name="currentSelection"
|
||||
android:defaultValue="@null"
|
||||
app:argType="org.thoughtcrime.securesms.recipients.RecipientId[]"
|
||||
app:nullable="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/notificationProfileDetailsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfileDetailsFragment"
|
||||
tools:layout="@layout/dsl_settings_fragment">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
app:argType="long" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationProfileDetailsFragment_to_selectRecipientsFragment"
|
||||
app:destination="@id/selectRecipientsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationProfileDetailsFragment_to_editNotificationProfileFragment"
|
||||
app:destination="@id/editNotificationProfileFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationProfileDetailsFragment_to_editNotificationProfileScheduleFragment"
|
||||
app:destination="@id/editNotificationProfileScheduleFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/editNotificationProfileScheduleFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragment"
|
||||
tools:layout="@layout/fragment_edit_notification_profile_schedule">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
app:argType="long" />
|
||||
|
||||
<argument
|
||||
android:name="createMode"
|
||||
app:argType="boolean" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_editNotificationProfileScheduleFragment_to_notificationProfileCreatedFragment"
|
||||
app:destination="@id/notificationProfileCreatedFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/addAllowedMembersFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/notificationProfileCreatedFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfileCreatedFragment"
|
||||
tools:layout="@layout/fragment_notification_profile_created">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
app:argType="long" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_notificationProfileCreatedFragment_to_notificationProfileDetailsFragment"
|
||||
app:destination="@id/notificationProfileDetailsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/notificationProfileCreatedFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/createUsernameFragment"
|
||||
android:name="org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment"
|
||||
tools:layout="@layout/username_edit_fragment">
|
||||
|
||||
<argument
|
||||
android:name="mode"
|
||||
android:defaultValue="NORMAL"
|
||||
app:argType="org.thoughtcrime.securesms.profiles.manage.UsernameEditMode" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/remoteBackupsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.RemoteBackupsSettingsFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment"
|
||||
app:destination="@id/backupsTypeSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/backupsTypeSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.chats.backups.type.BackupsTypeSettingsFragment" />
|
||||
|
||||
<include app:graph="@navigation/username_link_settings" />
|
||||
<include app:graph="@navigation/story_privacy_settings" />
|
||||
|
||||
</navigation>
|
||||
Reference in New Issue
Block a user