diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt deleted file mode 100644 index 73d86bba24..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt +++ /dev/null @@ -1,410 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.app.changenumber - -import androidx.lifecycle.SavedStateHandle -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.FlakyTest -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkObject -import okhttp3.mockwebserver.MockResponse -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.signal.core.util.ThreadUtil -import org.signal.libsignal.protocol.state.SignedPreKeyRecord -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.pin.SvrRepository -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.VerifyAccountRepository -import org.thoughtcrime.securesms.registration.VerifyResponseProcessor -import org.thoughtcrime.securesms.testing.Get -import org.thoughtcrime.securesms.testing.MockProvider -import org.thoughtcrime.securesms.testing.Post -import org.thoughtcrime.securesms.testing.Put -import org.thoughtcrime.securesms.testing.SignalActivityRule -import org.thoughtcrime.securesms.testing.assertIs -import org.thoughtcrime.securesms.testing.assertIsNot -import org.thoughtcrime.securesms.testing.assertIsNotNull -import org.thoughtcrime.securesms.testing.assertIsNull -import org.thoughtcrime.securesms.testing.assertIsSize -import org.thoughtcrime.securesms.testing.connectionFailure -import org.thoughtcrime.securesms.testing.failure -import org.thoughtcrime.securesms.testing.parsedRequestBody -import org.thoughtcrime.securesms.testing.success -import org.thoughtcrime.securesms.testing.timeout -import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest -import org.whispersystems.signalservice.api.kbs.MasterKey -import org.whispersystems.signalservice.api.push.ServiceId -import org.whispersystems.signalservice.api.push.ServiceId.PNI -import org.whispersystems.signalservice.api.svr.SecureValueRecovery -import org.whispersystems.signalservice.internal.push.AuthCredentials -import org.whispersystems.signalservice.internal.push.MismatchedDevices -import org.whispersystems.signalservice.internal.push.PreKeyState -import java.security.SecureRandom -import java.util.UUID - -@RunWith(AndroidJUnit4::class) -class ChangeNumberViewModelTest { - - @get:Rule - val harness = SignalActivityRule() - - private lateinit var viewModel: ChangeNumberViewModel - - @Before - fun setUp() { - ThreadUtil.runOnMainSync { - viewModel = ChangeNumberViewModel( - localNumber = harness.self.requireE164(), - changeNumberRepository = ChangeNumberRepository(), - savedState = SavedStateHandle(), - password = SignalStore.account.servicePassword!!, - verifyAccountRepository = VerifyAccountRepository(harness.application) - ) - - viewModel.setNewCountry(1) - viewModel.setNewNationalNumber("5555550102") - } - - mockkObject(SvrRepository) - } - - @After - fun tearDown() { - unmockkObject(SvrRepository) - InstrumentationApplicationDependencyProvider.clearHandlers() - } - - @Test - fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() { - // GIVEN - val aci = Recipient.self().requireServiceId() - val newPni = PNI.from(UUID.randomUUID()) - lateinit var changeNumberRequest: ChangePhoneNumberRequest - lateinit var setPreKeysRequest: PreKeyState - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, - Put("/v2/accounts/number") { r -> - changeNumberRequest = r.parsedRequestBody() - MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni)) - }, - Put("/v2/keys") { r -> - setPreKeysRequest = r.parsedRequestBody() - MockResponse().success() - }, - Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } - ) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow - - // THEN - assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) - } - - /** - * If we encounter a server error, this means the server ack our request and rejected it. In this - * case we know the change *did not* take on the server and can reset to a clean state. - */ - @Test - fun testChangeNumber_givenServerFailedApiCall() { - // GIVEN - val oldPni = Recipient.self().requirePni() - val oldE164 = Recipient.self().requireE164() - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, - Put("/v2/accounts/number") { MockResponse().failure(500) } - ) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet() - - // THEN - processor.isServerSentError() assertIs true - Recipient.self().requireE164() assertIs oldE164 - Recipient.self().requirePni() assertIs oldPni - SignalStore.misc.pendingChangeNumberMetadata.assertIsNull() - } - - /** - * If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change - * number on the server side. We have to do a whoami call to query the server for our details and then - * respond accordingly. - * - * In this case, the whoami is our old details, so we can know the change *did not* take on the server - * and can reset to a clean state. - */ - @Test - fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() { - // GIVEN - val aci = Recipient.self().requireServiceId() - val oldPni = Recipient.self().requirePni() - val oldE164 = Recipient.self().requireE164() - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, - Put("/v2/accounts/number") { MockResponse().connectionFailure() }, - Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) } - ) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet() - - // THEN - processor.isServerSentError() assertIs false - Recipient.self().requireE164() assertIs oldE164 - Recipient.self().requirePni() assertIs oldPni - SignalStore.misc.isChangeNumberLocked assertIs false - SignalStore.misc.pendingChangeNumberMetadata.assertIsNull() - } - - /** - * If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change - * number on the server side. We have to do a whoami call to query the server for our details and then - * respond accordingly. - * - * In this case, the whoami is our new details, so we can know the change *did* take on the server - * and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock - * and apply the pending state after confirming the change on the server. - */ - @Test - @FlakyTest - @Ignore("Test sometimes requires manual intervention to continue.") - fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() { - // GIVEN - val aci = Recipient.self().requireServiceId() - val oldPni = Recipient.self().requirePni() - val oldE164 = Recipient.self().requireE164() - val newPni = PNI.from(UUID.randomUUID()) - - lateinit var changeNumberRequest: ChangePhoneNumberRequest - lateinit var setPreKeysRequest: PreKeyState - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, - Put("/v2/accounts/number") { r -> - changeNumberRequest = r.parsedRequestBody() - MockResponse().timeout() - }, - Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) }, - Put("/v2/keys") { r -> - setPreKeysRequest = r.parsedRequestBody() - MockResponse().success() - }, - Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } - ) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet() - - // THEN - processor.isServerSentError() assertIs false - Recipient.self().requireE164() assertIs oldE164 - Recipient.self().requirePni() assertIs oldPni - SignalStore.misc.isChangeNumberLocked assertIs true - SignalStore.misc.pendingChangeNumberMetadata.assertIsNotNull() - - // WHEN AGAIN Processing lock - val scenario = harness.launchActivity() - scenario.onActivity {} - ThreadUtil.sleep(500) - - // THEN AGAIN - assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) - } - - @Test - fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() { - // GIVEN - val aci = Recipient.self().requireServiceId() - val newPni = PNI.from(UUID.randomUUID()) - - lateinit var changeNumberRequest: ChangePhoneNumberRequest - lateinit var setPreKeysRequest: PreKeyState - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, - Put("/v2/accounts/number") { r -> - changeNumberRequest = r.parsedRequestBody() - if (changeNumberRequest.registrationLock.isNullOrEmpty()) { - MockResponse().failure(423, MockProvider.lockedFailure) - } else { - MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni)) - } - }, - Put("/v2/keys") { r -> - setPreKeysRequest = r.parsedRequestBody() - MockResponse().success() - }, - Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } - ) - - every { SvrRepository.restoreMasterKeyPreRegistrationFromV2(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) - every { SvrRepository.restoreMasterKeyPreRegistrationFromV3(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor -> - processor.registrationLock() assertIs true - Recipient.self().requirePni() assertIsNot newPni - SignalStore.misc.pendingChangeNumberMetadata.assertIsNull() - } - - viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow - - // THEN - assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) - } - - @Test - fun testChangeNumber_givenMismatchedDevicesOnFirstCall() { - // GIVEN - val aci = Recipient.self().requireServiceId() - val newPni = PNI.from(UUID.randomUUID()) - lateinit var changeNumberRequest: ChangePhoneNumberRequest - lateinit var setPreKeysRequest: PreKeyState - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, - Put("/v2/accounts/number") { r -> - changeNumberRequest = r.parsedRequestBody() - if (changeNumberRequest.deviceMessages.isEmpty()) { - MockResponse().failure( - 409, - MismatchedDevices().apply { - missingDevices = listOf(2) - extraDevices = emptyList() - } - ) - } else { - MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni)) - } - }, - Get("/v2/keys/$aci/2") { - MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2)) - }, - Put("/v2/keys") { r -> - setPreKeysRequest = r.parsedRequestBody() - MockResponse().success() - }, - Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } - ) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow - - // THEN - assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) - } - - @Test - fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() { - // GIVEN - val aci = Recipient.self().requireServiceId() - val newPni = PNI.from(UUID.randomUUID()) - - lateinit var changeNumberRequest: ChangePhoneNumberRequest - lateinit var setPreKeysRequest: PreKeyState - - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) }, - Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) }, - Put("/v2/accounts/number") { r -> - changeNumberRequest = r.parsedRequestBody() - if (changeNumberRequest.registrationLock.isNullOrEmpty()) { - MockResponse().failure(423, MockProvider.lockedFailure) - } else if (changeNumberRequest.deviceMessages.isEmpty()) { - MockResponse().failure( - 409, - MismatchedDevices().apply { - missingDevices = listOf(2) - extraDevices = emptyList() - } - ) - } else if (changeNumberRequest.deviceMessages.size == 1) { - MockResponse().failure( - 409, - MismatchedDevices().apply { - missingDevices = listOf(2, 3) - extraDevices = emptyList() - } - ) - } else { - MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni)) - } - }, - Get("/v2/keys/$aci/2") { - MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2)) - }, - Get("/v2/keys/$aci/3") { - MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3)) - }, - Put("/v2/keys") { r -> - setPreKeysRequest = r.parsedRequestBody() - MockResponse().success() - }, - Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } - ) - - every { SvrRepository.restoreMasterKeyPreRegistrationFromV2(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) - every { SvrRepository.restoreMasterKeyPreRegistrationFromV3(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) - - // WHEN - viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow - viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor -> - processor.registrationLock() assertIs true - Recipient.self().requirePni() assertIsNot newPni - SignalStore.misc.pendingChangeNumberMetadata.assertIsNull() - } - - viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow - - // THEN - assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) - } - - private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) { - val pniProtocolStore = AppDependencies.protocolStore.pni() - val pniMetadataStore = SignalStore.account.pniPreKeys - - Recipient.self().requireE164() assertIs "+15555550102" - Recipient.self().requirePni() assertIs newPni - - SignalStore.account.pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!! - SignalStore.account.pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey - pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId - - val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId) - activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey - activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature - - setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey - setPreKeysRequest.preKeys assertIsSize 100 - - SignalStore.misc.pendingChangeNumberMetadata.assertIsNull() - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cbb242aa8..e9c56a4842 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -776,12 +776,6 @@ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:exported="false"/> - - - - - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java index 523ce35655..7cc7ebe936 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java @@ -16,7 +16,6 @@ import org.signal.core.util.logging.Log; import org.signal.core.util.tracing.Tracer; import org.signal.devicetransfer.TransferStatus; import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity; -import org.thoughtcrime.securesms.components.settings.app.changenumber.v2.ChangeNumberLockV2Activity; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity; @@ -28,8 +27,7 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity; import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity; +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity; import org.thoughtcrime.securesms.restore.RestoreActivity; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.AppStartup; @@ -181,7 +179,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements return STATE_TRANSFER_ONGOING; } else if (SignalStore.misc().isOldDeviceTransferLocked()) { return STATE_TRANSFER_LOCKED; - } else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class && getClass() != ChangeNumberLockV2Activity.class) { + } else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) { return STATE_CHANGE_NUMBER_LOCK; } else { return STATE_NORMAL; @@ -222,11 +220,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements } private Intent getPushRegistrationIntent() { - if (RemoteConfig.registrationV2()) { - return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent()); - } else { - return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent()); - } + return RegistrationActivity.newIntentForNewRegistration(this, getIntent()); } private Intent getEnterSignalPinIntent() { @@ -269,11 +263,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements } private Intent getChangeNumberLockIntent() { - if (RemoteConfig.registrationV2()) { - return ChangeNumberLockV2Activity.createIntent(this); - } else { - return ChangeNumberLockActivity.createIntent(this); - } + return ChangeNumberLockActivity.createIntent(this); } private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java index bcd2778ab7..c6eda98411 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java @@ -28,7 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment; +import org.thoughtcrime.securesms.restore.restorelocalbackup.PassphraseAsYouTypeFormatter; import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -170,7 +170,7 @@ public class BackupDialog { Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); positiveButton.setEnabled(false); - RestoreBackupFragment.PassphraseAsYouTypeFormatter formatter = new RestoreBackupFragment.PassphraseAsYouTypeFormatter(); + PassphraseAsYouTypeFormatter formatter = new PassphraseAsYouTypeFormatter(); prompt.addTextChangedListener(new AfterTextChanged(editable -> { formatter.afterTextChanged(editable); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java index a11d3ec59a..307daa4024 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UnauthorizedReminder.java @@ -6,7 +6,6 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; import org.thoughtcrime.securesms.util.TextSecurePreferences; public class UnauthorizedReminder extends Reminder { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index 06e6e4391b..83c38b552d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.CachedInflater import org.thoughtcrime.securesms.util.DynamicTheme -import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.navigation.safeNavigate private const val START_LOCATION = "app.settings.start.location" @@ -40,8 +39,7 @@ class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { if (intent?.hasExtra(ARG_NAV_GRAPH) != true) { - val navGraphResId = if (RemoteConfig.registrationV2) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings - intent?.putExtra(ARG_NAV_GRAPH, navGraphResId) + intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number) } super.onCreate(savedInstanceState, ready) @@ -197,9 +195,8 @@ class AppSettingsActivity : DSLSettingsActivity(), InAppPaymentComponent { fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, StartLocation.RECOVER_USERNAME) private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent { - val navGraphResId = if (RemoteConfig.registrationV2) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings return Intent(context, AppSettingsActivity::class.java) - .putExtra(ARG_NAV_GRAPH, navGraphResId) + .putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number) .putExtra(START_LOCATION, startLocation.code) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 48a0f56df2..fd41d05281 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -33,7 +33,7 @@ import org.thoughtcrime.securesms.events.ReminderUpdateEvent import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity import org.thoughtcrime.securesms.util.Environment import org.thoughtcrime.securesms.util.PlayStoreUtil import org.thoughtcrime.securesms.util.RemoteConfig @@ -114,7 +114,7 @@ class AppSettingsFragment : DSLSettingsFragment( PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()) } R.id.reminder_action_re_register -> { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())) + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index 83676f8ede..96a5965e8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.lock.v2.SvrConstants import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity import org.thoughtcrime.securesms.util.PlayStoreUtil import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -151,7 +151,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag clickPref( title = DSLSettingsText.from(R.string.preferences_account_reregister), onClick = { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())) + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberAccountLockedFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberAccountLockedFragment.kt index f17ca452e2..f9e4ea245b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberAccountLockedFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberAccountLockedFragment.kt @@ -1,27 +1,74 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.components.settings.app.changenumber +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.View -import androidx.appcompat.widget.Toolbar +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.registration.fragments.BaseAccountLockedFragment -import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel +import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView +import java.util.concurrent.TimeUnit -class ChangeNumberAccountLockedFragment : BaseAccountLockedFragment(R.layout.fragment_change_number_account_locked) { +/** + * Screen visible to the user when they are registration locked and have no SVR data. + */ +class ChangeNumberAccountLockedFragment : LoggingFragment(R.layout.fragment_change_number_account_locked) { + + companion object { + private val TAG = Log.tag(ChangeNumberAccountLockedFragment::class.java) + } + + private val viewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setNavigationOnClickListener { findNavController().navigateUp() } + setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title)) + + val description = view.findViewById(R.id.account_locked_description) + + viewModel.liveLockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> + description.text = getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)) + } + + view.findViewById(R.id.account_locked_next).setOnClickListener { onNext() } + view.findViewById(R.id.account_locked_learn_more).setOnClickListener { learnMore() } + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onNext() + } + } + ) } - override fun getViewModel(): BaseRegistrationViewModel { - return ChangeNumberUtil.getViewModel(this) + private fun learnMore() { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url))) + startActivity(intent) } - override fun onNext() { + private fun durationToDays(duration: Long): Long { + return if (duration != 0L) getLockoutDays(duration).toLong() else 7 + } + + private fun getLockoutDays(timeRemainingMs: Long): Int { + return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1 + } + + fun onNext() { findNavController().navigateUp() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberCaptchaV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberCaptchaFragment.kt similarity index 68% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberCaptchaV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberCaptchaFragment.kt index f73b412cf6..3e1672f371 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberCaptchaV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberCaptchaFragment.kt @@ -3,19 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 +package org.thoughtcrime.securesms.components.settings.app.changenumber import android.os.Bundle import android.view.View import androidx.fragment.app.activityViewModels -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.ui.captcha.CaptchaV2Fragment +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.ui.captcha.CaptchaFragment /** - * Screen visible to the user when they are to solve a captcha. @see [CaptchaV2Fragment] + * Screen visible to the user when they are to solve a captcha. @see [CaptchaFragment] */ -class ChangeNumberCaptchaV2Fragment : CaptchaV2Fragment() { - private val viewModel by activityViewModels() +class ChangeNumberCaptchaFragment : CaptchaFragment() { + private val viewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.addPresentedChallenge(Challenge.CAPTCHA) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberConfirmFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberConfirmFragment.kt index 5b3b6326ad..8c0fcc56b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberConfirmFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberConfirmFragment.kt @@ -1,24 +1,33 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import com.google.android.gms.auth.api.phone.SmsRetriever import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.PlayServicesUtil -import org.thoughtcrime.securesms.util.PlayServicesUtil.PlayServicesStatus import org.thoughtcrime.securesms.util.navigation.safeNavigate +/** + * Screen visible to the user for them to confirm their new phone number was entered correctly. + */ class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) { - private lateinit var viewModel: ChangeNumberViewModel + + companion object { + private val TAG = Log.tag(ChangeNumberConfirmFragment::class.java) + } + + private val viewModel by activityViewModels() 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() } @@ -33,35 +42,14 @@ class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_num editNumber.setOnClickListener { findNavController().navigateUp() } val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number) - changeNumber.setOnClickListener { onConfirm() } - } - - private fun onConfirm() { - val playServicesAvailable = PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesStatus.SUCCESS - - if (playServicesAvailable) { - val client = SmsRetriever.getClient(requireContext()) - val task = client.startSmsRetriever() - - task.addOnSuccessListener { - Log.i(TAG, "Successfully registered SMS listener.") - navigateToVerify(smsListenerEnabled = true) + changeNumber.setOnClickListener { + viewModel.registerSmsListenerWithCompletionListener(requireContext()) { + navigateToVerify(it) } - - task.addOnFailureListener { e -> - Log.w(TAG, "Failed to register SMS listener.", e) - navigateToVerify() - } - } else { - navigateToVerify() } } private fun navigateToVerify(smsListenerEnabled: Boolean = false) { - findNavController().safeNavigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment, ChangeNumberVerifyFragmentArgs.Builder().setSmsListenerEnabled(smsListenerEnabled).build().toBundle()) - } - - companion object { - private val TAG = Log.tag(ChangeNumberConfirmFragment::class.java) + findNavController().safeNavigate(ChangeNumberConfirmFragmentDirections.actionChangePhoneNumberConfirmFragmentToChangePhoneNumberVerifyFragment(smsListenerEnabled)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberEnterCodeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberEnterCodeV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt index 87f89d5475..23ab18b355 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberEnterCodeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 +package org.thoughtcrime.securesms.components.settings.app.changenumber import android.content.DialogInterface import android.os.Bundle @@ -24,12 +24,12 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterCodeBinding import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.registration.ReceivedSmsEvent +import org.thoughtcrime.securesms.registration.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -38,13 +38,13 @@ import org.thoughtcrime.securesms.util.visible /** * Screen used to enter the registration code provided by the service. */ -class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change_number_enter_code) { +class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_number_enter_code) { companion object { - private val TAG: String = Log.tag(ChangeNumberEnterCodeV2Fragment::class.java) + private val TAG: String = Log.tag(ChangeNumberEnterCodeFragment::class.java) } - private val viewModel by activityViewModels() + private val viewModel by activityViewModels() private val binding: FragmentChangeNumberEnterCodeBinding by ViewBinderDelegate(FragmentChangeNumberEnterCodeBinding::bind) private lateinit var phoneStateListener: SignalStrengthPhoneStateListener @@ -154,7 +154,7 @@ class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change private fun navigateUp() { if (SignalStore.misc.isChangeNumberLocked) { Log.d(TAG, "Change number locked, navigateUp") - startActivity(ChangeNumberLockV2Activity.createIntent(requireContext())) + startActivity(ChangeNumberLockActivity.createIntent(requireContext())) } else { Log.d(TAG, "navigateUp") findNavController().navigateUp() @@ -187,7 +187,7 @@ class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change binding.codeEntryLayout.keyboard.displayLocked().addListener( object : AssertedSuccessListener() { override fun onSuccess(result: Boolean?) { - findNavController().safeNavigate(ChangeNumberEnterCodeV2FragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked()) + findNavController().safeNavigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked()) } } ) @@ -198,7 +198,7 @@ class ChangeNumberEnterCodeV2Fragment : LoggingFragment(R.layout.fragment_change object : AssertedSuccessListener() { override fun onSuccess(result: Boolean?) { Log.i(TAG, "Account is registration locked, cannot register.") - findNavController().safeNavigate(ChangeNumberEnterCodeV2FragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining)) + findNavController().safeNavigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining)) } } ) 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 9a0419ab9a..1d253ba86d 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 @@ -1,86 +1,71 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.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.components.ViewBinderDelegate 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 import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.navigation.safeNavigate -private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country" -private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country" - +/** + * Screen for the user to enter their old and new phone numbers. + */ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number) { - private var binding: FragmentChangeNumberEnterPhoneNumberBinding? = null + companion object { + private val TAG: String = Log.tag(ChangeNumberEnterPhoneNumberFragment::class.java) - private val scrollView: ScrollView - get() = binding!!.changeNumberEnterPhoneNumberScroll + private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country" + private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country" + } - 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 + private val binding: FragmentChangeNumberEnterPhoneNumberBinding by ViewBinderDelegate(FragmentChangeNumberEnterPhoneNumberBinding::bind) + private val viewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding = FragmentChangeNumberEnterPhoneNumberBinding.bind(view) - - viewModel = getViewModel(this) - val toolbar: Toolbar = view.findViewById(R.id.toolbar) toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number) toolbar.setNavigationOnClickListener { findNavController().navigateUp() } - view.findViewById(R.id.change_number_enter_phone_number_continue).setOnClickListener { + binding.changeNumberEnterPhoneNumberContinue.setOnClickListener { onContinue() } val oldController = ChangeNumberInputController( requireContext(), - oldNumberCountryCode, - oldNumber, - oldNumberCountrySpinner, + binding.changeNumberEnterPhoneNumberOldNumberCountryCode, + binding.changeNumberEnterPhoneNumberOldNumberNumber, + binding.changeNumberEnterPhoneNumberOldNumberSpinner, false, object : ChangeNumberInputController.Callbacks { override fun onNumberFocused() { - scrollView.postDelayed({ scrollView.smoothScrollTo(0, oldNumber.bottom) }, 250) + binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberOldNumberNumber.bottom) }, 250) } override fun onNumberInputNext(view: View) { - newNumberCountryCode.requestFocus() + binding.changeNumberEnterPhoneNumberNewNumberCountryCode.requestFocus() } override fun onNumberInputDone(view: View) = Unit override fun onPickCountry(view: View) { - val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build() - - findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle()) + findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(OLD_NUMBER_COUNTRY_SELECT)) } override fun setNationalNumber(number: String) { @@ -95,13 +80,13 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c val newController = ChangeNumberInputController( requireContext(), - newNumberCountryCode, - newNumber, - newNumberCountrySpinner, + binding.changeNumberEnterPhoneNumberNewNumberCountryCode, + binding.changeNumberEnterPhoneNumberNewNumberNumber, + binding.changeNumberEnterPhoneNumberNewNumberSpinner, true, object : ChangeNumberInputController.Callbacks { override fun onNumberFocused() { - scrollView.postDelayed({ scrollView.smoothScrollTo(0, newNumber.bottom) }, 250) + binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberNewNumberNumber.bottom) }, 250) } override fun onNumberInputNext(view: View) = Unit @@ -111,9 +96,7 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c } override fun onPickCountry(view: View) { - val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(NEW_NUMBER_COUNTRY_SELECT).build() - - findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle()) + findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToCountryPickerFragment(NEW_NUMBER_COUNTRY_SELECT)) } override fun setNationalNumber(number: String) { @@ -130,50 +113,45 @@ class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_c viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY)) } - parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _, bundle -> + parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: 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) - } - - override fun onDestroyView() { - binding = null - super.onDestroyView() + viewModel.liveOldNumberState.observe(viewLifecycleOwner, oldController::updateNumber) + viewModel.liveNewNumberState.observe(viewLifecycleOwner, newController::updateNumber) } private fun onContinue() { - if (TextUtils.isEmpty(oldNumberCountryCode.text)) { + if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberCountryCode.text)) { Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show() return } - if (TextUtils.isEmpty(oldNumber.text)) { + if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberNumber.text)) { Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show() return } - if (TextUtils.isEmpty(newNumberCountryCode.text)) { + if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberCountryCode.text)) { Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show() return } - if (TextUtils.isEmpty(newNumber.text)) { + if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberNumber.text)) { Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show() return } when (viewModel.canContinue()) { - ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment) - ContinueStatus.INVALID_NUMBER -> { + ChangeNumberViewModel.ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(ChangeNumberEnterPhoneNumberFragmentDirections.actionEnterPhoneNumberChangeFragmentToChangePhoneNumberConfirmFragment()) + ChangeNumberViewModel.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 -> { + ChangeNumberViewModel.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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterSmsCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterSmsCodeFragment.kt deleted file mode 100644 index 1e9cad92cf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterSmsCodeFragment.kt +++ /dev/null @@ -1,77 +0,0 @@ -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.signal.core.util.logging.Log -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.BaseEnterSmsCodeFragment -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -private val TAG: String = Log.tag(ChangeNumberEnterSmsCodeFragment::class.java) - -class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment(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 { - Log.d(TAG, "Toolbar navigation clicked.") - navigateUp() - } - - view.findViewById(R.id.verify_header).setOnClickListener(null) - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - Log.d(TAG, "onBackPressed") - navigateUp() - } - } - ) - } - - private fun navigateUp() { - if (SignalStore.misc.isChangeNumberLocked) { - Log.d(TAG, "Change number locked, navigateUp") - startActivity(ChangeNumberLockActivity.createIntent(requireContext())) - } else { - Log.d(TAG, "navigateUp") - findNavController().navigateUp() - } - } - - override fun getViewModel(): ChangeNumberViewModel { - return getViewModel(this) - } - - override fun handleSuccessfulVerify() { - Log.d(TAG, "handleSuccessfulVerify") - displaySuccess { changeNumberSuccess() } - } - - override fun navigateToCaptcha() { - Log.d(TAG, "navigateToCaptcha") - findNavController().safeNavigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments()) - } - - override fun navigateToRegistrationLock(timeRemaining: Long) { - Log.d(TAG, "navigateToRegistrationLock") - findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining)) - } - - override fun navigateToKbsAccountLocked() { - Log.d(TAG, "navigateToKbsAccountLocked") - findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked()) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberFragment.kt index be135d09f2..dd67139ef5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberFragment.kt @@ -1,20 +1,38 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.databinding.FragmentChangePhoneNumberBinding import org.thoughtcrime.securesms.util.navigation.safeNavigate +/** + * Screen used to educate the user about what they're about to do (change their phone number) + */ class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_number) { + + companion object { + private val TAG = Log.tag(ChangeNumberFragment::class.java) + } + + private val binding: FragmentChangePhoneNumberBinding by ViewBinderDelegate(FragmentChangePhoneNumberBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val toolbar: Toolbar = view.findViewById(R.id.toolbar) toolbar.setNavigationOnClickListener { findNavController().navigateUp() } - view.findViewById(R.id.change_phone_number_continue).setOnClickListener { - findNavController().safeNavigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment) + binding.changePhoneNumberContinue.setOnClickListener { + findNavController().safeNavigate(ChangeNumberFragmentDirections.actionChangePhoneNumberFragmentToEnterPhoneNumberChangeFragment()) } } } 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 index 18da05fdc9..04432b3d94 100644 --- 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 @@ -1,14 +1,16 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.components.settings.app.changenumber -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.kotlin.subscribeBy -import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.PassphraseRequiredActivity @@ -18,10 +20,6 @@ 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.whispersystems.signalservice.api.push.ServiceId.PNI -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 @@ -29,17 +27,34 @@ private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java) */ class ChangeNumberLockActivity : PassphraseRequiredActivity() { + companion object { + private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java) + + @JvmStatic + fun createIntent(context: Context): Intent { + return Intent(context, ChangeNumberLockActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + } + } + + private val viewModel: ChangeNumberViewModel by viewModels() 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) + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + Log.d(TAG, "Back button press swallowed.") + } + } + ) setContentView(R.layout.activity_change_number_lock) - changeNumberRepository = ChangeNumberRepository() checkWhoAmI() } @@ -48,31 +63,11 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() { dynamicTheme.onResume(this) } - @SuppressLint("MissingSuperCall") - override fun onBackPressed() = Unit - private fun checkWhoAmI() { - disposables += changeNumberRepository - .whoAmI() - .flatMap { whoAmI -> - if (Objects.equals(whoAmI.number, SignalStore.account.e164)) { - Log.i(TAG, "Local and remote numbers match, nothing needs to be done.") - Single.just(false) - } else { - Log.i(TAG, "Local (${SignalStore.account.e164}) and remote (${whoAmI.number}) numbers do not match, updating local.") - Single - .just(true) - .flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) } - .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock) - .map { true } - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus) + viewModel.checkWhoAmI(::onChangeStatusConfirmed, ::onFailedToGetChangeNumberStatus) } private fun onChangeStatusConfirmed() { - SignalStore.misc.unlockChangeNumber() SignalStore.misc.clearPendingChangeNumberMetadata() MaterialAlertDialogBuilder(this) @@ -101,13 +96,4 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() { .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/ChangeNumberPinDiffersFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberPinDiffersFragment.kt index b8a0b92ef8..1db5e28081 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberPinDiffersFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberPinDiffersFragment.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.components.settings.app.changenumber import android.os.Bundle @@ -5,13 +10,31 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity +/** + * A screen to educate the user if their PIN differs from old number to new number. + */ class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) { + companion object { + private val TAG = Log.tag(ChangeNumberPinDiffersFragment::class.java) + } + + private val confirmCancelDialog = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question) + .setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { view.findViewById(R.id.change_number_pin_differs_keep_old_pin).setOnClickListener { changeNumberSuccess() @@ -27,17 +50,6 @@ class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_ changePin.launch(CreateSvrPinActivity.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() - } - } - ) + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, confirmCancelDialog) } } 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 a00763e8b2..4f0db0cbbd 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 @@ -1,25 +1,60 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.components.settings.app.changenumber import android.os.Bundle +import android.text.InputType +import android.view.KeyEvent import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess +import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment -import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel +import org.thoughtcrime.securesms.lock.v2.PinKeyboardType +import org.thoughtcrime.securesms.lock.v2.SvrConstants +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult +import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate +import org.thoughtcrime.securesms.registration.ui.registrationlock.RegistrationLockFragment +import org.thoughtcrime.securesms.registration.ui.registrationlock.RegistrationLockFragmentArgs import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.SupportEmailUtil +import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.whispersystems.signalservice.api.kbs.PinHashUtil +import java.util.concurrent.TimeUnit -class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) { +/** + * Screen presented to the user if the new account is registration locked, and allows them to enter their PIN. + */ +class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_change_number_registration_lock) { + + companion object { + private val TAG = Log.tag(RegistrationLockFragment::class.java) + } + + private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind) + + private val viewModel by activityViewModels() + + private var timeRemaining: Long = 0 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - + RegistrationViewDelegate.setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title)) val toolbar: Toolbar = view.findViewById(R.id.toolbar) toolbar.setNavigationOnClickListener { navigateUp() } @@ -31,20 +66,242 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo } } ) + + val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments()) + + timeRemaining = args.getTimeRemaining() + + binding.kbsLockForgotPin.visibility = View.GONE + binding.kbsLockForgotPin.setOnClickListener { handleForgottenPin(timeRemaining) } + + binding.kbsLockPinInput.setImeOptions(EditorInfo.IME_ACTION_DONE) + binding.kbsLockPinInput.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + ViewUtil.hideKeyboard(requireContext(), v!!) + handlePinEntry() + return@setOnEditorActionListener true + } + false + } + + enableAndFocusPinEntry() + + binding.kbsLockPinConfirm.setOnClickListener { + ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput) + handlePinEntry() + } + + binding.kbsLockKeyboardToggle.setOnClickListener { + val keyboardType: PinKeyboardType = getPinEntryKeyboardType() + updateKeyboard(keyboardType.other) + binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource) + } + + val keyboardType: PinKeyboardType = getPinEntryKeyboardType().getOther() + binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource) + + viewModel.liveLockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> timeRemaining = t } + + val triesRemaining: Int = viewModel.svrTriesRemaining + + if (triesRemaining <= 3) { + val daysRemaining = getLockoutDays(timeRemaining) + + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__not_many_tries_left) + .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() } + .show() + } + + if (triesRemaining < 5) { + binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining) + } + + viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate) } - override fun getViewModel(): BaseRegistrationViewModel { - return ChangeNumberUtil.getViewModel(this) + private fun onStateUpdate(state: ChangeNumberState) { + if (state.changeNumberOutcome == ChangeNumberOutcome.VerificationCodeWorked) { + handleSuccessfulPinEntry(state.enteredPin) + } } - override fun navigateToAccountLocked() { + private fun handlePinEntry() { + binding.kbsLockPinInput.setEnabled(false) + + val pin: String = binding.kbsLockPinInput.getText().toString() + + val trimmedLength = pin.replace(" ", "").length + if (trimmedLength == 0) { + Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) { + Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + viewModel.setEnteredPin(pin) + + binding.kbsLockPinConfirm.setSpinning() + viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleChangeNumberErrorResponse) + } + + private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) { + when (requestResult) { + is VerificationCodeRequestResult.Success -> Unit + is VerificationCodeRequestResult.RateLimited -> onRateLimited() + is VerificationCodeRequestResult.AttemptsExhausted, + is VerificationCodeRequestResult.RegistrationLocked -> { + navigateToAccountLocked() + } + + is VerificationCodeRequestResult.AlreadyVerified, + is VerificationCodeRequestResult.ChallengeRequired, + is VerificationCodeRequestResult.ExternalServiceFailure, + is VerificationCodeRequestResult.ImpossibleNumber, + is VerificationCodeRequestResult.InvalidTransportModeFailure, + is VerificationCodeRequestResult.MalformedRequest, + is VerificationCodeRequestResult.MustRetry, + is VerificationCodeRequestResult.NoSuchSession, + is VerificationCodeRequestResult.NonNormalizedNumber, + is VerificationCodeRequestResult.TokenNotAccepted, + is VerificationCodeRequestResult.UnknownError -> { + Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause()) + onError() + } + } + } + + private fun handleChangeNumberErrorResponse(result: ChangeNumberResult) { + when (result) { + is ChangeNumberResult.Success -> Unit + is ChangeNumberResult.RateLimited -> onRateLimited() + is ChangeNumberResult.AttemptsExhausted -> navigateToAccountLocked() + + is ChangeNumberResult.SvrWrongPin -> { + Log.i(TAG, "SVR returned a WrongPinException.") + onIncorrectKbsRegistrationLockPin(result.triesRemaining) + } + + is ChangeNumberResult.SvrNoData -> { + Log.i(TAG, "SVR returned a NoDataException.") + navigateToAccountLocked() + } + + is ChangeNumberResult.AuthorizationFailed, + is ChangeNumberResult.IncorrectRecoveryPassword, + is ChangeNumberResult.MalformedRequest, + is ChangeNumberResult.RegistrationLocked, + is ChangeNumberResult.UnknownError, + is ChangeNumberResult.ValidationError -> { + Log.w(TAG, "Unable to register account with registration lock", result.getCause()) + onError() + } + } + } + + private fun onIncorrectKbsRegistrationLockPin(svrTriesRemaining: Int) { + binding.kbsLockPinConfirm.cancelSpinning() + binding.kbsLockPinInput.getText().clear() + enableAndFocusPinEntry() + + if (svrTriesRemaining == 0) { + Log.w(TAG, "Account locked. User out of attempts on KBS.") + navigateToAccountLocked() + return + } + + if (svrTriesRemaining == 3) { + val daysRemaining = getLockoutDays(timeRemaining) + + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__incorrect_pin) + .setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining)) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + if (svrTriesRemaining > 5) { + binding.kbsLockPinInputLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again) + } else { + binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining) + binding.kbsLockForgotPin.visibility = View.VISIBLE + } + } + + private fun onRateLimited() { + binding.kbsLockPinConfirm.cancelSpinning() + enableAndFocusPinEntry() + + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationActivity_too_many_attempts) + .setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + fun onError() { + binding.kbsLockPinConfirm.cancelSpinning() + enableAndFocusPinEntry() + + Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show() + } + + private fun handleForgottenPin(timeRemainingMs: Long) { + val lockoutDays = getLockoutDays(timeRemainingMs) + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__forgot_your_pin) + .setMessage(requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, which -> sendEmailToSupport() } + .show() + } + + private fun getLockoutDays(timeRemainingMs: Long): Int { + return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1 + } + + private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String { + val resources = requireContext().resources + val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining) + val days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining) + + return "$tries $days" + } + + private fun enableAndFocusPinEntry() { + binding.kbsLockPinInput.setEnabled(true) + binding.kbsLockPinInput.setFocusable(true) + ViewUtil.focusAndShowKeyboard(binding.kbsLockPinInput) + } + + private fun getPinEntryKeyboardType(): PinKeyboardType { + val isNumeric = (binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER + + return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC + } + + private fun updateKeyboard(keyboard: PinKeyboardType) { + val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC + + binding.kbsLockPinInput.setInputType( + if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + ) + + binding.kbsLockPinInput.getText().clear() + } + + private fun navigateToAccountLocked() { findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked()) } - override fun handleSuccessfulPinEntry(pin: String) { + private fun handleSuccessfulPinEntry(pin: String) { val pinsDiffer: Boolean = SignalStore.svr.localPinHash?.let { !PinHashUtil.verifyLocalPinHash(it, pin) } ?: false - pinButton.cancelSpinning() + binding.kbsLockPinConfirm.cancelSpinning() if (pinsDiffer) { findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers()) @@ -53,22 +310,12 @@ class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layo } } - override fun sendEmailToSupport() { + private fun sendEmailToSupport() { val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin - val body: String = SupportEmailUtil.generateSupportEmailBody( - requireContext(), - subject, - null, - null - ) + val body: String = SupportEmailUtil.generateSupportEmailBody(requireContext(), subject, null, null) - CommunicationActions.openEmail( - requireContext(), - SupportEmailUtil.getSupportEmailAddress(requireContext()), - getString(subject), - body - ) + CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), getString(subject), body) } private fun 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 d2346b390f..bc12e3d9b6 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 @@ -1,9 +1,15 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.components.settings.app.changenumber import androidx.annotation.WorkerThread -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import okio.ByteString.Companion.toByteString import org.signal.core.util.logging.Log import org.signal.libsignal.protocol.IdentityKeyPair @@ -25,23 +31,21 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.pin.SvrRepository import org.thoughtcrime.securesms.pin.SvrWrongPinException import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.VerifyResponse import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.SignalServiceMessageSender import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest import org.whispersystems.signalservice.api.account.PreKeyUpload import org.whispersystems.signalservice.api.kbs.MasterKey -import org.whispersystems.signalservice.api.push.ServiceId.PNI +import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceIdType import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignedPreKeyEntity -import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity import org.whispersystems.signalservice.internal.push.OutgoingPushMessage -import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.SyncMessage import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import org.whispersystems.signalservice.internal.push.WhoAmIResponse @@ -49,14 +53,14 @@ import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevic import java.io.IOException import java.security.MessageDigest import java.security.SecureRandom -import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.ReentrantLock - -private val TAG: String = Log.tag(ChangeNumberRepository::class.java) +import kotlin.coroutines.resume +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** - * Provides various change number operations. All operations must run on [Schedulers.single] to support - * the global "I am changing the number" lock exclusivity. + * Repository to perform data operations during change number. + * + * @see [org.thoughtcrime.securesms.registration.data.RegistrationRepository] */ class ChangeNumberRepository( private val accountManager: SignalServiceAccountManager = AppDependencies.signalServiceAccountManager, @@ -64,157 +68,42 @@ class ChangeNumberRepository( ) { companion object { - /** - * This lock should be held by anyone who is performing a change number operation, so that two different parties cannot change the user's number - * at the same time. - */ - val CHANGE_NUMBER_LOCK = ReentrantLock() + private val TAG = Log.tag(ChangeNumberRepository::class.java) + } - /** - * Adds Rx operators to chain to acquire and release the [CHANGE_NUMBER_LOCK] on subscribe and on finish. - */ - fun acquireReleaseChangeNumberLock(upstream: Single): Single { - return upstream.doOnSubscribe { - CHANGE_NUMBER_LOCK.lock() - SignalStore.misc.lockChangeNumber() - } - .subscribeOn(Schedulers.single()) - .observeOn(Schedulers.single()) - .doFinally { - if (CHANGE_NUMBER_LOCK.isHeldByCurrentThread) { - CHANGE_NUMBER_LOCK.unlock() + fun whoAmI(): WhoAmIResponse { + return accountManager.whoAmI + } + + suspend fun ensureDecryptionsDrained(timeout: Duration = 15.seconds) = + withTimeoutOrNull(timeout) { + suspendCancellableCoroutine { + val drainedListener = object : Runnable { + override fun run() { + AppDependencies + .incomingMessageObserver + .removeDecryptionDrainedListener(this) + Log.d(TAG, "Decryptions drained.") + it.resume(true) } } - } - } - fun ensureDecryptionsDrained(): Completable { - return Completable.create { emitter -> - val drainedListener = object : Runnable { - override fun run() { - emitter.onComplete() + it.invokeOnCancellation { cancellationCause -> AppDependencies .incomingMessageObserver - .removeDecryptionDrainedListener(this) + .removeDecryptionDrainedListener(drainedListener) + Log.d(TAG, "Decryptions draining canceled.", cancellationCause) } - } - emitter.setCancellable { AppDependencies .incomingMessageObserver - .removeDecryptionDrainedListener(drainedListener) + .addDecryptionDrainedListener(drainedListener) + Log.d(TAG, "Waiting for decryption drain.") } - - AppDependencies - .incomingMessageObserver - .addDecryptionDrainedListener(drainedListener) - }.subscribeOn(Schedulers.single()) - .timeout(15, TimeUnit.SECONDS) - } - - fun changeNumber(sessionId: String? = null, recoveryPassword: String? = null, newE164: String): Single> { - check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null)) - - return Single.fromCallable { - var completed = false - var attempts = 0 - lateinit var changeNumberResponse: ServiceResponse - - while (!completed && attempts < 5) { - val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( - sessionId = sessionId, - recoveryPassword = recoveryPassword, - newE164 = newE164 - ) - - SignalStore.misc.setPendingChangeNumberMetadata(metadata) - - changeNumberResponse = accountManager.changeNumber(request) - - val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null) - if (possibleError is MismatchedDevicesException) { - messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices) - attempts++ - } else { - completed = true - } - } - - VerifyResponse.from( - response = changeNumberResponse, - masterKey = null, - pin = null, - aciPreKeyCollection = null, - pniPreKeyCollection = null - ) - }.subscribeOn(Schedulers.single()) - .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } - } - - fun changeNumber( - sessionId: String, - newE164: String, - pin: String, - svrAuthCredentials: SvrAuthCredentialSet - ): Single> { - return Single.fromCallable { - val masterKey: MasterKey - val registrationLock: String - - try { - masterKey = SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin) - registrationLock = masterKey.deriveRegistrationLock() - } catch (e: SvrWrongPinException) { - return@fromCallable ServiceResponse.forExecutionError(e) - } catch (e: SvrNoDataException) { - return@fromCallable ServiceResponse.forExecutionError(e) - } catch (e: IOException) { - return@fromCallable ServiceResponse.forExecutionError(e) - } - - var completed = false - var attempts = 0 - lateinit var changeNumberResponse: ServiceResponse - - while (!completed && attempts < 5) { - val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( - sessionId = sessionId, - newE164 = newE164, - registrationLock = registrationLock - ) - - SignalStore.misc.setPendingChangeNumberMetadata(metadata) - - changeNumberResponse = accountManager.changeNumber(request) - - val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null) - if (possibleError is MismatchedDevicesException) { - messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices) - attempts++ - } else { - completed = true - } - } - - VerifyResponse.from( - response = changeNumberResponse, - masterKey = masterKey, - pin = pin, - aciPreKeyCollection = null, - pniPreKeyCollection = null - ) - }.subscribeOn(Schedulers.single()) - .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } - } - - @Suppress("UsePropertyAccessSyntax") - fun whoAmI(): Single { - return Single.fromCallable { AppDependencies.signalServiceAccountManager.getWhoAmI() } - .subscribeOn(Schedulers.single()) - } + } @WorkerThread - fun changeLocalNumber(e164: String, pni: PNI): Single { + fun changeLocalNumber(e164: String, pni: ServiceId.PNI) { val oldStorageId: ByteArray? = Recipient.self().storageId SignalDatabase.recipients.updateSelfE164(e164, pni) val newStorageId: ByteArray? = Recipient.self().storageId @@ -243,7 +132,7 @@ class ChangeNumberRepository( throw AssertionError("No change number metadata") } - val originalPni = PNI.parseOrThrow(metadata.previousPni) + val originalPni = ServiceId.PNI.parseOrThrow(metadata.previousPni) if (originalPni == pni) { Log.i(TAG, "No change has occurred, PNI is unchanged: $pni") @@ -269,6 +158,8 @@ class ChangeNumberRepository( } pniMetadataStore.activeSignedPreKeyId = signedPreKey.id + Log.i(TAG, "Submitting prekeys with PNI identity key: ${pniIdentityKeyPair.publicKey.fingerprint}") + accountManager.setPreKeys( PreKeyUpload( serviceIdType = ServiceIdType.PNI, @@ -303,28 +194,100 @@ class ChangeNumberRepository( AppDependencies.jobManager.add(RefreshAttributesJob()) - return rotateCertificates() + rotateCertificates() } - @Suppress("UsePropertyAccessSyntax") - private fun rotateCertificates(): Single { + @WorkerThread + private fun rotateCertificates() { 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.ACI_AND_E164 -> accountManager.getSenderCertificate() - CertificateType.ACI_ONLY -> accountManager.getSenderCertificateForPhoneNumberPrivacy() - else -> throw AssertionError() - } - - Log.i(TAG, "Successfully got $certificateType certificate") - - SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate) + for (certificateType in certificateTypes) { + val certificate: ByteArray? = when (certificateType) { + CertificateType.ACI_AND_E164 -> accountManager.senderCertificate + CertificateType.ACI_ONLY -> accountManager.senderCertificateForPhoneNumberPrivacy + else -> throw AssertionError() } - }.subscribeOn(Schedulers.single()) + + Log.i(TAG, "Successfully got $certificateType certificate") + + SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate) + } + } + + suspend fun changeNumberWithRecoveryPassword(recoveryPassword: String, newE164: String): ChangeNumberResult { + return changeNumberInternal(recoveryPassword = recoveryPassword, newE164 = newE164) + } + + suspend fun changeNumberWithoutRegistrationLock(sessionId: String, newE164: String): ChangeNumberResult { + return changeNumberInternal(sessionId = sessionId, newE164 = newE164) + } + + suspend fun changeNumberWithRegistrationLock( + sessionId: String, + newE164: String, + pin: String, + svrAuthCredentials: SvrAuthCredentialSet + ): ChangeNumberResult { + val masterKey: MasterKey + + try { + masterKey = SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin) + } catch (e: SvrWrongPinException) { + return ChangeNumberResult.SvrWrongPin(e) + } catch (e: SvrNoDataException) { + return ChangeNumberResult.SvrNoData(e) + } catch (e: IOException) { + return ChangeNumberResult.UnknownError(e) + } + + val registrationLock = masterKey.deriveRegistrationLock() + return changeNumberInternal(sessionId = sessionId, registrationLock = registrationLock, newE164 = newE164) + } + + /** + * Sends a request to the service to change the phone number associated with this account. + */ + private suspend fun changeNumberInternal(sessionId: String? = null, recoveryPassword: String? = null, registrationLock: String? = null, newE164: String): ChangeNumberResult { + check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null)) + var completed = false + var attempts = 0 + lateinit var result: NetworkResult + + while (!completed && attempts < 5) { + Log.i(TAG, "Attempt #$attempts") + val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( + sessionId = sessionId, + recoveryPassword = recoveryPassword, + newE164 = newE164, + registrationLock = registrationLock + ) + + SignalStore.misc.setPendingChangeNumberMetadata(metadata) + withContext(Dispatchers.IO) { + result = accountManager.registrationApi.changeNumber(request) + } + + val possibleError = result.getCause() as? MismatchedDevicesException + if (possibleError != null) { + messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices) + attempts++ + } else { + completed = true + } + } + Log.i(TAG, "Returning change number network result.") + return ChangeNumberResult.from( + result.map { accountRegistrationResponse: VerifyAccountResponse -> + NumberChangeResult( + uuid = accountRegistrationResponse.uuid, + pni = accountRegistrationResponse.pni, + storageCapable = accountRegistrationResponse.storageCapable, + number = accountRegistrationResponse.number + ) + } + ) } @WorkerThread @@ -410,11 +373,12 @@ class ChangeNumberRepository( return ChangeNumberRequestData(request, metadata) } - fun verifyAccount(sessionId: String, code: String): Single> { - return Single.fromCallable { - accountManager.verifyAccount(code, sessionId) - }.subscribeOn(Schedulers.io()) - } + private data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata) - data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata) + data class NumberChangeResult( + val uuid: String, + val pni: String, + val storageCapable: Boolean, + val number: String + ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberResult.kt similarity index 91% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberResult.kt index 56c6923eca..65aaee7e62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberResult.kt @@ -3,10 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 +package org.thoughtcrime.securesms.components.settings.app.changenumber import org.thoughtcrime.securesms.pin.SvrWrongPinException -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationResult import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException @@ -23,7 +23,7 @@ import org.whispersystems.signalservice.internal.push.VerifyAccountResponse */ sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) { companion object { - fun from(networkResult: NetworkResult): ChangeNumberResult { + fun from(networkResult: NetworkResult): ChangeNumberResult { return when (networkResult) { is NetworkResult.Success -> Success(networkResult.result) is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) @@ -56,7 +56,7 @@ sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) { } } - class Success(val numberChangeResult: ChangeNumberV2Repository.NumberChangeResult) : ChangeNumberResult(null) + class Success(val numberChangeResult: ChangeNumberRepository.NumberChangeResult) : ChangeNumberResult(null) class IncorrectRecoveryPassword(cause: Throwable) : ChangeNumberResult(cause) class AuthorizationFailed(cause: Throwable) : ChangeNumberResult(cause) class MalformedRequest(cause: Throwable) : ChangeNumberResult(cause) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt similarity index 87% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt index 3f417bd4f9..0ef529aaa2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt @@ -3,16 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 +package org.thoughtcrime.securesms.components.settings.app.changenumber -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.internal.push.AuthCredentials /** - * State holder for [ChangeNumberV2ViewModel] + * State holder for [ChangeNumberViewModel] */ data class ChangeNumberState( val number: NumberViewState = NumberViewState.INITIAL, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberUtil.kt index 6e3bb010f0..6929cd05b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberUtil.kt @@ -1,37 +1,12 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber -import android.os.Bundle import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -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() { requireActivity().finish() 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 7b74607152..1b0332830f 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 @@ -1,109 +1,148 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.components.settings.app.changenumber -import android.content.Context -import android.content.DialogInterface.OnClickListener import android.os.Bundle import android.view.View import android.widget.TextView -import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Single -import org.signal.core.util.concurrent.LifecycleDisposable +import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess -import org.thoughtcrime.securesms.components.settings.app.changenumber.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.dualsim.MccMncProducer +import org.thoughtcrime.securesms.registration.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.util.navigation.safeNavigate -private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java) - +/** + * Screen to show while the change number is in-progress. + */ 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) + companion object { + private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java) } + private val viewModel by activityViewModels() + private var dialogVisible: Boolean = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val toolbar: Toolbar = view.findViewById(R.id.toolbar) toolbar.setTitle(R.string.ChangeNumberVerifyFragment__change_number) - toolbar.setNavigationOnClickListener { findNavController().navigateUp() } + toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + viewModel.resetLocalSessionState() + } val status: TextView = view.findViewById(R.id.change_phone_number_verify_status) status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber) - if (!requestingCaptcha || viewModel.hasCaptchaToken()) { - requestCode() - } else { - Log.d(TAG, "Captcha required.") - Toast.makeText(requireContext(), R.string.ChangeNumberVerifyFragment__captcha_required, Toast.LENGTH_SHORT).show() - findNavController().navigateUp() + viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate) + + requestCode() + } + + private fun onStateUpdate(state: ChangeNumberState) { + if (state.challengesRequested.contains(Challenge.CAPTCHA) && state.captchaToken.isNotNullOrBlank()) { + viewModel.submitCaptchaToken(requireContext()) + } else if (state.challengesRemaining.isNotEmpty()) { + handleChallenges(state.challengesRemaining) + } else if (state.changeNumberOutcome != null) { + handleRequestCodeResult(state.changeNumberOutcome) + } else if (!state.inProgress) { + Log.d(TAG, "Not in progress, navigating up.") + if (state.allowedToRequestCode) { + requestCode() + } else if (!dialogVisible) { + showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code) + } } } private fun requestCode() { - val mode = if (ChangeNumberVerifyFragmentArgs.fromBundle(requireArguments()).smsListenerEnabled) VerifyAccountRepository.Mode.SMS_WITH_LISTENER else VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER - val mccMncProducer = MccMncProducer(requireContext()) - lifecycleDisposable += viewModel - .ensureDecryptionsDrained() - .onErrorComplete() - .andThen(viewModel.changeNumberWithRecoveryPassword()) - .flatMap { changed -> - if (changed) { - Log.d(TAG, "Successfully changed number using recovery password.") - Single.just(RequestCodeResult.RecoveryPasswordWorked) - } else { - viewModel.requestVerificationCode(mode, mccMncProducer.mcc, mccMncProducer.mnc) - .map { p -> RequestCodeResult.RequestedVerificationCode(p) } - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { result -> - if (result is RequestCodeResult.RecoveryPasswordWorked) { - changeNumberSuccess() - return@subscribe - } - - val processor: RegistrationSessionProcessor = (result as RequestCodeResult.RequestedVerificationCode).processor - - if (processor.verificationCodeRequestSuccess()) { - Log.i(TAG, "Successfully requested SMS code.") - findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment) - } else if (processor.captchaRequired(viewModel.excludedChallenges)) { - Log.i(TAG, "Unable to request sms code due to captcha required") - findNavController().safeNavigate(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") - showErrorDialog(requireContext(), R.string.RegistrationActivity_rate_limited_to_service) { _, _ -> findNavController().navigateUp() } - } else { - Log.w(TAG, "Unable to request sms code", processor.error) - showErrorDialog(requireContext(), R.string.RegistrationActivity_unable_to_request_verification_code) { _, _ -> findNavController().navigateUp() } - } - } + val mode = if (ChangeNumberVerifyFragmentArgs.fromBundle(requireArguments()).smsListenerEnabled) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER + viewModel.initiateChangeNumberSession(requireContext(), mode) } - private fun showErrorDialog(context: Context, @StringRes message: Int, onPositiveButtonClickListener: OnClickListener?) { - MaterialAlertDialogBuilder(context).setMessage(message).setPositiveButton(android.R.string.ok, onPositiveButtonClickListener).show() + private fun handleRequestCodeResult(changeNumberOutcome: ChangeNumberOutcome) { + Log.d(TAG, "Handling request code result: ${changeNumberOutcome.javaClass.name}") + when (changeNumberOutcome) { + is ChangeNumberOutcome.RecoveryPasswordWorked -> { + Log.i(TAG, "Successfully changed number with recovery password.") + changeNumberSuccess() + } + + is ChangeNumberOutcome.ChangeNumberRequestOutcome -> { + when (val castResult = changeNumberOutcome.result) { + is VerificationCodeRequestResult.Success -> { + Log.i(TAG, "Successfully requested SMS code.") + findNavController().safeNavigate(ChangeNumberVerifyFragmentDirections.actionChangePhoneNumberVerifyFragmentToChangeNumberEnterCodeFragment()) + } + + is VerificationCodeRequestResult.ChallengeRequired -> { + Log.i(TAG, "Unable to request sms code due to challenges required: ${castResult.challenges.joinToString { it.key }}") + } + + is VerificationCodeRequestResult.RateLimited -> { + Log.i(TAG, "Unable to request sms code due to rate limit") + showErrorDialog(R.string.RegistrationActivity_rate_limited_to_service) + } + + is VerificationCodeRequestResult.TokenNotAccepted -> { + Log.i(TAG, "Token was not accepted.") + showErrorDialog(R.string.RegistrationActivity_additional_verification_required) + } + + else -> { + Log.w(TAG, "Unable to request sms code", castResult.getCause()) + showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code) + } + } + } + + is ChangeNumberOutcome.VerificationCodeWorked -> { + Log.i(TAG, "Successfully changed number with verification code.") + changeNumberSuccess() + } + } } - private sealed interface RequestCodeResult { - object RecoveryPasswordWorked : RequestCodeResult - class RequestedVerificationCode(val processor: RegistrationSessionProcessor) : RequestCodeResult + private fun handleChallenges(remainingChallenges: List) { + Log.i(TAG, "Handling challenge(s): ${remainingChallenges.joinToString { it.key }}") + when (remainingChallenges.first()) { + Challenge.CAPTCHA -> { + findNavController().safeNavigate(ChangeNumberVerifyFragmentDirections.actionChangePhoneNumberVerifyFragmentToCaptchaFragment()) + } + + Challenge.PUSH -> { + viewModel.requestAndSubmitPushToken(requireContext()) + } + } + } + + private fun showErrorDialog(@StringRes message: Int) { + if (dialogVisible) { + Log.i(TAG, "Dialog already being shown, failed to display dialog with message ${getString(message)}") + return + } + + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(message) + setPositiveButton(android.R.string.ok) { _, _ -> + findNavController().navigateUp() + viewModel.resetLocalSessionState() + } + show() + dialogVisible = true + } } } 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 aca694e67e..6b93cb5cdd 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 @@ -1,62 +1,82 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 android.content.Context import androidx.lifecycle.ViewModel -import androidx.savedstate.SavedStateRegistryOwner +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor +import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver -import org.thoughtcrime.securesms.registration.VerifyAccountRepository -import org.thoughtcrime.securesms.registration.VerifyResponse -import org.thoughtcrime.securesms.registration.VerifyResponseProcessor -import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor -import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs -import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel +import org.thoughtcrime.securesms.registration.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet -import org.thoughtcrime.securesms.util.DefaultValueLiveData -import org.whispersystems.signalservice.api.push.ServiceId.PNI -import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException -import org.whispersystems.signalservice.internal.ServiceResponse -import java.util.Objects +import org.thoughtcrime.securesms.util.dualsim.MccMncProducer +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse +import java.io.IOException +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock -private val TAG: String = Log.tag(ChangeNumberViewModel::class.java) +/** + * [ViewModel] for the change number flow. + * + * @see [RegistrationViewModel], from which this is derived. + */ +class ChangeNumberViewModel : ViewModel() { -class ChangeNumberViewModel( - private val localNumber: String, - private val changeNumberRepository: ChangeNumberRepository, - savedState: SavedStateHandle, - password: String, - verifyAccountRepository: VerifyAccountRepository, + companion object { + private val TAG = Log.tag(ChangeNumberViewModel::class.java) + + val CHANGE_NUMBER_LOCK = ReentrantLock() + } + + private val repository = ChangeNumberRepository() + private val store = MutableStateFlow(ChangeNumberState()) + private val serialContext = SignalExecutors.SERIAL.asCoroutineDispatcher() private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(AppDependencies.application) -) : BaseRegistrationViewModel(savedState, verifyAccountRepository, password) { - var oldNumberState: NumberViewState = NumberViewState.Builder().build() - private set + private val initialLocalNumber = SignalStore.account.e164 + private val password = SignalStore.account.servicePassword!! - private val liveOldNumberState = DefaultValueLiveData(oldNumberState) - private val liveNewNumberState = DefaultValueLiveData(number) + val uiState = store.asLiveData() + val liveOldNumberState = store.map { it.oldPhoneNumber }.asLiveData() + val liveNewNumberState = store.map { it.number }.asLiveData() + val liveLockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData() + val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData() init { try { val countryCode: Int = PhoneNumberUtil.getInstance() - .parse(localNumber, null) + .parse(SignalStore.account.e164!!, null) .countryCode - setOldCountry(countryCode) - setNewCountry(countryCode) + store.update { + it.copy( + number = it.number.toBuilder().countryCode(countryCode).build(), + oldPhoneNumber = it.oldPhoneNumber.toBuilder().countryCode(countryCode).build() + ) + } } catch (e: NumberParseException) { Log.i(TAG, "Unable to parse number for default country code") } @@ -69,45 +89,83 @@ class ChangeNumberViewModel( smsRetrieverReceiver.unregisterReceiver() } - fun getLiveOldNumber(): LiveData { - return liveOldNumberState - } + // region Public Getters and Setters - fun getLiveNewNumber(): LiveData { - return liveNewNumberState - } + val number: NumberViewState + get() = store.value.number - fun setOldNationalNumber(number: String) { - oldNumberState = oldNumberState.toBuilder() - .nationalNumber(number) - .build() + val oldNumberState: NumberViewState + get() = store.value.oldPhoneNumber - liveOldNumberState.value = oldNumberState + val svrTriesRemaining: Int + get() = store.value.svrTriesRemaining + + fun setOldNationalNumber(updatedNumber: String) { + store.update { + it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build()) + } } fun setOldCountry(countryCode: Int, country: String? = null) { - oldNumberState = oldNumberState.toBuilder() - .selectedCountryDisplayName(country) - .countryCode(countryCode) - .build() - - liveOldNumberState.value = oldNumberState + store.update { + it.copy(oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build()) + } } - fun setNewNationalNumber(number: String) { - setNationalNumber(number) - - liveNewNumberState.value = this.number + fun setNewNationalNumber(updatedNumber: String) { + store.update { + it.copy(number = number.toBuilder().nationalNumber(updatedNumber).build()) + } } fun setNewCountry(countryCode: Int, country: String? = null) { - onCountrySelected(country, countryCode) + store.update { + it.copy(number = number.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build()) + } + } - liveNewNumberState.value = this.number + fun setCaptchaResponse(token: String) { + Log.v(TAG, "setCaptchaResponse()") + store.update { + it.copy(captchaToken = token) + } + } + + fun setEnteredPin(pin: String) { + store.update { + it.copy(enteredPin = pin) + } + } + + fun incrementIncorrectCodeAttempts() { + store.update { + it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1) + } + } + + fun addPresentedChallenge(challenge: Challenge) { + Log.v(TAG, "addPresentedChallenge()") + store.update { + it.copy(challengesPresented = it.challengesPresented.plus(challenge)) + } + } + + fun removePresentedChallenge(challenge: Challenge) { + Log.v(TAG, "addPresentedChallenge()") + store.update { + it.copy(challengesPresented = it.challengesPresented.minus(challenge)) + } + } + + fun resetLocalSessionState() { + Log.v(TAG, "resetLocalSessionState()") + store.update { + it.copy(inProgress = false, changeNumberOutcome = null, captchaToken = null, challengesRequested = emptyList(), allowedToRequestCode = false) + } } fun canContinue(): ContinueStatus { - return if (oldNumberState.e164Number == localNumber) { + return if (oldNumberState.e164Number == initialLocalNumber) { if (number.isValid) { ContinueStatus.CAN_CONTINUE } else { @@ -118,129 +176,378 @@ class ChangeNumberViewModel( } } - fun ensureDecryptionsDrained(): Completable { - return changeNumberRepository.ensureDecryptionsDrained() - } + // endregion - override fun verifyCodeWithoutRegistrationLock(code: String): Single { - return super.verifyCodeWithoutRegistrationLock(code) - .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock) - .flatMap(this::attemptToUnlockChangeNumber) - } + // region Public actions - override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single { - return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin) - .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock) - .flatMap(this::attemptToUnlockChangeNumber) - } + fun checkWhoAmI(onSuccess: () -> Unit, onError: (Throwable) -> Unit) { + Log.v(TAG, "checkWhoAmI()") + viewModelScope.launch(Dispatchers.IO) { + try { + val whoAmI = repository.whoAmI() - private fun attemptToUnlockChangeNumber(processor: T): Single { - return if (processor.hasResult() || processor.isServerSentError()) { - SignalStore.misc.unlockChangeNumber() - SignalStore.misc.clearPendingChangeNumberMetadata() - 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() - SignalStore.misc.clearPendingChangeNumberMetadata() - } - processor + if (whoAmI.number == SignalStore.account.e164) { + return@launch bail { Log.i(TAG, "Local and remote numbers match, nothing needs to be done.") } } - .onErrorReturn { processor } + + Log.i(TAG, "Local (${SignalStore.account.e164}) and remote (${whoAmI.number}) numbers do not match, updating local.") + + withLockOnSerialExecutor { + repository.changeLocalNumber(whoAmI.number, ServiceId.PNI.parseOrThrow(whoAmI.pni)) + } + + withContext(Dispatchers.Main) { + onSuccess() + } + } catch (ioException: IOException) { + Log.w(TAG, "Encountered an exception when requesting whoAmI()", ioException) + withContext(Dispatchers.Main) { + onError(ioException) + } + } } } - override fun verifyAccountWithoutRegistrationLock(): Single> { - val sessionId = sessionId ?: throw IllegalStateException("No valid registration session") - - return changeNumberRepository.verifyAccount(sessionId, textCodeEntered) - .map { RegistrationSessionProcessor.RegistrationSessionProcessorForVerification(it) } - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess { - if (it.hasResult()) { - setCanSmsAtTime(it.getNextCodeViaSmsAttempt()) - setCanCallAtTime(it.getNextCodeViaCallAttempt()) - } - } - .observeOn(Schedulers.io()) - .flatMap { processor -> - if (processor.isAlreadyVerified() || processor.hasResult() && processor.isVerified()) { - changeNumberRepository.changeNumber(sessionId = sessionId, newE164 = number.e164Number) - } else if (processor.error == null) { - Single.just>(ServiceResponse.forApplicationError(IncorrectCodeException(), 403, null)) - } else { - Single.just>(ServiceResponse.coerceError(processor.response)) - } - } - } - - override fun verifyAccountWithRegistrationLock(pin: String, svrAuthCredentials: SvrAuthCredentialSet): Single> { - val sessionId = sessionId ?: throw IllegalStateException("No valid registration session") - return changeNumberRepository.changeNumber(sessionId, number.e164Number, pin, svrAuthCredentials) - } - - @WorkerThread - override fun onVerifySuccess(processor: VerifyResponseProcessor): Single { - return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni)) - .map { processor } - .onErrorReturn { t -> - Log.w(TAG, "Error attempting to change local number", t) - VerifyResponseWithoutKbs(ServiceResponse.forUnknownError(t)) - } - } - - override fun onVerifySuccessWithRegistrationLock(processor: VerifyResponseWithRegistrationLockProcessor, pin: String): Single { - return changeNumberRepository.changeLocalNumber(number.e164Number, PNI.parseOrThrow(processor.result.verifyAccountResponse.pni)) - .map { processor } - .onErrorReturn { t -> - Log.w(TAG, "Error attempting to change local number", t) - VerifyResponseWithRegistrationLockProcessor(ServiceResponse.forUnknownError(t), processor.svrAuthCredentials) - } - } - - fun changeNumberWithRecoveryPassword(): Single { - val recoveryPassword = SignalStore.svr.recoveryPassword - - return if (SignalStore.svr.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) + fun registerSmsListenerWithCompletionListener(context: Context, onComplete: (Boolean) -> Unit) { + Log.v(TAG, "registerSmsListenerWithCompletionListener()") + viewModelScope.launch { + val listenerRegistered = RegistrationRepository.registerSmsListener(context) + onComplete(listenerRegistered) } } - class Factory(owner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(owner, null) { - - override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { - val context: Application = AppDependencies.application - val localNumber: String = SignalStore.account.e164!! - val password: String = SignalStore.account.servicePassword!! - - val viewModel = ChangeNumberViewModel( - localNumber = localNumber, - changeNumberRepository = ChangeNumberRepository(), - savedState = handle, - password = password, - verifyAccountRepository = VerifyAccountRepository(context) + fun verifyCodeWithoutRegistrationLock(context: Context, code: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { + Log.v(TAG, "verifyCodeWithoutRegistrationLock()") + store.update { + it.copy( + inProgress = true, + enteredCode = code ) + } - return requireNotNull(modelClass.cast(viewModel)) + viewModelScope.launch { + verifyCodeInternal(context = context, pin = null, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler) } } + fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { + Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()") + store.update { it.copy(inProgress = true) } + + viewModelScope.launch { + verifyCodeInternal(context = context, pin = pin, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler) + } + } + + private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { + val sessionId = getOrCreateValidSession(context)?.body?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } + val registrationData = getRegistrationData(context) + + val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) + + if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) { + handleVerificationError(verificationResponse, verificationErrorHandler) + return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") } + } + + val result: ChangeNumberResult = if (pin == null) { + repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number) + } else { + repository.changeNumberWithRegistrationLock( + sessionId = sessionId, + newE164 = number.e164Number, + pin = pin, + svrAuthCredentials = SvrAuthCredentialSet( + svr2Credentials = store.value.svr2Credentials, + svr3Credentials = store.value.svr3Credentials + ) + ) + } + + if (result is ChangeNumberResult.Success) { + handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked) + } else { + handleChangeNumberError(result, numberChangeErrorHandler) + } + } + + fun submitCaptchaToken(context: Context) { + Log.v(TAG, "submitCaptchaToken()") + val e164 = number.e164Number + val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!") + store.update { + it.copy( + captchaToken = null, + inProgress = true, + changeNumberOutcome = null + ) + } + + viewModelScope.launch { + Log.d(TAG, "Getting session in order to submit captcha token…") + val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") } + if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.CAPTCHA)) { + Log.d(TAG, "Captcha submission no longer necessary, bailing.") + store.update { + it.copy( + inProgress = false, + changeNumberOutcome = null + ) + } + return@launch + } + Log.d(TAG, "Submitting captcha token…") + val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) + Log.d(TAG, "Captcha token submitted.") + store.update { + it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult)) + } + } + } + + fun requestAndSubmitPushToken(context: Context) { + Log.v(TAG, "validatePushToken()") + + addPresentedChallenge(Challenge.PUSH) + + val e164 = number.e164Number + + viewModelScope.launch { + Log.d(TAG, "Getting session in order to perform push token verification…") + val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") } + + if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) { + Log.d(TAG, "Push submission no longer necessary, bailing.") + store.update { + it.copy( + inProgress = false, + changeNumberOutcome = null + ) + } + return@launch + } + + Log.d(TAG, "Requesting push challenge token…") + val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password) + Log.d(TAG, "Push challenge token submitted.") + store.update { + it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult)) + } + } + } + + fun initiateChangeNumberSession(context: Context, mode: RegistrationRepository.Mode) { + Log.v(TAG, "changeNumber()") + store.update { it.copy(inProgress = true) } + viewModelScope.launch { + val encryptionDrained = repository.ensureDecryptionsDrained() ?: false + + if (!encryptionDrained) { + return@launch bail { Log.i(TAG, "Failed to drain encryption.") } + } + + val changed = changeNumberWithRecoveryPassword() + + if (changed) { + Log.d(TAG, "Successfully changed number using recovery password, which cleaned up after itself.") + return@launch + } + + requestVerificationCode(context, mode) + } + } + + // endregion + + // region Private actions + + private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) { + Log.v(TAG, "updateLocalStateFromSession()") + store.update { + it.copy(sessionId = response.body.id, challengesRequested = Challenge.parse(response.body.requestedInformation), allowedToRequestCode = response.body.allowedToRequestCode) + } + } + + private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { + Log.v(TAG, "getOrCreateValidSession()") + val e164 = number.e164Number + val mccMncProducer = MccMncProducer(context) + val existingSessionId = store.value.sessionId + return RegistrationViewModel.getOrCreateValidSession(context = context, existingSessionId = existingSessionId, e164 = e164, password = password, mcc = mccMncProducer.mcc, mnc = mccMncProducer.mnc, successListener = { freshMetadata -> + Log.v(TAG, "Valid session received, updating local state.") + updateLocalStateFromSession(freshMetadata) + }, errorHandler = { result -> + val requestCode: VerificationCodeRequestResult = when (result) { + is RegistrationSessionCreationResult.RateLimited -> VerificationCodeRequestResult.RateLimited(result.getCause(), result.timeRemaining) + is RegistrationSessionCreationResult.MalformedRequest -> VerificationCodeRequestResult.MalformedRequest(result.getCause()) + else -> VerificationCodeRequestResult.UnknownError(result.getCause()) + } + + store.update { + it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(requestCode)) + } + }) + } + + private suspend fun changeNumberWithRecoveryPassword(): Boolean { + Log.v(TAG, "changeNumberWithRecoveryPassword()") + SignalStore.svr.recoveryPassword?.let { recoveryPassword -> + if (SignalStore.svr.hasPin()) { + val result = repository.changeNumberWithRecoveryPassword(recoveryPassword = recoveryPassword, newE164 = number.e164Number) + + if (result is ChangeNumberResult.Success) { + handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked) + return true + } + + Log.d(TAG, "Encountered error while trying to change number with recovery password.", result.getCause()) + } + } + return false + } + + private suspend fun handleSuccessfulChangedRemoteNumber(e164: String, pni: ServiceId.PNI, changeNumberOutcome: ChangeNumberOutcome) { + var result = changeNumberOutcome + Log.v(TAG, "handleSuccessfulChangedRemoteNumber(${result.javaClass.simpleName}") + try { + withLockOnSerialExecutor { + repository.changeLocalNumber(e164, pni) + } + } catch (ioException: IOException) { + Log.w(TAG, "Failed to change local number!", ioException) + result = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(ioException)) + } + + store.update { + it.copy(inProgress = false, changeNumberOutcome = result) + } + } + + private fun handleVerificationError(result: VerificationCodeRequestResult, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit) { + Log.v(TAG, "handleVerificationError(${result.javaClass.simpleName}") + when (result) { + is VerificationCodeRequestResult.Success -> Unit + is VerificationCodeRequestResult.RegistrationLocked -> + store.update { + it.copy( + svr2Credentials = result.svr2Credentials, + svr3Credentials = result.svr3Credentials + ) + } + else -> Log.i(TAG, "Received exception during verification.", result.getCause()) + } + + verificationErrorHandler(result) + } + + private fun handleChangeNumberError(result: ChangeNumberResult, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { + Log.v(TAG, "handleChangeNumberError(${result.javaClass.simpleName}") + when (result) { + is ChangeNumberResult.Success -> Unit + is ChangeNumberResult.RegistrationLocked -> + store.update { + it.copy( + svr2Credentials = result.svr2Credentials, + svr3Credentials = result.svr3Credentials + ) + } + is ChangeNumberResult.SvrWrongPin -> { + store.update { + it.copy( + svrTriesRemaining = result.triesRemaining + ) + } + } + else -> Log.i(TAG, "Received exception during change number.", result.getCause()) + } + + numberChangeErrorHandler(result) + } + + private suspend fun requestVerificationCode(context: Context, mode: RegistrationRepository.Mode) { + Log.v(TAG, "requestVerificationCode()") + val e164 = number.e164Number + + val validSession = getOrCreateValidSession(context) + + if (validSession == null) { + Log.w(TAG, "Bailing on requesting verification code because could not create a session!") + resetLocalSessionState() + return + } + + val result = if (!validSession.body.allowedToRequestCode) { + val challenges = validSession.body.requestedInformation.joinToString() + Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") + VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)) + } else { + store.update { + it.copy(changeNumberOutcome = null, challengesRequested = emptyList()) + } + val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.body.id, e164 = e164, password = password, mode = mode) + Log.d(TAG, "SMS code request submitted") + response + } + + val challengesRequested = if (result is VerificationCodeRequestResult.ChallengeRequired) { + result.challenges + } else { + emptyList() + } + + Log.d(TAG, "Received result: ${result.javaClass.canonicalName}\nwith challenges: ${challengesRequested.joinToString { it.key }}") + + store.update { + it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result), challengesRequested = challengesRequested, inProgress = false) + } + } + + private suspend fun getRegistrationData(context: Context): RegistrationData { + val currentState = store.value + val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!") + val e164: String = number.e164Number ?: throw IllegalStateException("Can't construct registration data without E164!") + val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr.getRecoveryPassword() else null + val fcmToken = RegistrationRepository.getFcmToken(context) + return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword) + } + + // endregion + + // region Utility Functions + + /** + * Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened. + * + * @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages. + */ + private fun bail(logMessage: () -> Unit) { + logMessage() + store.update { + it.copy(inProgress = false) + } + } + + /** + * Anything that runs through this will be run serially, with locks. + */ + private suspend fun withLockOnSerialExecutor(action: () -> T): T = withContext(serialContext) { + Log.v(TAG, "withLock()") + val result = CHANGE_NUMBER_LOCK.withLock { + SignalStore.misc.lockChangeNumber() + Log.v(TAG, "Change number lock acquired.") + try { + action() + } finally { + SignalStore.misc.unlockChangeNumber() + } + } + Log.v(TAG, "Change number lock released.") + return@withContext result + } + + // endregion + enum class ContinueStatus { - CAN_CONTINUE, - INVALID_NUMBER, - OLD_NUMBER_DOESNT_MATCH + CAN_CONTINUE, INVALID_NUMBER, OLD_NUMBER_DOESNT_MATCH } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberAccountLockedV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberAccountLockedV2Fragment.kt deleted file mode 100644 index 4c84b3a30a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberAccountLockedV2Fragment.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.widget.TextView -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView -import java.util.concurrent.TimeUnit - -/** - * Screen visible to the user when they are registration locked and have no SVR data. - */ -class ChangeNumberAccountLockedV2Fragment : LoggingFragment(R.layout.fragment_change_number_account_locked) { - - companion object { - private val TAG = Log.tag(ChangeNumberAccountLockedV2Fragment::class.java) - } - - private val viewModel by activityViewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title)) - - val description = view.findViewById(R.id.account_locked_description) - - viewModel.liveLockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> - description.text = getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)) - } - - view.findViewById(R.id.account_locked_next).setOnClickListener { onNext() } - view.findViewById(R.id.account_locked_learn_more).setOnClickListener { learnMore() } - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - onNext() - } - } - ) - } - - private fun learnMore() { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url))) - startActivity(intent) - } - - private fun durationToDays(duration: Long): Long { - return if (duration != 0L) getLockoutDays(duration).toLong() else 7 - } - - private fun getLockoutDays(timeRemainingMs: Long): Int { - return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1 - } - - fun onNext() { - findNavController().navigateUp() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberConfirmV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberConfirmV2Fragment.kt deleted file mode 100644 index eb2c1a8ecd..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberConfirmV2Fragment.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.os.Bundle -import android.view.View -import android.widget.TextView -import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Screen visible to the user for them to confirm their new phone number was entered correctly. - */ -class ChangeNumberConfirmV2Fragment : LoggingFragment(R.layout.fragment_change_number_confirm) { - - companion object { - private val TAG = Log.tag(ChangeNumberConfirmV2Fragment::class.java) - } - - private val viewModel by activityViewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number) - toolbar.setNavigationOnClickListener { findNavController().navigateUp() } - - val confirmMessage: TextView = view.findViewById(R.id.change_number_confirm_new_number_message) - confirmMessage.text = getString(R.string.ChangeNumberConfirmFragment__you_are_about_to_change_your_phone_number_from_s_to_s, viewModel.oldNumberState.fullFormattedNumber, viewModel.number.fullFormattedNumber) - - val newNumber: TextView = view.findViewById(R.id.change_number_confirm_new_number) - newNumber.text = viewModel.number.fullFormattedNumber - - val editNumber: View = view.findViewById(R.id.change_number_confirm_edit_number) - editNumber.setOnClickListener { findNavController().navigateUp() } - - val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number) - changeNumber.setOnClickListener { - viewModel.registerSmsListenerWithCompletionListener(requireContext()) { - navigateToVerify(it) - } - } - } - - private fun navigateToVerify(smsListenerEnabled: Boolean = false) { - findNavController().safeNavigate( - R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment, - ChangeNumberVerifyV2FragmentArgs.Builder() - .setSmsListenerEnabled(smsListenerEnabled) - .build() - .toBundle() - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberEnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberEnterPhoneNumberV2Fragment.kt deleted file mode 100644 index 804bc162db..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberEnterPhoneNumberV2Fragment.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.os.Bundle -import android.text.TextUtils -import android.view.View -import android.widget.Toast -import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentChangeNumberEnterPhoneNumberV2Binding -import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment -import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs -import org.thoughtcrime.securesms.registration.util.ChangeNumberInputController -import org.thoughtcrime.securesms.util.Dialogs -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Screen for the user to enter their old and new phone numbers. - */ -class ChangeNumberEnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number_v2) { - - companion object { - private val TAG: String = Log.tag(ChangeNumberEnterPhoneNumberV2Fragment::class.java) - - private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country" - private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country" - } - - private val binding: FragmentChangeNumberEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentChangeNumberEnterPhoneNumberV2Binding::bind) - private val viewModel by activityViewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number) - toolbar.setNavigationOnClickListener { findNavController().navigateUp() } - - binding.changeNumberEnterPhoneNumberContinue.setOnClickListener { - onContinue() - } - - val oldController = ChangeNumberInputController( - requireContext(), - binding.changeNumberEnterPhoneNumberOldNumberCountryCode, - binding.changeNumberEnterPhoneNumberOldNumberNumber, - binding.changeNumberEnterPhoneNumberOldNumberSpinner, - false, - object : ChangeNumberInputController.Callbacks { - override fun onNumberFocused() { - binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberOldNumberNumber.bottom) }, 250) - } - - override fun onNumberInputNext(view: View) { - binding.changeNumberEnterPhoneNumberNewNumberCountryCode.requestFocus() - } - - override fun onNumberInputDone(view: View) = Unit - - override fun onPickCountry(view: View) { - val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build() - - findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle()) - } - - override fun setNationalNumber(number: String) { - viewModel.setOldNationalNumber(number) - } - - override fun setCountry(countryCode: Int) { - viewModel.setOldCountry(countryCode) - } - } - ) - - val newController = ChangeNumberInputController( - requireContext(), - binding.changeNumberEnterPhoneNumberNewNumberCountryCode, - binding.changeNumberEnterPhoneNumberNewNumberNumber, - binding.changeNumberEnterPhoneNumberNewNumberSpinner, - true, - object : ChangeNumberInputController.Callbacks { - override fun onNumberFocused() { - binding.changeNumberEnterPhoneNumberScroll.postDelayed({ binding.changeNumberEnterPhoneNumberScroll.smoothScrollTo(0, binding.changeNumberEnterPhoneNumberNewNumberNumber.bottom) }, 250) - } - - override fun onNumberInputNext(view: View) = Unit - - override fun onNumberInputDone(view: View) { - onContinue() - } - - override fun onPickCountry(view: View) { - val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(NEW_NUMBER_COUNTRY_SELECT).build() - - findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle()) - } - - override fun setNationalNumber(number: String) { - viewModel.setNewNationalNumber(number) - } - - override fun setCountry(countryCode: Int) { - viewModel.setNewCountry(countryCode) - } - } - ) - - parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle -> - viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY)) - } - - parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _: String, bundle: Bundle -> - viewModel.setNewCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY)) - } - - viewModel.liveOldNumberState.observe(viewLifecycleOwner, oldController::updateNumber) - viewModel.liveNewNumberState.observe(viewLifecycleOwner, newController::updateNumber) - } - - private fun onContinue() { - if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberCountryCode.text)) { - Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show() - return - } - - if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberOldNumberNumber.text)) { - Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show() - return - } - - if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberCountryCode.text)) { - Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show() - return - } - - if (TextUtils.isEmpty(binding.changeNumberEnterPhoneNumberNewNumberNumber.text)) { - Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show() - return - } - - when (viewModel.canContinue()) { - ChangeNumberV2ViewModel.ContinueStatus.CAN_CONTINUE -> findNavController().safeNavigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment) - ChangeNumberV2ViewModel.ContinueStatus.INVALID_NUMBER -> { - Dialogs.showAlertDialog( - context, - getString(R.string.RegistrationActivity_invalid_number), - String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.number.e164Number) - ) - } - ChangeNumberV2ViewModel.ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(R.string.ChangeNumberEnterPhoneNumberFragment__the_phone_number_you_entered_doesnt_match_your_accounts) - .setPositiveButton(android.R.string.ok, null) - .show() - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberLockV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberLockV2Activity.kt deleted file mode 100644 index 1f72f0c4b7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberLockV2Activity.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.OnBackPressedCallback -import androidx.activity.viewModels -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.MainActivity -import org.thoughtcrime.securesms.PassphraseRequiredActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter -import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme -import org.thoughtcrime.securesms.util.DynamicTheme - -/** - * A captive activity that can determine if an interrupted/erred change number request - * caused a disparity between the server and our locally stored number. - */ -class ChangeNumberLockV2Activity : PassphraseRequiredActivity() { - - companion object { - private val TAG: String = Log.tag(ChangeNumberLockV2Activity::class.java) - - @JvmStatic - fun createIntent(context: Context): Intent { - return Intent(context, ChangeNumberLockV2Activity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - } - } - } - - private val viewModel: ChangeNumberV2ViewModel by viewModels() - private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme() - - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { - dynamicTheme.onCreate(this) - - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - Log.d(TAG, "Back button press swallowed.") - } - } - ) - - setContentView(R.layout.activity_change_number_lock) - - checkWhoAmI() - } - - override fun onResume() { - super.onResume() - dynamicTheme.onResume(this) - } - - private fun checkWhoAmI() { - viewModel.checkWhoAmI(::onChangeStatusConfirmed, ::onFailedToGetChangeNumberStatus) - } - - private fun onChangeStatusConfirmed() { - SignalStore.misc.clearPendingChangeNumberMetadata() - - MaterialAlertDialogBuilder(this) - .setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed) - .setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(SignalStore.account.e164!!))) - .setPositiveButton(android.R.string.ok) { _, _ -> - startActivity(MainActivity.clearTop(this)) - finish() - } - .setCancelable(false) - .show() - } - - private fun onFailedToGetChangeNumberStatus(error: Throwable) { - Log.w(TAG, "Unable to determine status of change number", error) - - MaterialAlertDialogBuilder(this) - .setTitle(R.string.ChangeNumberLockActivity__change_status_unconfirmed) - .setMessage(getString(R.string.ChangeNumberLockActivity__we_could_not_determine_the_status_of_your_change_number_request, error.javaClass.simpleName)) - .setPositiveButton(R.string.ChangeNumberLockActivity__retry) { _, _ -> checkWhoAmI() } - .setNegativeButton(R.string.ChangeNumberLockActivity__leave) { _, _ -> finish() } - .setNeutralButton(R.string.ChangeNumberLockActivity__submit_debug_log) { _, _ -> - startActivity(Intent(this, SubmitDebugLogActivity::class.java)) - finish() - } - .setCancelable(false) - .show() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberPinDiffersV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberPinDiffersV2Fragment.kt deleted file mode 100644 index e7eca9116a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberPinDiffersV2Fragment.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.activity.result.contract.ActivityResultContracts -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess -import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity - -/** - * A screen to educate the user if their PIN differs from old number to new number. - */ -class ChangeNumberPinDiffersV2Fragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) { - - companion object { - private val TAG = Log.tag(ChangeNumberPinDiffersV2Fragment::class.java) - } - - private val confirmCancelDialog = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question) - .setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - view.findViewById(R.id.change_number_pin_differs_keep_old_pin).setOnClickListener { - changeNumberSuccess() - } - - val changePin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == CreateSvrPinActivity.RESULT_OK) { - changeNumberSuccess() - } - } - - view.findViewById(R.id.change_number_pin_differs_update_pin).setOnClickListener { - changePin.launch(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext())) - } - - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, confirmCancelDialog) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberRegistrationLockV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberRegistrationLockV2Fragment.kt deleted file mode 100644 index 4b5863635e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberRegistrationLockV2Fragment.kt +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.os.Bundle -import android.text.InputType -import android.view.KeyEvent -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.TextView -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess -import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.lock.v2.PinKeyboardType -import org.thoughtcrime.securesms.lock.v2.SvrConstants -import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult -import org.thoughtcrime.securesms.registration.v2.ui.registrationlock.RegistrationLockV2Fragment -import org.thoughtcrime.securesms.registration.v2.ui.registrationlock.RegistrationLockV2FragmentArgs -import org.thoughtcrime.securesms.util.CommunicationActions -import org.thoughtcrime.securesms.util.SupportEmailUtil -import org.thoughtcrime.securesms.util.ViewUtil -import org.thoughtcrime.securesms.util.navigation.safeNavigate -import org.whispersystems.signalservice.api.kbs.PinHashUtil -import java.util.concurrent.TimeUnit - -/** - * Screen presented to the user if the new account is registration locked, and allows them to enter their PIN. - */ -class ChangeNumberRegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_change_number_registration_lock) { - - companion object { - private val TAG = Log.tag(RegistrationLockV2Fragment::class.java) - } - - private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind) - - private val viewModel by activityViewModels() - - private var timeRemaining: Long = 0 - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - RegistrationViewDelegate.setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title)) - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setNavigationOnClickListener { navigateUp() } - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - navigateUp() - } - } - ) - - val args: RegistrationLockV2FragmentArgs = RegistrationLockV2FragmentArgs.fromBundle(requireArguments()) - - timeRemaining = args.getTimeRemaining() - - binding.kbsLockForgotPin.visibility = View.GONE - binding.kbsLockForgotPin.setOnClickListener { handleForgottenPin(timeRemaining) } - - binding.kbsLockPinInput.setImeOptions(EditorInfo.IME_ACTION_DONE) - binding.kbsLockPinInput.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - ViewUtil.hideKeyboard(requireContext(), v!!) - handlePinEntry() - return@setOnEditorActionListener true - } - false - } - - enableAndFocusPinEntry() - - binding.kbsLockPinConfirm.setOnClickListener { - ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput) - handlePinEntry() - } - - binding.kbsLockKeyboardToggle.setOnClickListener { - val keyboardType: PinKeyboardType = getPinEntryKeyboardType() - updateKeyboard(keyboardType.other) - binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource) - } - - val keyboardType: PinKeyboardType = getPinEntryKeyboardType().getOther() - binding.kbsLockKeyboardToggle.setIconResource(keyboardType.iconResource) - - viewModel.liveLockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> timeRemaining = t } - - val triesRemaining: Int = viewModel.svrTriesRemaining - - if (triesRemaining <= 3) { - val daysRemaining = getLockoutDays(timeRemaining) - - MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__not_many_tries_left) - .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() } - .show() - } - - if (triesRemaining < 5) { - binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining) - } - - viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate) - } - - private fun onStateUpdate(state: ChangeNumberState) { - if (state.changeNumberOutcome == ChangeNumberOutcome.VerificationCodeWorked) { - handleSuccessfulPinEntry(state.enteredPin) - } - } - - private fun handlePinEntry() { - binding.kbsLockPinInput.setEnabled(false) - - val pin: String = binding.kbsLockPinInput.getText().toString() - - val trimmedLength = pin.replace(" ", "").length - if (trimmedLength == 0) { - Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show() - enableAndFocusPinEntry() - return - } - - if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) { - Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() - enableAndFocusPinEntry() - return - } - - viewModel.setEnteredPin(pin) - - binding.kbsLockPinConfirm.setSpinning() - viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleChangeNumberErrorResponse) - } - - private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) { - when (requestResult) { - is VerificationCodeRequestResult.Success -> Unit - is VerificationCodeRequestResult.RateLimited -> onRateLimited() - is VerificationCodeRequestResult.AttemptsExhausted, - is VerificationCodeRequestResult.RegistrationLocked -> { - navigateToAccountLocked() - } - - is VerificationCodeRequestResult.AlreadyVerified, - is VerificationCodeRequestResult.ChallengeRequired, - is VerificationCodeRequestResult.ExternalServiceFailure, - is VerificationCodeRequestResult.ImpossibleNumber, - is VerificationCodeRequestResult.InvalidTransportModeFailure, - is VerificationCodeRequestResult.MalformedRequest, - is VerificationCodeRequestResult.MustRetry, - is VerificationCodeRequestResult.NoSuchSession, - is VerificationCodeRequestResult.NonNormalizedNumber, - is VerificationCodeRequestResult.TokenNotAccepted, - is VerificationCodeRequestResult.UnknownError -> { - Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause()) - onError() - } - } - } - - private fun handleChangeNumberErrorResponse(result: ChangeNumberResult) { - when (result) { - is ChangeNumberResult.Success -> Unit - is ChangeNumberResult.RateLimited -> onRateLimited() - is ChangeNumberResult.AttemptsExhausted -> navigateToAccountLocked() - - is ChangeNumberResult.SvrWrongPin -> { - Log.i(TAG, "SVR returned a WrongPinException.") - onIncorrectKbsRegistrationLockPin(result.triesRemaining) - } - - is ChangeNumberResult.SvrNoData -> { - Log.i(TAG, "SVR returned a NoDataException.") - navigateToAccountLocked() - } - - is ChangeNumberResult.AuthorizationFailed, - is ChangeNumberResult.IncorrectRecoveryPassword, - is ChangeNumberResult.MalformedRequest, - is ChangeNumberResult.RegistrationLocked, - is ChangeNumberResult.UnknownError, - is ChangeNumberResult.ValidationError -> { - Log.w(TAG, "Unable to register account with registration lock", result.getCause()) - onError() - } - } - } - - private fun onIncorrectKbsRegistrationLockPin(svrTriesRemaining: Int) { - binding.kbsLockPinConfirm.cancelSpinning() - binding.kbsLockPinInput.getText().clear() - enableAndFocusPinEntry() - - if (svrTriesRemaining == 0) { - Log.w(TAG, "Account locked. User out of attempts on KBS.") - navigateToAccountLocked() - return - } - - if (svrTriesRemaining == 3) { - val daysRemaining = getLockoutDays(timeRemaining) - - MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__incorrect_pin) - .setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining)) - .setPositiveButton(android.R.string.ok, null) - .show() - } - - if (svrTriesRemaining > 5) { - binding.kbsLockPinInputLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again) - } else { - binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining) - binding.kbsLockForgotPin.visibility = View.VISIBLE - } - } - - private fun onRateLimited() { - binding.kbsLockPinConfirm.cancelSpinning() - enableAndFocusPinEntry() - - MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationActivity_too_many_attempts) - .setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day) - .setPositiveButton(android.R.string.ok, null) - .show() - } - - fun onError() { - binding.kbsLockPinConfirm.cancelSpinning() - enableAndFocusPinEntry() - - Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show() - } - - private fun handleForgottenPin(timeRemainingMs: Long) { - val lockoutDays = getLockoutDays(timeRemainingMs) - MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__forgot_your_pin) - .setMessage(requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays)) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, which -> sendEmailToSupport() } - .show() - } - - private fun getLockoutDays(timeRemainingMs: Long): Int { - return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1 - } - - private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String { - val resources = requireContext().resources - val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining) - val days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining) - - return "$tries $days" - } - - private fun enableAndFocusPinEntry() { - binding.kbsLockPinInput.setEnabled(true) - binding.kbsLockPinInput.setFocusable(true) - ViewUtil.focusAndShowKeyboard(binding.kbsLockPinInput) - } - - private fun getPinEntryKeyboardType(): PinKeyboardType { - val isNumeric = (binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER - - return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC - } - - private fun updateKeyboard(keyboard: PinKeyboardType) { - val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC - - binding.kbsLockPinInput.setInputType( - if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD - ) - - binding.kbsLockPinInput.getText().clear() - } - - private fun navigateToAccountLocked() { - findNavController().safeNavigate(ChangeNumberRegistrationLockV2FragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked()) - } - - private fun handleSuccessfulPinEntry(pin: String) { - val pinsDiffer: Boolean = SignalStore.svr.localPinHash?.let { !PinHashUtil.verifyLocalPinHash(it, pin) } ?: false - - binding.kbsLockPinConfirm.cancelSpinning() - - if (pinsDiffer) { - findNavController().safeNavigate(ChangeNumberRegistrationLockV2FragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers()) - } else { - changeNumberSuccess() - } - } - - private fun sendEmailToSupport() { - val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin - - val body: String = SupportEmailUtil.generateSupportEmailBody(requireContext(), subject, null, null) - - CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), getString(subject), body) - } - - private fun navigateUp() { - if (SignalStore.misc.isChangeNumberLocked) { - startActivity(ChangeNumberLockV2Activity.createIntent(requireContext())) - } else { - findNavController().navigateUp() - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2Fragment.kt deleted file mode 100644 index 8de0647674..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2Fragment.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.os.Bundle -import android.view.View -import androidx.appcompat.widget.Toolbar -import androidx.navigation.fragment.findNavController -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentChangePhoneNumberV2Binding -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Screen used to educate the user about what they're about to do (change their phone number) - */ -class ChangeNumberV2Fragment : LoggingFragment(R.layout.fragment_change_phone_number_v2) { - - companion object { - private val TAG = Log.tag(ChangeNumberV2Fragment::class.java) - } - - private val binding: FragmentChangePhoneNumberV2Binding by ViewBinderDelegate(FragmentChangePhoneNumberV2Binding::bind) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setNavigationOnClickListener { findNavController().navigateUp() } - - binding.changePhoneNumberContinue.setOnClickListener { - findNavController().safeNavigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2Repository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2Repository.kt deleted file mode 100644 index aead961bc6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2Repository.kt +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import androidx.annotation.WorkerThread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import okio.ByteString.Companion.toByteString -import org.signal.core.util.logging.Log -import org.signal.libsignal.protocol.IdentityKeyPair -import org.signal.libsignal.protocol.SignalProtocolAddress -import org.signal.libsignal.protocol.state.KyberPreKeyRecord -import org.signal.libsignal.protocol.state.SignalProtocolStore -import org.signal.libsignal.protocol.state.SignedPreKeyRecord -import org.signal.libsignal.protocol.util.KeyHelper -import org.signal.libsignal.protocol.util.Medium -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.crypto.PreKeyUtil -import org.thoughtcrime.securesms.database.IdentityTable -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobs.RefreshAttributesJob -import org.thoughtcrime.securesms.keyvalue.CertificateType -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.pin.SvrRepository -import org.thoughtcrime.securesms.pin.SvrWrongPinException -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet -import org.thoughtcrime.securesms.storage.StorageSyncHelper -import org.whispersystems.signalservice.api.NetworkResult -import org.whispersystems.signalservice.api.SignalServiceAccountManager -import org.whispersystems.signalservice.api.SignalServiceMessageSender -import org.whispersystems.signalservice.api.SvrNoDataException -import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest -import org.whispersystems.signalservice.api.account.PreKeyUpload -import org.whispersystems.signalservice.api.kbs.MasterKey -import org.whispersystems.signalservice.api.push.ServiceId -import org.whispersystems.signalservice.api.push.ServiceIdType -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.api.push.SignedPreKeyEntity -import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity -import org.whispersystems.signalservice.internal.push.OutgoingPushMessage -import org.whispersystems.signalservice.internal.push.SyncMessage -import org.whispersystems.signalservice.internal.push.VerifyAccountResponse -import org.whispersystems.signalservice.internal.push.WhoAmIResponse -import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException -import java.io.IOException -import java.security.MessageDigest -import java.security.SecureRandom -import kotlin.coroutines.resume -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -/** - * Repository to perform data operations during change number. - * - * @see [org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository] - */ -class ChangeNumberV2Repository( - private val accountManager: SignalServiceAccountManager = AppDependencies.signalServiceAccountManager, - private val messageSender: SignalServiceMessageSender = AppDependencies.signalServiceMessageSender -) { - - companion object { - private val TAG = Log.tag(ChangeNumberV2Repository::class.java) - } - - fun whoAmI(): WhoAmIResponse { - return accountManager.whoAmI - } - - suspend fun ensureDecryptionsDrained(timeout: Duration = 15.seconds) = - withTimeoutOrNull(timeout) { - suspendCancellableCoroutine { - val drainedListener = object : Runnable { - override fun run() { - AppDependencies - .incomingMessageObserver - .removeDecryptionDrainedListener(this) - Log.d(TAG, "Decryptions drained.") - it.resume(true) - } - } - - it.invokeOnCancellation { cancellationCause -> - AppDependencies - .incomingMessageObserver - .removeDecryptionDrainedListener(drainedListener) - Log.d(TAG, "Decryptions draining canceled.", cancellationCause) - } - - AppDependencies - .incomingMessageObserver - .addDecryptionDrainedListener(drainedListener) - Log.d(TAG, "Waiting for decryption drain.") - } - } - - @WorkerThread - fun changeLocalNumber(e164: String, pni: ServiceId.PNI) { - val oldStorageId: ByteArray? = Recipient.self().storageId - SignalDatabase.recipients.updateSelfE164(e164, pni) - val newStorageId: ByteArray? = Recipient.self().storageId - - if (e164 != SignalStore.account.requireE164() && MessageDigest.isEqual(oldStorageId, newStorageId)) { - Log.w(TAG, "Self storage id was not rotated, attempting to rotate again") - SignalDatabase.recipients.rotateStorageId(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - val secondAttemptStorageId: ByteArray? = Recipient.self().storageId - if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) { - Log.w(TAG, "Second attempt also failed to rotate storage id") - } - } - - AppDependencies.recipientCache.clear() - - SignalStore.account.setE164(e164) - SignalStore.account.setPni(pni) - AppDependencies.resetProtocolStores() - - AppDependencies.groupsV2Authorization.clear() - - val metadata: PendingChangeNumberMetadata? = SignalStore.misc.pendingChangeNumberMetadata - if (metadata == null) { - Log.w(TAG, "No change number metadata, this shouldn't happen") - throw AssertionError("No change number metadata") - } - - val originalPni = ServiceId.PNI.parseOrThrow(metadata.previousPni) - - if (originalPni == pni) { - Log.i(TAG, "No change has occurred, PNI is unchanged: $pni") - } else { - val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray()) - val pniRegistrationId = metadata.pniRegistrationId - val pniSignedPreyKeyId = metadata.pniSignedPreKeyId - val pniLastResortKyberPreKeyId = metadata.pniLastResortKyberPreKeyId - - val pniProtocolStore = AppDependencies.protocolStore.pni() - val pniMetadataStore = SignalStore.account.pniPreKeys - - SignalStore.account.pniRegistrationId = pniRegistrationId - SignalStore.account.setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair) - - val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId) - val oneTimeEcPreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(pniProtocolStore, pniMetadataStore) - val lastResortKyberPreKey = pniProtocolStore.loadLastResortKyberPreKeys().firstOrNull { it.id == pniLastResortKyberPreKeyId } - val oneTimeKyberPreKeys = PreKeyUtil.generateAndStoreOneTimeKyberPreKeys(pniProtocolStore, pniMetadataStore) - - if (lastResortKyberPreKey == null) { - Log.w(TAG, "Last-resort kyber prekey is missing!") - } - - pniMetadataStore.activeSignedPreKeyId = signedPreKey.id - Log.i(TAG, "Submitting prekeys with PNI identity key: ${pniIdentityKeyPair.publicKey.fingerprint}") - - accountManager.setPreKeys( - PreKeyUpload( - serviceIdType = ServiceIdType.PNI, - signedPreKey = signedPreKey, - oneTimeEcPreKeys = oneTimeEcPreKeys, - lastResortKyberPreKey = lastResortKyberPreKey, - oneTimeKyberPreKeys = oneTimeKyberPreKeys - ) - ) - pniMetadataStore.isSignedPreKeyRegistered = true - pniMetadataStore.lastResortKyberPreKeyId = pniLastResortKyberPreKeyId - - pniProtocolStore.identities().saveIdentityWithoutSideEffects( - Recipient.self().id, - pni, - pniProtocolStore.identityKeyPair.publicKey, - IdentityTable.VerifiedStatus.VERIFIED, - true, - System.currentTimeMillis(), - true - ) - - SignalStore.misc.hasPniInitializedDevices = true - AppDependencies.groupsV2Authorization.clear() - } - - Recipient.self().live().refresh() - StorageSyncHelper.scheduleSyncForDataChange() - - AppDependencies.resetNetwork() - AppDependencies.incomingMessageObserver - - AppDependencies.jobManager.add(RefreshAttributesJob()) - - rotateCertificates() - } - - @WorkerThread - private fun rotateCertificates() { - val certificateTypes = SignalStore.phoneNumberPrivacy.allCertificateTypes - - Log.i(TAG, "Rotating these certificates $certificateTypes") - - for (certificateType in certificateTypes) { - val certificate: ByteArray? = when (certificateType) { - CertificateType.ACI_AND_E164 -> accountManager.senderCertificate - CertificateType.ACI_ONLY -> accountManager.senderCertificateForPhoneNumberPrivacy - else -> throw AssertionError() - } - - Log.i(TAG, "Successfully got $certificateType certificate") - - SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate) - } - } - - suspend fun changeNumberWithRecoveryPassword(recoveryPassword: String, newE164: String): ChangeNumberResult { - return changeNumberInternal(recoveryPassword = recoveryPassword, newE164 = newE164) - } - - suspend fun changeNumberWithoutRegistrationLock(sessionId: String, newE164: String): ChangeNumberResult { - return changeNumberInternal(sessionId = sessionId, newE164 = newE164) - } - - suspend fun changeNumberWithRegistrationLock( - sessionId: String, - newE164: String, - pin: String, - svrAuthCredentials: SvrAuthCredentialSet - ): ChangeNumberResult { - val masterKey: MasterKey - - try { - masterKey = SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin) - } catch (e: SvrWrongPinException) { - return ChangeNumberResult.SvrWrongPin(e) - } catch (e: SvrNoDataException) { - return ChangeNumberResult.SvrNoData(e) - } catch (e: IOException) { - return ChangeNumberResult.UnknownError(e) - } - - val registrationLock = masterKey.deriveRegistrationLock() - return changeNumberInternal(sessionId = sessionId, registrationLock = registrationLock, newE164 = newE164) - } - - /** - * Sends a request to the service to change the phone number associated with this account. - */ - private suspend fun changeNumberInternal(sessionId: String? = null, recoveryPassword: String? = null, registrationLock: String? = null, newE164: String): ChangeNumberResult { - check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null)) - var completed = false - var attempts = 0 - lateinit var result: NetworkResult - - while (!completed && attempts < 5) { - Log.i(TAG, "Attempt #$attempts") - val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( - sessionId = sessionId, - recoveryPassword = recoveryPassword, - newE164 = newE164, - registrationLock = registrationLock - ) - - SignalStore.misc.setPendingChangeNumberMetadata(metadata) - withContext(Dispatchers.IO) { - result = accountManager.registrationApi.changeNumber(request) - } - - val possibleError = result.getCause() as? MismatchedDevicesException - if (possibleError != null) { - messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices) - attempts++ - } else { - completed = true - } - } - Log.i(TAG, "Returning change number network result.") - return ChangeNumberResult.from( - result.map { accountRegistrationResponse: VerifyAccountResponse -> - NumberChangeResult( - uuid = accountRegistrationResponse.uuid, - pni = accountRegistrationResponse.pni, - storageCapable = accountRegistrationResponse.storageCapable, - number = accountRegistrationResponse.number - ) - } - ) - } - - @WorkerThread - private fun createChangeNumberRequest( - sessionId: String? = null, - recoveryPassword: String? = null, - newE164: String, - registrationLock: String? = null - ): ChangeNumberRequestData { - val selfIdentifier: String = SignalStore.account.requireAci().toString() - val aciProtocolStore: SignalProtocolStore = AppDependencies.protocolStore.aci() - - val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() - val deviceMessages = mutableListOf() - val devicePniSignedPreKeys = mutableMapOf() - val devicePniLastResortKyberPreKeys = mutableMapOf() - val pniRegistrationIds = mutableMapOf() - val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID - - val devices: List = listOf(primaryDeviceId) + aciProtocolStore.getSubDeviceSessions(selfIdentifier) - - devices - .filter { it == primaryDeviceId || aciProtocolStore.containsSession(SignalProtocolAddress(selfIdentifier, it)) } - .forEach { deviceId -> - // Signed Prekeys - val signedPreKeyRecord: SignedPreKeyRecord = if (deviceId == primaryDeviceId) { - PreKeyUtil.generateAndStoreSignedPreKey(AppDependencies.protocolStore.pni(), SignalStore.account.pniPreKeys, pniIdentity.privateKey) - } else { - PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) - } - devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature) - - // Last-resort kyber prekeys - val lastResortKyberPreKeyRecord: KyberPreKeyRecord = if (deviceId == primaryDeviceId) { - PreKeyUtil.generateAndStoreLastResortKyberPreKey(AppDependencies.protocolStore.pni(), SignalStore.account.pniPreKeys, pniIdentity.privateKey) - } else { - PreKeyUtil.generateLastResortKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) - } - devicePniLastResortKyberPreKeys[deviceId] = KyberPreKeyEntity(lastResortKyberPreKeyRecord.id, lastResortKyberPreKeyRecord.keyPair.publicKey, lastResortKyberPreKeyRecord.signature) - - // Registration Ids - var pniRegistrationId = -1 - - while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) { - pniRegistrationId = KeyHelper.generateRegistrationId(false) - } - pniRegistrationIds[deviceId] = pniRegistrationId - - // Device Messages - if (deviceId != primaryDeviceId) { - val pniChangeNumber = SyncMessage.PniChangeNumber( - identityKeyPair = pniIdentity.serialize().toByteString(), - signedPreKey = signedPreKeyRecord.serialize().toByteString(), - lastResortKyberPreKey = lastResortKyberPreKeyRecord.serialize().toByteString(), - registrationId = pniRegistrationId, - newE164 = newE164 - ) - - deviceMessages += messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId, pniChangeNumber) - } - } - - val request = ChangePhoneNumberRequest( - sessionId, - recoveryPassword, - newE164, - registrationLock, - pniIdentity.publicKey, - deviceMessages, - devicePniSignedPreKeys.mapKeys { it.key.toString() }, - devicePniLastResortKyberPreKeys.mapKeys { it.key.toString() }, - pniRegistrationIds.mapKeys { it.key.toString() } - ) - - val metadata = PendingChangeNumberMetadata( - previousPni = SignalStore.account.pni!!.toByteString(), - pniIdentityKeyPair = pniIdentity.serialize().toByteString(), - pniRegistrationId = pniRegistrationIds[primaryDeviceId]!!, - pniSignedPreKeyId = devicePniSignedPreKeys[primaryDeviceId]!!.keyId, - pniLastResortKyberPreKeyId = devicePniLastResortKyberPreKeys[primaryDeviceId]!!.keyId - ) - - return ChangeNumberRequestData(request, metadata) - } - - private data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata) - - data class NumberChangeResult( - val uuid: String, - val pni: String, - val storageCapable: Boolean, - val number: String - ) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt deleted file mode 100644 index d8d614931b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt +++ /dev/null @@ -1,553 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import com.google.i18n.phonenumbers.NumberParseException -import com.google.i18n.phonenumbers.PhoneNumberUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.signal.core.util.concurrent.SignalExecutors -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.registration.RegistrationData -import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel -import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState -import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet -import org.thoughtcrime.securesms.util.dualsim.MccMncProducer -import org.whispersystems.signalservice.api.push.ServiceId -import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse -import java.io.IOException -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock - -/** - * [ViewModel] for the change number flow. - * - * @see [RegistrationV2ViewModel], from which this is derived. - */ -class ChangeNumberV2ViewModel : ViewModel() { - - companion object { - private val TAG = Log.tag(ChangeNumberV2ViewModel::class.java) - - val CHANGE_NUMBER_LOCK = ReentrantLock() - } - - private val repository = ChangeNumberV2Repository() - private val store = MutableStateFlow(ChangeNumberState()) - private val serialContext = SignalExecutors.SERIAL.asCoroutineDispatcher() - private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(AppDependencies.application) - - private val initialLocalNumber = SignalStore.account.e164 - private val password = SignalStore.account.servicePassword!! - - val uiState = store.asLiveData() - val liveOldNumberState = store.map { it.oldPhoneNumber }.asLiveData() - val liveNewNumberState = store.map { it.number }.asLiveData() - val liveLockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData() - val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData() - - init { - try { - val countryCode: Int = PhoneNumberUtil.getInstance() - .parse(SignalStore.account.e164!!, null) - .countryCode - - store.update { - it.copy( - number = it.number.toBuilder().countryCode(countryCode).build(), - oldPhoneNumber = it.oldPhoneNumber.toBuilder().countryCode(countryCode).build() - ) - } - } catch (e: NumberParseException) { - Log.i(TAG, "Unable to parse number for default country code") - } - - smsRetrieverReceiver.registerReceiver() - } - - override fun onCleared() { - super.onCleared() - smsRetrieverReceiver.unregisterReceiver() - } - - // region Public Getters and Setters - - val number: NumberViewState - get() = store.value.number - - val oldNumberState: NumberViewState - get() = store.value.oldPhoneNumber - - val svrTriesRemaining: Int - get() = store.value.svrTriesRemaining - - fun setOldNationalNumber(updatedNumber: String) { - store.update { - it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build()) - } - } - - fun setOldCountry(countryCode: Int, country: String? = null) { - store.update { - it.copy(oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build()) - } - } - - fun setNewNationalNumber(updatedNumber: String) { - store.update { - it.copy(number = number.toBuilder().nationalNumber(updatedNumber).build()) - } - } - - fun setNewCountry(countryCode: Int, country: String? = null) { - store.update { - it.copy(number = number.toBuilder().selectedCountryDisplayName(country).countryCode(countryCode).build()) - } - } - - fun setCaptchaResponse(token: String) { - Log.v(TAG, "setCaptchaResponse()") - store.update { - it.copy(captchaToken = token) - } - } - - fun setEnteredPin(pin: String) { - store.update { - it.copy(enteredPin = pin) - } - } - - fun incrementIncorrectCodeAttempts() { - store.update { - it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1) - } - } - - fun addPresentedChallenge(challenge: Challenge) { - Log.v(TAG, "addPresentedChallenge()") - store.update { - it.copy(challengesPresented = it.challengesPresented.plus(challenge)) - } - } - - fun removePresentedChallenge(challenge: Challenge) { - Log.v(TAG, "addPresentedChallenge()") - store.update { - it.copy(challengesPresented = it.challengesPresented.minus(challenge)) - } - } - - fun resetLocalSessionState() { - Log.v(TAG, "resetLocalSessionState()") - store.update { - it.copy(inProgress = false, changeNumberOutcome = null, captchaToken = null, challengesRequested = emptyList(), allowedToRequestCode = false) - } - } - - fun canContinue(): ContinueStatus { - return if (oldNumberState.e164Number == initialLocalNumber) { - if (number.isValid) { - ContinueStatus.CAN_CONTINUE - } else { - ContinueStatus.INVALID_NUMBER - } - } else { - ContinueStatus.OLD_NUMBER_DOESNT_MATCH - } - } - - // endregion - - // region Public actions - - fun checkWhoAmI(onSuccess: () -> Unit, onError: (Throwable) -> Unit) { - Log.v(TAG, "checkWhoAmI()") - viewModelScope.launch(Dispatchers.IO) { - try { - val whoAmI = repository.whoAmI() - - if (whoAmI.number == SignalStore.account.e164) { - return@launch bail { Log.i(TAG, "Local and remote numbers match, nothing needs to be done.") } - } - - Log.i(TAG, "Local (${SignalStore.account.e164}) and remote (${whoAmI.number}) numbers do not match, updating local.") - - withLockOnSerialExecutor { - repository.changeLocalNumber(whoAmI.number, ServiceId.PNI.parseOrThrow(whoAmI.pni)) - } - - withContext(Dispatchers.Main) { - onSuccess() - } - } catch (ioException: IOException) { - Log.w(TAG, "Encountered an exception when requesting whoAmI()", ioException) - withContext(Dispatchers.Main) { - onError(ioException) - } - } - } - } - - fun registerSmsListenerWithCompletionListener(context: Context, onComplete: (Boolean) -> Unit) { - Log.v(TAG, "registerSmsListenerWithCompletionListener()") - viewModelScope.launch { - val listenerRegistered = RegistrationRepository.registerSmsListener(context) - onComplete(listenerRegistered) - } - } - - fun verifyCodeWithoutRegistrationLock(context: Context, code: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { - Log.v(TAG, "verifyCodeWithoutRegistrationLock()") - store.update { - it.copy( - inProgress = true, - enteredCode = code - ) - } - - viewModelScope.launch { - verifyCodeInternal(context = context, pin = null, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler) - } - } - - fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { - Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()") - store.update { it.copy(inProgress = true) } - - viewModelScope.launch { - verifyCodeInternal(context = context, pin = pin, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler) - } - } - - private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { - val sessionId = getOrCreateValidSession(context)?.body?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } - val registrationData = getRegistrationData(context) - - val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) - - if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) { - handleVerificationError(verificationResponse, verificationErrorHandler) - return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") } - } - - val result: ChangeNumberResult = if (pin == null) { - repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number) - } else { - repository.changeNumberWithRegistrationLock( - sessionId = sessionId, - newE164 = number.e164Number, - pin = pin, - svrAuthCredentials = SvrAuthCredentialSet( - svr2Credentials = store.value.svr2Credentials, - svr3Credentials = store.value.svr3Credentials - ) - ) - } - - if (result is ChangeNumberResult.Success) { - handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked) - } else { - handleChangeNumberError(result, numberChangeErrorHandler) - } - } - - fun submitCaptchaToken(context: Context) { - Log.v(TAG, "submitCaptchaToken()") - val e164 = number.e164Number - val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!") - store.update { - it.copy( - captchaToken = null, - inProgress = true, - changeNumberOutcome = null - ) - } - - viewModelScope.launch { - Log.d(TAG, "Getting session in order to submit captcha token…") - val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") } - if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.CAPTCHA)) { - Log.d(TAG, "Captcha submission no longer necessary, bailing.") - store.update { - it.copy( - inProgress = false, - changeNumberOutcome = null - ) - } - return@launch - } - Log.d(TAG, "Submitting captcha token…") - val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) - Log.d(TAG, "Captcha token submitted.") - store.update { - it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult)) - } - } - } - - fun requestAndSubmitPushToken(context: Context) { - Log.v(TAG, "validatePushToken()") - - addPresentedChallenge(Challenge.PUSH) - - val e164 = number.e164Number - - viewModelScope.launch { - Log.d(TAG, "Getting session in order to perform push token verification…") - val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") } - - if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) { - Log.d(TAG, "Push submission no longer necessary, bailing.") - store.update { - it.copy( - inProgress = false, - changeNumberOutcome = null - ) - } - return@launch - } - - Log.d(TAG, "Requesting push challenge token…") - val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password) - Log.d(TAG, "Push challenge token submitted.") - store.update { - it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult)) - } - } - } - - fun initiateChangeNumberSession(context: Context, mode: RegistrationRepository.Mode) { - Log.v(TAG, "changeNumber()") - store.update { it.copy(inProgress = true) } - viewModelScope.launch { - val encryptionDrained = repository.ensureDecryptionsDrained() ?: false - - if (!encryptionDrained) { - return@launch bail { Log.i(TAG, "Failed to drain encryption.") } - } - - val changed = changeNumberWithRecoveryPassword() - - if (changed) { - Log.d(TAG, "Successfully changed number using recovery password, which cleaned up after itself.") - return@launch - } - - requestVerificationCode(context, mode) - } - } - - // endregion - - // region Private actions - - private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) { - Log.v(TAG, "updateLocalStateFromSession()") - store.update { - it.copy(sessionId = response.body.id, challengesRequested = Challenge.parse(response.body.requestedInformation), allowedToRequestCode = response.body.allowedToRequestCode) - } - } - - private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { - Log.v(TAG, "getOrCreateValidSession()") - val e164 = number.e164Number - val mccMncProducer = MccMncProducer(context) - val existingSessionId = store.value.sessionId - return RegistrationV2ViewModel.getOrCreateValidSession(context = context, existingSessionId = existingSessionId, e164 = e164, password = password, mcc = mccMncProducer.mcc, mnc = mccMncProducer.mnc, successListener = { freshMetadata -> - Log.v(TAG, "Valid session received, updating local state.") - updateLocalStateFromSession(freshMetadata) - }, errorHandler = { result -> - val requestCode: VerificationCodeRequestResult = when (result) { - is RegistrationSessionCreationResult.RateLimited -> VerificationCodeRequestResult.RateLimited(result.getCause(), result.timeRemaining) - is RegistrationSessionCreationResult.MalformedRequest -> VerificationCodeRequestResult.MalformedRequest(result.getCause()) - else -> VerificationCodeRequestResult.UnknownError(result.getCause()) - } - - store.update { - it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(requestCode)) - } - }) - } - - private suspend fun changeNumberWithRecoveryPassword(): Boolean { - Log.v(TAG, "changeNumberWithRecoveryPassword()") - SignalStore.svr.recoveryPassword?.let { recoveryPassword -> - if (SignalStore.svr.hasPin()) { - val result = repository.changeNumberWithRecoveryPassword(recoveryPassword = recoveryPassword, newE164 = number.e164Number) - - if (result is ChangeNumberResult.Success) { - handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked) - return true - } - - Log.d(TAG, "Encountered error while trying to change number with recovery password.", result.getCause()) - } - } - return false - } - - private suspend fun handleSuccessfulChangedRemoteNumber(e164: String, pni: ServiceId.PNI, changeNumberOutcome: ChangeNumberOutcome) { - var result = changeNumberOutcome - Log.v(TAG, "handleSuccessfulChangedRemoteNumber(${result.javaClass.simpleName}") - try { - withLockOnSerialExecutor { - repository.changeLocalNumber(e164, pni) - } - } catch (ioException: IOException) { - Log.w(TAG, "Failed to change local number!", ioException) - result = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(ioException)) - } - - store.update { - it.copy(inProgress = false, changeNumberOutcome = result) - } - } - - private fun handleVerificationError(result: VerificationCodeRequestResult, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit) { - Log.v(TAG, "handleVerificationError(${result.javaClass.simpleName}") - when (result) { - is VerificationCodeRequestResult.Success -> Unit - is VerificationCodeRequestResult.RegistrationLocked -> - store.update { - it.copy( - svr2Credentials = result.svr2Credentials, - svr3Credentials = result.svr3Credentials - ) - } - else -> Log.i(TAG, "Received exception during verification.", result.getCause()) - } - - verificationErrorHandler(result) - } - - private fun handleChangeNumberError(result: ChangeNumberResult, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { - Log.v(TAG, "handleChangeNumberError(${result.javaClass.simpleName}") - when (result) { - is ChangeNumberResult.Success -> Unit - is ChangeNumberResult.RegistrationLocked -> - store.update { - it.copy( - svr2Credentials = result.svr2Credentials, - svr3Credentials = result.svr3Credentials - ) - } - is ChangeNumberResult.SvrWrongPin -> { - store.update { - it.copy( - svrTriesRemaining = result.triesRemaining - ) - } - } - else -> Log.i(TAG, "Received exception during change number.", result.getCause()) - } - - numberChangeErrorHandler(result) - } - - private suspend fun requestVerificationCode(context: Context, mode: RegistrationRepository.Mode) { - Log.v(TAG, "requestVerificationCode()") - val e164 = number.e164Number - - val validSession = getOrCreateValidSession(context) - - if (validSession == null) { - Log.w(TAG, "Bailing on requesting verification code because could not create a session!") - resetLocalSessionState() - return - } - - val result = if (!validSession.body.allowedToRequestCode) { - val challenges = validSession.body.requestedInformation.joinToString() - Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") - VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)) - } else { - store.update { - it.copy(changeNumberOutcome = null, challengesRequested = emptyList()) - } - val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.body.id, e164 = e164, password = password, mode = mode) - Log.d(TAG, "SMS code request submitted") - response - } - - val challengesRequested = if (result is VerificationCodeRequestResult.ChallengeRequired) { - result.challenges - } else { - emptyList() - } - - Log.d(TAG, "Received result: ${result.javaClass.canonicalName}\nwith challenges: ${challengesRequested.joinToString { it.key }}") - - store.update { - it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result), challengesRequested = challengesRequested, inProgress = false) - } - } - - private suspend fun getRegistrationData(context: Context): RegistrationData { - val currentState = store.value - val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!") - val e164: String = number.e164Number ?: throw IllegalStateException("Can't construct registration data without E164!") - val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr.getRecoveryPassword() else null - val fcmToken = RegistrationRepository.getFcmToken(context) - return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword) - } - - // endregion - - // region Utility Functions - - /** - * Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened. - * - * @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages. - */ - private fun bail(logMessage: () -> Unit) { - logMessage() - store.update { - it.copy(inProgress = false) - } - } - - /** - * Anything that runs through this will be run serially, with locks. - */ - private suspend fun withLockOnSerialExecutor(action: () -> T): T = withContext(serialContext) { - Log.v(TAG, "withLock()") - val result = CHANGE_NUMBER_LOCK.withLock { - SignalStore.misc.lockChangeNumber() - Log.v(TAG, "Change number lock acquired.") - try { - action() - } finally { - SignalStore.misc.unlockChangeNumber() - } - } - Log.v(TAG, "Change number lock released.") - return@withContext result - } - - // endregion - - enum class ContinueStatus { - CAN_CONTINUE, INVALID_NUMBER, OLD_NUMBER_DOESNT_MATCH - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberVerifyV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberVerifyV2Fragment.kt deleted file mode 100644 index 9567e7ba7e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberVerifyV2Fragment.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 - -import android.os.Bundle -import android.view.View -import android.widget.TextView -import androidx.annotation.StringRes -import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.isNotNullOrBlank -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess -import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberVerifyFragmentArgs -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Screen to show while the change number is in-progress. - */ -class ChangeNumberVerifyV2Fragment : LoggingFragment(R.layout.fragment_change_phone_number_verify) { - - companion object { - private val TAG: String = Log.tag(ChangeNumberVerifyV2Fragment::class.java) - } - - private val viewModel by activityViewModels() - private var dialogVisible: Boolean = false - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - toolbar.setTitle(R.string.ChangeNumberVerifyFragment__change_number) - toolbar.setNavigationOnClickListener { - findNavController().navigateUp() - viewModel.resetLocalSessionState() - } - - val status: TextView = view.findViewById(R.id.change_phone_number_verify_status) - status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber) - - viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate) - - requestCode() - } - - private fun onStateUpdate(state: ChangeNumberState) { - if (state.challengesRequested.contains(Challenge.CAPTCHA) && state.captchaToken.isNotNullOrBlank()) { - viewModel.submitCaptchaToken(requireContext()) - } else if (state.challengesRemaining.isNotEmpty()) { - handleChallenges(state.challengesRemaining) - } else if (state.changeNumberOutcome != null) { - handleRequestCodeResult(state.changeNumberOutcome) - } else if (!state.inProgress) { - Log.d(TAG, "Not in progress, navigating up.") - if (state.allowedToRequestCode) { - requestCode() - } else if (!dialogVisible) { - showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code) - } - } - } - - private fun requestCode() { - val mode = if (ChangeNumberVerifyFragmentArgs.fromBundle(requireArguments()).smsListenerEnabled) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER - viewModel.initiateChangeNumberSession(requireContext(), mode) - } - - private fun handleRequestCodeResult(changeNumberOutcome: ChangeNumberOutcome) { - Log.d(TAG, "Handling request code result: ${changeNumberOutcome.javaClass.name}") - when (changeNumberOutcome) { - is ChangeNumberOutcome.RecoveryPasswordWorked -> { - Log.i(TAG, "Successfully changed number with recovery password.") - changeNumberSuccess() - } - - is ChangeNumberOutcome.ChangeNumberRequestOutcome -> { - when (val castResult = changeNumberOutcome.result) { - is VerificationCodeRequestResult.Success -> { - Log.i(TAG, "Successfully requested SMS code.") - findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment) - } - - is VerificationCodeRequestResult.ChallengeRequired -> { - Log.i(TAG, "Unable to request sms code due to challenges required: ${castResult.challenges.joinToString { it.key }}") - } - - is VerificationCodeRequestResult.RateLimited -> { - Log.i(TAG, "Unable to request sms code due to rate limit") - showErrorDialog(R.string.RegistrationActivity_rate_limited_to_service) - } - - is VerificationCodeRequestResult.TokenNotAccepted -> { - Log.i(TAG, "Token was not accepted.") - showErrorDialog(R.string.RegistrationActivity_additional_verification_required) - } - - else -> { - Log.w(TAG, "Unable to request sms code", castResult.getCause()) - showErrorDialog(R.string.RegistrationActivity_unable_to_request_verification_code) - } - } - } - - is ChangeNumberOutcome.VerificationCodeWorked -> { - Log.i(TAG, "Successfully changed number with verification code.") - changeNumberSuccess() - } - } - } - - private fun handleChallenges(remainingChallenges: List) { - Log.i(TAG, "Handling challenge(s): ${remainingChallenges.joinToString { it.key }}") - when (remainingChallenges.first()) { - Challenge.CAPTCHA -> { - findNavController().safeNavigate(ChangeNumberVerifyV2FragmentDirections.actionChangePhoneNumberVerifyFragmentToCaptchaFragment()) - } - - Challenge.PUSH -> { - viewModel.requestAndSubmitPushToken(requireContext()) - } - } - } - - private fun showErrorDialog(@StringRes message: Int) { - if (dialogVisible) { - Log.i(TAG, "Dialog already being shown, failed to display dialog with message ${getString(message)}") - return - } - - MaterialAlertDialogBuilder(requireContext()).apply { - setMessage(message) - setPositiveButton(android.R.string.ok) { _, _ -> - findNavController().navigateUp() - viewModel.resetLocalSessionState() - } - show() - dialogVisible = true - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 3b9c67cd8b..b5ddab7712 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -273,7 +273,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity import org.thoughtcrime.securesms.revealable.ViewOnceUtil import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet @@ -3706,7 +3706,7 @@ class ConversationFragment : } override fun reRegisterAction() { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())) + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())) } override fun reviewJoinRequestsAction() { @@ -3781,7 +3781,7 @@ class ConversationFragment : } override fun onReRegisterClicked() { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())) + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())) } override fun onCancelGroupRequestClicked() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index df6096178a..945a43ae67 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -158,7 +158,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment; import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity; import org.thoughtcrime.securesms.search.MessageResult; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; @@ -794,7 +794,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode } else if (reminderActionId == R.id.reminder_action_fix_username_link) { startActivity(AppSettingsActivity.usernameLinkSettings(requireContext())); } else if (reminderActionId == R.id.reminder_action_re_register) { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())); + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/DeviceTransferFragment.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/DeviceTransferFragment.java index ee9e22e84e..e165e36fb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/DeviceTransferFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/DeviceTransferFragment.java @@ -42,7 +42,7 @@ public abstract class DeviceTransferFragment extends LoggingFragment { protected boolean transferFinished; public DeviceTransferFragment() { - super(R.layout.device_transfer_fragment); + super(R.layout.fragment_device_transfer); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceTransferFragment.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceTransferFragment.java index 5da93e0c4e..045d44b6dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceTransferFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/NewDeviceTransferFragment.java @@ -37,18 +37,18 @@ public final class NewDeviceTransferFragment extends DeviceTransferFragment { @Override protected void navigateToRestartTransfer() { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_newDeviceTransfer_to_newDeviceTransferInstructions); + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferInstructions()); } @Override protected void navigateAwayFromTransfer() { EventBus.getDefault().unregister(serverTaskListener); - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_restart_to_welcomeFragment); + requireActivity().finish(); } @Override protected void navigateToTransferComplete() { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_newDeviceTransfer_to_newDeviceTransferComplete); + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferComplete()); } private class ServerTaskListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java index a25886296d..686cb9cc8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java @@ -63,7 +63,7 @@ public final class TransferOrRestoreFragment extends LoggingFragment { private void launchSelection(BackupRestorationType restorationType) { switch (restorationType) { case DEVICE_TRANSFER -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_new_device_transfer_instructions); - case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_choose_backup); + case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transfer_or_restore_to_local_restore); case REMOTE_BACKUP -> {} default -> throw new IllegalArgumentException(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt index a4c759bb90..2254c4b7d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt @@ -14,7 +14,7 @@ import org.signal.libsignal.protocol.state.SignalProtocolStore import org.signal.libsignal.protocol.state.SignedPreKeyRecord import org.signal.libsignal.protocol.util.KeyHelper import org.signal.libsignal.protocol.util.Medium -import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberRepository +import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel import org.thoughtcrime.securesms.crypto.PreKeyUtil import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job @@ -87,7 +87,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base return } - ChangeNumberRepository.CHANGE_NUMBER_LOCK.lock() + ChangeNumberViewModel.CHANGE_NUMBER_LOCK.lock() try { if (SignalStore.misc.hasPniInitializedDevices) { Log.w(TAG, "We found out that things have been initialized after we got the lock! No need to do anything else.") @@ -112,7 +112,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base SignalStore.misc.hasPniInitializedDevices = true } finally { - ChangeNumberRepository.CHANGE_NUMBER_LOCK.unlock() + ChangeNumberViewModel.CHANGE_NUMBER_LOCK.unlock() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index 1a6b964bca..532dd5eac0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.payments.backup.RecoveryPhraseStates; import org.thoughtcrime.securesms.payments.backup.confirm.PaymentsRecoveryPhraseConfirmFragment; import org.thoughtcrime.securesms.payments.preferences.model.InfoCard; import org.thoughtcrime.securesms.payments.preferences.model.PaymentItem; -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.SpanUtil; @@ -262,7 +262,7 @@ public class PaymentsHomeFragment extends LoggingFragment { if (actionId == R.id.reminder_action_update_now) { PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); } else if (actionId == R.id.reminder_action_re_register) { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())); + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())); } }); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt index bcf5c39cdf..9beee642da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileFragment.kt @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.profiles.manage.EditProfileViewModel.AvatarState import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameDeleteResult import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity import org.thoughtcrime.securesms.util.NameUtil.getAbbreviation import org.thoughtcrime.securesms.util.PlayStoreUtil import org.thoughtcrime.securesms.util.livedata.LiveDataUtil @@ -389,7 +389,7 @@ class EditProfileFragment : LoggingFragment() { .setMessage(R.string.EditProfileFragment_unregistered_dialog_body) .setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() } .setPositiveButton(R.string.EditProfileFragment_unregistered_dialog_reregister_button) { d, _ -> - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())) + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())) d.dismiss() } .show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java deleted file mode 100644 index abce53ae7f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.thoughtcrime.securesms.registration; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; -import org.thoughtcrime.securesms.util.CommunicationActions; -import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; -import org.thoughtcrime.securesms.util.DynamicTheme; - - -public final class RegistrationNavigationActivity extends AppCompatActivity { - - private static final String TAG = Log.tag(RegistrationNavigationActivity.class); - - public static final String RE_REGISTRATION_EXTRA = "re_registration"; - - private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); - - private SmsRetrieverReceiver smsRetrieverReceiver; - private RegistrationViewModel viewModel; - - public static Intent newIntentForNewRegistration(@NonNull Context context, @Nullable Intent originalIntent) { - Intent intent = new Intent(context, RegistrationNavigationActivity.class); - intent.putExtra(RE_REGISTRATION_EXTRA, false); - - if (originalIntent != null) { - intent.setData(originalIntent.getData()); - } - - return intent; - } - - public static Intent newIntentForReRegistration(@NonNull Context context) { - Intent intent = new Intent(context, RegistrationNavigationActivity.class); - intent.putExtra(RE_REGISTRATION_EXTRA, true); - return intent; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - dynamicTheme.onCreate(this); - - super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class); - - setContentView(R.layout.activity_registration_navigation); - initializeChallengeListener(); - - if (getIntent() != null && getIntent().getData() != null) { - CommunicationActions.handlePotentialProxyLinkUrl(this, getIntent().getDataString()); - } - } - - @Override - protected void onResume() { - super.onResume(); - dynamicTheme.onResume(this); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - if (intent.getData() != null) { - CommunicationActions.handlePotentialProxyLinkUrl(this, intent.getDataString()); - } - - viewModel.setIsReregister(isReregister(intent)); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - shutdownChallengeListener(); - } - - private boolean isReregister(@NonNull Intent intent) { - return intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false); - } - - private void initializeChallengeListener() { - smsRetrieverReceiver = new SmsRetrieverReceiver(getApplication()); - smsRetrieverReceiver.registerReceiver(); - } - - private void shutdownChallengeListener() { - if (smsRetrieverReceiver != null) { - smsRetrieverReceiver.unregisterReceiver(); - smsRetrieverReceiver = null; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt index 7403649ecc..6074c25d00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data +package org.thoughtcrime.securesms.registration.data import android.app.backup.BackupManager import android.content.Context @@ -45,12 +45,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.registration.PushChallengeRequest import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.VerifyAccountRepository -import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult +import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.service.DirectoryRefreshListener import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/BackupAuthCheckResult.kt similarity index 97% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/BackupAuthCheckResult.kt index 2a8b6c1873..9df74e2b8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/BackupAuthCheckResult.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.svr.Svr3Credentials diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/Challenge.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/Challenge.kt similarity index 91% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/Challenge.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/Challenge.kt index 832229f09a..dc364b83a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/Challenge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/Challenge.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network import org.signal.core.util.logging.Log diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegisterAccountResult.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegisterAccountResult.kt index 466eeef6bd..858be7c09d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegisterAccountResult.kt @@ -3,10 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network import org.thoughtcrime.securesms.pin.SvrWrongPinException -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationResult.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationResult.kt index b4b38baffa..6b2d715fce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationResult.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network /** * This is a merging of the NetworkResult pattern and the Processor pattern of registration v1. diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt index 1d2d8199b9..f262cb2bd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network import org.signal.core.util.logging.Log import org.whispersystems.signalservice.api.NetworkResult diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/SubmitCaptchaResult.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/SubmitCaptchaResult.kt index f9e17bfd51..5102e83302 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/SubmitCaptchaResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/SubmitCaptchaResult.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network import org.signal.core.util.logging.Log import org.whispersystems.signalservice.api.NetworkResult diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt index fafc415a4d..915147dc8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt @@ -3,11 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.data.network +package org.thoughtcrime.securesms.registration.data.network import okio.IOException import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java deleted file mode 100644 index d9637424f7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import androidx.lifecycle.ViewModelProvider; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; - -public class AccountLockedFragment extends BaseAccountLockedFragment { - - public AccountLockedFragment() { - super(R.layout.account_locked_fragment); - } - - @Override - protected BaseRegistrationViewModel getViewModel() { - return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); - } - - @Override - protected void onNext() { - requireActivity().finish(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseAccountLockedFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseAccountLockedFragment.java deleted file mode 100644 index 7b67f5ed22..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseAccountLockedFragment.java +++ /dev/null @@ -1,73 +0,0 @@ -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; - -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; - -/** - * 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); - - setDebugLogSubmitMultiTapView(view.findViewById(R.id.account_locked_title)); - - 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(); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseEnterSmsCodeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseEnterSmsCodeFragment.java deleted file mode 100644 index 1df9351a2e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseEnterSmsCodeFragment.java +++ /dev/null @@ -1,442 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.os.Bundle; -import android.view.View; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.activity.OnBackPressedCallback; -import androidx.annotation.CallSuper; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.navigation.Navigation; - -import com.google.android.material.button.MaterialButton; -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.ActionCountDownButton; -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.VerifyResponseProcessor; -import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel; -import org.signal.core.util.concurrent.LifecycleDisposable; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; -import org.thoughtcrime.securesms.util.dualsim.MccMncProducer; -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; - -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated; - -/** - * Base fragment used by registration and change number flow to input an SMS verification code or request a - * phone code after requesting SMS. - * - * @param - The concrete view model used by the subclasses, for ease of access in said subclass - */ -public abstract class BaseEnterSmsCodeFragment extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback { - - private static final String TAG = Log.tag(BaseEnterSmsCodeFragment.class); - - private ScrollView scrollView; - private TextView subheader; - private VerificationCodeView verificationCodeView; - private VerificationPinKeyboard keyboard; - private ActionCountDownButton callMeCountDown; - private ActionCountDownButton resendSmsCountDown; - private MaterialButton wrongNumber; - private MaterialButton bottomSheetButton; - private boolean autoCompleting; - - private ViewModel viewModel; - - protected final LifecycleDisposable disposables = new LifecycleDisposable(); - - public BaseEnterSmsCodeFragment(@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); - subheader = view.findViewById(R.id.verification_subheader); - verificationCodeView = view.findViewById(R.id.code); - keyboard = view.findViewById(R.id.keyboard); - callMeCountDown = view.findViewById(R.id.call_me_count_down); - resendSmsCountDown = view.findViewById(R.id.resend_sms_count_down); - wrongNumber = view.findViewById(R.id.wrong_number); - bottomSheetButton = view.findViewById(R.id.having_trouble_button); - - new SignalStrengthPhoneStateListener(this, this); - - connectKeyboard(verificationCodeView, keyboard); - ViewUtil.hideKeyboard(requireContext(), view); - - setOnCodeFullyEnteredListener(verificationCodeView); - - wrongNumber.setOnClickListener(v -> returnToPhoneEntryScreen()); - bottomSheetButton.setOnClickListener( v -> showBottomSheet()); - - callMeCountDown.setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in); - resendSmsCountDown.setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in); - - callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest()); - resendSmsCountDown.setOnClickListener(v -> handleSmsRequest()); - - callMeCountDown.setListener((v, remaining) -> { - if (remaining <= 30) { - scrollView.smoothScrollTo(0, v.getBottom()); - callMeCountDown.setListener(null); - } - }); - - resendSmsCountDown.setListener((v, remaining) -> { - if (remaining <= 30) { - scrollView.smoothScrollTo(0, v.getBottom()); - resendSmsCountDown.setListener(null); - } - }); - - - disposables.bindTo(getViewLifecycleOwner().getLifecycle()); - viewModel = getViewModel(); - viewModel.getIncorrectCodeAttempts().observe(getViewLifecycleOwner(), (attempts) -> { - if (attempts >= 3) { - bottomSheetButton.setVisibility(View.VISIBLE); - } - }); - - requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - viewModel.resetSession(); - this.remove(); - requireActivity().getOnBackPressedDispatcher().onBackPressed(); - } - }); - } - - protected abstract ViewModel getViewModel(); - - protected abstract void handleSuccessfulVerify(); - - protected abstract void navigateToCaptcha(); - - protected abstract void navigateToRegistrationLock(long timeRemaining); - - protected abstract void navigateToKbsAccountLocked(); - - private void returnToPhoneEntryScreen() { - viewModel.resetSession(); - Navigation.findNavController(requireView()).navigateUp(); - } - - private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) { - verificationCodeView.setOnCompleteListener(code -> { - - callMeCountDown.setVisibility(View.INVISIBLE); - resendSmsCountDown.setVisibility(View.INVISIBLE); - wrongNumber.setVisibility(View.INVISIBLE); - keyboard.displayProgress(); - - Disposable verify = viewModel.verifyCodeWithoutRegistrationLock(code) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((VerifyResponseProcessor 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.isRegistrationLockPresentAndSvrExhausted()) { - LockedException lockedException = processor.getLockedException(); - handleRegistrationLock(lockedException.getTimeRemaining()); - } 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() { - @Override - public void onSuccess(Boolean result) { - runAfterAnimation.run(); - } - }); - } - - protected void handleRateLimited() { - keyboard.displayFailure().addListener(new AssertedSuccessListener() { - @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); - resendSmsCountDown.setVisibility(View.VISIBLE); - wrongNumber.setVisibility(View.VISIBLE); - verificationCodeView.clear(); - keyboard.displayKeyboard(); - }) - .show(); - } - }); - } - - protected void handleRegistrationLock(long timeRemaining) { - keyboard.displayLocked().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean r) { - navigateToRegistrationLock(timeRemaining); - } - }); - } - - protected void handleSvrAccountLocked() { - navigateToKbsAccountLocked(); - } - - protected void handleIncorrectCodeError() { - viewModel.incrementIncorrectCodeAttempts(); - - Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show(); - keyboard.displayFailure().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean result) { - callMeCountDown.setVisibility(View.VISIBLE); - resendSmsCountDown.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() { - @Override - public void onSuccess(Boolean result) { - callMeCountDown.setVisibility(View.VISIBLE); - resendSmsCountDown.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 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 convertVerificationCodeToDigits(@Nullable String code) { - if (code == null || code.length() != 6) { - return Collections.emptyList(); - } - - List 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_phone_number_verification_dialog_title, - R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number, - viewModel.getNumber().getE164Number(), - () -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.PHONE_CALL), - this::returnToPhoneEntryScreen); - } - - private void handleSmsRequest() { - showConfirmNumberDialogIfTranslated(requireContext(), - R.string.RegistrationActivity_phone_number_verification_dialog_title, - R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number, - viewModel.getNumber().getE164Number(), - () -> handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode.SMS_WITH_LISTENER), - this::returnToPhoneEntryScreen); - } - - private void handleCodeCallRequestAfterConfirm(VerifyAccountRepository.Mode mode) { - MccMncProducer mccMncProducer = new MccMncProducer(requireContext()); - Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(processor -> { - if (processor.hasResult()) { - Toast.makeText(requireContext(), getCodeRequestedToastText(mode), Toast.LENGTH_LONG).show(); - } else if (processor.captchaRequired(viewModel.getExcludedChallenges())) { - navigateToCaptcha(); - } else if (processor.rateLimit()) { - handleRateLimited(); - } 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); - } - - @StringRes - private int getCodeRequestedToastText(VerifyAccountRepository.Mode mode) { - switch (mode) { - case PHONE_CALL: - return R.string.RegistrationActivity_call_requested; - case SMS_WITH_LISTENER: - case SMS_WITHOUT_LISTENER: - return R.string.RegistrationActivity_sms_requested; - default: - return R.string.RegistrationActivity_code_requested; - } - } - - 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(); - String sessionE164 = viewModel.getSessionE164(); - if (sessionE164 == null) { - returnToPhoneEntryScreen(); - return; - } - - subheader.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber())); - - Disposable request = viewModel.validateSession(sessionE164) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(processor -> { - if (!processor.hasResult()) { - Log.d(TAG, "Network error."); - returnToPhoneEntryScreen(); - } else if (processor.isInvalidSession()) { - Log.d(TAG, "Registration session is invalid."); - returnToPhoneEntryScreen(); - } else if (processor.cannotSubmitVerificationAttempt()) { - Log.d(TAG, "Cannot submit any more verification attempts."); - returnToPhoneEntryScreen(); - } else if (processor.mustWaitToSubmitProof()) { - Log.d(TAG, "Blocked from submitting proof at this time."); - handleRateLimited(); - } - // else session state is valid and server is ready to accept code - }); - - disposables.add(request); - - viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> { - if (callAtTime > 0) { - callMeCountDown.setVisibility(View.VISIBLE); - callMeCountDown.startCountDownTo(callAtTime); - } else { - callMeCountDown.setVisibility(View.INVISIBLE); - } - }); - viewModel.getCanSmsAtTime().observe(getViewLifecycleOwner(), smsAtTime -> { - if (smsAtTime > 0) { - resendSmsCountDown.setVisibility(View.VISIBLE); - resendSmsCountDown.startCountDownTo(smsAtTime); - } else { - resendSmsCountDown.setVisibility(View.INVISIBLE); - } - }); - } - - - private void showBottomSheet() { - ContactSupportBottomSheetFragment bottomSheet = new ContactSupportBottomSheetFragment(); - bottomSheet.show(getChildFragmentManager(), "support_bottom_sheet"); - } - - @Override - public void onNoCellSignalPresent() { - // TODO animate in bottom sheet - } - - @Override - public void onCellSignalPresent() { - // TODO animate away bottom sheet - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java deleted file mode 100644 index 027a4d3420..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java +++ /dev/null @@ -1,283 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -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 com.google.android.material.button.MaterialButton; -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.registration.viewmodel.BaseRegistrationViewModel; -import org.signal.core.util.concurrent.LifecycleDisposable; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton; - -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.disposables.Disposable; - -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; - -/** - * 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. - */ - public static final int MINIMUM_PIN_LENGTH = 4; - - private EditText pinEntry; - private View forgotPin; - protected CircularProgressMaterialButton pinButton; - private TextView errorLabel; - private MaterialButton 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.setIconResource(keyboardType.getIconResource()); - }); - - PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther(); - keyboardToggle.setIconResource(keyboardType.getIconResource()); - - disposables.bindTo(getViewLifecycleOwner().getLifecycle()); - viewModel = getViewModel(); - - viewModel.getLockedTimeRemaining() - .observe(getViewLifecycleOwner(), t -> timeRemaining = t); - - Integer triesRemaining = viewModel.getSvrTriesRemaining(); - - if (triesRemaining != null) { - 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; - } - - pinButton.setSpinning(); - - Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(processor -> { - if (processor.hasResult()) { - handleSuccessfulPinEntry(pin); - } else if (processor.wrongPin()) { - onIncorrectKbsRegistrationLockPin(Objects.requireNonNull(processor.getSvrTriesRemaining())); - } else if (processor.isRegistrationLockPresentAndSvrExhausted() || processor.registrationLock()) { - 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(int svrTriesRemaining) { - pinButton.cancelSpinning(); - pinEntry.getText().clear(); - enableAndFocusPinEntry(); - - viewModel.setSvrTriesRemaining(svrTriesRemaining); - - if (svrTriesRemaining == 0) { - Log.w(TAG, "Account locked. User out of attempts on KBS."); - onAccountLocked(); - return; - } - - if (svrTriesRemaining == 3) { - int daysRemaining = getLockoutDays(timeRemaining); - - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.RegistrationLockFragment__incorrect_pin) - .setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining)) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - - if (svrTriesRemaining > 5) { - errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again); - } else { - errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining)); - forgotPin.setVisibility(View.VISIBLE); - } - } - - public void onRateLimited() { - pinButton.cancelSpinning(); - 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() { - pinButton.cancelSpinning(); - 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 void enableAndFocusPinEntry() { - pinEntry.setEnabled(true); - pinEntry.setFocusable(true); - ViewUtil.focusAndShowKeyboard(pinEntry); - } - - protected abstract void handleSuccessfulPinEntry(@NonNull String pin); - - protected abstract void sendEmailToSupport(); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java deleted file mode 100644 index 0b158b1e87..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.annotation.SuppressLint; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.fragment.NavHostFragment; - -import org.thoughtcrime.securesms.BuildConfig; -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 { - - public static final String EXTRA_VIEW_MODEL_PROVIDER = "view_model_provider"; - - private BaseRegistrationViewModel viewModel; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_registration_captcha, container, false); - } - - @Override - @SuppressLint("SetJavaScriptEnabled") - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - WebView webView = view.findViewById(R.id.registration_captcha_web_view); - - webView.getSettings().setJavaScriptEnabled(true); - webView.clearCache(true); - - webView.setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url != null && url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) { - handleToken(url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length())); - return true; - } - return false; - } - }); - - webView.loadUrl(BuildConfig.SIGNAL_CAPTCHA_URL); - - CaptchaViewModelProvider provider = null; - if (getArguments() != null) { - provider = (CaptchaViewModelProvider) requireArguments().getSerializable(EXTRA_VIEW_MODEL_PROVIDER); - } - - if (provider == null) { - viewModel = new ViewModelProvider( - 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); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java deleted file mode 100644 index d5591e7593..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.provider.DocumentsContract; -import android.text.method.LinkMovementMethod; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.text.HtmlCompat; -import androidx.navigation.Navigation; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.LoggingFragment; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.util.navigation.SafeNavigation; - -public class ChooseBackupFragment extends LoggingFragment { - - private static final String TAG = Log.tag(ChooseBackupFragment.class); - - private static final short OPEN_FILE_REQUEST_CODE = 3862; - - @Override - public @Nullable View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) - { - return inflater.inflate(R.layout.fragment_registration_choose_backup, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - View chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button); - chooseBackupButton.setOnClickListener(this::onChooseBackupSelected); - - TextView learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more); - learnMore.setText(HtmlCompat.fromHtml(String.format("%s", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0)); - learnMore.setMovementMethod(LinkMovementMethod.getInstance()); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode == OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { - ChooseBackupFragmentDirections.ActionRestore restore = ChooseBackupFragmentDirections.actionRestore(); - - restore.setUri(data.getData()); - - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), restore); - } - } - - private void onChooseBackupSelected(@NonNull View view) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - - intent.setType("application/octet-stream"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); - - if (Build.VERSION.SDK_INT >= 26) { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory()); - } - - try { - startActivityForResult(intent, OPEN_FILE_REQUEST_CODE); - } catch (ActivityNotFoundException e) { - Toast.makeText(requireContext(), R.string.ChooseBackupFragment__no_file_browser_available, Toast.LENGTH_LONG).show(); - Log.w(TAG, "No matching activity!", e); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java index 0db1656f96..adc00c7ac1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java @@ -1,3 +1,8 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.registration.fragments; import android.os.Bundle; @@ -20,8 +25,8 @@ import androidx.loader.content.Loader; import androidx.navigation.fragment.NavHostFragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel; import org.thoughtcrime.securesms.database.loaders.CountryListLoader; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import java.util.ArrayList; import java.util.Map; @@ -32,7 +37,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM public static final String KEY_COUNTRY_CODE = "country_code"; private EditText countryFilter; - private RegistrationViewModel model; + private ChangeNumberViewModel model; private String resultKey; @Override @@ -50,7 +55,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM } if (resultKey == null) { - model = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); + model = new ViewModelProvider(requireActivity()).get(ChangeNumberViewModel.class); } countryFilter = view.findViewById(R.id.country_search); @@ -67,7 +72,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM String countryName = item.get("country_name"); if (resultKey == null) { - model.onCountrySelected(countryName, countryCode); + model.setNewCountry(countryCode, countryName); } else { Bundle result = new Bundle(); result.putString(KEY_COUNTRY, countryName); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java deleted file mode 100644 index aa5c9a0864..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java +++ /dev/null @@ -1,494 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ScrollView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavController; -import androidx.navigation.Navigation; -import androidx.navigation.fragment.NavHostFragment; - -import com.google.android.gms.auth.api.phone.SmsRetriever; -import com.google.android.gms.auth.api.phone.SmsRetrieverClient; -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.android.material.textfield.TextInputLayout; -import com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.Phonenumber; - -import org.signal.core.util.ThreadUtil; -import org.signal.core.util.concurrent.LifecycleDisposable; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.LoggingFragment; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor; -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.CommunicationActions; -import org.thoughtcrime.securesms.util.Debouncer; -import org.thoughtcrime.securesms.util.Dialogs; -import org.thoughtcrime.securesms.util.PlayServicesUtil; -import org.thoughtcrime.securesms.util.SupportEmailUtil; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.thoughtcrime.securesms.util.dualsim.MccMncProducer; -import org.thoughtcrime.securesms.util.navigation.SafeNavigation; -import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton; - -import java.util.Locale; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.disposables.Disposable; - -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated; - -public final class EnterPhoneNumberFragment extends LoggingFragment implements RegistrationNumberInputController.Callbacks { - - private static final String TAG = Log.tag(EnterPhoneNumberFragment.class); - - private TextInputLayout countryCode; - private TextInputLayout number; - private CircularProgressMaterialButton register; - private View cancel; - private ScrollView scrollView; - private RegistrationViewModel viewModel; - - private final LifecycleDisposable disposables = new LifecycleDisposable(); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header)); - - countryCode = view.findViewById(R.id.country_code); - number = view.findViewById(R.id.number); - cancel = view.findViewById(R.id.cancel_button); - scrollView = view.findViewById(R.id.scroll_view); - register = view.findViewById(R.id.registerButton); - - RegistrationNumberInputController controller = new RegistrationNumberInputController(requireContext(), - this, - Objects.requireNonNull(number.getEditText()), - countryCode); - register.setOnClickListener(v -> handleRegister(requireContext())); - - disposables.bindTo(getViewLifecycleOwner().getLifecycle()); - viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); - - if (viewModel.isReregister()) { - cancel.setVisibility(View.VISIBLE); - cancel.setOnClickListener(v -> requireActivity().finish()); - } else { - cancel.setVisibility(View.GONE); - } - - viewModel.getLiveNumber().observe(getViewLifecycleOwner(), controller::updateNumberFormatter); - - if (viewModel.hasCaptchaToken()) { - ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250); - } - - Toolbar toolbar = view.findViewById(R.id.toolbar); - ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); - final ActionBar supportActionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setTitle(null); - } - - final NumberViewState viewModelNumber = viewModel.getNumber(); - if (viewModelNumber.getCountryCode() == 0) { - controller.prepopulateCountryCode(); - } - controller.setNumberAndCountryCode(viewModelNumber); - - ViewUtil.focusAndShowKeyboard(number.getEditText()); - - if (viewModel.hasUserSkippedReRegisterFlow() && viewModel.shouldAutoShowSmsConfirmDialog()) { - viewModel.setAutoShowSmsConfirmDialog(false); - ThreadUtil.runOnMainDelayed(() -> handleRegister(requireContext()), 250); - } - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - inflater.inflate(R.menu.enter_phone_number, menu); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == R.id.phone_menu_use_proxy) { - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), EnterPhoneNumberFragmentDirections.actionEditProxy()); - return true; - } else { - return false; - } - } - - private void handleRegister(@NonNull Context context) { - if (viewModel.getNumber().getCountryCode() == 0) { - showErrorDialog(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code)); - return; - } - - if (TextUtils.isEmpty(viewModel.getNumber().getNationalNumber())) { - showErrorDialog(context, getString(R.string.RegistrationActivity_please_enter_a_valid_phone_number_to_register)); - return; - } - - final NumberViewState number = viewModel.getNumber(); - final String e164number = number.getE164Number(); - - if (!number.isValid()) { - Dialogs.showAlertDialog(context, - getString(R.string.RegistrationActivity_invalid_number), - String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number)); - return; - } - - PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context); - - if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) { - confirmNumberPrompt(context, e164number, () -> onE164EnteredSuccessfully(context, true)); - } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) { - confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context)); - } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) { - GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show(); - } else { - Dialogs.showAlertDialog(context, - getString(R.string.RegistrationActivity_play_services_error), - getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)); - } - } - - private void onE164EnteredSuccessfully(@NonNull Context context, boolean fcmSupported) { - enterInProgressUiState(); - Log.d(TAG, "E164 entered successfully."); - Disposable disposable = viewModel.canEnterSkipSmsFlow() - .observeOn(AndroidSchedulers.mainThread()) - .onErrorReturnItem(false) - .subscribe(canEnter -> { - if (canEnter) { - Log.i(TAG, "Entering skip flow."); - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment()); - } else { - Log.i(TAG, "Unable to collect necessary data to enter skip flow, returning to normal"); - handleRequestVerification(context, fcmSupported); - } - }); - disposables.add(disposable); - } - - private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) { - if (fcmSupported) { - SmsRetrieverClient client = SmsRetriever.getClient(context); - Task task = client.startSmsRetriever(); - AtomicBoolean handled = new AtomicBoolean(false); - - Debouncer debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(5)); - debouncer.publish(() -> { - if (!handled.getAndSet(true)) { - Log.w(TAG, "Timed out waiting for SMS listener!"); - requestVerificationCode(Mode.SMS_WITHOUT_LISTENER); - } - }); - - task.addOnSuccessListener(none -> { - if (!handled.getAndSet(true)) { - Log.i(TAG, "Successfully registered SMS listener."); - requestVerificationCode(Mode.SMS_WITH_LISTENER); - } else { - Log.w(TAG, "Successfully registered listener after timeout."); - } - debouncer.clear(); - }); - - task.addOnFailureListener(e -> { - if (!handled.getAndSet(true)) { - Log.w(TAG, "Failed to register SMS listener.", e); - requestVerificationCode(Mode.SMS_WITHOUT_LISTENER); - } else { - Log.w(TAG, "Failed to register listener after timeout."); - } - debouncer.clear(); - }); - - task.addOnCanceledListener(() -> { - if (!handled.getAndSet(true)) { - Log.w(TAG, "SMS listener registration canceled."); - requestVerificationCode(Mode.SMS_WITHOUT_LISTENER); - } else { - Log.w(TAG, "SMS listener registration canceled after timeout."); - } - debouncer.clear(); - }); - - } else { - Log.i(TAG, "FCM is not supported, using no SMS listener"); - requestVerificationCode(Mode.SMS_WITHOUT_LISTENER); - } - } - - private void enterInProgressUiState() { - register.setSpinning(); - countryCode.setEnabled(false); - number.setEnabled(false); - cancel.setVisibility(View.GONE); - } - - private void exitInProgressUiState() { - register.cancelSpinning(); - countryCode.setEnabled(true); - number.setEnabled(true); - if (viewModel.isReregister()) { - cancel.setVisibility(View.VISIBLE); - } - } - - private void requestVerificationCode(@NonNull Mode mode) { - NavController navController = NavHostFragment.findNavController(this); - MccMncProducer mccMncProducer = new MccMncProducer(requireContext()); - final DialogInterface.OnClickListener proceedToNextScreen = (dialog, which) -> SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); - Disposable request = viewModel.requestVerificationCode(mode, mccMncProducer.getMcc(), mccMncProducer.getMnc()) - .doOnSubscribe(unused -> SignalStore.account().setRegistered(false)) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((RegistrationSessionProcessor processor) -> { - Context context = getContext(); - if (context == null) { - Log.w(TAG, "[requestVerificationCode] Invalid context! Skipping."); - return; - } - - if (processor.verificationCodeRequestSuccess()) { - disposables.add(updateFcmTokenValue()); - SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); - } else if (processor.captchaRequired(viewModel.getExcludedChallenges())) { - Log.i(TAG, "Unable to request sms code due to captcha required"); - SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionRequestCaptcha()); - } else if (processor.exhaustedVerificationCodeAttempts()) { - Log.i(TAG, "Unable to request sms code due to exhausting attempts"); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_rate_limited_to_service)); - } else if (processor.rateLimit()) { - Log.i(TAG, "Unable to request sms code due to rate limit"); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_rate_limited_to_try_again, formatMillisecondsToString(processor.getRateLimit()))); - } else if (processor.isImpossibleNumber()) { - Log.w(TAG, "Impossible number", processor.getError()); - Dialogs.showAlertDialog(requireContext(), - context.getString(R.string.RegistrationActivity_invalid_number), - String.format(context.getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber())); - } else if (processor.isNonNormalizedNumber()) { - handleNonNormalizedNumberError(processor.getOriginalNumber(), processor.getNormalizedNumber(), mode); - } else if (processor.isTokenRejected()) { - Log.i(TAG, "The server did not accept the information.", processor.getError()); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)); - } else if (processor.externalServiceFailure()) { - Log.w(TAG, "The server reported a failure with an external service.", processor.getError()); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen); - } else if (processor.invalidTransportModeFailure()) { - Log.w(TAG, "The server reported an invalid transport mode failure.", processor.getError()); - new MaterialAlertDialogBuilder(context) - .setMessage(R.string.RegistrationActivity_we_couldnt_send_you_a_verification_code) - .setPositiveButton(R.string.RegistrationActivity_voice_call, (dialog, which) -> requestVerificationCode(Mode.PHONE_CALL)) - .setNegativeButton(R.string.RegistrationActivity_cancel, null) - .show(); - } else if ( processor.isMalformedRequest()){ - Log.w(TAG, "The server reported a malformed request.", processor.getError()); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen); - - } else if (processor.isRetryException()) { - Log.w(TAG, "The server reported a failure that is retryable.", processor.getError()); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service), proceedToNextScreen); - } else { - Log.i(TAG, "Unknown error during verification code request", processor.getError()); - showErrorDialog(context, context.getString(R.string.RegistrationActivity_unable_to_connect_to_service)); - } - - exitInProgressUiState(); - }); - - disposables.add(request); - } - - private Disposable updateFcmTokenValue() { - return viewModel.updateFcmTokenValue().subscribe(); - } - - private String formatMillisecondsToString(long milliseconds) { - long totalSeconds = milliseconds / 1000; - long HH = totalSeconds / 3600; - long MM = (totalSeconds % 3600) / 60; - long SS = totalSeconds % 60; - return String.format(Locale.getDefault(), "%02d:%02d:%02d", HH, MM, SS); - } - - public void showErrorDialog(Context context, String msg) { - showErrorDialog(context, msg, null); - } - - public void showErrorDialog(Context context, String msg, DialogInterface.OnClickListener positiveButtonListener) { - new MaterialAlertDialogBuilder(context).setMessage(msg).setPositiveButton(android.R.string.ok, positiveButtonListener).show(); - } - - @Override - public void onNumberFocused() { - scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250); - } - - @Override - public void onNumberInputDone(@NonNull View view) { - ViewUtil.hideKeyboard(requireContext(), view); - handleRegister(requireContext()); - } - - @Override - public void setNationalNumber(@NonNull String number) { - viewModel.setNationalNumber(number); - } - - @Override - public void setCountry(int countryCode) { - viewModel.onCountrySelected(null, countryCode); - } - - @Override - public void onStart() { - super.onStart(); - String sessionE164 = viewModel.getSessionE164(); - if (sessionE164 != null && viewModel.getSessionId() != null && viewModel.getCaptchaToken() == null) { - checkIfSessionIsInProgressAndAdvance(sessionE164); - } - } - - private void checkIfSessionIsInProgressAndAdvance(@NonNull String sessionE164) { - NavController navController = NavHostFragment.findNavController(this); - Disposable request = viewModel.validateSession(sessionE164) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(processor -> { - if (processor.hasResult() && processor.canSubmitProofImmediately()) { - try { - viewModel.restorePhoneNumberStateFromE164(sessionE164); - SafeNavigation.safeNavigate(navController, EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); - } catch (NumberParseException numberParseException) { - viewModel.resetSession(); - } - } else { - viewModel.resetSession(); - } - }); - - disposables.add(request); - } - - private void handleNonNormalizedNumberError(@NonNull String originalNumber, @NonNull String normalizedNumber, @NonNull Mode mode) { - try { - Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null); - - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.RegistrationActivity_non_standard_number_format) - .setMessage(getString(R.string.RegistrationActivity_the_number_you_entered_appears_to_be_a_non_standard, originalNumber, normalizedNumber)) - .setNegativeButton(android.R.string.no, (d, i) -> d.dismiss()) - .setNeutralButton(R.string.RegistrationActivity_contact_signal_support, (d, i) -> { - String subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format); - String body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null); - - CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body); - d.dismiss(); - }) - .setPositiveButton(R.string.yes, (d, i) -> { - countryCode.getEditText().setText(String.valueOf(phoneNumber.getCountryCode())); - number.getEditText().setText(String.valueOf(phoneNumber.getNationalNumber())); - requestVerificationCode(mode); - d.dismiss(); - }) - .show(); - } catch (NumberParseException e) { - Log.w(TAG, "Failed to parse number!", e); - - Dialogs.showAlertDialog(requireContext(), - getString(R.string.RegistrationActivity_invalid_number), - String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber())); - } - } - - private void handlePromptForNoPlayServices(@NonNull Context context) { - Log.d(TAG, "Device does not have Play Services, showing consent dialog."); - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.RegistrationActivity_missing_google_play_services) - .setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services) - .setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> onE164EnteredSuccessfully(context, false)) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - private void confirmNumberPrompt(@NonNull Context context, - @NonNull String e164number, - @NonNull Runnable onConfirmed) - { - enterInProgressUiState(); - - disposables.add( - viewModel.canEnterSkipSmsFlow() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(canSkipSms -> { - Log.d(TAG, "Showing confirm number dialog. canSkipSms = " + canSkipSms + " hasUserSkipped = " + viewModel.hasUserSkippedReRegisterFlow()); - final EditText editText = this.number.getEditText(); - showConfirmNumberDialogIfTranslated(context, - viewModel.hasUserSkippedReRegisterFlow() ? R.string.RegistrationActivity_additional_verification_required - : R.string.RegistrationActivity_phone_number_verification_dialog_title, - canSkipSms ? null - : R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number, - e164number, - () -> { - Log.d(TAG, "User confirmed number."); - if (editText != null) { - ViewUtil.hideKeyboard(context, editText); - } - onConfirmed.run(); - }, - () -> { - Log.d(TAG, "User canceled confirm number, returning to edit number."); - exitInProgressUiState(); - if (editText != null) { - ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(editText); - } - }); - } - ) - ); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterSmsCodeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterSmsCodeFragment.java deleted file mode 100644 index 64a9318ccb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterSmsCodeFragment.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.Navigation; -import androidx.navigation.fragment.NavHostFragment; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; -import org.thoughtcrime.securesms.util.RemoteConfig; -import org.signal.core.util.concurrent.SimpleTask; -import org.thoughtcrime.securesms.util.navigation.SafeNavigation; - -import java.io.IOException; - -public final class EnterSmsCodeFragment extends BaseEnterSmsCodeFragment implements SignalStrengthPhoneStateListener.Callback { - - private static final String TAG = Log.tag(EnterSmsCodeFragment.class); - - public EnterSmsCodeFragment() { - super(R.layout.fragment_registration_enter_code); - } - - @Override - protected @NonNull RegistrationViewModel getViewModel() { - return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); - } - - @Override - protected void handleSuccessfulVerify() { - SimpleTask.run(() -> { - long startTime = System.currentTimeMillis(); - try { - RemoteConfig.refreshSync(); - Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags."); - } catch (IOException e) { - Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e); - } - return null; - }, none -> displaySuccess(() -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), EnterSmsCodeFragmentDirections.actionSuccessfulRegistration()))); - } - - @Override - protected void navigateToRegistrationLock(long timeRemaining) { - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), - EnterSmsCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining)); - } - - @Override - protected void navigateToCaptcha() { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterSmsCodeFragmentDirections.actionRequestCaptcha()); - } - - @Override - protected void navigateToKbsAccountLocked() { - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionAccountLocked()); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt deleted file mode 100644 index c37801b045..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.registration.fragments - -import android.os.Build -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.LocalContext -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.navArgs -import org.thoughtcrime.securesms.compose.ComposeFragment -import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel -import org.thoughtcrime.securesms.util.BackupUtil - -/** - * Fragment displayed during registration which allows a user to read through - * what permissions are granted to Signal and why, and a means to either skip - * granting those permissions or continue to grant via system dialogs. - */ -class GrantPermissionsFragment : ComposeFragment() { - - private val args by navArgs() - private val viewModel by activityViewModels() - private val isSearchingForBackup = mutableStateOf(false) - - @Composable - override fun FragmentContent() { - val isSearchingForBackup by this.isSearchingForBackup - - GrantPermissionsScreen( - deviceBuildVersion = Build.VERSION.SDK_INT, - isSearchingForBackup = isSearchingForBackup, - isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current), - onNextClicked = this::onNextClicked, - onNotNowClicked = this::onNotNowClicked - ) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) - } - - private fun onNextClicked() { - when (args.welcomeAction) { - WelcomeAction.CONTINUE -> { - WelcomeFragment.continueClicked( - this, - viewModel, - { isSearchingForBackup.value = true }, - { isSearchingForBackup.value = false }, - GrantPermissionsFragmentDirections.actionSkipRestore(), - GrantPermissionsFragmentDirections.actionRestore() - ) - } - - WelcomeAction.RESTORE_BACKUP -> { - WelcomeFragment.restoreFromBackupClicked( - this, - viewModel, - GrantPermissionsFragmentDirections.actionTransferOrRestore() - ) - } - } - } - - private fun onNotNowClicked() { - when (args.welcomeAction) { - WelcomeAction.CONTINUE -> { - WelcomeFragment.gatherInformationAndContinue( - this, - viewModel, - { isSearchingForBackup.value = true }, - { isSearchingForBackup.value = false }, - GrantPermissionsFragmentDirections.actionSkipRestore(), - GrantPermissionsFragmentDirections.actionRestore() - ) - } - - WelcomeAction.RESTORE_BACKUP -> { - WelcomeFragment.gatherInformationAndChooseBackup( - this, - viewModel, - GrantPermissionsFragmentDirections.actionTransferOrRestore() - ) - } - } - } - - /** - * Which welcome action the user selected which prompted this - * screen. - */ - enum class WelcomeAction { - CONTINUE, - RESTORE_BACKUP - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt deleted file mode 100644 index bab50b5ccb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt +++ /dev/null @@ -1,282 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments - -import android.os.Bundle -import android.text.InputType -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels -import androidx.navigation.Navigation -import androidx.navigation.fragment.findNavController -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.signal.core.util.concurrent.LifecycleDisposable -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.databinding.PinRestoreEntryFragmentBinding -import org.thoughtcrime.securesms.lock.v2.PinKeyboardType -import org.thoughtcrime.securesms.lock.v2.SvrConstants -import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor -import org.thoughtcrime.securesms.registration.viewmodel.ReRegisterWithPinViewModel -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel -import org.thoughtcrime.securesms.util.CommunicationActions -import org.thoughtcrime.securesms.util.SupportEmailUtil -import org.thoughtcrime.securesms.util.ViewUtil -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -/** - * Using a recovery password or restored KBS token attempt to register in the skip flow. - */ -class ReRegisterWithPinFragment : LoggingFragment(R.layout.pin_restore_entry_fragment) { - - companion object { - private val TAG = Log.tag(ReRegisterWithPinFragment::class.java) - } - - private var _binding: PinRestoreEntryFragmentBinding? = null - private val binding: PinRestoreEntryFragmentBinding - get() = _binding!! - - private val registrationViewModel: RegistrationViewModel by activityViewModels() - private val reRegisterViewModel: ReRegisterWithPinViewModel by viewModels() - - private val disposables = LifecycleDisposable() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - _binding = PinRestoreEntryFragmentBinding.bind(view) - - disposables.bindTo(viewLifecycleOwner.lifecycle) - - RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.pinRestorePinTitle) - - binding.pinRestorePinDescription.setText(R.string.RegistrationLockFragment__enter_the_pin_you_created_for_your_account) - - binding.pinRestoreForgotPin.visibility = View.GONE - binding.pinRestoreForgotPin.setOnClickListener { onNeedHelpClicked() } - - binding.pinRestoreSkipButton.setOnClickListener { onSkipClicked() } - - binding.pinRestorePinInput.imeOptions = EditorInfo.IME_ACTION_DONE - binding.pinRestorePinInput.setOnEditorActionListener { v, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - ViewUtil.hideKeyboard(requireContext(), v!!) - handlePinEntry() - return@setOnEditorActionListener true - } - false - } - - enableAndFocusPinEntry() - - binding.pinRestorePinConfirm.setOnClickListener { - handlePinEntry() - } - - binding.pinRestoreKeyboardToggle.setOnClickListener { - val currentKeyboardType: PinKeyboardType = getPinEntryKeyboardType() - updateKeyboard(currentKeyboardType.other) - binding.pinRestoreKeyboardToggle.setIconResource(currentKeyboardType.iconResource) - } - - binding.pinRestoreKeyboardToggle.setIconResource(getPinEntryKeyboardType().other.iconResource) - - reRegisterViewModel.updateSvrTriesRemaining(registrationViewModel.svrTriesRemaining) - - disposables += reRegisterViewModel.triesRemaining.subscribe(this::updateTriesRemaining) - } - - override fun onDestroyView() { - _binding = null - super.onDestroyView() - } - - private fun handlePinEntry() { - val pin: String? = binding.pinRestorePinInput.text?.toString() - - val trimmedLength = pin?.trim()?.length ?: 0 - if (trimmedLength == 0) { - Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show() - enableAndFocusPinEntry() - return - } - - if (trimmedLength < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) { - Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() - enableAndFocusPinEntry() - return - } - - disposables += registrationViewModel.verifyReRegisterWithPin(pin!!) - .doOnSubscribe { - ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput) - binding.pinRestorePinInput.isEnabled = false - binding.pinRestorePinConfirm.setSpinning() - } - .doAfterTerminate { - binding.pinRestorePinInput.isEnabled = true - binding.pinRestorePinConfirm.cancelSpinning() - } - .subscribe { processor -> - if (processor.hasResult()) { - Log.i(TAG, "Successfully re-registered via skip flow") - try { - findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment) - return@subscribe - } catch (ise: IllegalStateException) { - Log.w(TAG, "Could not get parent activity fragment manager!") - } - - try { - val hostActivity = activity - if (hostActivity != null) { - Navigation.findNavController(hostActivity, R.id.nav_host_fragment).safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment) - return@subscribe - } else { - Log.w(TAG, "Could not get parent activity!") - } - } catch (ise: IllegalStateException) { - Log.w(TAG, "Could not find navigation host fragment!") - } - - activity?.let { - Log.w(TAG, "Could not navigate to registration complete. Finishing activity gracefully.") - it.finish() - } - - return@subscribe - } - - reRegisterViewModel.hasIncorrectGuess = true - - if (processor is VerifyResponseWithRegistrationLockProcessor && processor.wrongPin()) { - reRegisterViewModel.updateSvrTriesRemaining(processor.svrTriesRemaining) - if (processor.svrTriesRemaining != null) { - registrationViewModel.svrTriesRemaining = processor.svrTriesRemaining - } - return@subscribe - } else if (processor.isRegistrationLockPresentAndSvrExhausted()) { - Log.w(TAG, "Unable to continue skip flow, KBS is locked") - onAccountLocked() - } else if (processor.isIncorrectRegistrationRecoveryPassword()) { - Log.w(TAG, "Registration recovery password was incorrect. Moving to SMS verification.") - onSkipPinEntry() - } else if (processor.isServerSentError()) { - Log.i(TAG, "Error from server, not likely recoverable", processor.error) - genericErrorDialog() - } else { - Log.i(TAG, "Unexpected error occurred", processor.error) - genericErrorDialog() - } - } - } - - private fun updateTriesRemaining(triesRemaining: Int) { - if (reRegisterViewModel.hasIncorrectGuess) { - if (triesRemaining == 1 && !reRegisterViewModel.isLocalVerification) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.PinRestoreEntryFragment_incorrect_pin) - .setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining)) - .setPositiveButton(android.R.string.ok, null) - .show() - } - - if (triesRemaining > 5) { - binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin) - } else { - binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining) - } - binding.pinRestoreForgotPin.visibility = View.VISIBLE - } else { - if (triesRemaining == 1) { - binding.pinRestoreForgotPin.visibility = View.VISIBLE - if (!reRegisterViewModel.isLocalVerification) { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining)) - .setPositiveButton(android.R.string.ok, null) - .show() - } - } - } - - if (triesRemaining == 0) { - Log.w(TAG, "Account locked. User out of attempts on KBS.") - onAccountLocked() - } - } - - private fun onAccountLocked() { - Log.d(TAG, "Showing Incorrect PIN dialog. Is local verification: ${reRegisterViewModel.isLocalVerification}") - val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.PinRestoreEntryFragment_incorrect_pin) - .setMessage(message) - .setCancelable(false) - .setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() } - .setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) } - .show() - } - - private fun enableAndFocusPinEntry() { - binding.pinRestorePinInput.isEnabled = true - binding.pinRestorePinInput.isFocusable = true - ViewUtil.focusAndShowKeyboard(binding.pinRestorePinInput) - } - - private fun getPinEntryKeyboardType(): PinKeyboardType { - val isNumeric = binding.pinRestorePinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER - return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC - } - - private fun updateKeyboard(keyboard: PinKeyboardType) { - val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC - binding.pinRestorePinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD - binding.pinRestorePinInput.text?.clear() - } - - private fun onNeedHelpClicked() { - val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.PinRestoreEntryFragment_need_help) - .setMessage(getString(message, SvrConstants.MINIMUM_PIN_LENGTH)) - .setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() } - .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> - val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null) - - CommunicationActions.openEmail( - requireContext(), - SupportEmailUtil.getSupportEmailAddress(requireContext()), - getString(R.string.ReRegisterWithPinFragment_support_email_subject), - body - ) - } - .setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null) - .show() - } - - private fun onSkipClicked() { - val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry) - .setMessage(message) - .setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() } - .setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null) - .show() - } - - private fun onSkipPinEntry() { - Log.d(TAG, "User skipping PIN entry.") - registrationViewModel.setUserSkippedReRegisterFlow(true) - findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_enterPhoneNumberFragment) - } - - private fun genericErrorDialog() { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(R.string.RegistrationActivity_error_connecting_to_service) - .setPositiveButton(android.R.string.ok, null) - .show() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.kt deleted file mode 100644 index bd05c6666e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments - -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.navigation.ActivityNavigator -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.MainActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob -import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob -import org.thoughtcrime.securesms.jobs.ProfileUploadJob -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity -import org.thoughtcrime.securesms.pin.PinRestoreActivity -import org.thoughtcrime.securesms.profiles.AvatarHelper -import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.registration.RegistrationUtil -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel - -/** - * [RegistrationCompleteFragment] is not visible to the user, but functions as basically a redirect towards one of: - * - [PIN Restore flow activity](org.thoughtcrime.securesms.pin.PinRestoreActivity) - * - [Profile](org.thoughtcrime.securesms.profiles.edit.EditProfileActivity) / [PIN creation](org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity) flow activities (this class chains the necessary activities together as an intent) - * - Exit registration flow and progress to conversation list - */ -class RegistrationCompleteFragment : LoggingFragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_registration_blank, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val activity = requireActivity() - val viewModel: RegistrationViewModel by viewModels(ownerProducer = { requireActivity() }) - - if (SignalStore.misc.hasLinkedDevices) { - SignalStore.misc.shouldShowLinkedDevicesReminder = viewModel.isReregister - } - - if (SignalStore.storageService.needsAccountRestore()) { - Log.i(TAG, "Performing pin restore.") - activity.startActivity(Intent(activity, PinRestoreActivity::class.java)) - } else { - val isProfileNameEmpty = Recipient.self().profileName.isEmpty - val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id) - val needsProfile = isProfileNameEmpty || isAvatarEmpty - val needsPin = !SignalStore.svr.hasPin() && !viewModel.isReregister - - Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin") - - if (!needsProfile && !needsPin) { - AppDependencies.jobManager - .startChain(ProfileUploadJob()) - .then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())) - .enqueue() - RegistrationUtil.maybeMarkRegistrationComplete() - } - - var startIntent = MainActivity.clearTop(activity) - - if (needsPin) { - startIntent = chainIntents(CreateSvrPinActivity.getIntentForPinCreate(activity), startIntent) - } - - if (needsProfile) { - startIntent = chainIntents(CreateProfileActivity.getIntentForUserProfile(activity), startIntent) - } - - activity.startActivity(startIntent) - } - - activity.finish() - ActivityNavigator.applyPopAnimationsToPendingTransition(activity) - } - - private fun chainIntents(sourceIntent: Intent, nextIntent: Intent): Intent { - sourceIntent.putExtra("next_intent", nextIntent) - return sourceIntent - } - - companion object { - private val TAG = Log.tag(RegistrationCompleteFragment::class.java) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java index 8228bd6efa..883523e07f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationConstants.java @@ -1,3 +1,8 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.registration.fragments; public final class RegistrationConstants { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java deleted file mode 100644 index 403f270930..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.Navigation; - -import org.signal.core.util.concurrent.SimpleTask; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob; -import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -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.RemoteConfig; -import org.signal.core.util.Stopwatch; -import org.thoughtcrime.securesms.util.SupportEmailUtil; -import org.thoughtcrime.securesms.util.navigation.SafeNavigation; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public final class RegistrationLockFragment extends BaseRegistrationLockFragment { - - private static final String TAG = Log.tag(RegistrationLockFragment.class); - - public RegistrationLockFragment() { - super(R.layout.fragment_registration_lock); - } - - @Override - protected BaseRegistrationViewModel getViewModel() { - return new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); - } - - @Override - protected void navigateToAccountLocked() { - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionAccountLocked()); - } - - @Override - protected void handleSuccessfulPinEntry(@NonNull String pin) { - SignalStore.pin().setKeyboardType(getPinEntryKeyboardType()); - - SimpleTask.run(() -> { - SignalStore.onboarding().clearAll(); - - Stopwatch stopwatch = new Stopwatch("RegistrationLockRestore"); - - AppDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN); - stopwatch.split("AccountRestore"); - - AppDependencies - .getJobManager() - .startChain(new StorageSyncJob()) - .then(new ReclaimUsernameAndLinkJob()) - .enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10)); - stopwatch.split("ContactRestore"); - - try { - RemoteConfig.refreshSync(); - } catch (IOException e) { - Log.w(TAG, "Failed to refresh flags.", e); - } - stopwatch.split("RemoteConfig"); - - stopwatch.stop(TAG); - - return null; - }, none -> { - pinButton.cancelSpinning(); - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), RegistrationLockFragmentDirections.actionSuccessfulRegistration()); - }); - } - - @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, - null, - null); - CommunicationActions.openEmail(requireContext(), - SupportEmailUtil.getSupportEmailAddress(requireContext()), - getString(subject), - body); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java deleted file mode 100644 index ab2cef3a39..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java +++ /dev/null @@ -1,494 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.text.Editable; -import android.text.Spanned; -import android.text.TextWatcher; -import android.text.style.ReplacementSpan; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.annotation.StringRes; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.Navigation; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.signal.core.util.ThreadUtil; -import org.signal.core.util.concurrent.SimpleTask; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.AppInitialization; -import org.thoughtcrime.securesms.LoggingFragment; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.backup.BackupEvent; -import org.thoughtcrime.securesms.backup.BackupPassphrase; -import org.thoughtcrime.securesms.backup.FullBackupImporter; -import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; -import org.thoughtcrime.securesms.database.NoExternalStorageException; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; -import org.thoughtcrime.securesms.service.LocalBackupListener; -import org.thoughtcrime.securesms.util.BackupUtil; -import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.navigation.SafeNavigation; -import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton; - -import java.io.IOException; -import java.util.Locale; - -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; - -public final class RestoreBackupFragment extends LoggingFragment { - - private static final String TAG = Log.tag(RestoreBackupFragment.class); - private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782; - - private TextView restoreBackupSize; - private TextView restoreBackupTime; - private TextView restoreBackupProgress; - private CircularProgressMaterialButton restoreButton; - private View skipRestoreButton; - private RegistrationViewModel viewModel; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_registration_restore_backup, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header)); - - Log.i(TAG, "Backup restore."); - - restoreBackupSize = view.findViewById(R.id.backup_size_text); - restoreBackupTime = view.findViewById(R.id.backup_created_text); - restoreBackupProgress = view.findViewById(R.id.backup_progress_text); - restoreButton = view.findViewById(R.id.restore_button); - skipRestoreButton = view.findViewById(R.id.skip_restore_button); - - skipRestoreButton.setOnClickListener((v) -> { - Log.i(TAG, "User skipped backup restore."); - SafeNavigation.safeNavigate(Navigation.findNavController(view), - RestoreBackupFragmentDirections.actionSkip()); - }); - - viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); - - if (viewModel.isReregister()) { - Log.i(TAG, "Skipping backup restore during re-register."); - SafeNavigation.safeNavigate(Navigation.findNavController(view), - RestoreBackupFragmentDirections.actionSkipNoReturn()); - return; - } - - if (viewModel.hasBackupCompleted()) { - onBackupComplete(); - return; - } - - if (SignalStore.settings().isBackupEnabled()) { - Log.i(TAG, "Backups enabled, so a backup must have been previously restored."); - SafeNavigation.safeNavigate(Navigation.findNavController(view), - RestoreBackupFragmentDirections.actionSkipNoReturn()); - return; - } - - RestoreBackupFragmentArgs args = RestoreBackupFragmentArgs.fromBundle(requireArguments()); - if ((Build.VERSION.SDK_INT < 29 || BackupUtil.isUserSelectionRequired(requireContext())) && args.getUri() != null) { - Log.i(TAG, "Restoring backup from passed uri"); - initializeBackupForUri(view, args.getUri()); - - return; - } - - if (BackupUtil.canUserAccessBackupDirectory(requireContext())) { - initializeBackupDetection(view); - } else { - Log.i(TAG, "Skipping backup detection. We don't have the permission."); - SafeNavigation.safeNavigate(Navigation.findNavController(view), - RestoreBackupFragmentDirections.actionSkipNoReturn()); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode == OPEN_DOCUMENT_TREE_RESULT_CODE && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { - Uri backupDirectoryUri = data.getData(); - int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | - Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - - SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri); - requireContext().getContentResolver() - .takePersistableUriPermission(backupDirectoryUri, takeFlags); - - enableBackups(requireContext()); - - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), - RestoreBackupFragmentDirections.actionBackupRestored()); - } - } - - private void initializeBackupForUri(@NonNull View view, @NonNull Uri uri) { - getFromUri(requireContext(), uri, backup -> handleBackupInfo(view, backup)); - } - - @SuppressLint("StaticFieldLeak") - private void initializeBackupDetection(@NonNull View view) { - searchForBackup(backup -> handleBackupInfo(view, backup)); - } - - private void handleBackupInfo(@NonNull View view, @Nullable BackupUtil.BackupInfo backup) { - Context context = getContext(); - if (context == null) { - Log.i(TAG, "No context on fragment, must have navigated away."); - return; - } - - if (backup == null) { - Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since."); - SafeNavigation.safeNavigate(Navigation.findNavController(view), - RestoreBackupFragmentDirections.actionNoBackupFound()); - } else { - restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize()))); - restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp()))); - - restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup)); - } - } - - interface OnBackupSearchResultListener { - - @MainThread - void run(@Nullable BackupUtil.BackupInfo backup); - } - - static void searchForBackup(@NonNull OnBackupSearchResultListener listener) { - new AsyncTask() { - @Override - protected @Nullable - BackupUtil.BackupInfo doInBackground(Void... voids) { - try { - return BackupUtil.getLatestBackup(); - } catch (NoExternalStorageException e) { - Log.w(TAG, e); - return null; - } - } - - @Override - protected void onPostExecute(@Nullable BackupUtil.BackupInfo backup) { - listener.run(backup); - } - }.execute(); - } - - static void getFromUri(@NonNull Context context, - @NonNull Uri backupUri, - @NonNull OnBackupSearchResultListener listener) - { - SimpleTask.run(() -> { - try { - return BackupUtil.getBackupInfoFromSingleUri(context, backupUri); - } catch (BackupUtil.BackupFileException e) { - Log.w(TAG, "Could not restore backup.", e); - postToastForBackupRestorationFailure(context, e); - return null; - } - }, - listener::run); - } - - private static void postToastForBackupRestorationFailure(@NonNull Context context, @NonNull BackupUtil.BackupFileException exception) { - final @StringRes int errorResId; - switch (exception.getState()) { - case READABLE: - throw new AssertionError("Unexpected error state."); - case NOT_FOUND: - errorResId = R.string.RestoreBackupFragment__backup_not_found; - break; - case UNSUPPORTED_FILE_EXTENSION: - errorResId = R.string.RestoreBackupFragment__backup_has_a_bad_extension; - break; - default: - errorResId = R.string.RestoreBackupFragment__backup_could_not_be_read; - } - - ThreadUtil.postToMain(() -> Toast.makeText(context, errorResId, Toast.LENGTH_LONG).show()); - } - - private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) { - View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null); - EditText prompt = view.findViewById(R.id.restore_passphrase_input); - - prompt.addTextChangedListener(new PassphraseAsYouTypeFormatter()); - - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.RegistrationActivity_enter_backup_passphrase) - .setView(view) - .setPositiveButton(R.string.RegistrationActivity_restore, (dialog, which) -> { - InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(prompt.getWindowToken(), 0); - - restoreButton.setSpinning(); - skipRestoreButton.setVisibility(View.INVISIBLE); - - String passphrase = prompt.getText().toString(); - - restoreAsynchronously(context, backup, passphrase); - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - - Log.i(TAG, "Prompt for backup passphrase shown to user."); - } - - @SuppressLint("StaticFieldLeak") - private void restoreAsynchronously(@NonNull Context context, - @NonNull BackupUtil.BackupInfo backup, - @NonNull String passphrase) - { - new AsyncTask() { - @Override - protected BackupImportResult doInBackground(Void... voids) { - try { - Log.i(TAG, "Starting backup restore."); - DataRestoreConstraint.setRestoringData(true); - - SQLiteDatabase database = SignalDatabase.getBackupDatabase(); - - BackupPassphrase.set(context, passphrase); - - if (!FullBackupImporter.validatePassphrase(context, backup.getUri(), passphrase)) { - return BackupImportResult.FAILURE_UNKNOWN; - } - - FullBackupImporter.importFile(context, - AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), - database, - backup.getUri(), - passphrase); - - SignalDatabase.runPostBackupRestoreTasks(database); - NotificationChannels.getInstance().restoreContactNotificationChannels(); - - enableBackups(context); - - AppInitialization.onPostBackupRestore(context); - - Log.i(TAG, "Backup restore complete."); - return BackupImportResult.SUCCESS; - } catch (FullBackupImporter.DatabaseDowngradeException e) { - Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e); - return BackupImportResult.FAILURE_VERSION_DOWNGRADE; - } catch (FullBackupImporter.ForeignKeyViolationException e) { - Log.w(TAG, "Failed due to foreign key constraint violations.", e); - return BackupImportResult.FAILURE_FOREIGN_KEY; - } catch (IOException e) { - Log.w(TAG, e); - return BackupImportResult.FAILURE_UNKNOWN; - } finally { - DataRestoreConstraint.setRestoringData(false); - } - } - - @Override - protected void onPostExecute(@NonNull BackupImportResult result) { - viewModel.markBackupCompleted(); - restoreButton.cancelSpinning(); - skipRestoreButton.setVisibility(View.VISIBLE); - - restoreBackupProgress.setText(""); - - switch (result) { - case SUCCESS: - Log.i(TAG, "Successful backup restore."); - break; - case FAILURE_VERSION_DOWNGRADE: - Toast.makeText(context, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show(); - break; - case FAILURE_FOREIGN_KEY: - Toast.makeText(context, R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show(); - break; - case FAILURE_UNKNOWN: - Toast.makeText(context, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show(); - break; - } - } - }.execute(); - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - public void onResume() { - super.onResume(); - if (viewModel != null && viewModel.hasBackupCompleted()) { - onBackupComplete(); - } - } - - @Override - public void onStop() { - super.onStop(); - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEvent(@NonNull BackupEvent event) { - long count = event.getCount(); - - if (count == 0) { - restoreBackupProgress.setText(R.string.RegistrationActivity_checking); - } else { - restoreBackupProgress.setText(getString(R.string.RegistrationActivity_d_messages_so_far, count)); - } - - restoreButton.setSpinning(); - skipRestoreButton.setVisibility(View.INVISIBLE); - - if (event.getType() == BackupEvent.Type.FINISHED) { - onBackupComplete(); - } - } - - private void onBackupComplete() { - if (BackupUtil.isUserSelectionRequired(requireContext()) && !BackupUtil.canUserAccessBackupDirectory(requireContext())) { - displayConfirmationDialog(requireContext()); - } else { - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), - RestoreBackupFragmentDirections.actionBackupRestored()); - } - } - - private void enableBackups(@NonNull Context context) { - if (BackupUtil.canUserAccessBackupDirectory(context)) { - LocalBackupListener.setNextBackupTimeToIntervalFromNow(context); - SignalStore.settings().setBackupEnabled(true); - LocalBackupListener.schedule(context); - } - } - - @RequiresApi(29) - private void displayConfirmationDialog(@NonNull Context context) { - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.RestoreBackupFragment__restore_complete) - .setMessage(R.string.RestoreBackupFragment__to_continue_using_backups_please_choose_a_folder) - .setPositiveButton(R.string.RestoreBackupFragment__choose_folder, (dialog, which) -> { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | - Intent.FLAG_GRANT_READ_URI_PERMISSION); - - startActivityForResult(intent, OPEN_DOCUMENT_TREE_RESULT_CODE); - }) - .setNegativeButton(R.string.RestoreBackupFragment__not_now, (dialog, which) -> { - BackupPassphrase.set(context, null); - dialog.dismiss(); - - SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), - RestoreBackupFragmentDirections.actionBackupRestored()); - }) - .setCancelable(false) - .show(); - } - - private enum BackupImportResult { - SUCCESS, - FAILURE_VERSION_DOWNGRADE, - FAILURE_FOREIGN_KEY, - FAILURE_UNKNOWN - } - - public static class PassphraseAsYouTypeFormatter implements TextWatcher { - - private static final int GROUP_SIZE = 5; - - @Override - public void afterTextChanged(Editable editable) { - removeSpans(editable); - - addSpans(editable); - } - - private static void removeSpans(Editable editable) { - SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class); - - for (SpaceSpan span : paddingSpans) { - editable.removeSpan(span); - } - } - - private static void addSpans(Editable editable) { - final int length = editable.length(); - - for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) { - editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - if (editable.length() > BackupUtil.PASSPHRASE_LENGTH) { - editable.delete(BackupUtil.PASSPHRASE_LENGTH, editable.length()); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - } - - /** - * A {@link ReplacementSpan} adds a small space after a single character. - * Based on https://stackoverflow.com/a/51949578 - */ - private static class SpaceSpan extends ReplacementSpan { - - @Override - public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { - return (int) (paint.measureText(text, start, end) * 1.7f); - } - - @Override - public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { - canvas.drawText(text.subSequence(start, end).toString(), x, y, paint); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java deleted file mode 100644 index 2659e40c8b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java +++ /dev/null @@ -1,257 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.ActivityNavigator; -import androidx.navigation.NavDirections; -import androidx.navigation.Navigation; -import androidx.navigation.fragment.NavHostFragment; - -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.Phonenumber; - -import org.greenrobot.eventbus.EventBus; -import org.signal.core.util.logging.Log; -import org.signal.devicetransfer.DeviceToDeviceTransferService; -import org.signal.devicetransfer.TransferStatus; -import org.thoughtcrime.securesms.LoggingFragment; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; -import org.thoughtcrime.securesms.util.BackupUtil; -import org.thoughtcrime.securesms.util.CommunicationActions; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.navigation.SafeNavigation; -import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton; - -import java.util.Optional; - -import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; - -public final class WelcomeFragment extends LoggingFragment { - - private static final String TAG = Log.tag(WelcomeFragment.class); - - private CircularProgressMaterialButton continueButton; - private RegistrationViewModel viewModel; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_registration_welcome, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - viewModel = new ViewModelProvider(requireActivity()).get(RegistrationViewModel.class); - - if (viewModel.isReregister()) { - if (viewModel.hasRestoreFlowBeenShown()) { - Log.i(TAG, "We've come back to the home fragment on a restore, user must be backing out"); - if (!Navigation.findNavController(view).popBackStack()) { - FragmentActivity activity = requireActivity(); - activity.finish(); - ActivityNavigator.applyPopAnimationsToPendingTransition(activity); - } - return; - } - - initializeNumber(requireContext(), viewModel); - - Log.i(TAG, "Skipping restore because this is a reregistration."); - viewModel.setWelcomeSkippedOnRestore(); - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), - WelcomeFragmentDirections.actionSkipRestore()); - } else { - - setDebugLogSubmitMultiTapView(view.findViewById(R.id.image)); - setDebugLogSubmitMultiTapView(view.findViewById(R.id.title)); - - continueButton = view.findViewById(R.id.welcome_continue_button); - continueButton.setOnClickListener(v -> onContinueClicked()); - - Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore); - restoreFromBackup.setOnClickListener(v -> onRestoreFromBackupClicked()); - - TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button); - welcomeTermsButton.setOnClickListener(v -> onTermsClicked()); - - if (!canUserSelectBackup()) { - restoreFromBackup.setText(R.string.registration_activity__transfer_account); - } - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } - - @Override - public void onResume() { - super.onResume(); - if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null) { - Log.i(TAG, "Found existing transferStatus, redirect to transfer flow"); - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_welcomeFragment_to_deviceTransferSetup); - } else { - DeviceToDeviceTransferService.stop(requireContext()); - } - } - - private void onContinueClicked() { - if (Permissions.isRuntimePermissionsRequired()) { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), - WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.CONTINUE)); - } else { - gatherInformationAndContinue( - this, - viewModel, - () -> continueButton.setSpinning(), - () -> continueButton.cancelSpinning(), - WelcomeFragmentDirections.actionSkipRestore(), - WelcomeFragmentDirections.actionRestore() - ); - } - } - - private void onRestoreFromBackupClicked() { - if (Permissions.isRuntimePermissionsRequired()) { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), - WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP)); - } else { - gatherInformationAndChooseBackup(this, viewModel, WelcomeFragmentDirections.actionTransferOrRestore()); - } - } - - static void continueClicked(@NonNull Fragment fragment, - @NonNull RegistrationViewModel viewModel, - @NonNull Runnable onSearchForBackupStarted, - @NonNull Runnable onSearchForBackupFinished, - @NonNull NavDirections actionSkipRestore, - @NonNull NavDirections actionRestore) - { - boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext()); - - Permissions.with(fragment) - .request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)) - .ifNecessary() - .onAnyResult(() -> gatherInformationAndContinue(fragment, - viewModel, - onSearchForBackupStarted, - onSearchForBackupFinished, - actionSkipRestore, - actionRestore)) - .execute(); - } - - static void restoreFromBackupClicked(@NonNull Fragment fragment, - @NonNull RegistrationViewModel viewModel, - @NonNull NavDirections actionTransferOrRestore) - { - boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext()); - - Permissions.with(fragment) - .request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)) - .ifNecessary() - .onAnyResult(() -> gatherInformationAndChooseBackup(fragment, viewModel, actionTransferOrRestore)) - .execute(); - } - - static void gatherInformationAndContinue( - @NonNull Fragment fragment, - @NonNull RegistrationViewModel viewModel, - @NonNull Runnable onSearchForBackupStarted, - @NonNull Runnable onSearchForBackupFinished, - @NonNull NavDirections actionSkipRestore, - @NonNull NavDirections actionRestore - ) { - onSearchForBackupStarted.run(); - - RestoreBackupFragment.searchForBackup(backup -> { - Context context = fragment.getContext(); - if (context == null) { - Log.i(TAG, "No context on fragment, must have navigated away."); - return; - } - - TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true); - - initializeNumber(fragment.requireContext(), viewModel); - - onSearchForBackupFinished.run(); - - if (backup == null) { - Log.i(TAG, "Skipping backup. No backup found, or no permission to look."); - SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment), - actionSkipRestore); - } else { - SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment), - actionRestore); - } - }); - } - - static void gatherInformationAndChooseBackup(@NonNull Fragment fragment, - @NonNull RegistrationViewModel viewModel, - @NonNull NavDirections actionTransferOrRestore) { - TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true); - - initializeNumber(fragment.requireContext(), viewModel); - - SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment), - actionTransferOrRestore); - } - - @SuppressLint("MissingPermission") - private static void initializeNumber(@NonNull Context context, @NonNull RegistrationViewModel viewModel) { - Optional localNumber = Optional.empty(); - - if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { - localNumber = Util.getDeviceNumber(context); - } else { - Log.i(TAG, "No phone permission"); - } - - if (localNumber.isPresent()) { - Log.i(TAG, "Phone number detected"); - Phonenumber.PhoneNumber phoneNumber = localNumber.get(); - String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL); - - viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber); - } else { - Log.i(TAG, "No number detected"); - Optional simCountryIso = Util.getSimCountryIso(context); - - if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) { - viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), ""); - } - } - } - - private void onTermsClicked() { - CommunicationActions.openBrowserLink(requireContext(), RegistrationConstants.TERMS_AND_CONDITIONS_URL); - } - - private boolean canUserSelectBackup() { - return BackupUtil.isUserSelectionRequired(requireContext()) && - !viewModel.isReregister() && - !SignalStore.settings().isBackupEnabled(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomePermissions.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomePermissions.kt index 0151f4c0d3..9748c7afac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomePermissions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomePermissions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Signal Messenger, LLC + * Copyright 2024 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt similarity index 81% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt index be10c9a0b6..0558cad930 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui +package org.thoughtcrime.securesms.registration.ui import android.content.Context import android.content.Intent @@ -23,19 +23,19 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver -import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity +import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.RemoteConfig /** * Activity to hold the entire registration process. */ -class RegistrationV2Activity : BaseActivity() { +class RegistrationActivity : BaseActivity() { - private val TAG = Log.tag(RegistrationV2Activity::class.java) + private val TAG = Log.tag(RegistrationActivity::class.java) private val dynamicTheme = DynamicNoActionBarTheme() - val sharedViewModel: RegistrationV2ViewModel by viewModels() + val sharedViewModel: RegistrationViewModel by viewModels() private var smsRetrieverReceiver: SmsRetrieverReceiver? = null @@ -49,6 +49,8 @@ class RegistrationV2Activity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_registration_navigation_v2) + sharedViewModel.isReregister = intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false) + sharedViewModel.checkpoint.observe(this) { if (it >= RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE) { handleSuccessfulVerify() @@ -85,11 +87,11 @@ class RegistrationV2Activity : BaseActivity() { val startIntent = MainActivity.clearTop(this).apply { if (needsPin) { - putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity)) + putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationActivity)) } else if (!SignalStore.registration.hasSkippedTransferOrRestore() && RemoteConfig.messageBackups) { - putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationV2Activity)) + putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationActivity)) } else if (needsProfile) { - putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity)) + putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationActivity)) } } @@ -113,12 +115,21 @@ class RegistrationV2Activity : BaseActivity() { } companion object { + const val RE_REGISTRATION_EXTRA: String = "re_registration" @JvmStatic fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent { - return Intent(context, RegistrationV2Activity::class.java).apply { + return Intent(context, RegistrationActivity::class.java).apply { + putExtra(RE_REGISTRATION_EXTRA, false) setData(originalIntent.data) } } + + @JvmStatic + fun newIntentForReRegistration(context: Context): Intent { + return Intent(context, RegistrationActivity::class.java).apply { + putExtra(RE_REGISTRATION_EXTRA, true) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationCheckpoint.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationCheckpoint.kt index b03e961264..cd5c706a2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationCheckpoint.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui +package org.thoughtcrime.securesms.registration.ui /** * An ordered list of checkpoints of the registration process. diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationExtensions.kt similarity index 86% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationExtensions.kt index 9b76f73561..6cd4c70d11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationExtensions.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui +package org.thoughtcrime.securesms.registration.ui import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt similarity index 91% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt index 10802188b1..a6a4e5502b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationState.kt @@ -3,21 +3,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui +package org.thoughtcrime.securesms.registration.ui import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.Phonenumber import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge +import org.thoughtcrime.securesms.registration.data.network.Challenge import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.internal.push.AuthCredentials /** * State holder shared across all of registration. */ -data class RegistrationV2State( +data class RegistrationState( val sessionId: String? = null, val enteredCode: String = "", val phoneNumber: Phonenumber.PhoneNumber? = fetchExistingE164FromValues(), @@ -50,7 +50,7 @@ data class RegistrationV2State( val challengesRemaining: List = challengesRequested.filterNot { it in challengesPresented } companion object { - private val TAG = Log.tag(RegistrationV2State::class) + private val TAG = Log.tag(RegistrationState::class) private fun fetchExistingE164FromValues(): Phonenumber.PhoneNumber? { val existingE164 = SignalStore.registration.sessionE164 diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 063247ca3b..dd1283f824 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui +package org.thoughtcrime.securesms.registration.ui import android.Manifest import android.content.Context @@ -36,29 +36,29 @@ import org.thoughtcrime.securesms.pin.SvrRepository import org.thoughtcrime.securesms.pin.SvrWrongPinException import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationUtil -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository -import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AlreadyVerified -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ExternalServiceFailure -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ImpossibleNumber -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MalformedRequest -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.MustRetry -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NoSuchSession -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.NonNormalizedNumber -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RateLimited -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.RegistrationLocked -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError +import org.thoughtcrime.securesms.registration.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AttemptsExhausted +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ExternalServiceFailure +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ImpossibleNumber +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MalformedRequest +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MustRetry +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NoSuchSession +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NonNormalizedNumber +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RateLimited +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RegistrationLocked +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.Util @@ -74,9 +74,9 @@ import kotlin.time.Duration.Companion.minutes /** * ViewModel shared across all of registration. */ -class RegistrationV2ViewModel : ViewModel() { +class RegistrationViewModel : ViewModel() { - private val store = MutableStateFlow(RegistrationV2State()) + private val store = MutableStateFlow(RegistrationState()) private val password = Util.getSecret(18) private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> @@ -102,8 +102,13 @@ class RegistrationV2ViewModel : ViewModel() { val svrTriesRemaining: Int get() = store.value.svrTriesRemaining - val isReregister: Boolean + var isReregister: Boolean get() = store.value.isReRegister + set(value) { + store.update { + it.copy(isReRegister = value) + } + } val phoneNumber: Phonenumber.PhoneNumber? get() = store.value.phoneNumber @@ -857,7 +862,7 @@ class RegistrationV2ViewModel : ViewModel() { } companion object { - private val TAG = Log.tag(RegistrationV2ViewModel::class.java) + private val TAG = Log.tag(RegistrationViewModel::class.java) private suspend fun restoreBackupTier() = withContext(Dispatchers.IO) { val startTime = System.currentTimeMillis() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/accountlocked/AccountLockedV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/accountlocked/AccountLockedFragment.kt similarity index 87% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/accountlocked/AccountLockedV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/accountlocked/AccountLockedFragment.kt index 1e525ef344..1d6f0ea6d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/accountlocked/AccountLockedV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/accountlocked/AccountLockedFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.accountlocked +package org.thoughtcrime.securesms.registration.ui.accountlocked import android.content.Intent import android.net.Uri @@ -15,14 +15,14 @@ import androidx.fragment.app.activityViewModels import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import kotlin.time.Duration.Companion.milliseconds /** * Screen educating the user that they need to wait some number of days to register. */ -class AccountLockedV2Fragment : LoggingFragment(R.layout.account_locked_fragment) { - private val viewModel by activityViewModels() +class AccountLockedFragment : LoggingFragment(R.layout.account_locked_fragment) { + private val viewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/CaptchaFragment.kt similarity index 86% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/CaptchaFragment.kt index dfa4402fb0..a7ece8959d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/CaptchaV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/CaptchaFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.captcha +package org.thoughtcrime.securesms.registration.ui.captcha import android.annotation.SuppressLint import android.os.Bundle @@ -17,12 +17,12 @@ import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaV2Binding +import org.thoughtcrime.securesms.databinding.FragmentRegistrationCaptchaBinding import org.thoughtcrime.securesms.registration.fragments.RegistrationConstants -abstract class CaptchaV2Fragment : LoggingFragment(R.layout.fragment_registration_captcha_v2) { +abstract class CaptchaFragment : LoggingFragment(R.layout.fragment_registration_captcha) { - private val binding: FragmentRegistrationCaptchaV2Binding by ViewBinderDelegate(FragmentRegistrationCaptchaV2Binding::bind) + private val binding: FragmentRegistrationCaptchaBinding by ViewBinderDelegate(FragmentRegistrationCaptchaBinding::bind) private val backListener = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/RegistrationCaptchaV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/RegistrationCaptchaFragment.kt similarity index 64% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/RegistrationCaptchaV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/RegistrationCaptchaFragment.kt index 3a97b8b18a..4e59ec7706 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/captcha/RegistrationCaptchaV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/captcha/RegistrationCaptchaFragment.kt @@ -3,22 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.captcha +package org.thoughtcrime.securesms.registration.ui.captcha import android.os.Bundle import android.view.View import androidx.fragment.app.activityViewModels -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel /** * Screen that displays a captcha as part of the registration flow. - * This subclass plugs in [RegistrationV2ViewModel] to the shared super class. + * This subclass plugs in [RegistrationViewModel] to the shared super class. * - * @see CaptchaV2Fragment + * @see CaptchaFragment */ -class RegistrationCaptchaV2Fragment : CaptchaV2Fragment() { - private val sharedViewModel by activityViewModels() +class RegistrationCaptchaFragment : CaptchaFragment() { + private val sharedViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedViewModel.addPresentedChallenge(Challenge.CAPTCHA) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt index 3e29bf9c5f..ca2c1cd123 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.entercode +package org.thoughtcrime.securesms.registration.ui.entercode import android.content.DialogInterface import android.os.Bundle @@ -21,16 +21,16 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding +import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding import org.thoughtcrime.securesms.registration.ReceivedSmsEvent +import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener -import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.visible @@ -38,16 +38,16 @@ import org.thoughtcrime.securesms.util.visible /** * The final screen of account registration, where the user enters their verification code. */ -class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_code_v2) { +class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) { companion object { private const val BOTTOM_SHEET_TAG = "support_bottom_sheet" } - private val TAG = Log.tag(EnterCodeV2Fragment::class.java) + private val TAG = Log.tag(EnterCodeFragment::class.java) - private val sharedViewModel by activityViewModels() - private val binding: FragmentRegistrationEnterCodeV2Binding by ViewBinderDelegate(FragmentRegistrationEnterCodeV2Binding::bind) + private val sharedViewModel by activityViewModels() + private val binding: FragmentRegistrationEnterCodeBinding by ViewBinderDelegate(FragmentRegistrationEnterCodeBinding::bind) private lateinit var phoneStateListener: SignalStrengthPhoneStateListener @@ -158,7 +158,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter binding.keyboard.displayLocked().addListener( object : AssertedSuccessListener() { override fun onSuccess(result: Boolean?) { - findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionAccountLocked()) + findNavController().safeNavigate(EnterCodeFragmentDirections.actionAccountLocked()) } } ) @@ -168,7 +168,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter binding.keyboard.displayLocked().addListener( object : AssertedSuccessListener() { override fun onSuccess(result: Boolean?) { - findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionRequireKbsLockPin(timeRemaining)) + findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining)) } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/grantpermissions/GrantPermissionsFragment.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/grantpermissions/GrantPermissionsFragment.kt index e365981e1f..e2b3dc61a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/grantpermissions/GrantPermissionsFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions +package org.thoughtcrime.securesms.registration.ui.grantpermissions import android.app.Activity import android.content.pm.PackageManager @@ -25,8 +25,8 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -35,10 +35,10 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate * Screen in account registration that provides rationales for the suggested runtime permissions. */ @RequiresApi(23) -class GrantPermissionsV2Fragment : ComposeFragment() { +class GrantPermissionsFragment : ComposeFragment() { - private val sharedViewModel by activityViewModels() - private val args by navArgs() + private val sharedViewModel by activityViewModels() + private val args by navArgs() private val isSearchingForBackup = mutableStateOf(false) private val requestPermissionLauncher = registerForActivityResult( @@ -50,7 +50,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() { when (val resultCode = result.resultCode) { Activity.RESULT_OK -> { sharedViewModel.onBackupSuccessfullyRestored() - NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber()) + NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber()) } Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.") else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode") @@ -102,7 +102,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() { private fun proceedToNextScreen() { when (welcomeAction) { - WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber()) + WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber()) WelcomeAction.RESTORE_BACKUP -> { val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity()) launchRestoreActivity.launch(restoreIntent) @@ -120,6 +120,6 @@ class GrantPermissionsV2Fragment : ComposeFragment() { } companion object { - private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java) + private val TAG = Log.tag(GrantPermissionsFragment::class.java) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt index 78c1f3365f..97487e1c59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.phonenumber +package org.thoughtcrime.securesms.registration.ui.phonenumber import android.content.Context import android.content.DialogInterface @@ -40,19 +40,19 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberV2Binding +import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberBinding import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import org.thoughtcrime.securesms.registration.data.RegistrationRepository +import org.thoughtcrime.securesms.registration.data.network.Challenge +import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView +import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.ui.RegistrationState +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel +import org.thoughtcrime.securesms.registration.ui.toE164 import org.thoughtcrime.securesms.registration.util.CountryPrefix -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository -import org.thoughtcrime.securesms.registration.v2.data.network.Challenge -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel -import org.thoughtcrime.securesms.registration.v2.ui.toE164 import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.PlayServicesUtil @@ -67,12 +67,12 @@ import kotlin.time.Duration.Companion.milliseconds /** * Screen in registration where the user enters their phone number. */ -class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number_v2) { +class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number) { - private val TAG = Log.tag(EnterPhoneNumberV2Fragment::class.java) - private val sharedViewModel by activityViewModels() - private val fragmentViewModel by viewModels() - private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind) + private val TAG = Log.tag(EnterPhoneNumberFragment::class.java) + private val sharedViewModel by activityViewModels() + private val fragmentViewModel by viewModels() + private val binding: FragmentRegistrationEnterPhoneNumberBinding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberBinding::bind) private val skipToNextScreen: DialogInterface.OnClickListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> moveToVerificationEntryScreen() } @@ -139,7 +139,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedViewModel.setPhoneNumber(null) } - if (fragmentState.error != EnterPhoneNumberV2State.Error.NONE) { + if (fragmentState.error != EnterPhoneNumberState.Error.NONE) { presentLocalError(fragmentState) } } @@ -237,7 +237,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } } - private fun presentRegisterButton(sharedState: RegistrationV2State) { + private fun presentRegisterButton(sharedState: RegistrationState) { binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber) if (sharedState.inProgress) { binding.registerButton.setSpinning() @@ -246,11 +246,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } } - private fun presentLocalError(state: EnterPhoneNumberV2State) { + private fun presentLocalError(state: EnterPhoneNumberState) { when (state.error) { - EnterPhoneNumberV2State.Error.NONE -> Unit + EnterPhoneNumberState.Error.NONE -> Unit - EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER -> { + EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER -> { MaterialAlertDialogBuilder(requireContext()).apply { setTitle(R.string.RegistrationActivity_invalid_number) setMessage( @@ -266,15 +266,15 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } } - EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING -> { + EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING -> { handlePromptForNoPlayServices() } - EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE -> { + EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE -> { GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show() } - EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT -> { + EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT -> { MaterialAlertDialogBuilder(requireContext()).apply { setTitle(R.string.RegistrationActivity_play_services_error) setMessage(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable) @@ -340,7 +340,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } private fun moveToCaptcha() { - findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionRequestCaptcha()) + findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha()) } private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) { @@ -400,7 +400,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedViewModel.uiState.value?.let { value -> val now = System.currentTimeMillis() if (value.phoneNumber == null) { - fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER) + fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER) sharedViewModel.setInProgress(false) } else if (now < value.nextSmsTimestamp) { moveToVerificationEntryScreen() @@ -411,9 +411,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } } - private fun onFcmTokenRetrieved(value: RegistrationV2State) { + private fun onFcmTokenRetrieved(value: RegistrationState) { if (value.phoneNumber == null) { - fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER) + fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER) sharedViewModel.setInProgress(false) } else { presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = false) @@ -440,23 +440,23 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } PlayServicesUtil.PlayServicesStatus.MISSING -> { - fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING) + fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING) return false } PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> { - fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE) + fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_NEEDS_UPDATE) return false } PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> { - fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT) + fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_TRANSIENT) return false } null -> { Log.w(TAG, "Null result received from PlayServicesUtil, marking Play Services as missing.") - fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING) + fragmentViewModel.setError(EnterPhoneNumberState.Error.PLAY_SERVICES_MISSING) return false } } @@ -516,12 +516,12 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } private fun moveToEnterPinScreen() { - findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment()) + findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionReRegisterWithPinV2Fragment()) sharedViewModel.setInProgress(false) } private fun moveToVerificationEntryScreen() { - findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode()) + findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()) sharedViewModel.setInProgress(false) } @@ -530,8 +530,8 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio findNavController().popBackStack() } - private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback(sharedViewModel.uiState) { - override fun onValue(value: RegistrationV2State): Boolean { + private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback(sharedViewModel.uiState) { + override fun onValue(value: RegistrationState): Boolean { val fcmRetrieved = value.isFcmSupported if (fcmRetrieved) { onFcmTokenRetrieved(value) @@ -547,7 +547,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return if (menuItem.itemId == R.id.phone_menu_use_proxy) { - NavHostFragment.findNavController(this@EnterPhoneNumberV2Fragment).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEditProxy()) + NavHostFragment.findNavController(this@EnterPhoneNumberFragment).safeNavigate(EnterPhoneNumberFragmentDirections.actionEditProxy()) true } else { false diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt new file mode 100644 index 0000000000..226c3bf98a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberState.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.ui.phonenumber + +import android.text.TextWatcher +import org.thoughtcrime.securesms.registration.data.RegistrationRepository + +/** + * State holder for the phone number entry screen, including phone number and Play Services errors. + */ +data class EnterPhoneNumberState(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) { + enum class Error { + NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt similarity index 85% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt index 475ebae7a6..2ca2ad4c21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberViewModel.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.phonenumber +package org.thoughtcrime.securesms.registration.ui.phonenumber import android.telephony.PhoneNumberFormattingTextWatcher import android.text.TextWatcher @@ -19,17 +19,17 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.thoughtcrime.securesms.registration.util.CountryPrefix -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository /** * ViewModel for the phone number entry screen. */ -class EnterPhoneNumberV2ViewModel : ViewModel() { +class EnterPhoneNumberViewModel : ViewModel() { - private val TAG = Log.tag(EnterPhoneNumberV2ViewModel::class.java) + private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java) - private val store = MutableStateFlow(EnterPhoneNumberV2State()) + private val store = MutableStateFlow(EnterPhoneNumberState()) val uiState = store.asLiveData() val formatter: TextWatcher? @@ -85,11 +85,11 @@ class EnterPhoneNumberV2ViewModel : ViewModel() { } } - fun parsePhoneNumber(state: EnterPhoneNumberV2State): PhoneNumber { + fun parsePhoneNumber(state: EnterPhoneNumberState): PhoneNumber { return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode) } - fun isEnteredNumberValid(state: EnterPhoneNumberV2State): Boolean { + fun isEnteredNumberValid(state: EnterPhoneNumberState): Boolean { return try { PhoneNumberUtil.getInstance().isValidNumber(parsePhoneNumber(state)) } catch (ex: NumberParseException) { @@ -114,10 +114,10 @@ class EnterPhoneNumberV2ViewModel : ViewModel() { } fun clearError() { - setError(EnterPhoneNumberV2State.Error.NONE) + setError(EnterPhoneNumberState.Error.NONE) } - fun setError(error: EnterPhoneNumberV2State.Error) { + fun setError(error: EnterPhoneNumberState.Error) { store.update { it.copy(error = error) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt index 6d548e704c..5a674feb5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/registrationlock/RegistrationLockV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.registrationlock +package org.thoughtcrime.securesms.registration.ui.registrationlock import android.os.Bundle import android.text.InputType @@ -23,25 +23,25 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.lock.v2.SvrConstants +import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView -import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult -import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationResult -import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.SupportEmailUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate import java.util.concurrent.TimeUnit -class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registration_lock) { +class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_lock) { companion object { - private val TAG = Log.tag(RegistrationLockV2Fragment::class.java) + private val TAG = Log.tag(RegistrationLockFragment::class.java) } private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind) - private val viewModel by activityViewModels() + private val viewModel by activityViewModels() private var timeRemaining: Long = 0 @@ -49,7 +49,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio super.onViewCreated(view, savedInstanceState) setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title)) - val args: RegistrationLockV2FragmentArgs = RegistrationLockV2FragmentArgs.fromBundle(requireArguments()) + val args: RegistrationLockFragmentArgs = RegistrationLockFragmentArgs.fromBundle(requireArguments()) timeRemaining = args.getTimeRemaining() @@ -140,7 +140,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio is VerificationCodeRequestResult.Success -> Unit is VerificationCodeRequestResult.RateLimited -> onRateLimited() is VerificationCodeRequestResult.AttemptsExhausted -> { - findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked()) + findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked()) } is VerificationCodeRequestResult.RegistrationLocked -> { @@ -162,7 +162,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio is RegisterAccountResult.Success -> Unit is RegisterAccountResult.RateLimited -> onRateLimited() is RegisterAccountResult.AttemptsExhausted -> { - findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked()) + findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked()) } is RegisterAccountResult.RegistrationLocked -> { @@ -174,7 +174,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio is RegisterAccountResult.SvrWrongPin -> onIncorrectKbsRegistrationLockPin(result.triesRemaining) is RegisterAccountResult.SvrNoData -> { - findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked()) + findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked()) } else -> { @@ -191,7 +191,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio if (svrTriesRemaining == 0) { Log.w(TAG, "Account locked. User out of attempts on KBS.") - findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionAccountLocked()) + findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked()) return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt index f5e1f79251..57e33ef070 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin +package org.thoughtcrime.securesms.registration.ui.reregisterwithpin import android.os.Bundle import android.text.InputType @@ -21,23 +21,23 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.databinding.FragmentRegistrationPinRestoreEntryV2Binding import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.lock.v2.SvrConstants +import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate -import org.thoughtcrime.securesms.registration.v2.data.network.RegisterAccountResult -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.ui.RegistrationState +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.SupportEmailUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate -class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) { +class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) { companion object { - private val TAG = Log.tag(ReRegisterWithPinV2Fragment::class.java) + private val TAG = Log.tag(ReRegisterWithPinFragment::class.java) } - private val registrationViewModel by activityViewModels() - private val reRegisterViewModel by viewModels() + private val registrationViewModel by activityViewModels() + private val reRegisterViewModel by viewModels() private val binding: FragmentRegistrationPinRestoreEntryV2Binding by ViewBinderDelegate(FragmentRegistrationPinRestoreEntryV2Binding::bind) @@ -79,11 +79,11 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati registrationViewModel.uiState.observe(viewLifecycleOwner, ::updateViewState) } - private fun updateViewState(state: RegistrationV2State) { + private fun updateViewState(state: RegistrationState) { if (state.networkError != null) { genericErrorDialog() } else if (!state.canSkipSms) { - findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) + findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) } else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) { Log.w(TAG, "Unable to continue skip flow, KBS is locked") onAccountLocked() @@ -263,7 +263,7 @@ class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registrati is RegisterAccountResult.IncorrectRecoveryPassword -> { registrationViewModel.setUserSkippedReRegisterFlow(true) - findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) + findNavController().safeNavigate(ReRegisterWithPinFragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) } is RegisterAccountResult.AttemptsExhausted, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinState.kt similarity index 66% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2State.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinState.kt index 7f8bf609fa..cbfb7a5bc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinState.kt @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin +package org.thoughtcrime.securesms.registration.ui.reregisterwithpin -data class ReRegisterWithPinV2State( +data class ReRegisterWithPinState( val isLocalVerification: Boolean = false, val hasIncorrectGuess: Boolean = false, val localPinMatches: Boolean = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinViewModel.kt similarity index 71% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2ViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinViewModel.kt index df6cf649aa..acd1016000 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/reregisterwithpin/ReRegisterWithPinViewModel.kt @@ -3,19 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin +package org.thoughtcrime.securesms.registration.ui.reregisterwithpin import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import org.signal.core.util.logging.Log -class ReRegisterWithPinV2ViewModel : ViewModel() { +class ReRegisterWithPinViewModel : ViewModel() { companion object { - private val TAG = Log.tag(ReRegisterWithPinV2ViewModel::class.java) + private val TAG = Log.tag(ReRegisterWithPinViewModel::class.java) } - private val store = MutableStateFlow(ReRegisterWithPinV2State()) + private val store = MutableStateFlow(ReRegisterWithPinState()) val isLocalVerification: Boolean get() = store.value.isLocalVerification diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreActivity.kt similarity index 99% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreActivity.kt index 23e4f75d05..84e34a1055 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RemoteRestoreActivity.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.restore +package org.thoughtcrime.securesms.registration.ui.restore import android.content.Context import android.content.Intent diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/welcome/WelcomeFragment.kt similarity index 76% rename from app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/registration/ui/welcome/WelcomeFragment.kt index 0c4c5adbe0..bd421f357f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/welcome/WelcomeFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.registration.v2.ui.welcome +package org.thoughtcrime.securesms.registration.ui.welcome import android.app.Activity import android.content.pm.PackageManager @@ -18,13 +18,13 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Binding +import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeBinding import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel -import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment +import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel +import org.thoughtcrime.securesms.registration.ui.grantpermissions.GrantPermissionsFragment import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.CommunicationActions @@ -36,15 +36,15 @@ import org.thoughtcrime.securesms.util.visible /** * First screen that is displayed on the very first app launch. */ -class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome_v2) { - private val sharedViewModel by activityViewModels() - private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind) +class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome) { + private val sharedViewModel by activityViewModels() + private val binding: FragmentRegistrationWelcomeBinding by ViewBinderDelegate(FragmentRegistrationWelcomeBinding::bind) private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> when (val resultCode = result.resultCode) { Activity.RESULT_OK -> { sharedViewModel.onBackupSuccessfullyRestored() - findNavController().safeNavigate(WelcomeV2FragmentDirections.actionGoToRegistration()) + findNavController().safeNavigate(WelcomeFragmentDirections.actionGoToRegistration()) } Activity.RESULT_CANCELED -> { Log.w(TAG, "Backup restoration canceled.") @@ -67,10 +67,10 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome private fun onContinueClicked() { TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true) if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) { - findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE)) + findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsFragment.WelcomeAction.CONTINUE)) } else { sharedViewModel.maybePrefillE164(requireContext()) - findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore()) + findNavController().safeNavigate(WelcomeFragmentDirections.actionSkipRestore()) } } @@ -85,7 +85,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome private fun onTransferOrRestoreClicked() { if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) { - findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.RESTORE_BACKUP)) + findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP)) } else { sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED) @@ -95,7 +95,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome } companion object { - private val TAG = Log.tag(WelcomeV2Fragment::class.java) + private val TAG = Log.tag(WelcomeFragment::class.java) private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal" } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/util/CountryPrefix.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/util/CountryPrefix.kt new file mode 100644 index 0000000000..06dd997a7c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/util/CountryPrefix.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.util + +data class CountryPrefix(val digits: Int, val regionCode: String) { + override fun toString(): String { + return "+$digits" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationNumberInputController.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationNumberInputController.kt deleted file mode 100644 index 8521cd7ae4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/util/RegistrationNumberInputController.kt +++ /dev/null @@ -1,179 +0,0 @@ -package org.thoughtcrime.securesms.registration.util - -import android.content.Context -import android.text.Editable -import android.text.TextUtils -import android.text.TextWatcher -import android.view.KeyEvent -import android.view.View -import android.view.View.OnFocusChangeListener -import android.view.inputmethod.EditorInfo -import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.TextView -import com.google.android.material.textfield.MaterialAutoCompleteTextView -import com.google.android.material.textfield.TextInputLayout -import com.google.i18n.phonenumbers.AsYouTypeFormatter -import com.google.i18n.phonenumbers.PhoneNumberUtil -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState - -/** - * Handle the logic and formatting of phone number input specifically for registration number the flow. - */ -class RegistrationNumberInputController( - val context: Context, - val callbacks: Callbacks, - private val phoneNumberInputLayout: EditText, - countryCodeInputLayout: TextInputLayout -) { - private val spinnerView: MaterialAutoCompleteTextView = countryCodeInputLayout.editText as MaterialAutoCompleteTextView - private val supportedCountryPrefixes: List = PhoneNumberUtil.getInstance().supportedCallingCodes - .map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) } - .sortedBy { it.digits.toString() } - private val spinnerAdapter: ArrayAdapter = ArrayAdapter(context, R.layout.registration_country_code_dropdown_item, supportedCountryPrefixes) - private val countryCodeEntryListener = CountryCodeEntryListener() - - private var countryFormatter: AsYouTypeFormatter? = null - private var isUpdating = true - - init { - setUpNumberInput() - - spinnerView.threshold = 100 - spinnerView.setAdapter(spinnerAdapter) - spinnerView.addTextChangedListener(countryCodeEntryListener) - } - - fun prepopulateCountryCode() { - if (spinnerView.editableText.isBlank()) { - spinnerView.setText(supportedCountryPrefixes[0].toString()) - } - } - - private fun advanceToPhoneNumberInput() { - if (!isUpdating) { - phoneNumberInputLayout.requestFocus() - } - val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0 - phoneNumberInputLayout.setSelection(numberLength, numberLength) - } - - private fun setUpNumberInput() { - phoneNumberInputLayout.addTextChangedListener(NumberChangedListener()) - phoneNumberInputLayout.onFocusChangeListener = OnFocusChangeListener { v: View?, hasFocus: Boolean -> - if (hasFocus) { - callbacks.onNumberFocused() - } - } - phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE - phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - callbacks.onNumberInputDone(v!!) - return@setOnEditorActionListener true - } - false - } - } - - fun setNumberAndCountryCode(numberViewState: NumberViewState) { - val countryCode = numberViewState.countryCode - - isUpdating = true - phoneNumberInputLayout.setText(numberViewState.nationalNumber) - if (numberViewState.countryCode != 0) { - spinnerView.setText(supportedCountryPrefixes.first { it.digits == numberViewState.countryCode }.toString()) - } - val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode) - setCountryFormatter(regionCode) - - isUpdating = false - } - - fun updateNumberFormatter(numberViewState: NumberViewState) { - val countryCode = numberViewState.countryCode - - isUpdating = true - val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode) - setCountryFormatter(regionCode) - - isUpdating = false - } - - private fun setCountryFormatter(regionCode: String?) { - val util = PhoneNumberUtil.getInstance() - countryFormatter = if (regionCode != null) util.getAsYouTypeFormatter(regionCode) else null - reformatText(phoneNumberInputLayout.text) - } - - private fun reformatText(editable: Editable): String? { - if (TextUtils.isEmpty(editable)) { - return null - } - val countryFormatter: AsYouTypeFormatter = countryFormatter ?: return null - countryFormatter.clear() - var formattedNumber: String? = null - val justDigits = StringBuilder() - for (character in editable) { - if (Character.isDigit(character)) { - formattedNumber = countryFormatter.inputDigit(character) - justDigits.append(character) - } - } - if (formattedNumber != null && editable.toString() != formattedNumber) { - editable.replace(0, editable.length, formattedNumber) - } - return if (justDigits.isEmpty()) { - null - } else { - justDigits.toString() - } - } - - inner class NumberChangedListener : TextWatcher { - override fun afterTextChanged(s: Editable) { - val number: String = reformatText(s) ?: return - if (!isUpdating) { - callbacks.setNationalNumber(number) - } - } - - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - } - - inner class CountryCodeEntryListener : TextWatcher { - - override fun afterTextChanged(s: Editable?) { - if (s.isNullOrEmpty()) { - return - } - - if (s[0] != '+') { - s.insert(0, "+") - } - - supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let { - setCountryFormatter(it.regionCode) - callbacks.setCountry(it.digits) - advanceToPhoneNumberInput() - } - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - } - - interface Callbacks { - fun onNumberFocused() - fun onNumberInputDone(view: View) - fun setNationalNumber(number: String) - fun setCountry(countryCode: Int) - } -} - -data class CountryPrefix(val digits: Int, val regionCode: String) { - override fun toString(): String { - return "+$digits" - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt deleted file mode 100644 index 9a098f5e40..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.registration.v2.ui.phonenumber - -import android.text.TextWatcher -import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository - -/** - * State holder for the phone number entry screen, including phone number and Play Services errors. - */ -data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) { - enum class Error { - NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseEnterCodeViewModelDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseEnterCodeViewModelDelegate.java deleted file mode 100644 index 367f58a9dd..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseEnterCodeViewModelDelegate.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.thoughtcrime.securesms.registration.viewmodel; - -public final class BaseEnterCodeViewModelDelegate { -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java deleted file mode 100644 index e01ac0e0a1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java +++ /dev/null @@ -1,397 +0,0 @@ -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 com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.Phonenumber; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor; -import org.thoughtcrime.securesms.registration.VerifyAccountRepository; -import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; -import org.thoughtcrime.securesms.registration.VerifyResponse; -import org.thoughtcrime.securesms.registration.VerifyResponseProcessor; -import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor; -import org.thoughtcrime.securesms.registration.VerifyResponseHitRegistrationLock; -import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs; -import org.whispersystems.signalservice.internal.ServiceResponse; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -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 perform - * the specific verify operations for each flow. - */ -public abstract class BaseRegistrationViewModel extends ViewModel { - - private static final String TAG = Log.tag(BaseRegistrationViewModel.class); - - 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_PUSH_TIMED_OUT = "PUSH_TIMED_OUT"; - private static final String STATE_INCORRECT_CODE_ATTEMPTS = "STATE_INCORRECT_CODE_ATTEMPTS"; - private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER"; - private static final String STATE_SVR_AUTH = "SVR_AUTH"; - private static final String STATE_SVR_TRIES_REMAINING = "SVR_TRIES_REMAINING"; - 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_CAN_SMS_AT_TIME = "CAN_SMS_AT_TIME"; - private static final String STATE_RECOVERY_PASSWORD = "RECOVERY_PASSWORD"; - - protected final SavedStateHandle savedState; - protected final VerifyAccountRepository verifyAccountRepository; - - public BaseRegistrationViewModel(@NonNull SavedStateHandle savedStateHandle, - @NonNull VerifyAccountRepository verifyAccountRepository, - @NonNull String password) - { - this.savedState = savedStateHandle; - - this.verifyAccountRepository = verifyAccountRepository; - - setInitialDefaultValue(STATE_NUMBER, NumberViewState.INITIAL); - setInitialDefaultValue(STATE_REGISTRATION_SECRET, password); - setInitialDefaultValue(STATE_VERIFICATION_CODE, ""); - setInitialDefaultValue(STATE_INCORRECT_CODE_ATTEMPTS, 0); - setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000)); - setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.svr().getRecoveryPassword()); - setInitialDefaultValue(STATE_PUSH_TIMED_OUT, false); - } - - protected void setInitialDefaultValue(@NonNull String key, @Nullable T initialValue) { - if (!savedState.contains(key) || savedState.get(key) == null) { - savedState.set(key, initialValue); - } - } - - public @Nullable String getSessionId() { - return SignalStore.registration().getSessionId(); - } - - public void setSessionId(String sessionId) { - SignalStore.registration().setSessionId(sessionId); - } - - public @Nullable String getSessionE164() { - return SignalStore.registration().getSessionE164(); - } - - public void setSessionE164(String sessionE164) { - SignalStore.registration().setSessionE164(sessionE164); - } - - public void resetSession() { - setSessionE164(null); - setSessionId(null); - } - - public @NonNull NumberViewState getNumber() { - //noinspection ConstantConditions - return savedState.get(STATE_NUMBER); - } - - public @NonNull LiveData getLiveNumber() { - return savedState.getLiveData(STATE_NUMBER); - } - - public void restorePhoneNumberStateFromE164(String e164) throws NumberParseException { - Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(e164, null); - onCountrySelected(null, phoneNumber.getCountryCode()); - setNationalNumber(String.valueOf(phoneNumber.getNationalNumber())); - } - - 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 incrementIncorrectCodeAttempts() { - //noinspection ConstantConditions - savedState.set(STATE_INCORRECT_CODE_ATTEMPTS, (Integer) savedState.get(STATE_INCORRECT_CODE_ATTEMPTS) + 1); - } - - public LiveData getIncorrectCodeAttempts() { - return savedState.getLiveData(STATE_INCORRECT_CODE_ATTEMPTS, 0); - } - - public void markPushChallengeTimedOut() { - savedState.set(STATE_PUSH_TIMED_OUT, true); - } - - public List getExcludedChallenges() { - ArrayList challengeKeys = new ArrayList<>(); - if (Boolean.TRUE.equals(savedState.get(STATE_PUSH_TIMED_OUT))) { - challengeKeys.add(RegistrationSessionProcessor.PUSH_CHALLENGE_KEY); - } - return challengeKeys; - } - - protected void setSvrAuthCredentials(SvrAuthCredentialSet credentials) { - savedState.set(STATE_SVR_AUTH, credentials); - } - - protected @Nullable SvrAuthCredentialSet getSvrAuthCredentials() { - return savedState.get(STATE_SVR_AUTH); - } - - public @Nullable Integer getSvrTriesRemaining() { - return savedState.get(STATE_SVR_TRIES_REMAINING); - } - - public void setSvrTriesRemaining(@Nullable Integer triesRemaining) { - savedState.set(STATE_SVR_TRIES_REMAINING, triesRemaining); - } - - public void setRecoveryPassword(@Nullable String recoveryPassword) { - savedState.set(STATE_RECOVERY_PASSWORD, recoveryPassword); - } - - public @Nullable String getRecoveryPassword() { - return savedState.get(STATE_RECOVERY_PASSWORD); - } - - public LiveData getLockedTimeRemaining() { - return savedState.getLiveData(STATE_TIME_REMAINING, 0L); - } - - public LiveData getCanCallAtTime() { - return savedState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L); - } - - public LiveData getCanSmsAtTime() { - return savedState.getLiveData(STATE_CAN_SMS_AT_TIME, 0L); - } - - public void setLockedTimeRemaining(long lockedTimeRemaining) { - savedState.set(STATE_TIME_REMAINING, lockedTimeRemaining); - } - - public void setCanCallAtTime(long callingTimestamp) { - savedState.getLiveData(STATE_CAN_CALL_AT_TIME).postValue(callingTimestamp); - } - - public void setCanSmsAtTime(long smsTimestamp) { - savedState.getLiveData(STATE_CAN_SMS_AT_TIME).postValue(smsTimestamp); - } - - public Single requestVerificationCode(@NonNull Mode mode, @Nullable String mcc, @Nullable String mnc) { - - final String e164 = getNumber().getE164Number(); - - return getValidSession(e164, mcc, mnc) - .flatMap(processor -> { - if (!processor.hasResult()) { - return Single.just(processor); - } - - String sessionId = processor.getSessionId(); - setSessionId(sessionId); - setSessionE164(e164); - - return handleRequiredChallenges(processor, e164); - }) - .flatMap(processor -> { - if (!processor.hasResult()) { - return Single.just(processor); - } - - if (!processor.isAllowedToRequestCode()) { - return Single.just(processor); - } - - String sessionId = processor.getSessionId(); - clearCaptchaResponse(); - return verifyAccountRepository.requestVerificationCode(sessionId, - getNumber().getE164Number(), - getRegistrationSecret(), - mode) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new); - }) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess((RegistrationSessionProcessor processor) -> { - if (processor.hasResult() && processor.isAllowedToRequestCode()) { - setCanSmsAtTime(processor.getNextCodeViaSmsAttempt()); - setCanCallAtTime(processor.getNextCodeViaCallAttempt()); - } - }); - } - - public Single validateSession(String e164) { - String storedSessionId = null; - if (e164.equals(getSessionE164())) { - storedSessionId = getSessionId(); - } - return verifyAccountRepository.validateSession(storedSessionId, e164, getRegistrationSecret()) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new); - } - - public Single getValidSession(String e164, @Nullable String mcc, @Nullable String mnc) { - return validateSession(e164) - .flatMap(processor -> { - if (processor.isInvalidSession()) { - return verifyAccountRepository.requestValidSession(e164, getRegistrationSecret(), mcc, mnc) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new) - .doOnSuccess(createSessionProcessor -> { - if (createSessionProcessor.pushChallengeTimedOut()) { - Log.w(TAG, "Registration push challenge timed out."); - markPushChallengeTimedOut(); - } - }); - } else { - return Single.just(processor); - } - }); - } - - public Single handleRequiredChallenges(RegistrationSessionProcessor processor, String e164) { - final String sessionId = processor.getSessionId(); - - if (processor.isAllowedToRequestCode()) { - Log.d(TAG, "All challenges satisfied."); - return Single.just(processor); - } - - if (hasCaptchaToken() && processor.captchaRequired(getExcludedChallenges())) { - Log.d(TAG, "Submitting completed captcha challenge"); - final String captcha = Objects.requireNonNull(getCaptchaToken()); - clearCaptchaResponse(); - return verifyAccountRepository.verifyCaptcha(sessionId, captcha, e164, getRegistrationSecret()) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new); - } else { - String challenge = processor.getChallenge(getExcludedChallenges()); - Log.d(TAG, "Handling challenge of type " + challenge); - if (challenge != null) { - switch (challenge) { - case RegistrationSessionProcessor.PUSH_CHALLENGE_KEY: - return verifyAccountRepository.requestAndVerifyPushToken(sessionId, - getNumber().getE164Number(), - getRegistrationSecret()) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForSession::new); - - case RegistrationSessionProcessor.CAPTCHA_KEY: - // fall through to passing the processor back so that the eventual subscriber will check captchaRequired() and handle accordingly - default: - break; - } - } - } - - return Single.just(processor); - } - - public Single verifyCodeWithoutRegistrationLock(@NonNull String code) { - onVerificationCodeEntered(code); - - return verifyAccountWithoutRegistrationLock() - .flatMap(response -> { - if (response.getResult().isPresent() && response.getResult().get().getMasterKey() != null) { - return onVerifySuccessWithRegistrationLock(new VerifyResponseWithRegistrationLockProcessor(response, null), response.getResult().get().getPin()); - } - - VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(response); - if (processor.hasResult()) { - return onVerifySuccess(processor); - } else if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) { - return Single.just(new VerifyResponseHitRegistrationLock(processor.getResponse())); - } - return Single.just(processor); - }) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess(processor -> { - if (processor.registrationLock() && !processor.isRegistrationLockPresentAndSvrExhausted()) { - setLockedTimeRemaining(processor.getLockedException().getTimeRemaining()); - setSvrTriesRemaining(processor.getSvrTriesRemaining()); - setSvrAuthCredentials(processor.getSvrAuthCredentials()); - } else if (processor.isRegistrationLockPresentAndSvrExhausted()) { - setLockedTimeRemaining(processor.getLockedException().getTimeRemaining()); - } - }); - } - - public Single verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) { - SvrAuthCredentialSet authCredentials = Objects.requireNonNull(getSvrAuthCredentials()); - - return verifyAccountWithRegistrationLock(pin, authCredentials) - .map(r -> new VerifyResponseWithRegistrationLockProcessor(r, authCredentials)) - .flatMap(processor -> { - if (processor.hasResult()) { - return onVerifySuccessWithRegistrationLock(processor, pin); - } else if (processor.wrongPin()) { - return Single.just(new VerifyResponseWithRegistrationLockProcessor(processor.getResponse(), authCredentials)); - } - return Single.just(processor); - }) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess(processor -> { - if (processor.wrongPin()) { - setSvrTriesRemaining(processor.getSvrTriesRemaining()); - } - }); - } - - protected abstract Single> verifyAccountWithoutRegistrationLock(); - - protected abstract Single> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull SvrAuthCredentialSet svrAuthCredentials); - - protected abstract Single onVerifySuccess(@NonNull VerifyResponseProcessor processor); - - protected abstract Single onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin); - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java deleted file mode 100644 index 4e8780f22d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.thoughtcrime.securesms.registration.viewmodel; - -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -public final class LocalCodeRequestRateLimiter implements Parcelable { - - private final long timePeriod; - private final Map dataMap; - - public LocalCodeRequestRateLimiter(long timePeriod) { - this.timePeriod = timePeriod; - this.dataMap = new HashMap<>(); - } - - @MainThread - public boolean canRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) { - Data data = dataMap.get(mode); - - return data == null || !data.limited(e164Number, currentTime); - } - - /** - * Call this when the server has returned that it was successful in requesting a code via the specified mode. - */ - @MainThread - public void onSuccessfulRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) { - dataMap.put(mode, new Data(e164Number, currentTime + timePeriod)); - } - - /** - * Call this if a mode was unsuccessful in sending. - */ - @MainThread - public void onUnsuccessfulRequest() { - dataMap.clear(); - } - - static class Data { - - final String e164Number; - final long limitedUntil; - - Data(@NonNull String e164Number, long limitedUntil) { - this.e164Number = e164Number; - this.limitedUntil = limitedUntil; - } - - boolean limited(String e164Number, long currentTime) { - return this.e164Number.equals(e164Number) && currentTime < limitedUntil; - } - } - - public static final Creator CREATOR = new Creator() { - @Override - public LocalCodeRequestRateLimiter createFromParcel(Parcel in) { - long timePeriod = in.readLong(); - int numberOfMapEntries = in.readInt(); - - LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod); - - for (int i = 0; i < numberOfMapEntries; i++) { - Mode mode = Mode.values()[in.readInt()]; - String e164Number = in.readString(); - long limitedUntil = in.readLong(); - - localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil)); - } - return localCodeRequestRateLimiter; - } - - @Override - public LocalCodeRequestRateLimiter[] newArray(int size) { - return new LocalCodeRequestRateLimiter[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(timePeriod); - dest.writeInt(dataMap.size()); - - for (Map.Entry a : dataMap.entrySet()) { - dest.writeInt(a.getKey().ordinal()); - dest.writeString(a.getValue().e164Number); - dest.writeLong(a.getValue().limitedUntil); - } - } -} - diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/ReRegisterWithPinViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/ReRegisterWithPinViewModel.kt deleted file mode 100644 index 9b10098372..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/ReRegisterWithPinViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.thoughtcrime.securesms.registration.viewmodel - -import androidx.lifecycle.ViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.subjects.BehaviorSubject - -/** - * Used during re-registration flow when pin entry is required to skip SMS verification. Mostly tracks - * guesses remaining in both the local and remote check flows. - */ -class ReRegisterWithPinViewModel : ViewModel() { - var isLocalVerification: Boolean = false - private set - - var hasIncorrectGuess: Boolean = false - - private val _triesRemaining: BehaviorSubject = BehaviorSubject.createDefault(10) - val triesRemaining: Observable = _triesRemaining.observeOn(AndroidSchedulers.mainThread()) - - fun updateSvrTriesRemaining(triesRemaining: Int?) { - if (triesRemaining == null) { - isLocalVerification = true - if (hasIncorrectGuess) { - _triesRemaining.onNext((_triesRemaining.value!! - 1).coerceAtLeast(0)) - } - } else { - _triesRemaining.onNext(triesRemaining) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java deleted file mode 100644 index 8795dc7bd3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ /dev/null @@ -1,458 +0,0 @@ -package org.thoughtcrime.securesms.registration.viewmodel; - -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; -import androidx.lifecycle.AbstractSavedStateViewModelFactory; -import androidx.lifecycle.SavedStateHandle; -import androidx.lifecycle.ViewModel; -import androidx.savedstate.SavedStateRegistryOwner; - -import org.signal.core.util.Stopwatch; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob; -import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.pin.SvrWrongPinException; -import org.thoughtcrime.securesms.pin.SvrRepository; -import org.thoughtcrime.securesms.registration.RegistrationData; -import org.thoughtcrime.securesms.registration.RegistrationRepository; -import org.thoughtcrime.securesms.registration.RegistrationSessionProcessor; -import org.thoughtcrime.securesms.registration.VerifyAccountRepository; -import org.thoughtcrime.securesms.registration.VerifyResponse; -import org.thoughtcrime.securesms.registration.VerifyResponseProcessor; -import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor; -import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs; -import org.thoughtcrime.securesms.util.RemoteConfig; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.api.SvrNoDataException; -import org.whispersystems.signalservice.api.kbs.MasterKey; -import org.whispersystems.signalservice.api.kbs.PinHashUtil; -import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException; -import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException; -import org.whispersystems.signalservice.internal.ServiceResponse; -import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse; -import org.signal.core.util.Base64; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public final class RegistrationViewModel extends BaseRegistrationViewModel { - - private static final String TAG = Log.tag(RegistrationViewModel.class); - - 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_BACKUP_COMPLETED = "BACKUP_COMPLETED"; - - private final RegistrationRepository registrationRepository; - - private boolean userSkippedReRegisterFlow = false; - private boolean autoShowSmsConfirmDialog = false; - - public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle, - boolean isReregister, - @NonNull VerifyAccountRepository verifyAccountRepository, - @NonNull RegistrationRepository registrationRepository) - { - super(savedStateHandle, verifyAccountRepository, Util.getSecret(18)); - - this.registrationRepository = registrationRepository; - - setInitialDefaultValue(STATE_RESTORE_FLOW_SHOWN, false); - setInitialDefaultValue(STATE_BACKUP_COMPLETED, false); - - this.savedState.set(STATE_IS_REREGISTER, isReregister); - } - - public boolean isReregister() { - //noinspection ConstantConditions - return savedState.get(STATE_IS_REREGISTER); - } - - public void onNumberDetected(int countryCode, String nationalNumber) { - setViewState(getNumber().toBuilder() - .countryCode(countryCode) - .nationalNumber(nationalNumber) - .build()); - } - - public @Nullable String getFcmToken() { - String token = savedState.get(STATE_FCM_TOKEN); - if (token == null || token.isEmpty()) { - return null; - } - return token; - } - - @MainThread - public void setFcmToken(@Nullable String fcmToken) { - savedState.set(STATE_FCM_TOKEN, fcmToken); - } - - public void setWelcomeSkippedOnRestore() { - savedState.set(STATE_RESTORE_FLOW_SHOWN, true); - } - - public boolean hasRestoreFlowBeenShown() { - //noinspection ConstantConditions - return savedState.get(STATE_RESTORE_FLOW_SHOWN); - } - - public void setIsReregister(boolean isReregister) { - savedState.set(STATE_IS_REREGISTER, isReregister); - } - - public void markBackupCompleted() { - savedState.set(STATE_BACKUP_COMPLETED, true); - } - - public boolean hasBackupCompleted() { - Boolean completed = savedState.get(STATE_BACKUP_COMPLETED); - return completed != null ? completed : false; - } - - public boolean hasUserSkippedReRegisterFlow() { - return userSkippedReRegisterFlow; - } - - public void setUserSkippedReRegisterFlow(boolean userSkippedReRegisterFlow) { - Log.i(TAG, "User skipped re-register flow."); - this.userSkippedReRegisterFlow = userSkippedReRegisterFlow; - if (userSkippedReRegisterFlow) { - setAutoShowSmsConfirmDialog(true); - } - } - - public boolean shouldAutoShowSmsConfirmDialog() { - return autoShowSmsConfirmDialog; - } - - public void setAutoShowSmsConfirmDialog(boolean autoShowSmsConfirmDialog) { - this.autoShowSmsConfirmDialog = autoShowSmsConfirmDialog; - } - - @Override - protected Single> verifyAccountWithoutRegistrationLock() { - final String sessionId = getSessionId(); - if (sessionId == null) { - throw new IllegalStateException("No valid registration session"); - } - return verifyAccountRepository.verifyAccount(sessionId, getRegistrationData()) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess(processor -> { - if (processor.hasResult()) { - setCanSmsAtTime(processor.getNextCodeViaSmsAttempt()); - setCanCallAtTime(processor.getNextCodeViaCallAttempt()); - } - }) - .observeOn(Schedulers.io()) - .flatMap(processor -> { - if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) { - return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), null, null); - } else if (processor.getError() == null) { - return Single.just(ServiceResponse.forApplicationError(new IncorrectCodeException(), 403, null)); - } else { - return Single.just(ServiceResponse.coerceError(processor.getResponse())); - } - }) - .flatMap(verifyAccountWithoutKbsResponse -> { - VerifyResponseProcessor processor = new VerifyResponseWithoutKbs(verifyAccountWithoutKbsResponse); - String pin = SignalStore.svr().getPin(); - - if ((processor.isRegistrationLockPresentAndSvrExhausted() || processor.registrationLock()) && SignalStore.svr().getRegistrationLockToken() != null && pin != null) { - return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> SignalStore.svr().getOrCreateMasterKey()) - .map(verifyAccountWithPinResponse -> { - if (verifyAccountWithPinResponse.getResult().isPresent() && verifyAccountWithPinResponse.getResult().get().getMasterKey() != null) { - return verifyAccountWithPinResponse; - } else { - return verifyAccountWithoutKbsResponse; - } - }); - } else { - return Single.just(verifyAccountWithoutKbsResponse); - } - }) - .onErrorReturn(ServiceResponse::forUnknownError); - } - - @Override - protected Single> verifyAccountWithRegistrationLock(@NonNull String pin, @NonNull SvrAuthCredentialSet svrAuthCredentials) { - final String sessionId = getSessionId(); - if (sessionId == null) { - throw new IllegalStateException("No valid registration session"); - } - return verifyAccountRepository.verifyAccount(sessionId, getRegistrationData()) - .map(RegistrationSessionProcessor.RegistrationSessionProcessorForVerification::new) - .doOnSuccess(processor -> { - if (processor.hasResult()) { - setCanSmsAtTime(processor.getNextCodeViaSmsAttempt()); - setCanCallAtTime(processor.getNextCodeViaCallAttempt()); - } - }) - .>flatMap(processor -> { - if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) { - return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> SvrRepository.restoreMasterKeyPreRegistration(svrAuthCredentials, pin)); - } else { - return Single.just(ServiceResponse.coerceError(processor.getResponse())); - } - }) - .onErrorReturn(ServiceResponse::forUnknownError); - } - - @Override - protected Single onVerifySuccess(@NonNull VerifyResponseProcessor processor) { - return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), false) - .map(VerifyResponseWithoutKbs::new); - } - - @Override - protected Single onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin) { - return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), true) - .map(processor::updatedIfRegistrationFailed); - } - - private RegistrationData getRegistrationData() { - return new RegistrationData(getTextCodeEntered(), - getNumber().getE164Number(), - getRegistrationSecret(), - registrationRepository.getRegistrationId(), - registrationRepository.getProfileKey(getNumber().getE164Number()), - getFcmToken(), - registrationRepository.getPniRegistrationId(), - getSessionId() != null ? null : getRecoveryPassword()); - } - - public @NonNull Single verifyReRegisterWithPin(@NonNull String pin) { - return Single.fromCallable(() -> verifyReRegisterWithPinInternal(pin)) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .flatMap(data -> { - if (data.canProceed) { - return updateFcmTokenValue().subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .onErrorReturnItem("") - .flatMap(s -> verifyReRegisterWithRecoveryPassword(pin, data.masterKey)); - } else { - throw new IncorrectRegistrationRecoveryPasswordException(); - } - }) - .onErrorReturn(t -> new VerifyResponseWithRegistrationLockProcessor(ServiceResponse.forUnknownError(t), getSvrAuthCredentials())) - .map(p -> { - if (p instanceof VerifyResponseWithRegistrationLockProcessor) { - VerifyResponseWithRegistrationLockProcessor lockProcessor = (VerifyResponseWithRegistrationLockProcessor) p; - if (lockProcessor.wrongPin() && lockProcessor.getSvrTriesRemaining() != null) { - return new VerifyResponseWithRegistrationLockProcessor(lockProcessor.getResponse(), lockProcessor.getSvrAuthCredentials()); - } - } - - return p; - }) - .doOnSuccess(p -> { - if (p.hasResult()) { - restoreFromStorageService(); - } - }) - .observeOn(AndroidSchedulers.mainThread()); - } - - @WorkerThread - private @NonNull ReRegistrationData verifyReRegisterWithPinInternal(@NonNull String pin) - throws SvrWrongPinException, IOException, SvrNoDataException - { - String localPinHash = SignalStore.svr().getLocalPinHash(); - - if (hasRecoveryPassword() && localPinHash != null) { - if (PinHashUtil.verifyLocalPinHash(localPinHash, pin)) { - Log.i(TAG, "Local pin matches input, attempting registration"); - return ReRegistrationData.canProceed(SignalStore.svr().getOrCreateMasterKey()); - } else { - throw new SvrWrongPinException(0); - } - } else { - SvrAuthCredentialSet authCredentials = getSvrAuthCredentials(); - if (authCredentials == null) { - Log.w(TAG, "No SVR auth credentials, abort skip flow"); - return ReRegistrationData.cannotProceed(); - } - - MasterKey masterKey = SvrRepository.restoreMasterKeyPreRegistration(authCredentials, pin); - - setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword()); - setSvrTriesRemaining(10); - return ReRegistrationData.canProceed(masterKey); - } - } - - private Single verifyReRegisterWithRecoveryPassword(@NonNull String pin, @NonNull MasterKey masterKey) { - RegistrationData registrationData = getRegistrationData(); - if (registrationData.getRecoveryPassword() == null) { - throw new IllegalStateException("No valid recovery password"); - } - - return verifyAccountRepository.registerAccount(null, registrationData, null, null) - .observeOn(Schedulers.io()) - .onErrorReturn(ServiceResponse::forUnknownError) - .map(VerifyResponseWithoutKbs::new) - .flatMap(processor -> { - if (processor.registrationLock()) { - setSvrAuthCredentials(processor.getSvrAuthCredentials()); - return verifyAccountRepository.registerAccount(null, registrationData, pin, () -> masterKey) - .onErrorReturn(ServiceResponse::forUnknownError) - .map(r -> new VerifyResponseWithRegistrationLockProcessor(r, processor.getSvrAuthCredentials())); - } else { - return Single.just(processor); - } - }) - .flatMap(processor -> { - if (processor.hasResult()) { - VerifyResponse verifyResponse = processor.getResult(); - boolean setRegistrationLockEnabled = verifyResponse.getMasterKey() != null; - - if (!setRegistrationLockEnabled) { - verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), masterKey, pin, verifyResponse.getAciPreKeyCollection(), verifyResponse.getPniPreKeyCollection()); - } - - return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled) - .map(r -> new VerifyResponseWithRegistrationLockProcessor(r, getSvrAuthCredentials())); - } else { - return Single.just(processor); - } - }); - } - - public @NonNull Single canEnterSkipSmsFlow() { - if (userSkippedReRegisterFlow) { - Log.d(TAG, "User skipped re-register flow."); - return Single.just(false); - } - - Log.d(TAG, "Querying if user can enter skip SMS flow."); - return Single.just(hasRecoveryPassword()) - .flatMap(hasRecoveryPassword -> { - Log.i(TAG, "Checking if user has existing recovery password: " + hasRecoveryPassword); - if (hasRecoveryPassword) { - return Single.just(true); - } else { - return checkForValidSvrAuthCredentials(); - } - }); - } - - private Single checkForValidSvrAuthCredentials() { - final List svrAuthTokenList = SignalStore.svr().getSvr2AuthTokens(); - List usernamePasswords = svrAuthTokenList - .stream() - .limit(10) - .map(t -> { - try { - return new String(Base64.decode(t.replace("Basic ", "").trim()), StandardCharsets.ISO_8859_1); - } catch (IOException e) { - return null; - } - }) - .collect(Collectors.toList()); - - if (usernamePasswords.isEmpty()) { - Log.d(TAG, "No valid SVR tokens in local store."); - return Single.just(false); - } - - Log.d(TAG, "Valid tokens in local store, validating with SVR."); - return registrationRepository.getSvrAuthCredential(getRegistrationData(), usernamePasswords) - .flatMap(p -> { - if (p.hasValidSvr2AuthCredential()) { - Log.d(TAG, "Saving valid SVR2 auth credential."); - setSvrAuthCredentials(new SvrAuthCredentialSet(p.requireSvr2AuthCredential(), null)); - return Single.just(true); - } else { - Log.d(TAG, "SVR2 response contained no valid SVR2 auth credentials."); - return Single.just(false); - } - }) - .onErrorReturnItem(false) - .observeOn(AndroidSchedulers.mainThread()); - } - - public Single updateFcmTokenValue() { - return verifyAccountRepository.getFcmToken().observeOn(AndroidSchedulers.mainThread()).doOnSuccess(this::setFcmToken); - } - - private void restoreFromStorageService() { - SignalStore.onboarding().clearAll(); - - Stopwatch stopwatch = new Stopwatch("ReRegisterRestore"); - - AppDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN); - stopwatch.split("AccountRestore"); - - AppDependencies - .getJobManager() - .startChain(new StorageSyncJob()) - .then(new ReclaimUsernameAndLinkJob()) - .enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10)); - stopwatch.split("ContactRestore"); - - try { - RemoteConfig.refreshSync(); - } catch (IOException e) { - Log.w(TAG, "Failed to refresh flags.", e); - } - stopwatch.split("RemoteConfig"); - - stopwatch.stop(TAG); - } - - private boolean hasRecoveryPassword() { - return getRecoveryPassword() != null && Objects.equals(getRegistrationData().getE164(), SignalStore.account().getE164()); - } - - private static class ReRegistrationData { - public boolean canProceed; - public MasterKey masterKey; - - private ReRegistrationData(boolean canProceed, @Nullable MasterKey masterKey) { - this.canProceed = canProceed; - this.masterKey = masterKey; - } - - public static ReRegistrationData cannotProceed() { - return new ReRegistrationData(false, null); - } - - public static ReRegistrationData canProceed(@NonNull MasterKey masterKey) { - return new ReRegistrationData(true, masterKey); - } - } - - public static final class Factory extends AbstractSavedStateViewModelFactory { - private final boolean isReregister; - - public Factory(@NonNull SavedStateRegistryOwner owner, boolean isReregister) { - super(owner, null); - this.isReregister = isReregister; - } - - @Override - protected @NonNull T create(@NonNull String key, @NonNull Class modelClass, @NonNull SavedStateHandle handle) { - //noinspection ConstantConditions - return modelClass.cast(new RegistrationViewModel(handle, - isReregister, - new VerifyAccountRepository(AppDependencies.getApplication()), - new RegistrationRepository(AppDependencies.getApplication()))); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt index 6dd45b2b4e..70a236c45f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity +import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/choosebackup/ChooseBackupV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/choosebackup/ChooseBackupFragment.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/restore/choosebackup/ChooseBackupV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/restore/choosebackup/ChooseBackupFragment.kt index cdc6dea473..5c56cbb979 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/choosebackup/ChooseBackupV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/choosebackup/ChooseBackupFragment.kt @@ -21,7 +21,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentChooseBackupV2Binding +import org.thoughtcrime.securesms.databinding.FragmentChooseBackupBinding import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate import org.thoughtcrime.securesms.restore.RestoreViewModel @@ -30,9 +30,9 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate /** * This fragment presents a button to the user to browse their local file system for a legacy backup file. */ -class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v2) { +class ChooseBackupFragment : LoggingFragment(R.layout.fragment_choose_backup) { private val sharedViewModel by activityViewModels() - private val binding: FragmentChooseBackupV2Binding by ViewBinderDelegate(FragmentChooseBackupV2Binding::bind) + private val binding: FragmentChooseBackupBinding by ViewBinderDelegate(FragmentChooseBackupBinding::bind) private val pickMedia = registerForActivityResult(BackupFileContract()) { if (it != null) { @@ -57,7 +57,7 @@ class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v private fun onUserChoseBackupFile(backupFileUri: Uri) { sharedViewModel.setBackupFileUri(backupFileUri) - NavHostFragment.findNavController(this).safeNavigate(ChooseBackupV2FragmentDirections.actionChooseLocalBackupFragmentToRestoreLocalBackupFragment()) + NavHostFragment.findNavController(this).safeNavigate(ChooseBackupFragmentDirections.actionChooseLocalBackupFragmentToRestoreLocalBackupFragment()) } private class BackupFileContract : ActivityResultContracts.GetContent() { @@ -72,6 +72,6 @@ class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v } companion object { - private val TAG = Log.tag(ChooseBackupV2Fragment::class.java) + private val TAG = Log.tag(ChooseBackupFragment::class.java) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/devicetransfer/DeviceTransferV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/devicetransfer/DeviceTransferFragment.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/restore/devicetransfer/DeviceTransferV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/restore/devicetransfer/DeviceTransferFragment.kt index 3d23650d45..4e511edf63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/devicetransfer/DeviceTransferV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/devicetransfer/DeviceTransferFragment.kt @@ -19,15 +19,15 @@ import org.signal.devicetransfer.TransferStatus import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentDeviceTransferV2Binding +import org.thoughtcrime.securesms.databinding.FragmentDeviceTransferBinding import org.thoughtcrime.securesms.restore.RestoreViewModel import org.thoughtcrime.securesms.util.visible -sealed class DeviceTransferV2Fragment : LoggingFragment(R.layout.fragment_device_transfer_v2) { +sealed class DeviceTransferFragment : LoggingFragment(R.layout.fragment_device_transfer) { private val onBackPressed = OnBackPressed() private val transferModeListener = TransferModeListener() protected val navigationViewModel: RestoreViewModel by activityViewModels() - protected val binding: FragmentDeviceTransferV2Binding by ViewBinderDelegate(FragmentDeviceTransferV2Binding::bind) + protected val binding: FragmentDeviceTransferBinding by ViewBinderDelegate(FragmentDeviceTransferBinding::bind) protected var transferFinished: Boolean = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteFragment.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteFragment.kt index 661d8ddedd..44b1979a77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteFragment.kt @@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.restore.RestoreActivity * This approximates that by taking the place of [EnterPhoneNumberFragment], * then bridging us back to [RegistrationV2Activity] by immediately closing the [RestoreActivity]. */ -class RestoreCompleteV2Fragment : LoggingFragment(R.layout.fragment_registration_blank) { +class RestoreCompleteFragment : LoggingFragment(R.layout.fragment_registration_blank) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -33,6 +33,6 @@ class RestoreCompleteV2Fragment : LoggingFragment(R.layout.fragment_registration } companion object { - private val TAG = Log.tag(RestoreCompleteV2Fragment::class.java) + private val TAG = Log.tag(RestoreCompleteFragment::class.java) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/PassphraseAsYouTypeFormatter.java b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/PassphraseAsYouTypeFormatter.java new file mode 100644 index 0000000000..5bfa875390 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/PassphraseAsYouTypeFormatter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.restore.restorelocalbackup; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Editable; +import android.text.Spanned; +import android.text.TextWatcher; +import android.text.style.ReplacementSpan; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.util.BackupUtil; + +public class PassphraseAsYouTypeFormatter implements TextWatcher { + + private static final int GROUP_SIZE = 5; + + @Override + public void afterTextChanged(Editable editable) { + removeSpans(editable); + + addSpans(editable); + } + + private static void removeSpans(Editable editable) { + SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class); + + for (SpaceSpan span : paddingSpans) { + editable.removeSpan(span); + } + } + + private static void addSpans(Editable editable) { + final int length = editable.length(); + + for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) { + editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (editable.length() > BackupUtil.PASSPHRASE_LENGTH) { + editable.delete(BackupUtil.PASSPHRASE_LENGTH, editable.length()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + /** + * A {@link ReplacementSpan} adds a small space after a single character. + * Based on https://stackoverflow.com/a/51949578 + */ + private static class SpaceSpan extends ReplacementSpan { + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + return (int) (paint.measureText(text, start, end) * 1.7f); + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { + canvas.drawText(text.subSequence(start, end).toString(), x, y, paint); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt index 086f2afcd4..3b092babee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt @@ -23,10 +23,9 @@ import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.BackupEvent import org.thoughtcrime.securesms.components.ViewBinderDelegate -import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Binding +import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupBinding import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView -import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.restore.RestoreRepository import org.thoughtcrime.securesms.restore.RestoreViewModel @@ -41,7 +40,7 @@ import java.util.Locale /** * This fragment is used to monitor and manage an in-progress backup restore. */ -class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_local_backup_v2) { +class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_local_backup) { private val navigationViewModel: RestoreViewModel by activityViewModels() private val restoreLocalBackupViewModel: RestoreLocalBackupViewModel by viewModels( factoryProducer = ViewModelFactory.factoryProducer { @@ -49,7 +48,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc RestoreLocalBackupViewModel(fileBackupUri) } ) - private val binding: FragmentRestoreLocalBackupV2Binding by ViewBinderDelegate(FragmentRestoreLocalBackupV2Binding::bind) + private val binding: FragmentRestoreLocalBackupBinding by ViewBinderDelegate(FragmentRestoreLocalBackupBinding::bind) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -68,8 +67,6 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc findNavController().navigateUp() } - // TODO [regv2]: check for re-register and skip ahead to phone number entry - if (SignalStore.settings.isBackupEnabled) { Log.i(TAG, "Backups enabled, so a backup must have been previously restored.") onBackupCompletedSuccessfully() @@ -158,7 +155,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc private fun presentProgressEnded() { binding.restoreButton.cancelSpinning() binding.cancelLocalRestoreButton.visible = true - binding.backupProgressText.text = "" + binding.backupProgressText.text = null } private fun presentRestoreProgress(backupProgressCount: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt index d65f267c19..3eae9cc613 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt @@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.databinding.TransferOrRestoreOptionsBottomShee import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity -import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity +import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.util.visible diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt index 12c97dd876..a2818976a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt @@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreV2Binding import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate -import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity +import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity import org.thoughtcrime.securesms.restore.RestoreViewModel import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.SpanUtil @@ -74,7 +74,7 @@ class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_r NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionNewDeviceTransferInstructions()) } BackupRestorationType.LOCAL_BACKUP -> { - NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToRestore()) + NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToLocalRestore()) } BackupRestorationType.REMOTE_BACKUP -> { startActivity(RemoteRestoreActivity.getIntent(requireContext())) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index 7e96aeb261..eb36c63098 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -50,7 +50,7 @@ import org.thoughtcrime.securesms.main.SearchBinder import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet import org.thoughtcrime.securesms.stories.StoryTextPostModel import org.thoughtcrime.securesms.stories.StoryViewerArgs @@ -187,7 +187,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l } R.id.reminder_action_re_register -> { - startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext())) + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java index eefb874eb7..fdd522cc48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java @@ -24,7 +24,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity; public class Dialogs { public static void showAlertDialog(Context context, String title, String message) { @@ -85,7 +85,7 @@ public class Dialogs { .setMessage(R.string.ReregisterSignalDialog__message) .setNegativeButton(R.string.ReregisterSignalDialog__cancel_action, null) .setPositiveButton(R.string.ReregisterSignalDialog__reregister_action, (d, w) -> { - context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context)); + context.startActivity(RegistrationActivity.newIntentForReRegistration(context)); }) .show(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 5e6ba4fa5e..6d5962cdd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1012,16 +1012,6 @@ object RemoteConfig { hotSwappable = true ) - /** Whether or not to use the V2 refactor of registration. */ - @JvmStatic - @get:JvmName("registrationV2") - val registrationV2: Boolean by remoteBoolean( - key = "android.registration.v2", - defaultValue = true, - hotSwappable = false, - active = false - ) - /** Whether unauthenticated chat web socket is backed by libsignal-net */ @JvmStatic @get:JvmName("libSignalWebSocketEnabled") diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index a8bb20fa38..e4a444523e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationIds; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity; import java.util.ArrayList; import java.util.Arrays; @@ -1144,7 +1144,7 @@ public class TextSecurePreferences { private static void notifyUnregisteredReceived(Context context) { PendingIntent reRegistrationIntent = PendingIntent.getActivity(context, 0, - RegistrationNavigationActivity.newIntentForReRegistration(context), + RegistrationActivity.newIntentForReRegistration(context), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntentFlags.immutable()); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) .setSmallIcon(R.drawable.ic_signal_logo_large) diff --git a/app/src/main/res/layout/activity_registration_navigation.xml b/app/src/main/res/layout/activity_registration_navigation.xml deleted file mode 100644 index 6c015abb3c..0000000000 --- a/app/src/main/res/layout/activity_registration_navigation.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_registration_navigation_v2.xml b/app/src/main/res/layout/activity_registration_navigation_v2.xml index 37a71416cd..a0e54602d5 100644 --- a/app/src/main/res/layout/activity_registration_navigation_v2.xml +++ b/app/src/main/res/layout/activity_registration_navigation_v2.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".registration.v2.ui.RegistrationV2Activity"> + tools:context=".registration.ui.RegistrationActivity"> + app:navGraph="@navigation/registration" /> \ No newline at end of file diff --git a/app/src/main/res/layout/device_transfer_fragment.xml b/app/src/main/res/layout/device_transfer_fragment.xml deleted file mode 100644 index 627cc63b29..0000000000 --- a/app/src/main/res/layout/device_transfer_fragment.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_change_number_enter_code.xml b/app/src/main/res/layout/fragment_change_number_enter_code.xml index 20e4aa889d..80aa9fea95 100644 --- a/app/src/main/res/layout/fragment_change_number_enter_code.xml +++ b/app/src/main/res/layout/fragment_change_number_enter_code.xml @@ -7,7 +7,7 @@ diff --git a/app/src/main/res/layout/fragment_change_number_enter_phone_number_v2.xml b/app/src/main/res/layout/fragment_change_number_enter_phone_number_v2.xml deleted file mode 100644 index 2de821b8eb..0000000000 --- a/app/src/main/res/layout/fragment_change_number_enter_phone_number_v2.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_change_phone_number.xml b/app/src/main/res/layout/fragment_change_phone_number.xml index 8ca3153f7c..559404fa1e 100644 --- a/app/src/main/res/layout/fragment_change_phone_number.xml +++ b/app/src/main/res/layout/fragment_change_phone_number.xml @@ -1,7 +1,5 @@ diff --git a/app/src/main/res/layout/fragment_change_phone_number_v2.xml b/app/src/main/res/layout/fragment_change_phone_number_v2.xml deleted file mode 100644 index 559404fa1e..0000000000 --- a/app/src/main/res/layout/fragment_change_phone_number_v2.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_choose_backup_v2.xml b/app/src/main/res/layout/fragment_choose_backup.xml similarity index 100% rename from app/src/main/res/layout/fragment_choose_backup_v2.xml rename to app/src/main/res/layout/fragment_choose_backup.xml diff --git a/app/src/main/res/layout/fragment_device_transfer_v2.xml b/app/src/main/res/layout/fragment_device_transfer.xml similarity index 100% rename from app/src/main/res/layout/fragment_device_transfer_v2.xml rename to app/src/main/res/layout/fragment_device_transfer.xml diff --git a/app/src/main/res/layout/fragment_registration_captcha.xml b/app/src/main/res/layout/fragment_registration_captcha.xml index 26cffc8a82..5087d02906 100644 --- a/app/src/main/res/layout/fragment_registration_captcha.xml +++ b/app/src/main/res/layout/fragment_registration_captcha.xml @@ -1,7 +1,6 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_choose_backup.xml b/app/src/main/res/layout/fragment_registration_choose_backup.xml deleted file mode 100644 index 40bc0670b9..0000000000 --- a/app/src/main/res/layout/fragment_registration_choose_backup.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_enter_code.xml b/app/src/main/res/layout/fragment_registration_enter_code.xml index 2d524588e8..8e833dfc7c 100644 --- a/app/src/main/res/layout/fragment_registration_enter_code.xml +++ b/app/src/main/res/layout/fragment_registration_enter_code.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context=".registration.fragments.EnterSmsCodeFragment" - tools:viewBindingIgnore="true"> + tools:context=".registration.ui.entercode.EnterCodeFragment"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_enter_phone_number.xml b/app/src/main/res/layout/fragment_registration_enter_phone_number.xml index 858daeade6..ee3ec61304 100644 --- a/app/src/main/res/layout/fragment_registration_enter_phone_number.xml +++ b/app/src/main/res/layout/fragment_registration_enter_phone_number.xml @@ -6,8 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - tools:context=".registration.fragments.EnterPhoneNumberFragment" - tools:viewBindingIgnore="true"> + tools:context="org.thoughtcrime.securesms.registration.ui.phonenumber.EnterPhoneNumberFragment"> @@ -62,14 +62,15 @@ android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/RegistrationActivity_phone_number_description" - app:materialThemeOverlay="@style/Signal.ThemeOverlay.TextInputLayout"> + android:theme="@style/Signal.ThemeOverlay.TextInputLayout" + app:editTextStyle="@style/Signal.ThemeOverlay.TextInputLayout"> + android:inputType="phone" + android:textColor="?attr/colorOnSurface"> diff --git a/app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml b/app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml deleted file mode 100644 index 2caf1ee412..0000000000 --- a/app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_welcome.xml b/app/src/main/res/layout/fragment_registration_welcome.xml index 9384ff8a10..5f5a350be4 100644 --- a/app/src/main/res/layout/fragment_registration_welcome.xml +++ b/app/src/main/res/layout/fragment_registration_welcome.xml @@ -1,7 +1,5 @@ diff --git a/app/src/main/res/layout/fragment_registration_welcome_v2.xml b/app/src/main/res/layout/fragment_registration_welcome_v2.xml deleted file mode 100644 index 5f5a350be4..0000000000 --- a/app/src/main/res/layout/fragment_registration_welcome_v2.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_restore_local_backup_v2.xml b/app/src/main/res/layout/fragment_restore_local_backup.xml similarity index 100% rename from app/src/main/res/layout/fragment_restore_local_backup_v2.xml rename to app/src/main/res/layout/fragment_restore_local_backup.xml diff --git a/app/src/main/res/navigation/app_settings_change_number.xml b/app/src/main/res/navigation/app_settings_change_number.xml index cf70228145..f0d8eaf50c 100644 --- a/app/src/main/res/navigation/app_settings_change_number.xml +++ b/app/src/main/res/navigation/app_settings_change_number.xml @@ -63,7 +63,6 @@ @@ -74,7 +73,6 @@ tools:layout="@layout/fragment_change_phone_number_verify"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/app_settings_with_change_number_v2.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml similarity index 99% rename from app/src/main/res/navigation/app_settings_with_change_number_v2.xml rename to app/src/main/res/navigation/app_settings_with_change_number.xml index a21bbea854..d6c0bbcc79 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number_v2.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -197,7 +197,7 @@ app:popExitAnim="@anim/fragment_close_exit" /> - + + android:id="@+id/signup_v2" + app:startDestination="@id/welcomeV2Fragment"> - - - - - - - - + app:argType="org.thoughtcrime.securesms.registration.ui.grantpermissions.GrantPermissionsFragment$WelcomeAction" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/registration_v2.xml b/app/src/main/res/navigation/registration_v2.xml deleted file mode 100644 index 7e561f46c9..0000000000 --- a/app/src/main/res/navigation/registration_v2.xml +++ /dev/null @@ -1,243 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/restore.xml b/app/src/main/res/navigation/restore.xml index 5f166150f9..a9870015e0 100644 --- a/app/src/main/res/navigation/restore.xml +++ b/app/src/main/res/navigation/restore.xml @@ -28,7 +28,7 @@ app:destination="@+id/transferOrRestoreMoreOptionsDialog" /> + tools:layout="@layout/fragment_choose_backup"> - - + tools:layout="@layout/fragment_restore_local_backup"> + tools:layout="@layout/fragment_device_transfer"> + android:name="org.thoughtcrime.securesms.restore.restorecomplete.RestoreCompleteFragment" /> \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java index 353c14ec55..5457bdfceb 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashKbsDataTest.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; import org.signal.core.util.StreamUtil; import org.signal.libsignal.svr2.PinHash; -import org.thoughtcrime.securesms.registration.v2.testdata.KbsTestVector; +import org.thoughtcrime.securesms.registration.testdata.KbsTestVector; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.kbs.KbsData; import org.whispersystems.signalservice.api.kbs.MasterKey; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java index e05605a08f..08ab6ecf42 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinHashUtil_normalize_Test.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; import org.signal.core.util.StreamUtil; -import org.thoughtcrime.securesms.registration.v2.testdata.PinSanitationVector; +import org.thoughtcrime.securesms.registration.testdata.PinSanitationVector; import org.whispersystems.signalservice.api.kbs.PinHashUtil; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.JsonUtil; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java index 380876fabf..611b8f6628 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/PinValidityChecker_validity_Test.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.registration.v2; import org.junit.Test; import org.signal.core.util.StreamUtil; -import org.thoughtcrime.securesms.registration.v2.testdata.PinValidityVector; +import org.thoughtcrime.securesms.registration.testdata.PinValidityVector; import org.whispersystems.signalservice.api.kbs.PinValidityChecker; import org.whispersystems.signalservice.internal.util.JsonUtil; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java index 2aa3640cb3..4e5826fcca 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/KbsTestVector.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.registration.v2.testdata; +package org.thoughtcrime.securesms.registration.testdata; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java index 5c2485d267..3efb84c7b1 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinSanitationVector.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.registration.v2.testdata; +package org.thoughtcrime.securesms.registration.testdata; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinValidityVector.java b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinValidityVector.java index 099e055b72..7688e9ed31 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinValidityVector.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/v2/testdata/PinValidityVector.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.registration.v2.testdata; +package org.thoughtcrime.securesms.registration.testdata; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java deleted file mode 100644 index b15028f385..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.thoughtcrime.securesms.registration.viewmodel; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; - -public final class LocalCodeRequestRateLimiterTest { - - @Test - public void initially_can_request() { - LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - - assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); - } - - @Test - public void cant_request_within_same_time_period() { - LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - - assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); - - limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000); - - assertFalse(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000)); - } - - @Test - public void can_request_within_same_time_period_if_different_number() { - LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - - assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); - - limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000); - - assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000)); - } - - @Test - public void can_request_within_same_time_period_if_different_mode() { - LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - - assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); - - limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000); - - assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000)); - } - - @Test - public void can_request_after_time_period() { - LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - - assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); - - limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000); - - assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001)); - } - - @Test - public void can_request_within_same_time_period_if_an_unsuccessful_request_is_seen() { - LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - - assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); - - limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000); - - limiter.onUnsuccessfulRequest(); - - assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000)); - } -} \ No newline at end of file