Initial work to support Change Number.

fork-5.53.8
Cody Henthorne 2021-09-03 17:07:05 -04:00
rodzic e09d162c1e
commit f2ab0b6423
61 zmienionych plików z 3177 dodań i 1176 usunięć

Wyświetl plik

@ -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"

Wyświetl plik

@ -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() }
}
)
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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),

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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) }
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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()
}
}
}
}

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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()
}
}
)
}
}

Wyświetl plik

@ -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
)
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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()
}
}
)
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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,

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -0,0 +1,4 @@
package org.thoughtcrime.securesms.registration.viewmodel;
public final class BaseEnterCodeViewModelDelegate {
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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 &&

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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)
}
}
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -7,7 +7,7 @@
<corners android:radius="4dp" />
<stroke
android:color="@color/core_ultramarine"
android:color="@color/signal_accent_primary"
android:width="2dp" />
</shape>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"/>

Wyświetl plik

@ -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 -->

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 cant 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>

Wyświetl plik

@ -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'],

Wyświetl plik

@ -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"

Wyświetl plik

@ -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.
*

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;

Wyświetl plik

@ -146,4 +146,5 @@ message AccountRecord {
Payments payments = 16;
uint32 universalExpireTimer = 17;
bool primarySendsSms = 18;
string e164 = 19;
}