kopia lustrzana https://github.com/ryukoposting/Signal-Android
Initial work to support Change Number.
rodzic
e09d162c1e
commit
f2ab0b6423
|
@ -419,12 +419,12 @@ dependencies {
|
|||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.3.1'
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.fragment.app.setFragmentResultListener
|
|||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
|
@ -110,7 +111,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
|||
putParcelable(SELECT_AVATAR_MEDIA, it)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
},
|
||||
{
|
||||
setFragmentResult(
|
||||
|
@ -119,7 +120,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
|||
putBoolean(SELECT_AVATAR_CLEAR, true)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||
.setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0))
|
||||
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,6 +99,9 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||
@JvmStatic
|
||||
fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS)
|
||||
|
||||
@JvmStatic
|
||||
fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER)
|
||||
|
||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||
return Intent(context, AppSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||
|
@ -110,7 +114,8 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||
BACKUPS(1),
|
||||
HELP(2),
|
||||
PROXY(3),
|
||||
NOTIFICATIONS(4);
|
||||
NOTIFICATIONS(4),
|
||||
CHANGE_NUMBER(5);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int?): StartLocation {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
|
|||
import org.thoughtcrime.securesms.lock.v2.KbsConstants
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
|
||||
|
@ -103,6 +104,15 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
|||
|
||||
sectionHeaderPref(R.string.AccountSettingsFragment__account)
|
||||
|
||||
if (FeatureFlags.changeNumber()) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
|
||||
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseAccountLockedFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
|
||||
class ChangeNumberAccountLockedFragment : BaseAccountLockedFragment(R.layout.fragment_change_number_account_locked) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseRegistrationViewModel {
|
||||
return ChangeNumberUtil.getViewModel(this)
|
||||
}
|
||||
|
||||
override fun onNext() {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel = ChangeNumberUtil.getViewModel(this)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
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 { findNavController().navigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseEnterCodeFragment
|
||||
|
||||
class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.title = viewModel.number.fullFormattedNumber
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.verify_header).setOnClickListener(null)
|
||||
}
|
||||
|
||||
override fun getViewModel(): ChangeNumberViewModel {
|
||||
return getViewModel(this)
|
||||
}
|
||||
|
||||
override fun handleSuccessfulVerify() {
|
||||
displaySuccess { changeNumberSuccess() }
|
||||
}
|
||||
|
||||
override fun navigateToCaptcha() {
|
||||
findNavController().navigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
|
||||
}
|
||||
|
||||
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
||||
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
|
||||
override fun navigateToKbsAccountLocked() {
|
||||
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel.ContinueStatus
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment
|
||||
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController
|
||||
import org.thoughtcrime.securesms.util.Dialogs
|
||||
|
||||
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
|
||||
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
|
||||
|
||||
class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number) {
|
||||
|
||||
private lateinit var scrollView: ScrollView
|
||||
|
||||
private lateinit var oldNumberCountrySpinner: Spinner
|
||||
private lateinit var oldNumberCountryCode: LabeledEditText
|
||||
private lateinit var oldNumber: LabeledEditText
|
||||
|
||||
private lateinit var newNumberCountrySpinner: Spinner
|
||||
private lateinit var newNumberCountryCode: LabeledEditText
|
||||
private lateinit var newNumber: LabeledEditText
|
||||
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel = getViewModel(this)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_number_enter_phone_number_continue).setOnClickListener {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
scrollView = view.findViewById(R.id.change_number_enter_phone_number_scroll)
|
||||
|
||||
oldNumberCountrySpinner = view.findViewById(R.id.change_number_enter_phone_number_old_number_spinner)
|
||||
oldNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_old_number_country_code)
|
||||
oldNumber = view.findViewById(R.id.change_number_enter_phone_number_old_number_number)
|
||||
|
||||
val oldController = RegistrationNumberInputController(
|
||||
requireContext(),
|
||||
oldNumberCountryCode,
|
||||
oldNumber,
|
||||
oldNumberCountrySpinner,
|
||||
false,
|
||||
object : RegistrationNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
scrollView.postDelayed({ scrollView.smoothScrollTo(0, oldNumber.bottom) }, 250)
|
||||
}
|
||||
|
||||
override fun onNumberInputNext(view: View) {
|
||||
newNumberCountryCode.requestFocus()
|
||||
}
|
||||
|
||||
override fun onNumberInputDone(view: View) = Unit
|
||||
|
||||
override fun onPickCountry(view: View) {
|
||||
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build()
|
||||
|
||||
findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
|
||||
}
|
||||
|
||||
override fun setNationalNumber(number: String) {
|
||||
viewModel.setOldNationalNumber(number)
|
||||
}
|
||||
|
||||
override fun setCountry(countryCode: Int) {
|
||||
viewModel.setOldCountry(countryCode)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
newNumberCountrySpinner = view.findViewById(R.id.change_number_enter_phone_number_new_number_spinner)
|
||||
newNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_new_number_country_code)
|
||||
newNumber = view.findViewById(R.id.change_number_enter_phone_number_new_number_number)
|
||||
|
||||
val newController = RegistrationNumberInputController(
|
||||
requireContext(),
|
||||
newNumberCountryCode,
|
||||
newNumber,
|
||||
newNumberCountrySpinner,
|
||||
true,
|
||||
object : RegistrationNumberInputController.Callbacks {
|
||||
override fun onNumberFocused() {
|
||||
scrollView.postDelayed({ scrollView.smoothScrollTo(0, newNumber.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().navigate(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) { _, bundle ->
|
||||
viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
|
||||
viewModel.setNewCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
|
||||
}
|
||||
|
||||
viewModel.getLiveOldNumber().observe(viewLifecycleOwner, oldController::updateNumber)
|
||||
viewModel.getLiveNewNumber().observe(viewLifecycleOwner, newController::updateNumber)
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
if (TextUtils.isEmpty(oldNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(oldNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(newNumberCountryCode.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(newNumber.text)) {
|
||||
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
when (viewModel.canContinue()) {
|
||||
ContinueStatus.CAN_CONTINUE -> findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment)
|
||||
ContinueStatus.INVALID_NUMBER -> {
|
||||
Dialogs.showAlertDialog(
|
||||
context, getString(R.string.RegistrationActivity_invalid_number), String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.number.e164Number)
|
||||
)
|
||||
}
|
||||
ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> {
|
||||
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,19 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_number) {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
|
||||
view.findViewById<View>(R.id.change_phone_number_continue).setOnClickListener {
|
||||
findNavController().navigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
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.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
|
||||
|
||||
class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) {
|
||||
|
||||
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 == CreateKbsPinActivity.RESULT_OK) {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.change_number_pin_differs_update_pin).setOnClickListener {
|
||||
changePin.launch(CreateKbsPinActivity.getIntentForPinChangeFromSettings(requireContext()))
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.PinHashing
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
|
||||
class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseRegistrationViewModel {
|
||||
return ChangeNumberUtil.getViewModel(this)
|
||||
}
|
||||
|
||||
override fun navigateToAccountLocked() {
|
||||
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
|
||||
}
|
||||
|
||||
override fun handleSuccessfulPinEntry(pin: String) {
|
||||
val pinsDiffer: Boolean = SignalStore.kbsValues().localPinHash?.let { !PinHashing.verifyLocalPinHash(it, pin) } ?: false
|
||||
|
||||
cancelSpinning(pinButton)
|
||||
|
||||
if (pinsDiffer) {
|
||||
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
|
||||
} else {
|
||||
changeNumberSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
override 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
|
||||
class ChangeNumberRepository(private val context: Context) {
|
||||
|
||||
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
|
||||
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun changeNumber(
|
||||
code: String,
|
||||
newE164: String,
|
||||
pin: String,
|
||||
tokenData: TokenData
|
||||
): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
|
||||
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String): Single<Unit> {
|
||||
TextSecurePreferences.setLocalNumber(context, e164)
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).updateSelfPhone(e164)
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
|
||||
return rotateCertificates()
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
private fun rotateCertificates(): Single<Unit> {
|
||||
val certificateTypes = SignalStore.phoneNumberPrivacy().allCertificateTypes
|
||||
|
||||
Log.i(TAG, "Rotating these certificates $certificateTypes")
|
||||
|
||||
return Single.fromCallable {
|
||||
for (certificateType in certificateTypes) {
|
||||
val certificate: ByteArray? = when (certificateType) {
|
||||
CertificateType.UUID_AND_E164 -> accountManager.getSenderCertificate()
|
||||
CertificateType.UUID_ONLY -> accountManager.getSenderCertificateForPhoneNumberPrivacy()
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully got $certificateType certificate")
|
||||
|
||||
SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.fragments.CaptchaFragment
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
|
||||
/**
|
||||
* Helpers for various aspects of the change number flow.
|
||||
*/
|
||||
object ChangeNumberUtil {
|
||||
@JvmStatic
|
||||
fun getViewModel(fragment: Fragment): ChangeNumberViewModel {
|
||||
val navController = NavHostFragment.findNavController(fragment)
|
||||
return ViewModelProvider(
|
||||
navController.getViewModelStoreOwner(R.id.app_settings_change_number),
|
||||
ChangeNumberViewModel.Factory(navController.getBackStackEntry(R.id.app_settings_change_number))
|
||||
).get(ChangeNumberViewModel::class.java)
|
||||
}
|
||||
|
||||
fun getCaptchaArguments(): Bundle {
|
||||
return Bundle().apply {
|
||||
putSerializable(
|
||||
CaptchaFragment.EXTRA_VIEW_MODEL_PROVIDER,
|
||||
object : CaptchaFragment.CaptchaViewModelProvider {
|
||||
override fun get(fragment: CaptchaFragment): BaseRegistrationViewModel = getViewModel(fragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.changeNumberSuccess() {
|
||||
findNavController().navigate(R.id.action_pop_app_settings_change_number)
|
||||
Toast.makeText(requireContext(), R.string.ChangeNumber__your_phone_number_has_been_changed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
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.getCaptchaArguments
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
|
||||
|
||||
class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phone_number_verify) {
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
private var requestingCaptcha: Boolean = false
|
||||
|
||||
private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleDisposable.bindTo(lifecycle)
|
||||
viewModel = getViewModel(this)
|
||||
}
|
||||
|
||||
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() }
|
||||
|
||||
val status: TextView = view.findViewById(R.id.change_phone_number_verify_status)
|
||||
status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber)
|
||||
|
||||
if (!requestingCaptcha || viewModel.hasCaptchaToken()) {
|
||||
requestCode()
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.ChangeNumberVerifyFragment__captcha_required, Toast.LENGTH_SHORT).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
lifecycleDisposable.add(
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.app.Application
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs
|
||||
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberViewModel::class.java)
|
||||
|
||||
class ChangeNumberViewModel(
|
||||
private val localNumber: String,
|
||||
private val changeNumberRepository: ChangeNumberRepository,
|
||||
savedState: SavedStateHandle,
|
||||
password: String,
|
||||
verifyAccountRepository: VerifyAccountRepository,
|
||||
kbsRepository: KbsRepository,
|
||||
) : BaseRegistrationViewModel(savedState, verifyAccountRepository, kbsRepository, password) {
|
||||
|
||||
var oldNumberState: NumberViewState = NumberViewState.Builder().build()
|
||||
private set
|
||||
|
||||
private val liveOldNumberState = DefaultValueLiveData(oldNumberState)
|
||||
private val liveNewNumberState = DefaultValueLiveData(number)
|
||||
|
||||
init {
|
||||
try {
|
||||
val countryCode: Int = PhoneNumberUtil.getInstance()
|
||||
.parse(localNumber, null)
|
||||
.countryCode
|
||||
|
||||
setOldCountry(countryCode)
|
||||
setNewCountry(countryCode)
|
||||
} catch (e: NumberParseException) {
|
||||
Log.i(TAG, "Unable to parse number for default country code")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLiveOldNumber(): LiveData<NumberViewState> {
|
||||
return liveOldNumberState
|
||||
}
|
||||
|
||||
fun getLiveNewNumber(): LiveData<NumberViewState> {
|
||||
return liveNewNumberState
|
||||
}
|
||||
|
||||
fun setOldNationalNumber(number: String) {
|
||||
oldNumberState = oldNumberState.toBuilder()
|
||||
.nationalNumber(number)
|
||||
.build()
|
||||
|
||||
liveOldNumberState.value = oldNumberState
|
||||
}
|
||||
|
||||
fun setOldCountry(countryCode: Int, country: String? = null) {
|
||||
oldNumberState = oldNumberState.toBuilder()
|
||||
.selectedCountryDisplayName(country)
|
||||
.countryCode(countryCode)
|
||||
.build()
|
||||
|
||||
liveOldNumberState.value = oldNumberState
|
||||
}
|
||||
|
||||
fun setNewNationalNumber(number: String) {
|
||||
setNationalNumber(number)
|
||||
|
||||
liveNewNumberState.value = this.number
|
||||
}
|
||||
|
||||
fun setNewCountry(countryCode: Int, country: String? = null) {
|
||||
onCountrySelected(country, countryCode)
|
||||
|
||||
liveNewNumberState.value = this.number
|
||||
}
|
||||
|
||||
fun canContinue(): ContinueStatus {
|
||||
return if (oldNumberState.e164Number == localNumber) {
|
||||
if (number.isValid) {
|
||||
ContinueStatus.CAN_CONTINUE
|
||||
} else {
|
||||
ContinueStatus.INVALID_NUMBER
|
||||
}
|
||||
} else {
|
||||
ContinueStatus.OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
||||
|
||||
override fun verifyAccountWithoutRegistrationLock(): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number)
|
||||
}
|
||||
|
||||
override fun verifyAccountWithRegistrationLock(pin: String, kbsTokenData: TokenData): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
|
||||
return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number, pin, kbsTokenData)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onVerifySuccess(processor: VerifyAccountResponseProcessor): Single<VerifyAccountResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number)
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
VerifyAccountResponseWithoutKbs(ServiceResponse.forUnknownError(t))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVerifySuccessWithRegistrationLock(processor: VerifyCodeWithRegistrationLockResponseProcessor, pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
|
||||
return changeNumberRepository.changeLocalNumber(number.e164Number)
|
||||
.map { processor }
|
||||
.onErrorReturn { t ->
|
||||
Log.w(TAG, "Error attempting to change local number", t)
|
||||
VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.forUnknownError(t), processor.token)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(owner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(owner, null) {
|
||||
|
||||
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
||||
val context: Application = ApplicationDependencies.getApplication()
|
||||
val localNumber: String = TextSecurePreferences.getLocalNumber(context)
|
||||
val password: String = TextSecurePreferences.getPushServerPassword(context)
|
||||
|
||||
val viewModel = ChangeNumberViewModel(
|
||||
localNumber = localNumber,
|
||||
changeNumberRepository = ChangeNumberRepository(context),
|
||||
savedState = handle,
|
||||
password = password,
|
||||
verifyAccountRepository = VerifyAccountRepository(context),
|
||||
kbsRepository = KbsRepository()
|
||||
)
|
||||
|
||||
return requireNotNull(modelClass.cast(viewModel))
|
||||
}
|
||||
}
|
||||
|
||||
enum class ContinueStatus {
|
||||
CAN_CONTINUE,
|
||||
INVALID_NUMBER,
|
||||
OLD_NUMBER_DOESNT_MATCH
|
||||
}
|
||||
}
|
|
@ -2034,6 +2034,25 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void updateSelfPhone(@NonNull String e164) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
RecipientId id = Recipient.self().getId();
|
||||
RecipientId newId = getAndPossiblyMerge(Recipient.self().requireUuid(), e164, true);
|
||||
|
||||
if (id.equals(newId)) {
|
||||
Log.i(TAG, "[updateSelfPhone] Phone updated for self");
|
||||
} else {
|
||||
throw new AssertionError("[updateSelfPhone] Self recipient id changed when updating phone. old: " + id + " new: " + newId);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void setUsername(@NonNull RecipientId id, @Nullable String username) {
|
||||
if (username != null) {
|
||||
Optional<RecipientId> existingUsername = getByUsername(username);
|
||||
|
|
|
@ -83,6 +83,7 @@ public class ApplicationMigrations {
|
|||
static final int ANNOUNCEMENT_GROUP_CAPABILITY = 41;
|
||||
static final int STICKER_MY_DAILY_LIFE = 42;
|
||||
static final int SENDER_KEY_3 = 43;
|
||||
static final int CHANGE_NUMBER_SYNC = 44;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 43;
|
||||
|
@ -367,6 +368,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.SENDER_KEY_3, new AttributesMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.CHANGE_NUMBER_SYNC) {
|
||||
jobs.put(Version.CHANGE_NUMBER_SYNC, new AccountRecordMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever;
|
||||
import com.google.android.gms.common.api.CommonStatusCodes;
|
||||
|
@ -59,7 +59,7 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = ViewModelProviders.of(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class);
|
||||
|
||||
setContentView(R.layout.activity_registration_navigation);
|
||||
initializeChallengeListener();
|
||||
|
|
|
@ -1,68 +1,24 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
public class AccountLockedFragment extends BaseAccountLockedFragment {
|
||||
|
||||
public class AccountLockedFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.account_locked_fragment, container, false);
|
||||
public AccountLockedFragment() {
|
||||
super(R.layout.account_locked_fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
TextView description = view.findViewById(R.id.account_locked_description);
|
||||
|
||||
RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(),
|
||||
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
|
||||
);
|
||||
|
||||
view.findViewById(R.id.account_locked_next).setOnClickListener(v -> onNext());
|
||||
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(v -> learnMore());
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onNext();
|
||||
}
|
||||
});
|
||||
protected BaseRegistrationViewModel getViewModel() {
|
||||
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
private void learnMore() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private static long durationToDays(Long duration) {
|
||||
return duration != null ? getLockoutDays(duration) : 7;
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void onNext() {
|
||||
@Override
|
||||
protected void onNext() {
|
||||
requireActivity().finish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Base fragment used by registration and change number flow to show an account as locked.
|
||||
*/
|
||||
public abstract class BaseAccountLockedFragment extends LoggingFragment {
|
||||
|
||||
public BaseAccountLockedFragment(int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
TextView description = view.findViewById(R.id.account_locked_description);
|
||||
|
||||
BaseRegistrationViewModel viewModel = getViewModel();
|
||||
viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(),
|
||||
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
|
||||
);
|
||||
|
||||
view.findViewById(R.id.account_locked_next).setOnClickListener(v -> onNext());
|
||||
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(v -> learnMore());
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void learnMore() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private static long durationToDays(long duration) {
|
||||
return duration != 0L ? getLockoutDays(duration) : 7;
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
protected abstract BaseRegistrationViewModel getViewModel();
|
||||
|
||||
protected abstract void onNext();
|
||||
}
|
|
@ -0,0 +1,387 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.registration.CallMeCountDownView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
/**
|
||||
* Base fragment used by registration and change number flow to input an SMS verification code or request a
|
||||
* phone code after requesting SMS.
|
||||
*
|
||||
* @param <ViewModel> - The concrete view model used by the subclasses, for ease of access in said subclass
|
||||
*/
|
||||
public abstract class BaseEnterCodeFragment<ViewModel extends BaseRegistrationViewModel> extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(BaseEnterCodeFragment.class);
|
||||
|
||||
private ScrollView scrollView;
|
||||
private TextView header;
|
||||
private VerificationCodeView verificationCodeView;
|
||||
private VerificationPinKeyboard keyboard;
|
||||
private CallMeCountDownView callMeCountDown;
|
||||
private View wrongNumber;
|
||||
private View noCodeReceivedHelp;
|
||||
private View serviceWarning;
|
||||
private boolean autoCompleting;
|
||||
|
||||
private ViewModel viewModel;
|
||||
|
||||
protected final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
public BaseEnterCodeFragment(@LayoutRes int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
|
||||
|
||||
scrollView = view.findViewById(R.id.scroll_view);
|
||||
header = view.findViewById(R.id.verify_header);
|
||||
verificationCodeView = view.findViewById(R.id.code);
|
||||
keyboard = view.findViewById(R.id.keyboard);
|
||||
callMeCountDown = view.findViewById(R.id.call_me_count_down);
|
||||
wrongNumber = view.findViewById(R.id.wrong_number);
|
||||
noCodeReceivedHelp = view.findViewById(R.id.no_code);
|
||||
serviceWarning = view.findViewById(R.id.cell_service_warning);
|
||||
|
||||
new SignalStrengthPhoneStateListener(this, this);
|
||||
|
||||
connectKeyboard(verificationCodeView, keyboard);
|
||||
ViewUtil.hideKeyboard(requireContext(), view);
|
||||
|
||||
setOnCodeFullyEnteredListener(verificationCodeView);
|
||||
|
||||
wrongNumber.setOnClickListener(v -> onWrongNumber());
|
||||
|
||||
callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest());
|
||||
|
||||
callMeCountDown.setListener((v, remaining) -> {
|
||||
if (remaining <= 30) {
|
||||
scrollView.smoothScrollTo(0, v.getBottom());
|
||||
callMeCountDown.setListener(null);
|
||||
}
|
||||
});
|
||||
|
||||
noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport());
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = getViewModel();
|
||||
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
|
||||
if (attempts >= 3) {
|
||||
noCodeReceivedHelp.setVisibility(View.VISIBLE);
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000);
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.onStartEnterCode();
|
||||
}
|
||||
|
||||
protected abstract ViewModel getViewModel();
|
||||
|
||||
protected abstract void handleSuccessfulVerify();
|
||||
|
||||
protected abstract void navigateToCaptcha();
|
||||
|
||||
protected abstract void navigateToRegistrationLock(long timeRemaining);
|
||||
|
||||
protected abstract void navigateToKbsAccountLocked();
|
||||
|
||||
private void onWrongNumber() {
|
||||
Navigation.findNavController(requireView()).navigateUp();
|
||||
}
|
||||
|
||||
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
|
||||
verificationCodeView.setOnCompleteListener(code -> {
|
||||
|
||||
callMeCountDown.setVisibility(View.INVISIBLE);
|
||||
wrongNumber.setVisibility(View.INVISIBLE);
|
||||
keyboard.displayProgress();
|
||||
|
||||
Disposable verify = viewModel.verifyCodeWithoutRegistrationLock(code)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (!processor.hasResult()) {
|
||||
Log.w(TAG, "post verify: ", processor.getError());
|
||||
}
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulVerify();
|
||||
} else if (processor.rateLimit()) {
|
||||
handleRateLimited();
|
||||
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
LockedException lockedException = processor.getLockedException();
|
||||
handleRegistrationLock(lockedException.getTimeRemaining());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
handleKbsAccountLocked();
|
||||
} else if (processor.authorizationFailed()) {
|
||||
handleIncorrectCodeError();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code", processor.getError());
|
||||
handleGeneralError();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(verify);
|
||||
});
|
||||
}
|
||||
|
||||
protected void displaySuccess(@NonNull Runnable runAfterAnimation) {
|
||||
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
runAfterAnimation.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleRateLimited() {
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
|
||||
|
||||
builder.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleRegistrationLock(long timeRemaining) {
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
navigateToRegistrationLock(timeRemaining);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleKbsAccountLocked() {
|
||||
navigateToKbsAccountLocked();
|
||||
}
|
||||
|
||||
protected void handleIncorrectCodeError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleGeneralError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onVerificationCodeReceived(@NonNull ReceivedSmsEvent event) {
|
||||
verificationCodeView.clear();
|
||||
|
||||
List<Integer> parsedCode = convertVerificationCodeToDigits(event.getCode());
|
||||
|
||||
autoCompleting = true;
|
||||
|
||||
final int size = parsedCode.size();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
final int index = i;
|
||||
verificationCodeView.postDelayed(() -> {
|
||||
verificationCodeView.append(parsedCode.get(index));
|
||||
if (index == size - 1) {
|
||||
autoCompleting = false;
|
||||
}
|
||||
}, i * 200L);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Integer> convertVerificationCodeToDigits(@Nullable String code) {
|
||||
if (code == null || code.length() != 6) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Integer> result = new ArrayList<>(code.length());
|
||||
|
||||
try {
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
result.add(Integer.parseInt(Character.toString(code.charAt(i))));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Failed to convert code into digits.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequest() {
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
|
||||
viewModel.getNumber().getE164Number(),
|
||||
this::handlePhoneCallRequestAfterConfirm,
|
||||
this::onWrongNumber);
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequestAfterConfirm() {
|
||||
Disposable request = viewModel.requestVerificationCode(VerifyAccountRepository.Mode.PHONE_CALL)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_call_requested, Toast.LENGTH_LONG).show();
|
||||
} else if (processor.captchaRequired()) {
|
||||
navigateToCaptcha();
|
||||
} else if (processor.rateLimit()) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request phone code", processor.getError());
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) {
|
||||
keyboard.setOnKeyPressListener(key -> {
|
||||
if (!autoCompleting) {
|
||||
if (key >= 0) {
|
||||
verificationCodeView.append(key);
|
||||
} else {
|
||||
verificationCodeView.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
|
||||
|
||||
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
|
||||
}
|
||||
|
||||
private void sendEmailToSupport() {
|
||||
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
|
||||
R.string.RegistrationActivity_code_support_subject,
|
||||
null,
|
||||
null);
|
||||
CommunicationActions.openEmail(requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(R.string.RegistrationActivity_code_support_subject),
|
||||
body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoCellSignalPresent() {
|
||||
if (serviceWarning.getVisibility() == View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
serviceWarning.setVisibility(View.VISIBLE);
|
||||
serviceWarning.animate()
|
||||
.alpha(1)
|
||||
.setListener(null)
|
||||
.start();
|
||||
|
||||
scrollView.postDelayed(() -> {
|
||||
if (serviceWarning.getVisibility() == View.VISIBLE) {
|
||||
scrollView.smoothScrollTo(0, serviceWarning.getBottom());
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCellSignalPresent() {
|
||||
if (serviceWarning.getVisibility() != View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
serviceWarning.animate()
|
||||
.alpha(0)
|
||||
.setListener(new Animator.AnimatorListener() {
|
||||
@Override public void onAnimationEnd(Animator animation) {
|
||||
serviceWarning.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override public void onAnimationRepeat(Animator animation) {}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.pin.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
/**
|
||||
* Base fragment used by registration and change number flow to deal with a registration locked account.
|
||||
*/
|
||||
public abstract class BaseRegistrationLockFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(BaseRegistrationLockFragment.class);
|
||||
|
||||
/**
|
||||
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
|
||||
*/
|
||||
private static final int MINIMUM_PIN_LENGTH = 4;
|
||||
|
||||
private EditText pinEntry;
|
||||
private View forgotPin;
|
||||
protected CircularProgressButton pinButton;
|
||||
private TextView errorLabel;
|
||||
private TextView keyboardToggle;
|
||||
private long timeRemaining;
|
||||
|
||||
private BaseRegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
public BaseRegistrationLockFragment(int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
|
||||
|
||||
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
|
||||
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
|
||||
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
|
||||
forgotPin = view.findViewById(R.id.kbs_lock_forgot_pin);
|
||||
|
||||
RegistrationLockFragmentArgs args = RegistrationLockFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
timeRemaining = args.getTimeRemaining();
|
||||
|
||||
forgotPin.setVisibility(View.GONE);
|
||||
forgotPin.setOnClickListener(v -> handleForgottenPin(timeRemaining));
|
||||
|
||||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
handlePinEntry();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
pinButton.setOnClickListener((v) -> {
|
||||
ViewUtil.hideKeyboard(requireContext(), pinEntry);
|
||||
handlePinEntry();
|
||||
});
|
||||
|
||||
keyboardToggle.setOnClickListener((v) -> {
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType();
|
||||
|
||||
updateKeyboard(keyboardType.getOther());
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = getViewModel();
|
||||
|
||||
viewModel.getLockedTimeRemaining()
|
||||
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
|
||||
|
||||
TokenData keyBackupCurrentToken = viewModel.getKeyBackupCurrentToken();
|
||||
|
||||
if (keyBackupCurrentToken != null) {
|
||||
int triesRemaining = keyBackupCurrentToken.getTriesRemaining();
|
||||
if (triesRemaining <= 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining < 5) {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract BaseRegistrationViewModel getViewModel();
|
||||
|
||||
private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) {
|
||||
Resources resources = requireContext().getResources();
|
||||
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
|
||||
String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining);
|
||||
|
||||
return tries + " " + days;
|
||||
}
|
||||
|
||||
protected PinKeyboardType getPinEntryKeyboardType() {
|
||||
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
|
||||
|
||||
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
|
||||
}
|
||||
|
||||
private void handlePinEntry() {
|
||||
pinEntry.setEnabled(false);
|
||||
|
||||
final String pin = pinEntry.getText().toString();
|
||||
|
||||
int trimmedLength = pin.replace(" ", "").length();
|
||||
if (trimmedLength == 0) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedLength < MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
setSpinning(pinButton);
|
||||
|
||||
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulPinEntry(pin);
|
||||
} else if (processor.wrongPin()) {
|
||||
onIncorrectKbsRegistrationLockPin(processor.getToken());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
onKbsAccountLocked();
|
||||
} else if (processor.rateLimit()) {
|
||||
onRateLimited();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", processor.getError());
|
||||
onError();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(verify);
|
||||
}
|
||||
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
viewModel.setKeyBackupTokenData(tokenData);
|
||||
|
||||
int triesRemaining = tokenData.getTriesRemaining();
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
return;
|
||||
}
|
||||
|
||||
if (triesRemaining == 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
forgotPin.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onRateLimited() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
public void onKbsAccountLocked() {
|
||||
onAccountLocked();
|
||||
}
|
||||
|
||||
public void onError() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void handleForgottenPin(long timeRemainingMs) {
|
||||
int lockoutDays = getLockoutDays(timeRemainingMs);
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
|
||||
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
|
||||
.show();
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void onAccountLocked() {
|
||||
navigateToAccountLocked();
|
||||
}
|
||||
|
||||
protected abstract void navigateToAccountLocked();
|
||||
|
||||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
|
||||
pinEntry.getText().clear();
|
||||
}
|
||||
|
||||
private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) {
|
||||
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.RegistrationLockFragment__enter_alphanumeric_pin;
|
||||
} else {
|
||||
return R.string.RegistrationLockFragment__enter_numeric_pin;
|
||||
}
|
||||
}
|
||||
|
||||
private void enableAndFocusPinEntry() {
|
||||
pinEntry.setEnabled(true);
|
||||
pinEntry.setFocusable(true);
|
||||
|
||||
if (pinEntry.requestFocus()) {
|
||||
ServiceUtil.getInputMethodManager(pinEntry.getContext()).showSoftInput(pinEntry, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void handleSuccessfulPinEntry(@NonNull String pin);
|
||||
|
||||
protected abstract void sendEmailToSupport();
|
||||
}
|
|
@ -11,19 +11,23 @@ import android.webkit.WebViewClient;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Fragment that displays a Captcha in a WebView.
|
||||
*/
|
||||
public final class CaptchaFragment extends LoggingFragment {
|
||||
|
||||
private RegistrationViewModel viewModel;
|
||||
public static final String EXTRA_VIEW_MODEL_PROVIDER = "view_model_provider";
|
||||
|
||||
private BaseRegistrationViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
@ -53,11 +57,25 @@ public final class CaptchaFragment extends LoggingFragment {
|
|||
|
||||
webView.loadUrl(RegistrationConstants.SIGNAL_CAPTCHA_URL);
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
CaptchaViewModelProvider provider = null;
|
||||
if (getArguments() != null) {
|
||||
provider = (CaptchaViewModelProvider) requireArguments().getSerializable(EXTRA_VIEW_MODEL_PROVIDER);
|
||||
}
|
||||
|
||||
if (provider == null) {
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
} else {
|
||||
viewModel = provider.get(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleToken(@NonNull String token) {
|
||||
viewModel.setCaptchaResponse(token);
|
||||
|
||||
NavHostFragment.findNavController(this).navigateUp();
|
||||
}
|
||||
|
||||
public interface CaptchaViewModelProvider extends Serializable {
|
||||
@NonNull BaseRegistrationViewModel get(@NonNull CaptchaFragment fragment);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,9 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
@ -29,8 +28,12 @@ import java.util.Map;
|
|||
|
||||
public final class CountryPickerFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<Map<String, String>>> {
|
||||
|
||||
public static final String KEY_COUNTRY = "country";
|
||||
public static final String KEY_COUNTRY_CODE = "country_code";
|
||||
|
||||
private EditText countryFilter;
|
||||
private RegistrationViewModel model;
|
||||
private String resultKey;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
|
@ -41,7 +44,14 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
|||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
model = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
if (getArguments() != null) {
|
||||
CountryPickerFragmentArgs arguments = CountryPickerFragmentArgs.fromBundle(requireArguments());
|
||||
resultKey = arguments.getResultKey();
|
||||
}
|
||||
|
||||
if (resultKey == null) {
|
||||
model = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
countryFilter = view.findViewById(R.id.country_search);
|
||||
|
||||
|
@ -56,14 +66,21 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
|||
int countryCode = Integer.parseInt(item.get("country_code").replace("+", ""));
|
||||
String countryName = item.get("country_name");
|
||||
|
||||
model.onCountrySelected(countryName, countryCode);
|
||||
if (resultKey == null) {
|
||||
model.onCountrySelected(countryName, countryCode);
|
||||
} else {
|
||||
Bundle result = new Bundle();
|
||||
result.putString(KEY_COUNTRY, countryName);
|
||||
result.putInt(KEY_COUNTRY_CODE, countryCode);
|
||||
getParentFragmentManager().setFragmentResult(resultKey, result);
|
||||
}
|
||||
|
||||
NavHostFragment.findNavController(this).navigateUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<ArrayList<Map<String, String>>> onCreateLoader(int id, @Nullable Bundle args) {
|
||||
return new CountryListLoader(getActivity());
|
||||
return new CountryListLoader(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -71,7 +88,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
|
|||
@NonNull ArrayList<Map<String, String>> results)
|
||||
{
|
||||
((TextView) getListView().getEmptyView()).setText(
|
||||
R.string.country_selection_fragment__no_matching_countries);
|
||||
R.string.country_selection_fragment__no_matching_countries);
|
||||
String[] from = { "country_name", "country_code" };
|
||||
int[] to = { R.id.country_name, R.id.country_code };
|
||||
|
||||
|
|
|
@ -1,163 +1,33 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.registration.CallMeCountDownView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
|
||||
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
|
||||
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class EnterCodeFragment extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback {
|
||||
public final class EnterCodeFragment extends BaseEnterCodeFragment<RegistrationViewModel> implements SignalStrengthPhoneStateListener.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(EnterCodeFragment.class);
|
||||
|
||||
private ScrollView scrollView;
|
||||
private TextView header;
|
||||
private VerificationCodeView verificationCodeView;
|
||||
private VerificationPinKeyboard keyboard;
|
||||
private CallMeCountDownView callMeCountDown;
|
||||
private View wrongNumber;
|
||||
private View noCodeReceivedHelp;
|
||||
private View serviceWarning;
|
||||
private boolean autoCompleting;
|
||||
|
||||
private PhoneStateListener signalStrengthListener;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_enter_code, container, false);
|
||||
public EnterCodeFragment() {
|
||||
super(R.layout.fragment_registration_enter_code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
|
||||
|
||||
scrollView = view.findViewById(R.id.scroll_view);
|
||||
header = view.findViewById(R.id.verify_header);
|
||||
verificationCodeView = view.findViewById(R.id.code);
|
||||
keyboard = view.findViewById(R.id.keyboard);
|
||||
callMeCountDown = view.findViewById(R.id.call_me_count_down);
|
||||
wrongNumber = view.findViewById(R.id.wrong_number);
|
||||
noCodeReceivedHelp = view.findViewById(R.id.no_code);
|
||||
serviceWarning = view.findViewById(R.id.cell_service_warning);
|
||||
|
||||
signalStrengthListener = new SignalStrengthPhoneStateListener(this, this);
|
||||
|
||||
connectKeyboard(verificationCodeView, keyboard);
|
||||
ViewUtil.hideKeyboard(requireContext(), view);
|
||||
|
||||
setOnCodeFullyEnteredListener(verificationCodeView);
|
||||
|
||||
wrongNumber.setOnClickListener(v -> onWrongNumber());
|
||||
|
||||
callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest());
|
||||
|
||||
callMeCountDown.setListener((v, remaining) -> {
|
||||
if (remaining <= 30) {
|
||||
scrollView.smoothScrollTo(0, v.getBottom());
|
||||
callMeCountDown.setListener(null);
|
||||
}
|
||||
});
|
||||
|
||||
noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport());
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
|
||||
if (attempts >= 3) {
|
||||
noCodeReceivedHelp.setVisibility(View.VISIBLE);
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000);
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.onStartEnterCode();
|
||||
protected @NonNull RegistrationViewModel getViewModel() {
|
||||
return ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
private void onWrongNumber() {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionWrongNumber());
|
||||
}
|
||||
|
||||
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
|
||||
verificationCodeView.setOnCompleteListener(code -> {
|
||||
|
||||
callMeCountDown.setVisibility(View.INVISIBLE);
|
||||
wrongNumber.setVisibility(View.INVISIBLE);
|
||||
keyboard.displayProgress();
|
||||
|
||||
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithoutRegistrationLock(code)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulRegistration();
|
||||
} else if (processor.rateLimit()) {
|
||||
handleRateLimited();
|
||||
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
LockedException lockedException = processor.getLockedException();
|
||||
handleRegistrationLock(lockedException.getTimeRemaining());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
handleKbsAccountLocked();
|
||||
} else if (processor.authorizationFailed()) {
|
||||
handleIncorrectCodeError();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code", processor.getError());
|
||||
handleGeneralError();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(verify);
|
||||
});
|
||||
}
|
||||
|
||||
public void handleSuccessfulRegistration() {
|
||||
@Override
|
||||
protected void handleSuccessfulVerify() {
|
||||
SimpleTask.run(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
|
@ -167,222 +37,22 @@ public final class EnterCodeFragment extends LoggingFragment implements SignalSt
|
|||
Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e);
|
||||
}
|
||||
return null;
|
||||
}, none -> {
|
||||
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
|
||||
}
|
||||
});
|
||||
});
|
||||
}, none -> displaySuccess(() -> Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration())));
|
||||
}
|
||||
|
||||
public void handleRateLimited() {
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
|
||||
|
||||
builder.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
@Override
|
||||
protected void navigateToRegistrationLock(long timeRemaining) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
|
||||
}
|
||||
|
||||
public void handleRegistrationLock(long timeRemaining) {
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false));
|
||||
}
|
||||
});
|
||||
@Override
|
||||
protected void navigateToCaptcha() {
|
||||
NavHostFragment.findNavController(this).navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
|
||||
}
|
||||
|
||||
public void handleKbsAccountLocked() {
|
||||
@Override
|
||||
protected void navigateToKbsAccountLocked() {
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
public void handleIncorrectCodeError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void handleGeneralError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
callMeCountDown.setVisibility(View.VISIBLE);
|
||||
wrongNumber.setVisibility(View.VISIBLE);
|
||||
verificationCodeView.clear();
|
||||
keyboard.displayKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onVerificationCodeReceived(@NonNull ReceivedSmsEvent event) {
|
||||
verificationCodeView.clear();
|
||||
|
||||
List<Integer> parsedCode = convertVerificationCodeToDigits(event.getCode());
|
||||
|
||||
autoCompleting = true;
|
||||
|
||||
final int size = parsedCode.size();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
final int index = i;
|
||||
verificationCodeView.postDelayed(() -> {
|
||||
verificationCodeView.append(parsedCode.get(index));
|
||||
if (index == size - 1) {
|
||||
autoCompleting = false;
|
||||
}
|
||||
}, i * 200);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Integer> convertVerificationCodeToDigits(@Nullable String code) {
|
||||
if (code == null || code.length() != 6) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Integer> result = new ArrayList<>(code.length());
|
||||
|
||||
try {
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
result.add(Integer.parseInt(Character.toString(code.charAt(i))));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Failed to convert code into digits.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequest() {
|
||||
showConfirmNumberDialogIfTranslated(requireContext(),
|
||||
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
|
||||
viewModel.getNumber().getE164Number(),
|
||||
this::handlePhoneCallRequestAfterConfirm,
|
||||
this::onWrongNumber);
|
||||
}
|
||||
|
||||
private void handlePhoneCallRequestAfterConfirm() {
|
||||
Disposable request = viewModel.requestVerificationCode(VerifyAccountRepository.Mode.PHONE_CALL)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_call_requested, Toast.LENGTH_LONG).show();
|
||||
} else if (processor.captchaRequired()) {
|
||||
NavHostFragment.findNavController(this).navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
|
||||
} else if (processor.rateLimit()) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request phone code", processor.getError());
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(request);
|
||||
}
|
||||
|
||||
private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) {
|
||||
keyboard.setOnKeyPressListener(key -> {
|
||||
if (!autoCompleting) {
|
||||
if (key >= 0) {
|
||||
verificationCodeView.append(key);
|
||||
} else {
|
||||
verificationCodeView.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
|
||||
|
||||
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
|
||||
}
|
||||
|
||||
private void sendEmailToSupport() {
|
||||
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
|
||||
R.string.RegistrationActivity_code_support_subject,
|
||||
null,
|
||||
null);
|
||||
CommunicationActions.openEmail(requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(R.string.RegistrationActivity_code_support_subject),
|
||||
body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoCellSignalPresent() {
|
||||
if (serviceWarning.getVisibility() == View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
serviceWarning.setVisibility(View.VISIBLE);
|
||||
serviceWarning.animate()
|
||||
.alpha(1)
|
||||
.setListener(null)
|
||||
.start();
|
||||
|
||||
scrollView.postDelayed(() -> {
|
||||
if (serviceWarning.getVisibility() == View.VISIBLE) {
|
||||
scrollView.smoothScrollTo(0, serviceWarning.getBottom());
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCellSignalPresent() {
|
||||
if (serviceWarning.getVisibility() != View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
serviceWarning.animate()
|
||||
.alpha(0)
|
||||
.setListener(new Animator.AnimatorListener() {
|
||||
@Override public void onAnimationEnd(Animator animation) {
|
||||
serviceWarning.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override public void onAnimationRepeat(Animator animation) {}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,20 +7,13 @@ import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpin
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
@ -29,7 +22,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
@ -41,15 +34,15 @@ import com.google.android.gms.common.ConnectionResult;
|
|||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.i18n.phonenumbers.AsYouTypeFormatter;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
|
@ -61,14 +54,12 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
|||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class EnterPhoneNumberFragment extends LoggingFragment {
|
||||
public final class EnterPhoneNumberFragment extends LoggingFragment implements RegistrationNumberInputController.Callbacks {
|
||||
|
||||
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
|
||||
|
||||
private LabeledEditText countryCode;
|
||||
private LabeledEditText number;
|
||||
private ArrayAdapter<String> countrySpinnerAdapter;
|
||||
private AsYouTypeFormatter countryFormatter;
|
||||
private CircularProgressButton register;
|
||||
private Spinner countrySpinner;
|
||||
private View cancel;
|
||||
|
@ -101,14 +92,17 @@ public final class EnterPhoneNumberFragment extends LoggingFragment {
|
|||
scrollView = view.findViewById(R.id.scroll_view);
|
||||
register = view.findViewById(R.id.registerButton);
|
||||
|
||||
initializeSpinner(countrySpinner);
|
||||
|
||||
setUpNumberInput();
|
||||
RegistrationNumberInputController controller = new RegistrationNumberInputController(requireContext(),
|
||||
countryCode,
|
||||
number,
|
||||
countrySpinner,
|
||||
false,
|
||||
this);
|
||||
|
||||
register.setOnClickListener(v -> handleRegister(requireContext()));
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
if (viewModel.isReregister()) {
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
|
@ -117,18 +111,12 @@ public final class EnterPhoneNumberFragment extends LoggingFragment {
|
|||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
NumberViewState number = viewModel.getNumber();
|
||||
|
||||
initNumber(number);
|
||||
|
||||
countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener());
|
||||
viewModel.getLiveNumber().observe(getViewLifecycleOwner(), controller::updateNumber);
|
||||
|
||||
if (viewModel.hasCaptchaToken()) {
|
||||
handleRegister(requireContext());
|
||||
ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250);
|
||||
}
|
||||
|
||||
countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
|
||||
|
||||
Toolbar toolbar = view.findViewById(R.id.toolbar);
|
||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(null);
|
||||
|
@ -149,28 +137,6 @@ public final class EnterPhoneNumberFragment extends LoggingFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void setUpNumberInput() {
|
||||
EditText numberInput = number.getInput();
|
||||
|
||||
numberInput.addTextChangedListener(new NumberChangedListener());
|
||||
|
||||
number.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250);
|
||||
}
|
||||
});
|
||||
|
||||
numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
numberInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
handleRegister(requireContext());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void handleRegister(@NonNull Context context) {
|
||||
if (TextUtils.isEmpty(countryCode.getText())) {
|
||||
Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
|
||||
|
@ -276,154 +242,35 @@ public final class EnterPhoneNumberFragment extends LoggingFragment {
|
|||
disposables.add(request);
|
||||
}
|
||||
|
||||
private void initializeSpinner(Spinner countrySpinner) {
|
||||
countrySpinnerAdapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item);
|
||||
countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
setCountryDisplay(getString(R.string.RegistrationActivity_select_your_country));
|
||||
|
||||
countrySpinner.setAdapter(countrySpinnerAdapter);
|
||||
countrySpinner.setOnTouchListener((view, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
pickCountry(view);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
countrySpinner.setOnKeyListener((view, keyCode, event) -> {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
|
||||
pickCountry(view);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@Override
|
||||
public void onNumberFocused() {
|
||||
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250);
|
||||
}
|
||||
|
||||
private void pickCountry(@NonNull View view) {
|
||||
@Override
|
||||
public void onNumberInputNext(@NonNull View view) {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNumberInputDone(@NonNull View view) {
|
||||
ViewUtil.hideKeyboard(requireContext(), view);
|
||||
handleRegister(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPickCountry(@NonNull View view) {
|
||||
Navigation.findNavController(view).navigate(R.id.action_pickCountry);
|
||||
}
|
||||
|
||||
private void initNumber(@NonNull NumberViewState numberViewState) {
|
||||
int countryCode = numberViewState.getCountryCode();
|
||||
String number = numberViewState.getNationalNumber();
|
||||
String regionDisplayName = numberViewState.getCountryDisplayName();
|
||||
|
||||
this.countryCode.setText(String.valueOf(countryCode));
|
||||
|
||||
setCountryDisplay(regionDisplayName);
|
||||
|
||||
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
|
||||
setCountryFormatter(regionCode);
|
||||
|
||||
if (!TextUtils.isEmpty(number)) {
|
||||
this.number.setText(String.valueOf(number));
|
||||
}
|
||||
@Override
|
||||
public void setNationalNumber(@NonNull String number) {
|
||||
viewModel.setNationalNumber(number);
|
||||
}
|
||||
|
||||
private void setCountryDisplay(String regionDisplayName) {
|
||||
countrySpinnerAdapter.clear();
|
||||
if (regionDisplayName == null) {
|
||||
countrySpinnerAdapter.add(getString(R.string.RegistrationActivity_select_your_country));
|
||||
} else {
|
||||
countrySpinnerAdapter.add(regionDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
private class CountryCodeChangedListener implements TextWatcher {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (TextUtils.isEmpty(s) || !TextUtils.isDigitsOnly(s)) {
|
||||
setCountryDisplay(null);
|
||||
countryFormatter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
int countryCode = Integer.parseInt(s.toString());
|
||||
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
|
||||
|
||||
setCountryFormatter(regionCode);
|
||||
|
||||
if (!TextUtils.isEmpty(regionCode) && !regionCode.equals("ZZ")) {
|
||||
number.requestFocus();
|
||||
|
||||
int numberLength = number.getText().length();
|
||||
number.getInput().setSelection(numberLength, numberLength);
|
||||
}
|
||||
|
||||
viewModel.onCountrySelected(null, countryCode);
|
||||
setCountryDisplay(viewModel.getNumber().getCountryDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
}
|
||||
|
||||
private class NumberChangedListener implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String number = reformatText(s);
|
||||
|
||||
if (number == null) return;
|
||||
|
||||
viewModel.setNationalNumber(number);
|
||||
|
||||
setCountryDisplay(viewModel.getNumber().getCountryDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private String reformatText(Editable s) {
|
||||
if (countryFormatter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(s)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
countryFormatter.clear();
|
||||
|
||||
String formattedNumber = null;
|
||||
StringBuilder justDigits = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (Character.isDigit(c)) {
|
||||
formattedNumber = countryFormatter.inputDigit(c);
|
||||
justDigits.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedNumber != null && !s.toString().equals(formattedNumber)) {
|
||||
s.replace(0, s.length(), formattedNumber);
|
||||
}
|
||||
|
||||
if (justDigits.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return justDigits.toString();
|
||||
}
|
||||
|
||||
private void setCountryFormatter(@Nullable String regionCode) {
|
||||
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||
|
||||
countryFormatter = regionCode != null ? util.getAsYouTypeFormatter(regionCode) : null;
|
||||
|
||||
reformatText(number.getText());
|
||||
@Override
|
||||
public void setCountry(int countryCode) {
|
||||
viewModel.onCountrySelected(null, countryCode);
|
||||
}
|
||||
|
||||
private void handlePromptForNoPlayServices(@NonNull Context context) {
|
||||
|
|
|
@ -1,314 +1,48 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
|
||||
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
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.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.pin.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class RegistrationLockFragment extends LoggingFragment {
|
||||
public final class RegistrationLockFragment extends BaseRegistrationLockFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationLockFragment.class);
|
||||
|
||||
/**
|
||||
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
|
||||
*/
|
||||
private static final int MINIMUM_PIN_LENGTH = 4;
|
||||
|
||||
private EditText pinEntry;
|
||||
private View forgotPin;
|
||||
private CircularProgressButton pinButton;
|
||||
private TextView errorLabel;
|
||||
private TextView keyboardToggle;
|
||||
private long timeRemaining;
|
||||
private boolean isV1RegistrationLock;
|
||||
private RegistrationViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_lock, container, false);
|
||||
public RegistrationLockFragment() {
|
||||
super(R.layout.fragment_registration_lock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
|
||||
|
||||
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
|
||||
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
|
||||
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
|
||||
forgotPin = view.findViewById(R.id.kbs_lock_forgot_pin);
|
||||
|
||||
RegistrationLockFragmentArgs args = RegistrationLockFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
timeRemaining = args.getTimeRemaining();
|
||||
isV1RegistrationLock = args.getIsV1RegistrationLock();
|
||||
|
||||
if (isV1RegistrationLock) {
|
||||
keyboardToggle.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
forgotPin.setVisibility(View.GONE);
|
||||
forgotPin.setOnClickListener(v -> handleForgottenPin(timeRemaining));
|
||||
|
||||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
handlePinEntry();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
pinButton.setOnClickListener((v) -> {
|
||||
ViewUtil.hideKeyboard(requireContext(), pinEntry);
|
||||
handlePinEntry();
|
||||
});
|
||||
|
||||
keyboardToggle.setOnClickListener((v) -> {
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType();
|
||||
|
||||
updateKeyboard(keyboardType.getOther());
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
|
||||
|
||||
viewModel.getLockedTimeRemaining()
|
||||
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
|
||||
|
||||
TokenData keyBackupCurrentToken = viewModel.getKeyBackupCurrentToken();
|
||||
|
||||
if (keyBackupCurrentToken != null) {
|
||||
int triesRemaining = keyBackupCurrentToken.getTriesRemaining();
|
||||
if (triesRemaining <= 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining < 5) {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
}
|
||||
}
|
||||
protected BaseRegistrationViewModel getViewModel() {
|
||||
return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class);
|
||||
}
|
||||
|
||||
private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) {
|
||||
Resources resources = requireContext().getResources();
|
||||
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
|
||||
String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining);
|
||||
|
||||
return tries + " " + days;
|
||||
}
|
||||
|
||||
private PinKeyboardType getPinEntryKeyboardType() {
|
||||
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
|
||||
|
||||
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
|
||||
}
|
||||
|
||||
private void handlePinEntry() {
|
||||
pinEntry.setEnabled(false);
|
||||
|
||||
final String pin = pinEntry.getText().toString();
|
||||
|
||||
int trimmedLength = pin.replace(" ", "").length();
|
||||
if (trimmedLength == 0) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedLength < MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
setSpinning(pinButton);
|
||||
|
||||
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
handleSuccessfulPinEntry();
|
||||
} else if (processor.wrongPin()) {
|
||||
onIncorrectKbsRegistrationLockPin(processor.getToken());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
onKbsAccountLocked();
|
||||
} else if (processor.rateLimit()) {
|
||||
onRateLimited();
|
||||
} else {
|
||||
Log.w(TAG, "Unable to verify code with registration lock", processor.getError());
|
||||
onError();
|
||||
}
|
||||
});
|
||||
|
||||
disposables.add(verify);
|
||||
}
|
||||
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
viewModel.setKeyBackupTokenData(tokenData);
|
||||
|
||||
int triesRemaining = tokenData.getTriesRemaining();
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
return;
|
||||
}
|
||||
|
||||
if (triesRemaining == 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
forgotPin.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void onRateLimited() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
|
||||
public void onKbsAccountLocked() {
|
||||
onAccountLocked();
|
||||
}
|
||||
|
||||
|
||||
public void onError() {
|
||||
cancelSpinning(pinButton);
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void handleForgottenPin(long timeRemainingMs) {
|
||||
int lockoutDays = getLockoutDays(timeRemainingMs);
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
|
||||
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
|
||||
.show();
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void onAccountLocked() {
|
||||
@Override
|
||||
protected void navigateToAccountLocked() {
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
|
||||
pinEntry.getText().clear();
|
||||
}
|
||||
|
||||
private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) {
|
||||
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.RegistrationLockFragment__enter_alphanumeric_pin;
|
||||
} else {
|
||||
return R.string.RegistrationLockFragment__enter_numeric_pin;
|
||||
}
|
||||
}
|
||||
|
||||
private void enableAndFocusPinEntry() {
|
||||
pinEntry.setEnabled(true);
|
||||
pinEntry.setFocusable(true);
|
||||
|
||||
if (pinEntry.requestFocus()) {
|
||||
ServiceUtil.getInputMethodManager(pinEntry.getContext()).showSoftInput(pinEntry, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSuccessfulPinEntry() {
|
||||
@Override
|
||||
protected void handleSuccessfulPinEntry(@NonNull String pin) {
|
||||
SignalStore.pinValues().setKeyboardType(getPinEntryKeyboardType());
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
|
@ -338,9 +72,9 @@ public final class RegistrationLockFragment extends LoggingFragment {
|
|||
});
|
||||
}
|
||||
|
||||
private void sendEmailToSupport() {
|
||||
int subject = isV1RegistrationLock ? R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v1_pin
|
||||
: R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin;
|
||||
@Override
|
||||
protected void sendEmailToSupport() {
|
||||
int subject = R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin;
|
||||
|
||||
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
|
||||
subject,
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
package org.thoughtcrime.securesms.registration.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.i18n.phonenumbers.AsYouTypeFormatter;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
|
||||
|
||||
/**
|
||||
* Handle the logic and formatting of phone number input for registration/change number flows.
|
||||
*/
|
||||
public final class RegistrationNumberInputController {
|
||||
|
||||
private final Context context;
|
||||
private final LabeledEditText countryCode;
|
||||
private final LabeledEditText number;
|
||||
private final boolean lastInput;
|
||||
private final Callbacks callbacks;
|
||||
|
||||
private ArrayAdapter<String> countrySpinnerAdapter;
|
||||
private AsYouTypeFormatter countryFormatter;
|
||||
private boolean isUpdating = true;
|
||||
|
||||
public RegistrationNumberInputController(@NonNull Context context,
|
||||
@NonNull LabeledEditText countryCode,
|
||||
@NonNull LabeledEditText number,
|
||||
@NonNull Spinner countrySpinner,
|
||||
boolean lastInput,
|
||||
@NonNull Callbacks callbacks)
|
||||
{
|
||||
this.context = context;
|
||||
this.countryCode = countryCode;
|
||||
this.number = number;
|
||||
this.lastInput = lastInput;
|
||||
this.callbacks = callbacks;
|
||||
|
||||
initializeSpinner(countrySpinner);
|
||||
setUpNumberInput();
|
||||
|
||||
this.countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener());
|
||||
this.countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
|
||||
}
|
||||
|
||||
private void setUpNumberInput() {
|
||||
EditText numberInput = number.getInput();
|
||||
|
||||
numberInput.addTextChangedListener(new NumberChangedListener());
|
||||
|
||||
number.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
callbacks.onNumberFocused();
|
||||
}
|
||||
});
|
||||
|
||||
numberInput.setImeOptions(lastInput ? EditorInfo.IME_ACTION_DONE : EditorInfo.IME_ACTION_NEXT);
|
||||
numberInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
callbacks.onNumberInputNext(v);
|
||||
return true;
|
||||
} else if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
callbacks.onNumberInputDone(v);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void initializeSpinner(@NonNull Spinner countrySpinner) {
|
||||
countrySpinnerAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item);
|
||||
countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
setCountryDisplay(context.getString(R.string.RegistrationActivity_select_your_country));
|
||||
|
||||
countrySpinner.setAdapter(countrySpinnerAdapter);
|
||||
|
||||
countrySpinner.setOnTouchListener((view, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
callbacks.onPickCountry(view);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
countrySpinner.setOnKeyListener((view, keyCode, event) -> {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
|
||||
callbacks.onPickCountry(view);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public void updateNumber(@NonNull NumberViewState numberViewState) {
|
||||
int countryCode = numberViewState.getCountryCode();
|
||||
String countryCodeString = String.valueOf(countryCode);
|
||||
String number = numberViewState.getNationalNumber();
|
||||
String regionDisplayName = numberViewState.getCountryDisplayName();
|
||||
|
||||
isUpdating = true;
|
||||
|
||||
setCountryDisplay(regionDisplayName);
|
||||
|
||||
if (this.countryCode.getText() == null || !this.countryCode.getText().toString().equals(countryCodeString)) {
|
||||
this.countryCode.setText(countryCodeString);
|
||||
|
||||
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
|
||||
setCountryFormatter(regionCode);
|
||||
}
|
||||
|
||||
if (!justDigits(this.number.getText()).equals(number) && !TextUtils.isEmpty(number)) {
|
||||
this.number.setText(number);
|
||||
}
|
||||
|
||||
isUpdating = false;
|
||||
}
|
||||
|
||||
private String justDigits(@Nullable Editable text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder justDigits = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (Character.isDigit(c)) {
|
||||
justDigits.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return justDigits.toString();
|
||||
}
|
||||
|
||||
private void setCountryDisplay(@Nullable String regionDisplayName) {
|
||||
countrySpinnerAdapter.clear();
|
||||
if (regionDisplayName == null) {
|
||||
countrySpinnerAdapter.add(context.getString(R.string.RegistrationActivity_select_your_country));
|
||||
} else {
|
||||
countrySpinnerAdapter.add(regionDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCountryFormatter(@Nullable String regionCode) {
|
||||
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||
|
||||
countryFormatter = regionCode != null ? util.getAsYouTypeFormatter(regionCode) : null;
|
||||
|
||||
reformatText(number.getText());
|
||||
}
|
||||
|
||||
private String reformatText(Editable s) {
|
||||
if (countryFormatter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(s)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
countryFormatter.clear();
|
||||
|
||||
String formattedNumber = null;
|
||||
StringBuilder justDigits = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (Character.isDigit(c)) {
|
||||
formattedNumber = countryFormatter.inputDigit(c);
|
||||
justDigits.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedNumber != null && !s.toString().equals(formattedNumber)) {
|
||||
s.replace(0, s.length(), formattedNumber);
|
||||
}
|
||||
|
||||
if (justDigits.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return justDigits.toString();
|
||||
}
|
||||
|
||||
private class NumberChangedListener implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String number = reformatText(s);
|
||||
|
||||
if (number == null) return;
|
||||
|
||||
if (!isUpdating) {
|
||||
callbacks.setNationalNumber(number);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class CountryCodeChangedListener implements TextWatcher {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (TextUtils.isEmpty(s) || !TextUtils.isDigitsOnly(s)) {
|
||||
setCountryDisplay(null);
|
||||
countryFormatter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
int countryCode = Integer.parseInt(s.toString());
|
||||
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
|
||||
|
||||
setCountryFormatter(regionCode);
|
||||
|
||||
if (!TextUtils.isEmpty(regionCode) && !regionCode.equals("ZZ")) {
|
||||
if (!isUpdating) {
|
||||
number.requestFocus();
|
||||
}
|
||||
|
||||
int numberLength = number.getText().length();
|
||||
number.getInput().setSelection(numberLength, numberLength);
|
||||
}
|
||||
|
||||
if (!isUpdating) {
|
||||
callbacks.setCountry(countryCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callbacks {
|
||||
void onNumberFocused();
|
||||
|
||||
void onNumberInputNext(@NonNull View view);
|
||||
|
||||
void onNumberInputDone(@NonNull View view);
|
||||
|
||||
void onPickCountry(@NonNull View view);
|
||||
|
||||
void setNationalNumber(@NonNull String number);
|
||||
|
||||
void setCountry(int countryCode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
public final class BaseEnterCodeViewModelDelegate {
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
package org.thoughtcrime.securesms.registration.viewmodel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository;
|
||||
import org.thoughtcrime.securesms.pin.TokenData;
|
||||
import org.thoughtcrime.securesms.registration.RequestVerificationCodeResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithFailedKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithSuccessfulKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
* Base view model used in registration and change number flow. Handles the storage of all data
|
||||
* shared between the two flows, orchestrating verification, and calling to subclasses to peform
|
||||
* the specific verify operations for each flow.
|
||||
*/
|
||||
public abstract class BaseRegistrationViewModel extends ViewModel {
|
||||
|
||||
private static final long FIRST_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(64);
|
||||
private static final long SUBSEQUENT_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(300);
|
||||
|
||||
private static final String STATE_NUMBER = "NUMBER";
|
||||
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
|
||||
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
|
||||
private static final String STATE_CAPTCHA = "CAPTCHA";
|
||||
private static final String STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS = "SUCCESSFUL_CODE_REQUEST_ATTEMPTS";
|
||||
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
|
||||
private static final String STATE_KBS_TOKEN = "KBS_TOKEN";
|
||||
private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
|
||||
private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME";
|
||||
|
||||
protected final SavedStateHandle savedState;
|
||||
protected final VerifyAccountRepository verifyAccountRepository;
|
||||
protected final KbsRepository kbsRepository;
|
||||
|
||||
public BaseRegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
|
||||
@NonNull VerifyAccountRepository verifyAccountRepository,
|
||||
@NonNull KbsRepository kbsRepository,
|
||||
@NonNull String password)
|
||||
{
|
||||
this.savedState = savedStateHandle;
|
||||
|
||||
this.verifyAccountRepository = verifyAccountRepository;
|
||||
this.kbsRepository = kbsRepository;
|
||||
|
||||
setInitialDefaultValue(STATE_NUMBER, NumberViewState.INITIAL);
|
||||
setInitialDefaultValue(STATE_REGISTRATION_SECRET, password);
|
||||
setInitialDefaultValue(STATE_VERIFICATION_CODE, "");
|
||||
setInitialDefaultValue(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
|
||||
}
|
||||
|
||||
protected <T> void setInitialDefaultValue(@NonNull String key, @NonNull T initialValue) {
|
||||
if (!savedState.contains(key) || savedState.get(key) == null) {
|
||||
savedState.set(key, initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull NumberViewState getNumber() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_NUMBER);
|
||||
}
|
||||
|
||||
public @NonNull LiveData<NumberViewState> getLiveNumber() {
|
||||
return savedState.getLiveData(STATE_NUMBER);
|
||||
}
|
||||
|
||||
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
|
||||
setViewState(getNumber().toBuilder()
|
||||
.selectedCountryDisplayName(selectedCountryName)
|
||||
.countryCode(countryCode).build());
|
||||
}
|
||||
|
||||
public void setNationalNumber(String number) {
|
||||
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
|
||||
setViewState(numberViewState);
|
||||
}
|
||||
|
||||
protected void setViewState(NumberViewState numberViewState) {
|
||||
if (!numberViewState.equals(getNumber())) {
|
||||
savedState.set(STATE_NUMBER, numberViewState);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull String getRegistrationSecret() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_REGISTRATION_SECRET);
|
||||
}
|
||||
|
||||
public @NonNull String getTextCodeEntered() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_VERIFICATION_CODE);
|
||||
}
|
||||
|
||||
public @Nullable String getCaptchaToken() {
|
||||
return savedState.get(STATE_CAPTCHA);
|
||||
}
|
||||
|
||||
public boolean hasCaptchaToken() {
|
||||
return getCaptchaToken() != null;
|
||||
}
|
||||
|
||||
public void setCaptchaResponse(@Nullable String captchaToken) {
|
||||
savedState.set(STATE_CAPTCHA, captchaToken);
|
||||
}
|
||||
|
||||
public void clearCaptchaResponse() {
|
||||
setCaptchaResponse(null);
|
||||
}
|
||||
|
||||
public void onVerificationCodeEntered(String code) {
|
||||
savedState.set(STATE_VERIFICATION_CODE, code);
|
||||
}
|
||||
|
||||
public void markASuccessfulAttempt() {
|
||||
//noinspection ConstantConditions
|
||||
savedState.set(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, (Integer) savedState.get(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS) + 1);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
|
||||
return savedState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
}
|
||||
|
||||
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
|
||||
//noinspection ConstantConditions
|
||||
return savedState.get(STATE_REQUEST_RATE_LIMITER);
|
||||
}
|
||||
|
||||
public void updateLimiter() {
|
||||
savedState.set(STATE_REQUEST_RATE_LIMITER, savedState.get(STATE_REQUEST_RATE_LIMITER));
|
||||
}
|
||||
|
||||
public @Nullable TokenData getKeyBackupCurrentToken() {
|
||||
return savedState.get(STATE_KBS_TOKEN);
|
||||
}
|
||||
|
||||
public void setKeyBackupTokenData(@Nullable TokenData tokenData) {
|
||||
savedState.set(STATE_KBS_TOKEN, tokenData);
|
||||
}
|
||||
|
||||
public LiveData<Long> getLockedTimeRemaining() {
|
||||
return savedState.getLiveData(STATE_TIME_REMAINING, 0L);
|
||||
}
|
||||
|
||||
public LiveData<Long> getCanCallAtTime() {
|
||||
return savedState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L);
|
||||
}
|
||||
|
||||
public void setLockedTimeRemaining(long lockedTimeRemaining) {
|
||||
savedState.set(STATE_TIME_REMAINING, lockedTimeRemaining);
|
||||
}
|
||||
|
||||
public void onStartEnterCode() {
|
||||
savedState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
|
||||
}
|
||||
|
||||
public void onCallRequested() {
|
||||
savedState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
|
||||
}
|
||||
|
||||
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull Mode mode) {
|
||||
String captcha = getCaptchaToken();
|
||||
clearCaptchaResponse();
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
onCallRequested();
|
||||
} else if (!getRequestLimiter().canRequest(mode, getNumber().getE164Number(), System.currentTimeMillis())) {
|
||||
return Single.just(RequestVerificationCodeResponseProcessor.forLocalRateLimit());
|
||||
}
|
||||
|
||||
return verifyAccountRepository.requestVerificationCode(getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
mode,
|
||||
captcha)
|
||||
.map(RequestVerificationCodeResponseProcessor::new)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
markASuccessfulAttempt();
|
||||
getRequestLimiter().onSuccessfulRequest(mode, getNumber().getE164Number(), System.currentTimeMillis());
|
||||
} else {
|
||||
getRequestLimiter().onUnsuccessfulRequest();
|
||||
}
|
||||
updateLimiter();
|
||||
});
|
||||
}
|
||||
|
||||
public Single<VerifyAccountResponseProcessor> verifyCodeWithoutRegistrationLock(@NonNull String code) {
|
||||
onVerificationCodeEntered(code);
|
||||
|
||||
return verifyAccountWithoutRegistrationLock()
|
||||
.map(VerifyAccountResponseWithoutKbs::new)
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return onVerifySuccess(processor);
|
||||
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
return kbsRepository.getToken(processor.getLockedException().getBasicStorageCredentials())
|
||||
.map(r -> r.getResult().isPresent() ? new VerifyAccountResponseWithSuccessfulKbs(processor.getResponse(), r.getResult().get())
|
||||
: new VerifyAccountResponseWithFailedKbs(r));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
setKeyBackupTokenData(processor.getTokenData());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<VerifyCodeWithRegistrationLockResponseProcessor> verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) {
|
||||
TokenData kbsTokenData = Objects.requireNonNull(getKeyBackupCurrentToken());
|
||||
|
||||
return verifyAccountWithRegistrationLock(pin, kbsTokenData)
|
||||
.map(r -> new VerifyCodeWithRegistrationLockResponseProcessor(r, kbsTokenData))
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return onVerifySuccessWithRegistrationLock(processor, pin);
|
||||
} else if (processor.wrongPin()) {
|
||||
TokenData newToken = TokenData.withResponse(kbsTokenData, processor.getTokenResponse());
|
||||
return Single.just(new VerifyCodeWithRegistrationLockResponseProcessor(processor.getResponse(), newToken));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.wrongPin()) {
|
||||
setKeyBackupTokenData(processor.getToken());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract Single<ServiceResponse<VerifyAccountResponse>> verifyAccountWithoutRegistrationLock();
|
||||
|
||||
protected abstract Single<ServiceResponse<VerifyAccountWithRegistrationLockResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull TokenData kbsTokenData);
|
||||
|
||||
protected abstract Single<VerifyAccountResponseProcessor> onVerifySuccess(@NonNull VerifyAccountResponseProcessor processor);
|
||||
|
||||
protected abstract Single<VerifyCodeWithRegistrationLockResponseProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyCodeWithRegistrationLockResponseProcessor processor, String pin);
|
||||
}
|
|
@ -4,7 +4,6 @@ import androidx.annotation.MainThread;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.savedstate.SavedStateRegistryOwner;
|
||||
|
@ -16,42 +15,22 @@ import org.thoughtcrime.securesms.registration.RegistrationData;
|
|||
import org.thoughtcrime.securesms.registration.RegistrationRepository;
|
||||
import org.thoughtcrime.securesms.registration.RequestVerificationCodeResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithFailedKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithSuccessfulKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs;
|
||||
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public final class RegistrationViewModel extends ViewModel {
|
||||
public final class RegistrationViewModel extends BaseRegistrationViewModel {
|
||||
|
||||
private static final long FIRST_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(64);
|
||||
private static final long SUBSEQUENT_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(300);
|
||||
private static final String STATE_FCM_TOKEN = "FCM_TOKEN";
|
||||
private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN";
|
||||
private static final String STATE_IS_REREGISTER = "IS_REREGISTER";
|
||||
|
||||
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
|
||||
private static final String STATE_NUMBER = "NUMBER";
|
||||
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
|
||||
private static final String STATE_CAPTCHA = "CAPTCHA";
|
||||
private static final String STATE_FCM_TOKEN = "FCM_TOKEN";
|
||||
private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN";
|
||||
private static final String STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS = "SUCCESSFUL_CODE_REQUEST_ATTEMPTS";
|
||||
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
|
||||
private static final String STATE_KBS_TOKEN = "KBS_TOKEN";
|
||||
private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
|
||||
private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME";
|
||||
private static final String STATE_IS_REREGISTER = "IS_REREGISTER";
|
||||
|
||||
private final SavedStateHandle registrationState;
|
||||
private final VerifyAccountRepository verifyAccountRepository;
|
||||
private final KbsRepository kbsRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
|
||||
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
|
||||
boolean isReregister,
|
||||
|
@ -59,83 +38,18 @@ public final class RegistrationViewModel extends ViewModel {
|
|||
@NonNull KbsRepository kbsRepository,
|
||||
@NonNull RegistrationRepository registrationRepository)
|
||||
{
|
||||
this.registrationState = savedStateHandle;
|
||||
this.verifyAccountRepository = verifyAccountRepository;
|
||||
this.kbsRepository = kbsRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
super(savedStateHandle, verifyAccountRepository, kbsRepository, Util.getSecret(18));
|
||||
|
||||
setInitialDefaultValue(this.registrationState, STATE_REGISTRATION_SECRET, Util.getSecret(18));
|
||||
setInitialDefaultValue(this.registrationState, STATE_NUMBER, NumberViewState.INITIAL);
|
||||
setInitialDefaultValue(this.registrationState, STATE_VERIFICATION_CODE, "");
|
||||
setInitialDefaultValue(this.registrationState, STATE_RESTORE_FLOW_SHOWN, false);
|
||||
setInitialDefaultValue(this.registrationState, STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
setInitialDefaultValue(this.registrationState, STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
|
||||
this.registrationRepository = registrationRepository;
|
||||
|
||||
this.registrationState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
setInitialDefaultValue(STATE_RESTORE_FLOW_SHOWN, false);
|
||||
|
||||
private static <T> void setInitialDefaultValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
|
||||
if (!savedStateHandle.contains(key) || savedStateHandle.get(key) == null) {
|
||||
savedStateHandle.set(key, initialValue);
|
||||
}
|
||||
this.savedState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
|
||||
public boolean isReregister() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_IS_REREGISTER);
|
||||
}
|
||||
|
||||
public @NonNull NumberViewState getNumber() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_NUMBER);
|
||||
}
|
||||
|
||||
public @NonNull String getTextCodeEntered() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_VERIFICATION_CODE);
|
||||
}
|
||||
|
||||
private @Nullable String getCaptchaToken() {
|
||||
return registrationState.get(STATE_CAPTCHA);
|
||||
}
|
||||
|
||||
public boolean hasCaptchaToken() {
|
||||
return getCaptchaToken() != null;
|
||||
}
|
||||
|
||||
private @NonNull String getRegistrationSecret() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_REGISTRATION_SECRET);
|
||||
}
|
||||
|
||||
public void setCaptchaResponse(@Nullable String captchaToken) {
|
||||
registrationState.set(STATE_CAPTCHA, captchaToken);
|
||||
}
|
||||
|
||||
private void clearCaptchaResponse() {
|
||||
setCaptchaResponse(null);
|
||||
}
|
||||
|
||||
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
|
||||
setViewState(getNumber().toBuilder()
|
||||
.selectedCountryDisplayName(selectedCountryName)
|
||||
.countryCode(countryCode).build());
|
||||
}
|
||||
|
||||
public void setNationalNumber(String number) {
|
||||
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
|
||||
setViewState(numberViewState);
|
||||
}
|
||||
|
||||
private void setViewState(NumberViewState numberViewState) {
|
||||
if (!numberViewState.equals(getNumber())) {
|
||||
registrationState.set(STATE_NUMBER, numberViewState);
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onVerificationCodeEntered(String code) {
|
||||
registrationState.set(STATE_VERIFICATION_CODE, code);
|
||||
return savedState.get(STATE_IS_REREGISTER);
|
||||
}
|
||||
|
||||
public void onNumberDetected(int countryCode, String nationalNumber) {
|
||||
|
@ -145,165 +59,67 @@ public final class RegistrationViewModel extends ViewModel {
|
|||
.build());
|
||||
}
|
||||
|
||||
private @Nullable String getFcmToken() {
|
||||
return registrationState.get(STATE_FCM_TOKEN);
|
||||
public @Nullable String getFcmToken() {
|
||||
return savedState.get(STATE_FCM_TOKEN);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setFcmToken(@Nullable String fcmToken) {
|
||||
registrationState.set(STATE_FCM_TOKEN, fcmToken);
|
||||
savedState.set(STATE_FCM_TOKEN, fcmToken);
|
||||
}
|
||||
|
||||
public void setWelcomeSkippedOnRestore() {
|
||||
registrationState.set(STATE_RESTORE_FLOW_SHOWN, true);
|
||||
savedState.set(STATE_RESTORE_FLOW_SHOWN, true);
|
||||
}
|
||||
|
||||
public boolean hasRestoreFlowBeenShown() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_RESTORE_FLOW_SHOWN);
|
||||
}
|
||||
|
||||
private void markASuccessfulAttempt() {
|
||||
//noinspection ConstantConditions
|
||||
registrationState.set(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, (Integer) registrationState.get(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS) + 1);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
|
||||
return registrationState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
|
||||
}
|
||||
|
||||
private @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
|
||||
//noinspection ConstantConditions
|
||||
return registrationState.get(STATE_REQUEST_RATE_LIMITER);
|
||||
}
|
||||
|
||||
private void updateLimiter() {
|
||||
registrationState.set(STATE_REQUEST_RATE_LIMITER, registrationState.get(STATE_REQUEST_RATE_LIMITER));
|
||||
}
|
||||
|
||||
public @Nullable TokenData getKeyBackupCurrentToken() {
|
||||
return registrationState.get(STATE_KBS_TOKEN);
|
||||
}
|
||||
|
||||
public void setKeyBackupTokenData(@Nullable TokenData tokenData) {
|
||||
registrationState.set(STATE_KBS_TOKEN, tokenData);
|
||||
}
|
||||
|
||||
public LiveData<Long> getLockedTimeRemaining() {
|
||||
return registrationState.getLiveData(STATE_TIME_REMAINING, 0L);
|
||||
}
|
||||
|
||||
public LiveData<Long> getCanCallAtTime() {
|
||||
return registrationState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L);
|
||||
}
|
||||
|
||||
public void setLockedTimeRemaining(long lockedTimeRemaining) {
|
||||
registrationState.set(STATE_TIME_REMAINING, lockedTimeRemaining);
|
||||
}
|
||||
|
||||
public void onStartEnterCode() {
|
||||
registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
|
||||
}
|
||||
|
||||
private void onCallRequested() {
|
||||
registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
|
||||
return savedState.get(STATE_RESTORE_FLOW_SHOWN);
|
||||
}
|
||||
|
||||
public void setIsReregister(boolean isReregister) {
|
||||
registrationState.set(STATE_IS_REREGISTER, isReregister);
|
||||
savedState.set(STATE_IS_REREGISTER, isReregister);
|
||||
}
|
||||
|
||||
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull Mode mode) {
|
||||
String captcha = getCaptchaToken();
|
||||
clearCaptchaResponse();
|
||||
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
onCallRequested();
|
||||
} else if (!getRequestLimiter().canRequest(mode, getNumber().getE164Number(), System.currentTimeMillis())) {
|
||||
return Single.just(RequestVerificationCodeResponseProcessor.forLocalRateLimit());
|
||||
}
|
||||
|
||||
return verifyAccountRepository.requestVerificationCode(getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
mode,
|
||||
captcha)
|
||||
.map(RequestVerificationCodeResponseProcessor::new)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
markASuccessfulAttempt();
|
||||
setFcmToken(processor.getResult().getFcmToken().orNull());
|
||||
getRequestLimiter().onSuccessfulRequest(mode, getNumber().getE164Number(), System.currentTimeMillis());
|
||||
} else {
|
||||
getRequestLimiter().onUnsuccessfulRequest();
|
||||
}
|
||||
updateLimiter();
|
||||
});
|
||||
@Override
|
||||
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull VerifyAccountRepository.Mode mode) {
|
||||
return super.requestVerificationCode(mode)
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
setFcmToken(processor.getResult().getFcmToken().orNull());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Single<VerifyAccountResponseProcessor> verifyCodeAndRegisterAccountWithoutRegistrationLock(@NonNull String code) {
|
||||
onVerificationCodeEntered(code);
|
||||
|
||||
RegistrationData registrationData = new RegistrationData(getTextCodeEntered(),
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken());
|
||||
|
||||
return verifyAccountRepository.verifyAccount(registrationData)
|
||||
.map(VerifyAccountResponseWithoutKbs::new)
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return registrationRepository.registerAccountWithoutRegistrationLock(registrationData, processor.getResult())
|
||||
.map(VerifyAccountResponseWithoutKbs::new);
|
||||
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
return kbsRepository.getToken(processor.getLockedException().getBasicStorageCredentials())
|
||||
.map(r -> r.getResult().isPresent() ? new VerifyAccountResponseWithSuccessfulKbs(processor.getResponse(), r.getResult().get())
|
||||
: new VerifyAccountResponseWithFailedKbs(r));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.registrationLock() && !processor.isKbsLocked()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
setKeyBackupTokenData(processor.getTokenData());
|
||||
} else if (processor.isKbsLocked()) {
|
||||
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
protected Single<ServiceResponse<VerifyAccountResponse>> verifyAccountWithoutRegistrationLock() {
|
||||
return verifyAccountRepository.verifyAccount(getRegistrationData());
|
||||
}
|
||||
|
||||
public Single<VerifyCodeWithRegistrationLockResponseProcessor> verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) {
|
||||
RegistrationData registrationData = new RegistrationData(getTextCodeEntered(),
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken());
|
||||
@Override
|
||||
protected Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull TokenData kbsTokenData) {
|
||||
return verifyAccountRepository.verifyAccountWithPin(getRegistrationData(), pin, kbsTokenData);
|
||||
}
|
||||
|
||||
TokenData kbsTokenData = Objects.requireNonNull(getKeyBackupCurrentToken());
|
||||
@Override
|
||||
protected Single<VerifyAccountResponseProcessor> onVerifySuccess(@NonNull VerifyAccountResponseProcessor processor) {
|
||||
return registrationRepository.registerAccountWithoutRegistrationLock(getRegistrationData(), processor.getResult())
|
||||
.map(VerifyAccountResponseWithoutKbs::new);
|
||||
}
|
||||
|
||||
return verifyAccountRepository.verifyAccountWithPin(registrationData, pin, kbsTokenData)
|
||||
.map(r -> new VerifyCodeWithRegistrationLockResponseProcessor(r, kbsTokenData))
|
||||
.flatMap(processor -> {
|
||||
if (processor.hasResult()) {
|
||||
return registrationRepository.registerAccountWithRegistrationLock(registrationData, processor.getResult(), pin)
|
||||
.map(processor::updatedIfRegistrationFailed);
|
||||
} else if (processor.wrongPin()) {
|
||||
TokenData newToken = TokenData.withResponse(kbsTokenData, processor.getTokenResponse());
|
||||
return Single.just(new VerifyCodeWithRegistrationLockResponseProcessor(processor.getResponse(), newToken));
|
||||
}
|
||||
return Single.just(processor);
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSuccess(processor -> {
|
||||
if (processor.wrongPin()) {
|
||||
setKeyBackupTokenData(processor.getToken());
|
||||
}
|
||||
});
|
||||
@Override
|
||||
protected Single<VerifyCodeWithRegistrationLockResponseProcessor> onVerifySuccessWithRegistrationLock(@NonNull VerifyCodeWithRegistrationLockResponseProcessor processor, String pin) {
|
||||
return registrationRepository.registerAccountWithRegistrationLock(getRegistrationData(), processor.getResult(), pin)
|
||||
.map(processor::updatedIfRegistrationFailed);
|
||||
}
|
||||
|
||||
private RegistrationData getRegistrationData() {
|
||||
return new RegistrationData(getTextCodeEntered(),
|
||||
getNumber().getE164Number(),
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken());
|
||||
}
|
||||
|
||||
public static final class Factory extends AbstractSavedStateViewModelFactory {
|
||||
|
|
|
@ -6,8 +6,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
|
@ -99,8 +97,9 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
boolean preferContactAvatars = remote.isPreferContactAvatars();
|
||||
int universalExpireTimer = remote.getUniversalExpireTimer();
|
||||
boolean primarySendsSms = local.isPrimarySendsSms();
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms);
|
||||
String e164 = local.getE164();
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
|
@ -127,6 +126,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
.setPayments(payments.isEnabled(), payments.getEntropy().orNull())
|
||||
.setUniversalExpireTimer(universalExpireTimer)
|
||||
.setPrimarySendsSms(primarySendsSms)
|
||||
.setE164(e164)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
@ -164,13 +164,15 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
boolean preferContactAvatars,
|
||||
SignalAccountRecord.Payments payments,
|
||||
int universalExpireTimer,
|
||||
boolean primarySendsSms)
|
||||
boolean primarySendsSms,
|
||||
String e164)
|
||||
{
|
||||
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
||||
Objects.equals(contact.getGivenName().or(""), givenName) &&
|
||||
Objects.equals(contact.getFamilyName().or(""), familyName) &&
|
||||
Objects.equals(contact.getAvatarUrlPath().or(""), avatarUrlPath) &&
|
||||
Objects.equals(contact.getPayments(), payments) &&
|
||||
Objects.equals(contact.getE164(), e164) &&
|
||||
Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&
|
||||
contact.isNoteToSelfArchived() == noteToSelfArchived &&
|
||||
contact.isNoteToSelfForcedUnread() == noteToSelfForcedUnread &&
|
||||
|
|
|
@ -129,6 +129,7 @@ public final class StorageSyncHelper {
|
|||
.setPayments(SignalStore.paymentsValues().mobileCoinPaymentsEnabled(), Optional.fromNullable(SignalStore.paymentsValues().getPaymentsEntropy()).transform(Entropy::getBytes).orNull())
|
||||
.setPrimarySendsSms(Util.isDefaultSmsProvider(context))
|
||||
.setUniversalExpireTimer(SignalStore.settings().getUniversalExpireTimer())
|
||||
.setE164(TextSecurePreferences.getLocalNumber(context))
|
||||
.build();
|
||||
|
||||
return SignalStorageRecord.forAccount(account);
|
||||
|
|
|
@ -83,6 +83,7 @@ public final class FeatureFlags {
|
|||
private static final String SUGGEST_SMS_BLACKLIST = "android.suggestSmsBlacklist";
|
||||
private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize";
|
||||
private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging";
|
||||
private static final String CHANGE_NUMBER_ENABLED = "android.changeNumber";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -124,7 +125,8 @@ public final class FeatureFlags {
|
|||
|
||||
@VisibleForTesting
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
|
||||
PHONE_NUMBER_PRIVACY_VERSION
|
||||
PHONE_NUMBER_PRIVACY_VERSION,
|
||||
CHANGE_NUMBER_ENABLED
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -392,6 +394,11 @@ public final class FeatureFlags {
|
|||
return getBoolean(GROUP_CALL_RINGING, false);
|
||||
}
|
||||
|
||||
/** Weather or not to show change number in the UI. */
|
||||
public static boolean changeNumber() {
|
||||
return getBoolean(CHANGE_NUMBER_ENABLED, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.util.rx
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
||||
/**
|
||||
* Convert a [Task] into a [Single].
|
||||
*/
|
||||
fun <T : Any> Task<T>.toSingle(): Single<T> {
|
||||
return Single.create { emitter ->
|
||||
addOnCompleteListener {
|
||||
if (it.isSuccessful && !emitter.isDisposed) {
|
||||
emitter.onSuccess(it.result)
|
||||
} else if (!emitter.isDisposed) {
|
||||
emitter.onError(it.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="112dp"
|
||||
android:height="112dp"
|
||||
android:viewportWidth="112"
|
||||
android:viewportHeight="112">
|
||||
<path
|
||||
android:pathData="M56,56m-56,0a56,56 0,1 1,112 0a56,56 0,1 1,-112 0"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.05"/>
|
||||
<path
|
||||
android:pathData="M69.3,16.1L42.9,16C39.1,16 36,19.1 36,22.9L35.9,89C35.9,92.8 39,95.9 42.8,95.9L69.1,96C72.9,96 76,92.9 76,89.1L76.2,23C76.2,19.2 73.1,16.1 69.3,16.1ZM72.8,89.1C72.8,91.1 71.1,92.8 69.1,92.8L42.7,92.7C40.7,92.7 39,91 39,89L39.1,22.9C39.1,20.9 40.8,19.2 42.8,19.2L69.2,19.3C71.2,19.3 72.9,21 72.9,23L72.8,89.1Z"
|
||||
android:fillColor="#B9B9B9"/>
|
||||
<path
|
||||
android:pathData="M72.8,89.1C72.8,91.1 71.1,92.8 69.1,92.8L42.7,92.7C40.7,92.7 39,91 39,89L39.1,22.9C39.1,20.9 40.8,19.2 42.8,19.2L69.2,19.3C71.2,19.3 72.9,21 72.9,23L72.8,89.1Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M46.31,47.42C45.72,48.25 45,49.8 46,52.83C47.15,55.825 48.917,58.546 51.186,60.814C53.454,63.083 56.175,64.85 59.17,66C62.17,67.07 63.75,66.3 64.58,65.71C65.309,65.209 65.887,64.517 66.25,63.71C66.496,63.238 66.562,62.692 66.435,62.175C66.308,61.658 65.997,61.204 65.56,60.9L62.13,58.5C61.701,58.195 61.176,58.055 60.653,58.106C60.129,58.158 59.641,58.397 59.28,58.78C58.89,59.19 58.64,59.47 58.28,59.78C58.102,59.996 57.849,60.138 57.571,60.177C57.293,60.216 57.011,60.149 56.78,59.99C55.86,59.307 54.991,58.558 54.18,57.75C53.393,56.943 52.664,56.081 52,55.17C51.841,54.939 51.774,54.657 51.813,54.379C51.852,54.101 51.994,53.848 52.21,53.67C52.56,53.36 52.84,53.11 53.21,52.72C53.593,52.359 53.832,51.871 53.884,51.347C53.935,50.824 53.795,50.299 53.49,49.87L51.12,46.44C50.816,46.003 50.362,45.692 49.845,45.565C49.328,45.438 48.782,45.504 48.31,45.75C47.503,46.113 46.811,46.691 46.31,47.42Z"
|
||||
android:fillColor="#2C6BED"/>
|
||||
</vector>
|
|
@ -7,7 +7,7 @@
|
|||
<corners android:radius="4dp" />
|
||||
|
||||
<stroke
|
||||
android:color="@color/core_ultramarine"
|
||||
android:color="@color/signal_accent_primary"
|
||||
android:width="2dp" />
|
||||
|
||||
</shape>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?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" />
|
||||
|
||||
<include
|
||||
layout="@layout/account_locked_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,85 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/change_number_confirm_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/change_number_confirm_edit_number"
|
||||
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_confirm_new_number_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/change_number_confirm_new_number"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="@string/ChangeNumberConfirmFragment__you_are_about_to_change_your_phone_number_from_s_to_s" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_number_confirm_new_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_confirm_new_number_message"
|
||||
tools:text="+1 (555) 555-5555" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/change_number_confirm_edit_number"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary.NoOutline"
|
||||
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/ChangeNumberConfirmFragment__edit_number"
|
||||
app:layout_constraintBottom_toTopOf="@+id/change_number_confirm_change_number"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_confirm_scroll" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/change_number_confirm_change_number"
|
||||
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/ChangeNumberConfirmFragment__change_number"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_number_confirm_edit_number" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,18 @@
|
|||
<?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" />
|
||||
|
||||
<include
|
||||
layout="@layout/fragment_registration_enter_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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>
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_number_pin_differs_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/ChangeNumberPinDiffersFragment__pins_do_not_match"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/change_number_pin_differs_keep_old_pin"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.20" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_number_pin_differs_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:minHeight="66dp"
|
||||
android:text="@string/ChangeNumberPinDiffersFragment__the_pin_associated_with_your_new_number_is_different_from_the_pin_associated_with_your_old_one"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/core_grey_60"
|
||||
app:layout_constraintTop_toBottomOf="@id/change_number_pin_differs_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/change_number_pin_differs_keep_old_pin"
|
||||
style="@style/Button.Borderless"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ChangeNumberPinDiffersFragment__keep_old_pin"
|
||||
app:layout_constraintBottom_toTopOf="@id/change_number_pin_differs_update_pin"
|
||||
tools:layout_editor_absoluteX="32dp" />
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/change_number_pin_differs_update_pin"
|
||||
style="@style/Button.Registration"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cpb_textIdle="@string/ChangeNumberPinDiffersFragment__update_pin"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -0,0 +1,18 @@
|
|||
<?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" />
|
||||
|
||||
<include
|
||||
layout="@layout/fragment_registration_lock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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>
|
|
@ -0,0 +1,37 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/dsl_settings_toolbar" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/change_phone_number_verify_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:indicatorColor="@color/signal_accent_primary"
|
||||
app:indicatorSize="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/change_phone_number_verify_status"
|
||||
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_horizontal"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_hint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/change_phone_number_verify_progress"
|
||||
tools:text="Verifying +1 555-555-5555" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -89,7 +89,7 @@
|
|||
android:layout_height="2dp"
|
||||
android:background="@drawable/toolbar_shadow"
|
||||
app:app_bar_layout_id="@+id/emoji_keyboard_search_appbar"
|
||||
app:layout_behavior=".keyboard.TopShadowBehavior" />
|
||||
app:layout_behavior="org.thoughtcrime.securesms.keyboard.TopShadowBehavior" />
|
||||
|
||||
<View
|
||||
android:id="@+id/emoji_keyboard_bottom_shadow"
|
||||
|
@ -97,6 +97,6 @@
|
|||
android:layout_height="3dp"
|
||||
android:background="@drawable/bottom_toolbar_shadow"
|
||||
app:bottom_bar_id="@+id/emoji_keyboard_bottom_bar"
|
||||
app:layout_behavior=".keyboard.BottomShadowBehavior" />
|
||||
app:layout_behavior="org.thoughtcrime.securesms.keyboard.BottomShadowBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -88,13 +88,13 @@
|
|||
android:layout_height="2dp"
|
||||
android:background="@drawable/toolbar_shadow"
|
||||
app:app_bar_layout_id="@+id/sticker_keyboard_search_appbar"
|
||||
app:layout_behavior=".keyboard.TopShadowBehavior" />
|
||||
app:layout_behavior="org.thoughtcrime.securesms.keyboard.TopShadowBehavior" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
android:background="@drawable/bottom_toolbar_shadow"
|
||||
app:bottom_bar_id="@+id/sticker_keyboard_packs_background"
|
||||
app:layout_behavior=".keyboard.BottomShadowBehavior" />
|
||||
app:layout_behavior="org.thoughtcrime.securesms.keyboard.BottomShadowBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
android:layout_marginStart="12dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:background="@color/white"
|
||||
android:textColor="@color/core_ultramarine"
|
||||
android:background="@color/signal_background_primary"
|
||||
android:textColor="@color/signal_text_primary_dialog"
|
||||
style="@style/Signal.Text.Caption"
|
||||
tools:text="Profile Name"/>
|
||||
|
||||
|
|
|
@ -107,6 +107,13 @@
|
|||
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"
|
||||
|
@ -130,6 +137,8 @@
|
|||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/app_settings_change_number" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/advancedPinSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedAdvancedPinPreferenceFragment"
|
||||
|
@ -410,6 +419,13 @@
|
|||
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" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
<?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.ChangeNumberFragment"
|
||||
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.ChangeNumberEnterPhoneNumberFragment"
|
||||
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.ChangeNumberConfirmFragment"
|
||||
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.ChangeNumberVerifyFragment"
|
||||
tools:layout="@layout/fragment_change_phone_number_verify">
|
||||
|
||||
<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.registration.fragments.CaptchaFragment"
|
||||
tools:layout="@layout/fragment_registration_captcha" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changeNumberEnterCodeFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberEnterCodeFragment"
|
||||
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.ChangeNumberRegistrationLockFragment"
|
||||
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.ChangeNumberAccountLockedFragment"
|
||||
tools:layout="@layout/fragment_change_number_account_locked" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/changeNumberPinDiffers"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberPinDiffersFragment"
|
||||
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>
|
|
@ -134,11 +134,6 @@
|
|||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_wrongNumber"
|
||||
app:popUpTo="@id/enterCodeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_requestCaptcha"
|
||||
app:destination="@id/captchaFragment"
|
||||
|
@ -194,10 +189,6 @@
|
|||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
|
||||
<argument
|
||||
android:name="isV1RegistrationLock"
|
||||
app:argType="boolean" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -85,6 +85,10 @@
|
|||
<item name="android:minHeight">48dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Widget.Button.Large.Secondary.NoOutline">
|
||||
<item name="strokeWidth">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Widget.Button.Medium.Secondary" parent="@style/Signal.Widget.Button.Base.Secondary">
|
||||
<item name="android:minHeight">36dp</item>
|
||||
<item name="strokeWidth">0dp</item>
|
||||
|
|
|
@ -3490,6 +3490,44 @@
|
|||
<string name="AccountSettingsFragment__account">Account</string>
|
||||
<string name="AccountSettingsFragment__youll_be_asked_less_frequently">You\'ll be asked less frequently over time</string>
|
||||
<string name="AccountSettingsFragment__require_your_signal_pin">Require your Signal PIN to register your phone number with Signal again</string>
|
||||
<string name="AccountSettingsFragment__change_phone_number">Change phone number</string>
|
||||
|
||||
<!-- ChangeNumberFragment -->
|
||||
<string name="ChangeNumberFragment__use_this_to_change_your_current_phone_number_to_a_new_phone_number">Use this to change your current phone number to a new phone number. You can’t undo this change.\n\nBefore continuing, make sure your new number can receive SMS or calls.</string>
|
||||
<string name="ChangeNumberFragment__continue">Continue</string>
|
||||
<string name="ChangeNumber__your_phone_number_has_been_changed">Your phone number has been changed.</string>
|
||||
|
||||
<!-- ChangeNumberEnterPhoneNumberFragment -->
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__change_number">Change Number</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__your_old_number">Your old number</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__old_phone_number">Old phone number</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__your_new_number">Your new number</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__new_phone_number">New phone number</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__the_phone_number_you_entered_doesnt_match_your_accounts">The phone number you entered doesn\'t match your account\'s.</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code">You must specify your old number\'s country code</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number">You must specify your old number</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code">You must specify your new number\'s country code</string>
|
||||
<string name="ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number">You must specify your new phone number</string>
|
||||
|
||||
<!-- ChangeNumberVerifyFragment -->
|
||||
<string name="ChangeNumberVerifyFragment__change_number">Change Number</string>
|
||||
<string name="ChangeNumberVerifyFragment__verifying_s">Verifying %1$s</string>
|
||||
<string name="ChangeNumberVerifyFragment__captcha_required">Captcha required</string>
|
||||
|
||||
<!-- ChangeNumberConfirmFragment -->
|
||||
<string name="ChangeNumberConfirmFragment__change_number">Change number</string>
|
||||
<string name="ChangeNumberConfirmFragment__you_are_about_to_change_your_phone_number_from_s_to_s">You are about to change your phone number from %1$s to %2$s.\n\nBefore proceeding, please verify that the below number is correct.</string>
|
||||
<string name="ChangeNumberConfirmFragment__edit_number">Edit number</string>
|
||||
|
||||
<!-- ChangeNumberRegistrationLockFragment -->
|
||||
<string name="ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin">Signal Change Number - Need Help with PIN for Android (v2 PIN)</string>
|
||||
|
||||
<!-- ChangeNumberPinDiffersFragment -->
|
||||
<string name="ChangeNumberPinDiffersFragment__pins_do_not_match">PINs do not match</string>
|
||||
<string name="ChangeNumberPinDiffersFragment__the_pin_associated_with_your_new_number_is_different_from_the_pin_associated_with_your_old_one">The PIN associated with your new number is different from the PIN associated with your old one. Would you like to keep your old PIN or update it?</string>
|
||||
<string name="ChangeNumberPinDiffersFragment__keep_old_pin">Keep old PIN</string>
|
||||
<string name="ChangeNumberPinDiffersFragment__update_pin">Update PIN</string>
|
||||
<string name="ChangeNumberPinDiffersFragment__keep_old_pin_question">Keep old pin?</string>
|
||||
|
||||
<!-- ChatsSettingsFragment -->
|
||||
<string name="ChatsSettingsFragment__keyboard">Keyboard</string>
|
||||
|
|
|
@ -78,14 +78,14 @@ dependencyVerification {
|
|||
['androidx.cursoradapter:cursoradapter:1.0.0',
|
||||
'a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564'],
|
||||
|
||||
['androidx.customview:customview:1.0.0',
|
||||
'20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2'],
|
||||
['androidx.customview:customview:1.1.0',
|
||||
'01f76ab043770a97b054046f9815717b82ce0355c02967d16c61981359dc189a'],
|
||||
|
||||
['androidx.documentfile:documentfile:1.0.0',
|
||||
'865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487'],
|
||||
|
||||
['androidx.drawerlayout:drawerlayout:1.0.0',
|
||||
'9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1'],
|
||||
['androidx.drawerlayout:drawerlayout:1.1.1',
|
||||
'2c5f0dca378eb78ca2c4403f9889c77daa3059302260f26a07fe9f63c08926fe'],
|
||||
|
||||
['androidx.dynamicanimation:dynamicanimation:1.0.0',
|
||||
'ce005162c229bf308d2d5b12fb6cad0874069cbbeaccee63a8193bd08d40de04'],
|
||||
|
@ -120,14 +120,14 @@ dependencyVerification {
|
|||
['androidx.legacy:legacy-support-v4:1.0.0',
|
||||
'78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-common-java8:2.1.0',
|
||||
['androidx.lifecycle:lifecycle-common-java8:2.3.1',
|
||||
'a1ec63c1bb973443cb731d78ec336c5e20e7ee35c89cbb32d36f92c55bb02542'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-common:2.3.1',
|
||||
'15848fb56db32f4c7cdc72b324003183d52a4884d6bf09be708ac7f587d139b5'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-extensions:2.1.0',
|
||||
'bd53c64b038585215b4959c1a388437a3ad525608a31c58e4283c3e371727d4d'],
|
||||
['androidx.lifecycle:lifecycle-extensions:2.2.0',
|
||||
'648c8de1d10b025d524a2e46ac994fc3f6bf186826c09ec1a62d250bf1b877ae'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.1',
|
||||
'6dd41c3c33daeb503fd87fbfff7043adb0be6c541a9c9e09bf531ca49520fddb'],
|
||||
|
@ -135,17 +135,17 @@ dependencyVerification {
|
|||
['androidx.lifecycle:lifecycle-livedata-core:2.3.1',
|
||||
'e55d38c372460f0a03997ddc950c67227511340fd74f8634d99d29653cd81ab1'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-livedata:2.1.0',
|
||||
'242e446bed3db36f0df0aab0cb7f91060bd2dab7adcad1117adf54e724cd1d26'],
|
||||
['androidx.lifecycle:lifecycle-livedata:2.3.1',
|
||||
'b1e061139126f883d4010f23fe7ba460adea432d453c88a00b868cbf9ac037ce'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-process:2.1.0',
|
||||
'8cddd0c7f4927bbf71fb71fca000786df82cc597c99463d6916ccbe4a205a9ac'],
|
||||
['androidx.lifecycle:lifecycle-process:2.2.0',
|
||||
'3a977e7778fc8418742d388409daaba7ea8fea8823d21ffb96e4c4236f715070'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0',
|
||||
'8729895193f5fa36814bd399c866a81a87270b802979869b1082127281bd2836'],
|
||||
['androidx.lifecycle:lifecycle-reactivestreams-ktx:2.3.1',
|
||||
'55daac9050fb8ed601c46dcc05096dcc3ea8d2e463a92dbb9e1a875db9ef3066'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-reactivestreams:2.1.0',
|
||||
'4b974fa3e691a6d205fccecfa930aa7880aabf9d80159de91bbaed1d351bc4ab'],
|
||||
['androidx.lifecycle:lifecycle-reactivestreams:2.3.1',
|
||||
'a5600258465fe03a96a5d6891e08cbb5bd8ce8d9af89ed54af9d3d8ccf801bdc'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-runtime-ktx:2.3.1',
|
||||
'7ad2987dd7f4075c0871a72cf07e9649d9cd790fc23dfab1972eca4710373873'],
|
||||
|
@ -153,8 +153,8 @@ dependencyVerification {
|
|||
['androidx.lifecycle:lifecycle-runtime:2.3.1',
|
||||
'dd294f4a689c71ff877fd41f3b67a3a62f7760d44ce420e6130f1fc3569d8f00'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-service:2.1.0',
|
||||
'23516745f34f16ff7850bb1eadd55cf193dd789cba428de4bca120433e3bfd69'],
|
||||
['androidx.lifecycle:lifecycle-service:2.2.0',
|
||||
'ca2801ffc069555afed8eddd2292130f436956452bc8bbad30fb56f8e4e382a0'],
|
||||
|
||||
['androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1',
|
||||
'5fb3591b6a54eeb3e204be0125d48eb987b8ea45a5048140036865482ccf9de9'],
|
||||
|
@ -177,17 +177,29 @@ dependencyVerification {
|
|||
['androidx.multidex:multidex:2.0.1',
|
||||
'42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09'],
|
||||
|
||||
['androidx.navigation:navigation-common:2.1.0',
|
||||
'f968fcaa2fd94b0d1275ce175ecfb4773678732ead9b4d81993ffd5bc3fe3c7c'],
|
||||
['androidx.navigation:navigation-common-ktx:2.3.5',
|
||||
'ea75e05f355a2d649e8b985e19f1dd313e4a13cb03b7e5b8471ff59f24f7787e'],
|
||||
|
||||
['androidx.navigation:navigation-fragment:2.1.0',
|
||||
'776ba1be826f8de7cb262f55ece262c5eb9947758cbd2e902298750521404dd2'],
|
||||
['androidx.navigation:navigation-common:2.3.5',
|
||||
'691199ad0d0d771943d04941f373b3b77eeb1638da853269920ef68f38fbd0ab'],
|
||||
|
||||
['androidx.navigation:navigation-runtime:2.1.0',
|
||||
'499029c016345a2a2130ee7a32670871757e5fc7e6d1b93be8962bb59fa5ce9d'],
|
||||
['androidx.navigation:navigation-fragment-ktx:2.3.5',
|
||||
'5183f76aeed999eef1a611779c4f6e18f64b9e31aed95b103f194caa704e63c9'],
|
||||
|
||||
['androidx.navigation:navigation-ui:2.1.0',
|
||||
'1ec0558d692982c5bcfcca6de5b5972723e6b4a9870aa7fc1eddf5e869f116ed'],
|
||||
['androidx.navigation:navigation-fragment:2.3.5',
|
||||
'eadcb93cc5e54b25287d5fd72510e1ec16543002316026200830ffefdc0f8838'],
|
||||
|
||||
['androidx.navigation:navigation-runtime-ktx:2.3.5',
|
||||
'a44fc52231a67d6afe7b8891d272507febddbd922e7b231c8bd36720b94a042b'],
|
||||
|
||||
['androidx.navigation:navigation-runtime:2.3.5',
|
||||
'66713d11414bfe9ec88aed6ca847a1654de1d995fb564362e558f2a81c77715a'],
|
||||
|
||||
['androidx.navigation:navigation-ui-ktx:2.3.5',
|
||||
'f5a1fc4f47f1b3d6cf14284c1f9f4207804cdefac215e046f879c9a2f7bc005b'],
|
||||
|
||||
['androidx.navigation:navigation-ui:2.3.5',
|
||||
'b80cb813fa61d4c9996c623fe1f6a7628e4fe0a867f7bd8a98f7dc729391a6bc'],
|
||||
|
||||
['androidx.preference:preference:1.0.0',
|
||||
'ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1'],
|
||||
|
@ -219,8 +231,8 @@ dependencyVerification {
|
|||
['androidx.tracing:tracing:1.0.0',
|
||||
'07b8b6139665b884a162eccf97891ca50f7f56831233bf25168ae04f7b568612'],
|
||||
|
||||
['androidx.transition:transition:1.2.0',
|
||||
'a1e059b3bc0b43a58dec0efecdcaa89c82d2bca552ea5bacf6656c46e853157e'],
|
||||
['androidx.transition:transition:1.3.0',
|
||||
'cd96f2448409d03e190056c96e1fe5f521aa67602ab52a5e41dcec2c94218f2a'],
|
||||
|
||||
['androidx.vectordrawable:vectordrawable-animated:1.1.0',
|
||||
'76da2c502371d9c38054df5e2b248d00da87809ed058f3363eae87ce5e2403f8'],
|
||||
|
|
|
@ -18,7 +18,7 @@ buildscript {
|
|||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.whispersystems.libsignal.logging.Log;
|
|||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
|
@ -28,6 +29,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
|||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.payments.CurrencyConversions;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
|
@ -55,8 +57,6 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated
|
|||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.payments.CurrencyConversions;
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
@ -316,6 +316,15 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public ServiceResponse<VerifyAccountResponse> changeNumber(String code, String e164NewNumber, String registrationLock) {
|
||||
try {
|
||||
VerifyAccountResponse response = this.pushServiceSocket.changeNumber(code, e164NewNumber, registrationLock);
|
||||
return ServiceResponse.forResult(response, 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh account attributes with server.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package org.whispersystems.signalservice.api.account;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public final class ChangePhoneNumberRequest {
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
private String code;
|
||||
|
||||
@JsonProperty("reglock")
|
||||
private String registrationLock;
|
||||
|
||||
public ChangePhoneNumberRequest(String number, String code, String registrationLock) {
|
||||
this.number = number;
|
||||
this.code = code;
|
||||
this.registrationLock = registrationLock;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
}
|
|
@ -138,6 +138,10 @@ public final class SignalAccountRecord implements SignalRecord {
|
|||
diff.add("PrimarySendsSms");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.getE164(), that.getE164())) {
|
||||
diff.add("E164");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
|
||||
diff.add("UnknownFields");
|
||||
}
|
||||
|
@ -224,6 +228,10 @@ public final class SignalAccountRecord implements SignalRecord {
|
|||
return proto.getPrimarySendsSms();
|
||||
}
|
||||
|
||||
public String getE164() {
|
||||
return proto.getE164();
|
||||
}
|
||||
|
||||
AccountRecord toProto() {
|
||||
return proto;
|
||||
}
|
||||
|
@ -488,6 +496,11 @@ public final class SignalAccountRecord implements SignalRecord {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setE164(String e164) {
|
||||
builder.setE164(e164);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalAccountRecord build() {
|
||||
AccountRecord proto = builder.build();
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
|||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
|
@ -133,6 +134,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Future;
|
||||
|
@ -178,6 +180,7 @@ public class PushServiceSocket {
|
|||
private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s";
|
||||
private static final String DELETE_USERNAME_PATH = "/v1/accounts/username";
|
||||
private static final String DELETE_ACCOUNT_PATH = "/v1/accounts/me";
|
||||
private static final String CHANGE_NUMBER_PATH = "/v1/accounts/number";
|
||||
|
||||
private static final String PREKEY_METADATA_PATH = "/v2/keys/";
|
||||
private static final String PREKEY_PATH = "/v2/keys/%s";
|
||||
|
@ -339,6 +342,16 @@ public class PushServiceSocket {
|
|||
return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
|
||||
}
|
||||
|
||||
public VerifyAccountResponse changeNumber(String code, String e164NewNumber, String registrationLock)
|
||||
throws IOException
|
||||
{
|
||||
ChangePhoneNumberRequest changePhoneNumberRequest = new ChangePhoneNumberRequest(e164NewNumber, code, registrationLock);
|
||||
String requestBody = JsonUtil.toJson(changePhoneNumberRequest);
|
||||
String responseBody = makeServiceRequest(CHANGE_NUMBER_PATH, "PUT", requestBody);
|
||||
|
||||
return new VerifyAccountResponse();
|
||||
}
|
||||
|
||||
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.util.Iterator;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -111,7 +112,7 @@ public class WebSocketConnection extends WebSocketListener {
|
|||
String filledUri;
|
||||
|
||||
if (credentialsProvider.isPresent()) {
|
||||
String identifier = credentialsProvider.get().getUuid() != null ? credentialsProvider.get().getUuid().toString() : credentialsProvider.get().getE164();
|
||||
String identifier = Objects.requireNonNull(credentialsProvider.get().getUuid()).toString();
|
||||
filledUri = String.format(wsUri, identifier, credentialsProvider.get().getPassword());
|
||||
} else {
|
||||
filledUri = wsUri;
|
||||
|
|
|
@ -146,4 +146,5 @@ message AccountRecord {
|
|||
Payments payments = 16;
|
||||
uint32 universalExpireTimer = 17;
|
||||
bool primarySendsSms = 18;
|
||||
string e164 = 19;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue