diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9f9a414930..1383e63659 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -391,6 +391,10 @@ + + destination, @Nullable Intent nextIntent) { final Intent intent = new Intent(this, destination); if (nextIntent != null) intent.putExtra("next_intent", nextIntent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt index 713f648366..c34b2ffe18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt @@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber import android.os.Bundle import android.view.View +import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.Toolbar import androidx.navigation.fragment.findNavController import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.registration.fragments.BaseEnterCodeFragment class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment(R.layout.fragment_change_number_enter_code) { @@ -17,9 +19,26 @@ class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment(R.id.verify_header).setOnClickListener(null) + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + navigateUp() + } + } + ) + } + + private fun navigateUp() { + if (SignalStore.misc().isChangeNumberLocked) { + startActivity(ChangeNumberLockActivity.createIntent(requireContext())) + } else { + findNavController().navigateUp() + } } override fun getViewModel(): ChangeNumberViewModel { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt new file mode 100644 index 0000000000..ecc17d0dd4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt @@ -0,0 +1,108 @@ +package org.thoughtcrime.securesms.components.settings.app.changenumber + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.MainActivity +import org.thoughtcrime.securesms.PassphraseRequiredActivity +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme +import org.thoughtcrime.securesms.util.DynamicTheme +import org.thoughtcrime.securesms.util.LifecycleDisposable +import org.thoughtcrime.securesms.util.TextSecurePreferences +import java.util.Objects + +private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java) + +/** + * A captive activity that can determine if an interrupted/erred change number request + * caused a disparity between the server and our locally stored number. + */ +class ChangeNumberLockActivity : PassphraseRequiredActivity() { + + private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme() + private val disposables: LifecycleDisposable = LifecycleDisposable() + private lateinit var changeNumberRepository: ChangeNumberRepository + + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + dynamicTheme.onCreate(this) + disposables.bindTo(lifecycle) + + setContentView(R.layout.activity_change_number_lock) + + changeNumberRepository = ChangeNumberRepository(applicationContext) + checkWhoAmI() + } + + override fun onResume() { + super.onResume() + dynamicTheme.onResume(this) + } + + override fun onBackPressed() = Unit + + private fun checkWhoAmI() { + disposables.add( + changeNumberRepository.whoAmI() + .flatMap { whoAmI -> + if (Objects.equals(whoAmI.number, TextSecurePreferences.getLocalNumber(this))) { + Log.i(TAG, "Local and remote numbers match, nothing needs to be done.") + Single.just(false) + } else { + Log.i(TAG, "Local (${TextSecurePreferences.getLocalNumber(this)}) and remote (${whoAmI.number}) numbers do not match, updating local.") + changeNumberRepository.changeLocalNumber(whoAmI.number) + .map { true } + } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus) + ) + } + + private fun onChangeStatusConfirmed() { + SignalStore.misc().unlockChangeNumber() + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed) + .setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(this)))) + .setPositiveButton(android.R.string.ok) { _, _ -> + startActivity(MainActivity.clearTop(this)) + finish() + } + .setCancelable(false) + .show() + } + + private fun onFailedToGetChangeNumberStatus(error: Throwable) { + Log.w(TAG, "Unable to determine status of change number", error) + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.ChangeNumberLockActivity__change_status_unconfirmed) + .setMessage(getString(R.string.ChangeNumberLockActivity__we_could_not_determine_the_status_of_your_change_number_request, error.javaClass.simpleName)) + .setPositiveButton(R.string.ChangeNumberLockActivity__retry) { _, _ -> checkWhoAmI() } + .setNegativeButton(R.string.ChangeNumberLockActivity__leave) { _, _ -> finish() } + .setNeutralButton(R.string.ChangeNumberLockActivity__submit_debug_log) { _, _ -> + startActivity(Intent(this, SubmitDebugLogActivity::class.java)) + finish() + } + .setCancelable(false) + .show() + } + + companion object { + @JvmStatic + fun createIntent(context: Context): Intent { + return Intent(context, ChangeNumberLockActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt index 16a078923f..a98e902269 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber import android.os.Bundle import android.view.View +import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.Toolbar import androidx.navigation.fragment.findNavController import org.thoughtcrime.securesms.R @@ -20,7 +21,16 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo super.onViewCreated(view, savedInstanceState) val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setNavigationOnClickListener { findNavController().navigateUp() } + toolbar.setNavigationOnClickListener { navigateUp() } + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + navigateUp() + } + } + ) } override fun getViewModel(): BaseRegistrationViewModel { @@ -60,4 +70,12 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo body ) } + + private fun navigateUp() { + if (SignalStore.misc().isChangeNumberLocked) { + startActivity(ChangeNumberLockActivity.createIntent(requireContext())) + } else { + findNavController().navigateUp() + } + } } 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 0dd487a5ce..9fd581b2f5 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 @@ -18,6 +18,7 @@ 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 +import org.whispersystems.signalservice.internal.push.WhoAmIResponse private val TAG: String = Log.tag(ChangeNumberRepository::class.java) @@ -51,12 +52,18 @@ class ChangeNumberRepository(private val context: Context) { }.subscribeOn(Schedulers.io()) } + @Suppress("UsePropertyAccessSyntax") + fun whoAmI(): Single { + return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() } + .subscribeOn(Schedulers.io()) + } + @WorkerThread fun changeLocalNumber(e164: String): Single { - TextSecurePreferences.setLocalNumber(context, e164) - DatabaseFactory.getRecipientDatabase(context).updateSelfPhone(e164) + TextSecurePreferences.setLocalNumber(context, e164) + ApplicationDependencies.closeConnections() ApplicationDependencies.getIncomingMessageObserver() 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 205fe98e18..48b9d91a12 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 @@ -12,18 +12,21 @@ 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.keyvalue.SignalStore 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.VerifyProcessor 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 +import java.util.Objects private val TAG: String = Log.tag(ChangeNumberViewModel::class.java) @@ -104,6 +107,35 @@ class ChangeNumberViewModel( } } + override fun verifyCodeWithoutRegistrationLock(code: String): Single { + return super.verifyCodeWithoutRegistrationLock(code) + .doOnSubscribe { SignalStore.misc().lockChangeNumber() } + .flatMap(this::attemptToUnlockChangeNumber) + } + + override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single { + return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin) + .doOnSubscribe { SignalStore.misc().lockChangeNumber() } + .flatMap(this::attemptToUnlockChangeNumber) + } + + private fun attemptToUnlockChangeNumber(processor: T): Single { + return if (processor.hasResult() || processor.isServerSentError()) { + SignalStore.misc().unlockChangeNumber() + Single.just(processor) + } else { + changeNumberRepository.whoAmI() + .map { whoAmI -> + if (Objects.equals(whoAmI.number, localNumber)) { + Log.i(TAG, "Local and remote numbers match, we can unlock.") + SignalStore.misc().unlockChangeNumber() + } + processor + } + .onErrorReturn { processor } + } + } + override fun verifyAccountWithoutRegistrationLock(): Single> { return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index a9cc647138..c659b51b97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -15,6 +15,7 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String CLIENT_DEPRECATED = "misc.client_deprecated"; private static final String OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked"; private static final String HAS_EVER_HAD_AN_AVATAR = "misc.has.ever.had.an.avatar"; + private static final String CHANGE_NUMBER_LOCK = "misc.change_number.lock"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -97,4 +98,16 @@ public final class MiscellaneousValues extends SignalStoreValues { public void markHasEverHadAnAvatar() { putBoolean(HAS_EVER_HAD_AN_AVATAR, true); } + + public boolean isChangeNumberLocked() { + return getBoolean(CHANGE_NUMBER_LOCK, false); + } + + public void lockChangeNumber() { + putBoolean(CHANGE_NUMBER_LOCK, true); + } + + public void unlockChangeNumber() { + putBoolean(CHANGE_NUMBER_LOCK, false); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt index 3072e32adb..0cb9e669c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.registration import org.thoughtcrime.securesms.pin.TokenData +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.ServiceResponseProcessor import org.whispersystems.signalservice.internal.push.LockedException @@ -9,7 +10,9 @@ import org.whispersystems.signalservice.internal.push.VerifyAccountResponse /** * Process responses from attempting to verify an account for use in account registration. */ -sealed class VerifyAccountResponseProcessor(response: ServiceResponse) : ServiceResponseProcessor(response) { +sealed class VerifyAccountResponseProcessor( + response: ServiceResponse +) : ServiceResponseProcessor(response), VerifyProcessor { open val tokenData: TokenData? = null @@ -33,6 +36,10 @@ sealed class VerifyAccountResponseProcessor(response: ServiceResponse, val token: TokenData -) : ServiceResponseProcessor(response) { +) : ServiceResponseProcessor(response), VerifyProcessor { public override fun rateLimit(): Boolean { return super.rateLimit() @@ -45,4 +46,10 @@ class VerifyCodeWithRegistrationLockResponseProcessor( return VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.coerceError(response), token) } + + override fun isServerSentError(): Boolean { + return error is NonSuccessfulResponseCodeException || + error is KeyBackupSystemWrongPinException || + error is KeyBackupSystemNoDataException + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyProcessor.kt new file mode 100644 index 0000000000..ce09ec01cb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyProcessor.kt @@ -0,0 +1,6 @@ +package org.thoughtcrime.securesms.registration + +interface VerifyProcessor { + fun hasResult(): Boolean + fun isServerSentError(): Boolean +} diff --git a/app/src/main/res/layout/activity_change_number_lock.xml b/app/src/main/res/layout/activity_change_number_lock.xml new file mode 100644 index 0000000000..21b02118af --- /dev/null +++ b/app/src/main/res/layout/activity_change_number_lock.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbd02b0dd3..7855510ff5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3531,6 +3531,16 @@ Update PIN Keep old pin? + + It looks like you tried to change your number but we were unable to determine if it was successful.\n\nRechecking now… + Change status confirmed + Your number has been confirmed as %1$s. If this is not your new number, please restart the change number process. + Change status unconfirmed + We could not determine the status of your change number request.\n\n(Error: %1$s) + Retry + Leave + Submit debug log + Keyboard Enter key sends diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 2527b1cb1f..ddaceec66c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -65,6 +65,7 @@ import org.whispersystems.signalservice.internal.push.RemoteConfigResponse; import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; +import org.whispersystems.signalservice.internal.push.WhoAmIResponse; import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import org.whispersystems.signalservice.internal.storage.protos.ReadOperation; @@ -169,6 +170,10 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getOwnUuid(); } + public WhoAmIResponse getWhoAmI() throws IOException { + return this.pushServiceSocket.getWhoAmI(); + } + public KeyBackupService getKeyBackupService(KeyStore iasKeyStore, String enclaveName, byte[] serviceId, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index bce80d5540..acfcee9dea 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -328,6 +328,10 @@ public class PushServiceSocket { } } + public WhoAmIResponse getWhoAmI() throws IOException { + return JsonUtil.fromJson(makeServiceRequest(WHO_AM_I, "GET", null), WhoAmIResponse.class); + } + public VerifyAccountResponse verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java index 6d50117f7f..8ca14e141e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java @@ -6,7 +6,14 @@ public class WhoAmIResponse { @JsonProperty private String uuid; + @JsonProperty + private String number; + public String getUuid() { return uuid; } + + public String getNumber() { + return number; + } }