mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Update username consistency error handling.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<UsernameSyncState> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Event, MegaphoneRecord> 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();
|
||||
|
||||
@@ -157,6 +157,6 @@ public class EditSelfProfileRepository implements EditProfileRepository {
|
||||
|
||||
@Override
|
||||
public void getCurrentUsername(@NonNull Consumer<Optional<String>> callback) {
|
||||
callback.accept(Recipient.self().getUsername());
|
||||
callback.accept(Optional.ofNullable(SignalStore.account().getUsername()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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().<UsernameState>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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
<ImageView
|
||||
@@ -168,9 +168,21 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/manage_profile_username_icon"
|
||||
app:layout_goneMarginEnd="48dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/username_error_indicator"
|
||||
tools:text="\@spiderman" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/username_error_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/symbol_error_circle_24"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:tint="@color/signal_alert_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
@@ -180,7 +192,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_username_container">
|
||||
|
||||
<ImageView
|
||||
@@ -204,8 +216,21 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/username_link_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/link_error_indicator"
|
||||
app:layout_goneMarginEnd="48dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/link_error_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/symbol_error_circle_24"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:tint="@color/signal_alert_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -573,6 +573,16 @@
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_direct_to_usernameLinkSettings"
|
||||
app:destination="@id/usernameLinkSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@id/app_settings"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Internal Settings -->
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<item name="reminder_action_cds_temporary_error_learn_more" type="id" />
|
||||
<item name="reminder_action_cds_permanent_error_learn_more" type="id" />
|
||||
|
||||
<item name="reminder_action_fix_username" type="id" />
|
||||
<item name="reminder_action_fix_username_and_link" type="id" />
|
||||
<item name="reminder_action_fix_username_link" type="id" />
|
||||
|
||||
<item name="status_bar_guideline" type="id" />
|
||||
<item name="navigation_bar_guideline" type="id" />
|
||||
|
||||
@@ -1015,7 +1015,9 @@
|
||||
|
||||
<!-- UsernameOutOfSyncReminder -->
|
||||
<!-- Displayed above the conversation list when a user needs to address an issue with their username -->
|
||||
<string name="UsernameOutOfSyncReminder__something_went_wrong">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.</string>
|
||||
<string name="UsernameOutOfSyncReminder__username_and_link_corrupt">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.</string>
|
||||
<!-- Displayed above the conversation list when a user needs to address an issue with their username link -->
|
||||
<string name="UsernameOutOfSyncReminder__link_corrupt">Something went wrong with your QR code and username link, it’s no longer valid. Create a new link to share with others.</string>
|
||||
<!-- Action text to navigate user to manually fix the issue with their username -->
|
||||
<string name="UsernameOutOfSyncReminder__fix_now">Fix now</string>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user