From 07aa058a46dc7eed1188004de779f23d8db94ecb Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 6 Nov 2023 14:33:30 -0500 Subject: [PATCH] Update username consistency error handling. --- ...wnProfileJob__checkUsernameIsInSyncTest.kt | 181 ------------------ .../reminder/UsernameOutOfSyncReminder.kt | 30 ++- .../settings/app/AppSettingsActivity.kt | 7 +- .../settings/app/AppSettingsFragment.kt | 3 +- .../main/UsernameLinkSettingsViewModel.kt | 10 +- .../ConversationListFragment.java | 8 +- .../securesms/jobs/RefreshOwnProfileJob.java | 94 ++++----- .../securesms/keyvalue/AccountValues.kt | 34 +++- .../securesms/megaphone/Megaphones.java | 2 +- .../edit/EditSelfProfileRepository.java | 2 +- .../profiles/manage/EditProfileFragment.kt | 16 +- .../profiles/manage/EditProfileViewModel.java | 3 +- .../profiles/manage/UsernameEditFragment.java | 5 +- .../profiles/manage/UsernameRepository.kt | 10 +- .../main/res/layout/edit_profile_fragment.xml | 31 ++- app/src/main/res/navigation/app_settings.xml | 10 + app/src/main/res/values/ids.xml | 3 +- app/src/main/res/values/strings.xml | 4 +- .../NonSuccessfulResponseCodeException.java | 4 + 19 files changed, 209 insertions(+), 248 deletions(-) delete mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob__checkUsernameIsInSyncTest.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob__checkUsernameIsInSyncTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob__checkUsernameIsInSyncTest.kt deleted file mode 100644 index 814ff60fcf..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob__checkUsernameIsInSyncTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -package org.thoughtcrime.securesms.jobs - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import okhttp3.mockwebserver.MockResponse -import org.junit.After -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.signal.core.util.Base64 -import org.signal.libsignal.usernames.Username -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.testing.Delete -import org.thoughtcrime.securesms.testing.Get -import org.thoughtcrime.securesms.testing.Put -import org.thoughtcrime.securesms.testing.SignalActivityRule -import org.thoughtcrime.securesms.testing.failure -import org.thoughtcrime.securesms.testing.success -import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse -import org.whispersystems.signalservice.internal.push.WhoAmIResponse - -@Suppress("ClassName") -@RunWith(AndroidJUnit4::class) -class RefreshOwnProfileJob__checkUsernameIsInSyncTest { - - @get:Rule - val harness = SignalActivityRule() - - @After - fun tearDown() { - InstrumentationApplicationDependencyProvider.clearHandlers() - SignalStore.account().usernameOutOfSync = false - } - - @Test - fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() { - // GIVEN - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Delete("/v1/accounts/username_hash") { MockResponse().success() } - ) - - // WHEN - RefreshOwnProfileJob.checkUsernameIsInSync() - } - - @Test - fun givenLocalUsernameDoesNotMatchServerUsername_whenICheckUsernameIsInSync_thenIExpectRetry() { - // GIVEN - var didReserve = false - var didConfirm = false - val username = "hello.32" - val serverUsername = "hello.3232" - SignalDatabase.recipients.setUsername(harness.self.id, username) - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Get("/v1/accounts/whoami") { r -> - MockResponse().success( - WhoAmIResponse().apply { - usernameHash = Base64.encodeUrlSafeWithoutPadding(Username.hash(serverUsername)) - } - ) - }, - Put("/v1/accounts/username_hash/reserve") { r -> - didReserve = true - MockResponse().success(ReserveUsernameResponse(Base64.encodeUrlSafeWithoutPadding(Username.hash(username)))) - }, - Put("/v1/accounts/username_hash/confirm") { r -> - didConfirm = true - MockResponse().success() - } - ) - - // WHEN - RefreshOwnProfileJob.checkUsernameIsInSync() - - // THEN - assertTrue(didReserve) - assertTrue(didConfirm) - assertFalse(SignalStore.account().usernameOutOfSync) - } - - @Test - fun givenLocalAndNoServer_whenICheckUsernameIsInSync_thenIExpectRetry() { - // GIVEN - var didReserve = false - var didConfirm = false - val username = "hello.32" - SignalDatabase.recipients.setUsername(harness.self.id, username) - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Get("/v1/accounts/whoami") { r -> - MockResponse().success(WhoAmIResponse()) - }, - Put("/v1/accounts/username_hash/reserve") { r -> - didReserve = true - MockResponse().success(ReserveUsernameResponse(Base64.encodeUrlSafeWithoutPadding(Username.hash(username)))) - }, - Put("/v1/accounts/username_hash/confirm") { r -> - didConfirm = true - MockResponse().success() - } - ) - - // WHEN - RefreshOwnProfileJob.checkUsernameIsInSync() - - // THEN - assertTrue(didReserve) - assertTrue(didConfirm) - assertFalse(SignalStore.account().usernameOutOfSync) - } - - @Test - fun givenLocalAndServerMatch_whenICheckUsernameIsInSync_thenIExpectNoRetry() { - // GIVEN - var didReserve = false - var didConfirm = false - val username = "hello.32" - SignalDatabase.recipients.setUsername(harness.self.id, username) - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Get("/v1/accounts/whoami") { r -> - MockResponse().success( - WhoAmIResponse().apply { - usernameHash = Base64.encodeUrlSafeWithoutPadding(Username.hash(username)) - } - ) - }, - Put("/v1/accounts/username_hash/reserve") { r -> - didReserve = true - MockResponse().success(ReserveUsernameResponse(Base64.encodeUrlSafeWithoutPadding(Username.hash(username)))) - }, - Put("/v1/accounts/username_hash/confirm") { r -> - didConfirm = true - MockResponse().success() - } - ) - - // WHEN - RefreshOwnProfileJob.checkUsernameIsInSync() - - // THEN - assertFalse(didReserve) - assertFalse(didConfirm) - assertFalse(SignalStore.account().usernameOutOfSync) - } - - @Test - fun givenMismatchAndReservationFails_whenICheckUsernameIsInSync_thenIExpectNoConfirm() { - // GIVEN - var didReserve = false - var didConfirm = false - val username = "hello.32" - SignalDatabase.recipients.setUsername(harness.self.id, username) - InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( - Get("/v1/accounts/whoami") { r -> - MockResponse().success( - WhoAmIResponse().apply { - usernameHash = Base64.encodeUrlSafeWithoutPadding(Username.hash("${username}23")) - } - ) - }, - Put("/v1/accounts/username_hash/reserve") { r -> - didReserve = true - MockResponse().failure(418) - }, - Put("/v1/accounts/username_hash/confirm") { r -> - didConfirm = true - MockResponse().success() - } - ) - - // WHEN - RefreshOwnProfileJob.checkUsernameIsInSync() - - // THEN - assertTrue(didReserve) - assertFalse(didConfirm) - assertTrue(SignalStore.account().usernameOutOfSync) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt index 56e7edd673..b36642e051 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/UsernameOutOfSyncReminder.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.components.reminder +import android.content.Context import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.keyvalue.AccountValues.UsernameSyncState import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.FeatureFlags @@ -8,17 +10,31 @@ import org.thoughtcrime.securesms.util.FeatureFlags * Displays a reminder message when the local username gets out of sync with * what the server thinks our username is. */ -class UsernameOutOfSyncReminder : Reminder(R.string.UsernameOutOfSyncReminder__something_went_wrong) { +class UsernameOutOfSyncReminder : Reminder(NO_RESOURCE) { init { + val action = if (SignalStore.account().usernameSyncState == UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) { + R.id.reminder_action_fix_username_and_link + } else { + R.id.reminder_action_fix_username_link + } + addAction( Action( R.string.UsernameOutOfSyncReminder__fix_now, - R.id.reminder_action_fix_username + action ) ) } + override fun getText(context: Context): CharSequence { + return if (SignalStore.account().usernameSyncState == UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) { + context.getString(R.string.UsernameOutOfSyncReminder__username_and_link_corrupt) + } else { + context.getString(R.string.UsernameOutOfSyncReminder__link_corrupt) + } + } + override fun isDismissable(): Boolean { return false } @@ -26,7 +42,15 @@ class UsernameOutOfSyncReminder : Reminder(R.string.UsernameOutOfSyncReminder__s companion object { @JvmStatic fun isEligible(): Boolean { - return FeatureFlags.usernames() && SignalStore.account().usernameOutOfSync + return if (FeatureFlags.usernames()) { + when (SignalStore.account().usernameSyncState) { + UsernameSyncState.USERNAME_AND_LINK_CORRUPTED -> true + UsernameSyncState.LINK_CORRUPTED -> true + UsernameSyncState.IN_SYNC -> false + } + } else { + false + } } } } 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 21eeace1be..d42f820669 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 @@ -65,6 +65,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent { ) StartLocation.PRIVACY -> AppSettingsFragmentDirections.actionDirectToPrivacy() StartLocation.LINKED_DEVICES -> AppSettingsFragmentDirections.actionDirectToDevices() + StartLocation.USERNAME_LINK -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings() } } @@ -188,6 +189,9 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent { @JvmStatic fun linkedDevices(context: Context): Intent = getIntentForStartLocation(context, StartLocation.LINKED_DEVICES) + @JvmStatic + fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.USERNAME_LINK) + private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent { return Intent(context, AppSettingsActivity::class.java) .putExtra(ARG_NAV_GRAPH, R.navigation.app_settings) @@ -209,7 +213,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent { CREATE_NOTIFICATION_PROFILE(10), NOTIFICATION_PROFILE_DETAILS(11), PRIVACY(12), - LINKED_DEVICES(13); + LINKED_DEVICES(13), + USERNAME_LINK(14); companion object { fun fromCode(code: Int?): StartLocation { 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 3f3dcae30d..5f2bdc5513 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 @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.events.ReminderUpdateEvent +import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient @@ -358,7 +359,7 @@ class AppSettingsFragment : DSLSettingsFragment( summaryView.visibility = View.VISIBLE avatarView.visibility = View.VISIBLE - if (FeatureFlags.usernames()) { + if (FeatureFlags.usernames() && SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.IN_SYNC) { qrButton.visibility = View.VISIBLE qrButton.isClickable = true qrButton.setOnClickListener { model.onQrButtonClicked() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt index 6d16a316a2..84e31fa1e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/usernamelinks/main/UsernameLinkSettingsViewModel.kt @@ -52,6 +52,10 @@ class UsernameLinkSettingsViewModel : ViewModel() { qrCodeState = if (qrData.isPresent) QrCodeState.Present(qrData.get()) else QrCodeState.NotSet ) } + + if (SignalStore.account().usernameLink == null) { + onUsernameLinkReset() + } } override fun onCleared() { @@ -107,12 +111,16 @@ class UsernameLinkSettingsViewModel : ViewModel() { UsernameLinkState.NotSet }, usernameLinkResetResult = result, - qrCodeState = if (components.isPresent && previousQrValue != null) { + qrCodeState = if (!components.isPresent && previousQrValue != null) { QrCodeState.Present(previousQrValue) } else { QrCodeState.NotSet } ) + + if (components.isPresent) { + usernameLink.onNext(components) + } } } 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 1179dd1089..1fe776e624 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -110,6 +110,7 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; import org.thoughtcrime.securesms.components.reminder.UsernameOutOfSyncReminder; +import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment; import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate; import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation; @@ -176,6 +177,7 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalProxyUtil; import org.thoughtcrime.securesms.util.SnapToTopDataObserver; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.WindowUtil; import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter; @@ -794,8 +796,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode CdsTemporaryErrorBottomSheet.show(getChildFragmentManager()); } else if (reminderActionId == R.id.reminder_action_cds_permanent_error_learn_more) { CdsPermanentErrorBottomSheet.show(getChildFragmentManager()); - } else if (reminderActionId == R.id.reminder_action_fix_username) { - startActivity(EditProfileActivity.getIntentForUsernameEdit(requireContext())); + } else if (reminderActionId == R.id.reminder_action_fix_username_and_link) { + startActivity(EditProfileActivity.getIntent(requireContext())); + } 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())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index ce9a437e25..3bcf12aa88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.AccountValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; @@ -32,6 +33,8 @@ import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import org.whispersystems.signalservice.api.push.UsernameLinkComponents; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; @@ -40,6 +43,7 @@ import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse; import org.whispersystems.signalservice.internal.push.WhoAmIResponse; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -250,58 +254,58 @@ public class RefreshOwnProfileJob extends BaseJob { } } - static void checkUsernameIsInSync() throws IOException { - if (TextUtils.isEmpty(SignalDatabase.recipients().getUsername(Recipient.self().getId()))) { - Log.i(TAG, "No local username. Clearing username from server."); - ApplicationDependencies.getSignalServiceAccountManager().deleteUsername(); - } else { - Log.i(TAG, "Local user has a username, attempting username synchronization."); - performLocalRemoteComparison(); - } - } + static void checkUsernameIsInSync() { + boolean validated = false; - private static void performLocalRemoteComparison() { try { - String localUsername = SignalDatabase.recipients().getUsername(Recipient.self().getId()); - boolean hasLocalUsername = !TextUtils.isEmpty(localUsername); - - if (!hasLocalUsername) { - return; - } + String localUsername = SignalStore.account().getUsername(); WhoAmIResponse whoAmIResponse = ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI(); - boolean hasServerUsername = !TextUtils.isEmpty(whoAmIResponse.getUsernameHash()); - String serverUsernameHash = whoAmIResponse.getUsernameHash(); - String localUsernameHash = Base64.encodeUrlSafeWithoutPadding(Username.hash(localUsername)); + String remoteUsernameHash = whoAmIResponse.getUsernameHash(); + String localUsernameHash = localUsername != null ? Base64.encodeUrlSafeWithoutPadding(new Username(localUsername).getHash()) : null; - if (!hasServerUsername) { - Log.w(TAG, "No remote username is set."); + if (!Objects.equals(localUsernameHash, remoteUsernameHash)) { + Log.w(TAG, "Local username hash does not match server username hash. Local hash: " + (TextUtils.isEmpty(localUsername) ? "empty" : "present") + ", Remote hash: " + (TextUtils.isEmpty(localUsername) ? "empty" : "present")); + SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED); + return; + } else { + Log.d(TAG, "Username validated."); } - - if (!Objects.equals(localUsernameHash, serverUsernameHash)) { - Log.w(TAG, "Local username hash does not match server username hash."); - } - - if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) { - Log.i(TAG, "Attempting to resynchronize username."); - tryToReserveAndConfirmLocalUsername(localUsername, localUsernameHash); - } - } catch (IOException | BaseUsernameException e) { - Log.w(TAG, "Failed perform synchronization check", e); - } - } - - private static void tryToReserveAndConfirmLocalUsername(@NonNull String localUsername, @NonNull String localUsernameHash) { - try { - ReserveUsernameResponse response = ApplicationDependencies.getSignalServiceAccountManager() - .reserveUsername(Collections.singletonList(localUsernameHash)); - - ApplicationDependencies.getSignalServiceAccountManager() - .confirmUsername(localUsername, response); } catch (IOException e) { - Log.d(TAG, "Failed to synchronize username.", e); - // TODO [greyson][usernames] Is this actually enough to trigger it? Shouldn't we wait until we know for sure, rather than have a network error? - SignalStore.account().setUsernameOutOfSync(true); + Log.w(TAG, "Failed perform synchronization check during username phase.", e); + } catch (BaseUsernameException e) { + Log.w(TAG, "Our local username data is invalid!", e); + SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED); + } + + try { + UsernameLinkComponents localUsernameLink = SignalStore.account().getUsernameLink(); + + if (localUsernameLink != null) { + byte[] remoteEncryptedUsername = ApplicationDependencies.getSignalServiceAccountManager().getEncryptedUsernameFromLinkServerId(localUsernameLink.getServerId()); + Username.UsernameLink combinedLink = new Username.UsernameLink(localUsernameLink.getEntropy(), remoteEncryptedUsername); + Username remoteUsername = Username.fromLink(combinedLink); + + if (!remoteUsername.getUsername().equals(SignalStore.account().getUsername())) { + Log.w(TAG, "The remote username decrypted ok, but the decrypted username did not match our local username!"); + SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.LINK_CORRUPTED); + SignalStore.account().setUsernameLink(null); + } else { + Log.d(TAG, "Username link validated."); + } + + validated = true; + } + } catch (IOException e) { + Log.w(TAG, "Failed perform synchronization check during the username link phase.", e); + } catch (BaseUsernameException e) { + Log.w(TAG, "Failed to decrypt username link using the remote encrypted username and our local entropy!", e); + SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.LINK_CORRUPTED); + SignalStore.account().setUsernameLink(null); + } + + if (validated) { + SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.IN_SYNC); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 1d0966073a..4daf08c129 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -6,6 +6,7 @@ import android.content.SharedPreferences import android.preference.PreferenceManager import androidx.annotation.VisibleForTesting import org.signal.core.util.Base64 +import org.signal.core.util.LongSerializer import org.signal.core.util.logging.Log import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKeyPair @@ -70,7 +71,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private const val KEY_USERNAME = "account.username" private const val KEY_USERNAME_LINK_ENTROPY = "account.username_link_entropy" private const val KEY_USERNAME_LINK_SERVER_ID = "account.username_link_server_id" - private const val KEY_USERNAME_OUT_OF_SYNC = "phoneNumberPrivacy.usernameOutOfSync" + private const val KEY_USERNAME_SYNC_STATE = "phoneNumberPrivacy.usernameSyncState" @VisibleForTesting const val KEY_E164 = "account.e164" @@ -396,7 +397,17 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal * There are some cases where our username may fall out of sync with the service. In particular, we may get a new value for our username from * storage service but then find that it doesn't match what's on the service. */ - var usernameOutOfSync: Boolean by booleanValue(KEY_USERNAME_OUT_OF_SYNC, false) + var usernameSyncState: UsernameSyncState by enumValue( + KEY_USERNAME_SYNC_STATE, + UsernameSyncState.IN_SYNC, + object : LongSerializer { + override fun serialize(data: UsernameSyncState): Long { + Log.i(TAG, "Marking username sync state as: $data") + return data.serialize() + } + override fun deserialize(data: Long): UsernameSyncState = UsernameSyncState.deserialize(data) + } + ) private fun clearLocalCredentials() { putString(KEY_SERVICE_PASSWORD, Util.getSecret(18)) @@ -494,4 +505,23 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private fun SharedPreferences.hasStringData(key: String): Boolean { return this.getString(key, null) != null } + + enum class UsernameSyncState(private val value: Long) { + /** Our username data is in sync with the service */ + IN_SYNC(1), + + /** Both our username and username link are out-of-sync with the service */ + USERNAME_AND_LINK_CORRUPTED(2), + + /** Our username link is out-of-sync with the service */ + LINK_CORRUPTED(3); + + fun serialize(): Long = value + + companion object { + fun deserialize(value: Long): UsernameSyncState { + return values().firstOrNull { it.value == value } ?: throw IllegalArgumentException("Invalud value: $value") + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index db437551fc..7b54aacaea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -466,7 +466,7 @@ public final class Megaphones { * Prompt megaphone 3 days after turning off phone number discovery when no username is set. */ private static boolean shouldShowSetUpYourUsernameMegaphone(@NonNull Map records) { - boolean hasUsername = SignalStore.account().isRegistered() && Recipient.self().getUsername().isPresent(); + boolean hasUsername = SignalStore.account().isRegistered() && SignalStore.account().getUsername() != null; boolean hasCompleted = MapUtil.mapOrDefault(records, Event.SET_UP_YOUR_USERNAME, MegaphoneRecord::isFinished, false); long phoneNumberDiscoveryDisabledAt = SignalStore.phoneNumberPrivacy().getPhoneNumberListingModeTimestamp(); PhoneNumberPrivacyValues.PhoneNumberListingMode listingMode = SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java index 2509c32643..b76341a361 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java @@ -157,6 +157,6 @@ public class EditSelfProfileRepository implements EditProfileRepository { @Override public void getCurrentUsername(@NonNull Consumer> callback) { - callback.accept(Recipient.self().getUsername()); + callback.accept(Optional.ofNullable(SignalStore.account().getUsername())); } } 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 9da1ff6f51..f4359b6a45 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 @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.self.none.BecomeASustainerFragment.Companion.show import org.thoughtcrime.securesms.components.emoji.EmojiUtil import org.thoughtcrime.securesms.databinding.EditProfileFragmentBinding +import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.profiles.ProfileName @@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.util.NameUtil.getAbbreviation import org.thoughtcrime.securesms.util.livedata.LiveDataUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.views.SimpleProgressDialog +import org.thoughtcrime.securesms.util.visible import java.util.Arrays import java.util.Optional @@ -131,11 +133,17 @@ class EditProfileFragment : LoggingFragment() { ) } - if (FeatureFlags.usernames() && SignalStore.account().username != null) { + if (FeatureFlags.usernames() && SignalStore.account().username != null && SignalStore.account().usernameSyncState != AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) { binding.usernameLinkContainer.setOnClickListener { findNavController().safeNavigate(EditProfileFragmentDirections.actionManageProfileFragmentToUsernameLinkFragment()) } + if (SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.LINK_CORRUPTED) { + binding.linkErrorIndicator.visibility = View.VISIBLE + } else { + binding.linkErrorIndicator.visibility = View.GONE + } + if (SignalStore.tooltips().showProfileSettingsQrCodeTooltop()) { binding.usernameLinkTooltip.visibility = View.VISIBLE binding.linkTooltipCloseButton.setOnClickListener { @@ -238,6 +246,12 @@ class EditProfileFragment : LoggingFragment() { } else { binding.manageProfileUsername.text = username } + + if (SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) { + binding.usernameErrorIndicator.visibility = View.VISIBLE + } else { + binding.usernameErrorIndicator.visibility = View.GONE + } } private fun presentAbout(about: String?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileViewModel.java index 2fd708482c..7684a8bf49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileViewModel.java @@ -16,6 +16,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; @@ -163,7 +164,7 @@ class EditProfileViewModel extends ViewModel { private void onRecipientChanged(@NonNull Recipient recipient) { profileName.postValue(recipient.getProfileName()); - username.postValue(recipient.getUsername().orElse(null)); + username.postValue(SignalStore.account().getUsername()); about.postValue(recipient.getAbout()); aboutEmoji.postValue(recipient.getAboutEmoji()); badge.postValue(Optional.ofNullable(recipient.getFeaturedBadge())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java index 7509734e80..8557a2953d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java @@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.FragmentResultContract; import org.signal.core.util.concurrent.LifecycleDisposable; @@ -104,7 +105,9 @@ public class UsernameEditFragment extends LoggingFragment { binding.usernameDoneButton.setOnClickListener(v -> viewModel.onUsernameSubmitted()); binding.usernameSkipButton.setOnClickListener(v -> viewModel.onUsernameSkipped()); - UsernameState usernameState = Recipient.self().getUsername().map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE); + String username = SignalStore.account().getUsername(); + UsernameState usernameState = username != null ? new UsernameState.Set(username) : UsernameState.NoUsername.INSTANCE; + binding.usernameText.setText(usernameState.getNickname()); binding.usernameText.addTextChangedListener(new SimpleTextWatcher() { @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt index 7234c79e87..93cec005fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt @@ -13,6 +13,7 @@ import org.signal.libsignal.usernames.Username import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkResetResult import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper @@ -95,6 +96,11 @@ class UsernameRepository { Log.d(TAG, "[createOrRotateUsernameLink] Creating username link...") val components = accountManager.createUsernameLink(username) SignalStore.account().usernameLink = components + + if (SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.LINK_CORRUPTED) { + SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC + } + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() Log.d(TAG, "[createOrRotateUsernameLink] Username link created.") @@ -188,7 +194,7 @@ class UsernameRepository { SignalStore.account().username = username.username SignalStore.account().usernameLink = null SignalDatabase.recipients.setUsername(Recipient.self().id, reserved.username) - SignalStore.account().usernameOutOfSync = false + SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC Log.i(TAG, "[confirmUsername] Successfully confirmed username.") if (tryToSetUsernameLink(username)) { @@ -234,7 +240,7 @@ class UsernameRepository { SignalDatabase.recipients.setUsername(Recipient.self().id, null) SignalStore.account().username = null SignalStore.account().usernameLink = null - SignalStore.account().usernameOutOfSync = false + SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC Log.i(TAG, "[deleteUsername] Successfully deleted the username.") UsernameDeleteResult.SUCCESS } catch (e: IOException) { diff --git a/app/src/main/res/layout/edit_profile_fragment.xml b/app/src/main/res/layout/edit_profile_fragment.xml index a04e741393..3b8ac69ab5 100644 --- a/app/src/main/res/layout/edit_profile_fragment.xml +++ b/app/src/main/res/layout/edit_profile_fragment.xml @@ -145,7 +145,7 @@ android:background="?selectableItemBackground" android:minHeight="72dp" android:paddingStart="@dimen/dsl_settings_gutter" - android:paddingEnd="@dimen/safety_number_recipient_row_item_gutter" + android:paddingEnd="@dimen/dsl_settings_gutter" app:layout_constraintTop_toBottomOf="@id/manage_profile_name_container"> + + + + + + diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index ced52aefbd..b03512ee37 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -18,7 +18,8 @@ - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72842cf9ec..cec4b9da66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1015,7 +1015,9 @@ - Something went wrong with your username, it\'s no longer assigned to your account. You can try and set it again or choose a new one. + Something went wrong with your username, it\'s no longer assigned to your account. You can try and set it again or choose a new one. + + Something went wrong with your QR code and username link, it’s no longer valid. Create a new link to share with others. Fix now diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java index bb0d406c05..df4e20cee1 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java @@ -29,6 +29,10 @@ public class NonSuccessfulResponseCodeException extends IOException { return code; } + public boolean is4xx() { + return code >= 400 && code < 500; + } + public boolean is5xx() { return code >= 500 && code < 600; }