diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt index 98f12b7a4d..44e8e9e152 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt @@ -89,8 +89,9 @@ abstract class DSLSettingsFragment( } override fun onDestroyView() { - super.onDestroyView() recyclerView = null + toolbar = null + super.onDestroyView() } fun setTitle(@StringRes resId: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterPhoneNumberFragment.kt index 6fcf49204a..f7f32563b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterPhoneNumberFragment.kt @@ -14,6 +14,7 @@ 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.databinding.FragmentChangeNumberEnterPhoneNumberBinding import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController @@ -25,19 +26,30 @@ 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 var binding: FragmentChangeNumberEnterPhoneNumberBinding? = null - private lateinit var oldNumberCountrySpinner: Spinner - private lateinit var oldNumberCountryCode: LabeledEditText - private lateinit var oldNumber: LabeledEditText + private val scrollView: ScrollView + get() = binding!!.changeNumberEnterPhoneNumberScroll - private lateinit var newNumberCountrySpinner: Spinner - private lateinit var newNumberCountryCode: LabeledEditText - private lateinit var newNumber: LabeledEditText + private val oldNumberCountrySpinner: Spinner + get() = binding!!.changeNumberEnterPhoneNumberOldNumberSpinner + private val oldNumberCountryCode: LabeledEditText + get() = binding!!.changeNumberEnterPhoneNumberOldNumberCountryCode + private val oldNumber: LabeledEditText + get() = binding!!.changeNumberEnterPhoneNumberOldNumberNumber + + private val newNumberCountrySpinner: Spinner + get() = binding!!.changeNumberEnterPhoneNumberNewNumberSpinner + private val newNumberCountryCode: LabeledEditText + get() = binding!!.changeNumberEnterPhoneNumberNewNumberCountryCode + private val newNumber: LabeledEditText + get() = binding!!.changeNumberEnterPhoneNumberNewNumberNumber private lateinit var viewModel: ChangeNumberViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding = FragmentChangeNumberEnterPhoneNumberBinding.bind(view) + viewModel = getViewModel(this) val toolbar: Toolbar = view.findViewById(R.id.toolbar) @@ -48,12 +60,6 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c 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 = ChangeNumberInputController( requireContext(), oldNumberCountryCode, @@ -87,10 +93,6 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c } ) - 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 = ChangeNumberInputController( requireContext(), newNumberCountryCode, @@ -136,6 +138,11 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c viewModel.getLiveNewNumber().observe(viewLifecycleOwner, newController::updateNumber) } + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + 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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index 233b381228..56aca551de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata import org.thoughtcrime.securesms.database.model.toProtoByteString import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.keyvalue.CertificateType import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.pin.KbsRepository @@ -86,16 +87,31 @@ class ChangeNumberRepository( fun ensureDecryptionsDrained(): Completable { return Completable.create { emitter -> + val drainedListener = object : Runnable { + override fun run() { + emitter.onComplete() + ApplicationDependencies + .getIncomingMessageObserver() + .removeDecryptionDrainedListener(this) + } + } + + emitter.setCancellable { + ApplicationDependencies + .getIncomingMessageObserver() + .removeDecryptionDrainedListener(drainedListener) + } + ApplicationDependencies .getIncomingMessageObserver() - .addDecryptionDrainedListener { - emitter.onComplete() - } + .addDecryptionDrainedListener(drainedListener) }.subscribeOn(Schedulers.single()) .timeout(15, TimeUnit.SECONDS) } - fun changeNumber(sessionId: String, newE164: String, pniUpdateMode: Boolean = false): Single> { + fun changeNumber(sessionId: String? = null, recoveryPassword: String? = null, newE164: String, pniUpdateMode: Boolean = false): Single> { + check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null)) + return Single.fromCallable { var completed = false var attempts = 0 @@ -104,8 +120,8 @@ class ChangeNumberRepository( while (!completed && attempts < 5) { val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( sessionId = sessionId, + recoveryPassword = recoveryPassword, newE164 = newE164, - registrationLock = null, pniUpdateMode = pniUpdateMode ) @@ -156,8 +172,7 @@ class ChangeNumberRepository( val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( sessionId = sessionId, newE164 = newE164, - registrationLock = registrationLock, - pniUpdateMode = false + registrationLock = registrationLock ) SignalStore.misc().setPendingChangeNumberMetadata(metadata) @@ -254,6 +269,8 @@ class ChangeNumberRepository( ApplicationDependencies.closeConnections() ApplicationDependencies.getIncomingMessageObserver() + ApplicationDependencies.getJobManager().add(RefreshAttributesJob()) + return rotateCertificates() } @@ -281,10 +298,11 @@ class ChangeNumberRepository( @Suppress("UsePropertyAccessSyntax") @WorkerThread private fun createChangeNumberRequest( - sessionId: String, + sessionId: String? = null, + recoveryPassword: String? = null, newE164: String, - registrationLock: String?, - pniUpdateMode: Boolean + registrationLock: String? = null, + pniUpdateMode: Boolean = false ): ChangeNumberRequestData { val selfIdentifier: String = SignalStore.account().requireAci().toString() val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci() @@ -338,7 +356,7 @@ class ChangeNumberRepository( val request = ChangePhoneNumberRequest( sessionId, - null, + recoveryPassword, newE164, registrationLock, pniIdentity.publicKey, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt index 740458788f..8f0cbe327f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt @@ -7,11 +7,14 @@ import android.widget.Toast import androidx.appcompat.widget.Toolbar import androidx.navigation.fragment.findNavController import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel +import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor import org.thoughtcrime.securesms.registration.VerifyAccountRepository import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.dualsim.MccMncProducer @@ -54,9 +57,24 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon lifecycleDisposable += viewModel .ensureDecryptionsDrained() .onErrorComplete() - .andThen(viewModel.requestVerificationCode(mode, mccMncProducer.mcc, mccMncProducer.mnc)) + .andThen(viewModel.changeNumberWithRecoveryPassword()) + .flatMap { changed -> + if (changed) { + Single.just(RequestCodeResult.RecoveryPasswordWorked) + } else { + viewModel.requestVerificationCode(mode, mccMncProducer.mcc, mccMncProducer.mnc) + .map { p -> RequestCodeResult.RequestedVerificationCode(p) } + } + } .observeOn(AndroidSchedulers.mainThread()) - .subscribe { processor -> + .subscribe { result -> + if (result is RequestCodeResult.RecoveryPasswordWorked) { + changeNumberSuccess() + return@subscribe + } + + val processor = (result as RequestCodeResult.RequestedVerificationCode).processor + if (processor.hasResult()) { findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment) } else if (processor.captchaRequired()) { @@ -74,4 +92,9 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon } } } + + private sealed interface RequestCodeResult { + object RecoveryPasswordWorked : RequestCodeResult + class RequestedVerificationCode(val processor: RegistrationSessionProcessor) : RequestCodeResult + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt index 8413bcfec7..ef9e406144 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt @@ -170,7 +170,7 @@ class ChangeNumberViewModel( .observeOn(Schedulers.io()) .flatMap { processor -> if (processor.isAlreadyVerified() || processor.hasResult() && processor.isVerified()) { - changeNumberRepository.changeNumber(sessionId, number.e164Number) + changeNumberRepository.changeNumber(sessionId = sessionId, newE164 = number.e164Number) } else if (processor.error == null) { Single.just>(ServiceResponse.forApplicationError(IncorrectCodeException(), 403, null)) } else { @@ -203,6 +203,24 @@ class ChangeNumberViewModel( } } + fun changeNumberWithRecoveryPassword(): Single { + val recoveryPassword = SignalStore.kbsValues().recoveryPassword + + return if (SignalStore.kbsValues().hasPin() && recoveryPassword != null) { + changeNumberRepository.changeNumber(recoveryPassword = recoveryPassword, newE164 = number.e164Number) + .map { r -> VerifyResponseWithoutKbs(r) } + .flatMap { p -> + if (p.hasResult()) { + onVerifySuccess(p).map { true } + } else { + Single.just(false) + } + } + } else { + Single.just(false) + } + } + class Factory(owner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(owner, null) { override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java index eceac5164e..afd8e9bc73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java @@ -131,6 +131,10 @@ public class IncomingMessageObserver { } } + public synchronized void removeDecryptionDrainedListener(@NonNull Runnable listener) { + decryptionDrainedListeners.remove(listener); + } + public boolean isDecryptionDrained() { return decryptionDrained; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java index 7d637cb1a4..432ee1b036 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java @@ -16,20 +16,22 @@ import androidx.core.content.ContextCompat; import org.thoughtcrime.securesms.R; +import java.lang.ref.WeakReference; + public class LongClickMovementMethod extends LinkMovementMethod { @SuppressLint("StaticFieldLeak") private static LongClickMovementMethod sInstance; - private final GestureDetector gestureDetector; - private View widget; - private LongClickCopySpan currentSpan; + private final GestureDetector gestureDetector; + private WeakReference widget; + private LongClickCopySpan currentSpan; private LongClickMovementMethod(final Context context) { gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onLongClick(widget); + if (currentSpan != null && widget != null && widget.get() != null) { + currentSpan.onLongClick(widget.get()); widget = null; currentSpan = null; } @@ -37,8 +39,8 @@ public class LongClickMovementMethod extends LinkMovementMethod { @Override public boolean onSingleTapUp(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onClick(widget); + if (currentSpan != null && widget != null && widget.get() != null) { + currentSpan.onClick(widget.get()); widget = null; currentSpan = null; } @@ -80,7 +82,7 @@ public class LongClickMovementMethod extends LinkMovementMethod { } this.currentSpan = aSingleSpan; - this.widget = widget; + this.widget = new WeakReference<>(widget); return gestureDetector.onTouchEvent(event); } else if (action == MotionEvent.ACTION_UP && Selection.getSelectionEnd(buffer) > 0){ Selection.setSelection(buffer, 0); diff --git a/app/src/main/res/layout/fragment_change_number_enter_phone_number.xml b/app/src/main/res/layout/fragment_change_number_enter_phone_number.xml index b51ec13df8..29f325cacc 100644 --- a/app/src/main/res/layout/fragment_change_number_enter_phone_number.xml +++ b/app/src/main/res/layout/fragment_change_number_enter_phone_number.xml @@ -1,8 +1,7 @@