From 1371663163cdfe322e29738400e81872462776fe Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Wed, 3 Jun 2026 10:03:01 -0400 Subject: [PATCH] Add capability for KT username syncs. --- .../securesms/testing/SignalActivityRule.kt | 2 +- .../org/signal/benchmark/setup/TestUsers.kt | 4 +- .../thoughtcrime/securesms/AppCapabilities.kt | 3 +- .../InternalConversationSettingsState.kt | 29 +++--- .../securesms/database/RecipientTable.kt | 4 +- .../database/RecipientTableCursorUtil.kt | 4 +- .../database/model/RecipientRecord.kt | 6 +- .../securesms/jobs/CheckKeyTransparencyJob.kt | 4 +- .../securesms/jobs/JobManagerFactories.java | 1 + .../jobs/MultiDeviceUsernameChangeSyncJob.kt | 95 +++++++++++++++++++ .../messages/SyncMessageProcessor.kt | 10 ++ .../profiles/manage/UsernameRepository.kt | 14 +++ .../securesms/recipients/Recipient.kt | 3 + .../v2/AppRegistrationNetworkController.kt | 3 +- .../org/thoughtcrime/securesms/ApiPlugin.kt | 2 +- .../database/RecipientDatabaseTestUtils.kt | 3 +- .../securesms/testutil/RecipientTestRule.kt | 2 +- .../dependencies/DemoNetworkController.kt | 6 +- .../pinsettings/PinSettingsViewModel.kt | 3 +- .../signal/registration/NetworkController.kt | 3 +- .../registration/RegistrationRepository.kt | 3 +- .../api/account/AccountAttributes.kt | 3 +- .../api/profiles/SignalServiceProfile.java | 10 +- .../src/main/protowire/SignalService.proto | 3 + 24 files changed, 187 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceUsernameChangeSyncJob.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index aa9fb600da..9bc03066b0 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -139,7 +139,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true)) SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.markRegistered(recipientId, aci) val otherIdentity = IdentityKeyPair.generate() diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt index 0cdbf71e64..5d4c3f0731 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt @@ -133,7 +133,7 @@ object TestUsers { val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true)) SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.markRegistered(recipientId, aci) val otherIdentity = IdentityKeyPair.generate() @@ -157,7 +157,7 @@ object TestUsers { val recipientId = RecipientId.from(SignalServiceAddress(otherClient.serviceId, otherClient.e164)) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, otherClient.profileKey) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true)) SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.markRegistered(recipientId, otherClient.serviceId) AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(otherClient.serviceId.toString(), 1), otherClient.identityKeyPair.publicKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt index 96d8e07660..c80d7e105c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt @@ -13,7 +13,8 @@ object AppCapabilities { storage = storageCapable, versionedExpirationTimer = true, attachmentBackfill = true, - spqr = true + spqr = true, + usernameChangeSyncMessage = false // TODO(michelle): Turn on once all clients support it and add a migration ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsState.kt index 8b7512779a..aefc4a4b7f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsState.kt @@ -7,7 +7,12 @@ package org.thoughtcrime.securesms.components.settings.conversation import androidx.annotation.WorkerThread import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.withStyle import org.signal.core.util.Base64 import org.signal.core.util.Hex import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository @@ -98,18 +103,18 @@ data class InternalConversationSettingsState( val capabilities: RecipientRecord.Capabilities? = SignalDatabase.recipients.getCapabilities(recipient.id) if (capabilities != null) { AnnotatedString("No capabilities right now.") - // Left as an example in case we add one in the future -// val style: SpanStyle = when (capabilities.storageServiceEncryptionV2) { -// Recipient.Capability.SUPPORTED -> SpanStyle(color = Color(0, 150, 0)) -// Recipient.Capability.NOT_SUPPORTED -> SpanStyle(color = Color.Red) -// Recipient.Capability.UNKNOWN -> SpanStyle(fontStyle = FontStyle.Italic) -// } -// -// buildAnnotatedString { -// withStyle(style = style) { -// append("SSREv2") -// } -// } + // Always leave one as an example in case we add one in the future + val style: SpanStyle = when (capabilities.usernameSyncMessages) { + Recipient.Capability.SUPPORTED -> SpanStyle(color = Color(0, 150, 0)) + Recipient.Capability.NOT_SUPPORTED -> SpanStyle(color = Color.Red) + Recipient.Capability.UNKNOWN -> SpanStyle(fontStyle = FontStyle.Italic) + } + + buildAnnotatedString { + withStyle(style = style) { + append("usernameSyncMessages") + } + } } else { AnnotatedString("Recipient not found!") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index bf12604c51..e93ae54166 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -424,6 +424,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long { var value: Long = 0 value = Bitmask.update(value, Capabilities.STORAGE_SERVICE_ENCRYPTION_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStorageServiceEncryptionV2).serialize().toLong()) + value = Bitmask.update(value, Capabilities.USERNAME_SYNC_MESSAGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isUsernameSyncMessages).serialize().toLong()) return value } } @@ -4953,8 +4954,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da // const val DELETE_SYNC = 9 // const val VERSIONED_EXPIRATION_TIMER = 10 const val STORAGE_SERVICE_ENCRYPTION_V2 = 11 + const val USERNAME_SYNC_MESSAGES = 12 - // IMPORTANT: We cannot sore more than 32 capabilities in the bitmask. + // IMPORTANT: We cannot store more than 32 capabilities in the bitmask. } enum class VibrateState(val id: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt index 9d1b0d1b5b..5e1b03bf8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt @@ -10,6 +10,7 @@ import android.database.Cursor import com.google.protobuf.InvalidProtocolBufferException import org.signal.core.models.ServiceId import org.signal.core.util.Base64 +import org.signal.core.util.Bitmask import org.signal.core.util.Util import org.signal.core.util.logging.Log import org.signal.core.util.optionalBlob @@ -174,7 +175,8 @@ object RecipientTableCursorUtil { fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities { val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES) return RecipientRecord.Capabilities( - rawBits = capabilities + rawBits = capabilities, + usernameSyncMessages = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.USERNAME_SYNC_MESSAGES, RecipientTable.Capabilities.BIT_LENGTH).toInt()) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index b44c3f79ae..2a4d812941 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -121,12 +121,14 @@ data class RecipientRecord( ) data class Capabilities( - val rawBits: Long + val rawBits: Long, + val usernameSyncMessages: Recipient.Capability ) { companion object { @JvmField val UNKNOWN = Capabilities( - rawBits = 0 + rawBits = 0, + usernameSyncMessages = Recipient.Capability.UNKNOWN ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckKeyTransparencyJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckKeyTransparencyJob.kt index 78998695e0..258893da78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckKeyTransparencyJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckKeyTransparencyJob.kt @@ -117,11 +117,11 @@ class CheckKeyTransparencyJob private constructor( aciIdentityKey = SignalStore.account.aciIdentityKey.publicKey, e164 = recipient.e164!!, unidentifiedAccessKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey).let { UnidentifiedAccess.deriveAccessKeyFrom(it) }, - usernameHash = SignalStore.account.username?.let { Username(it).hash }, + usernameHash = SignalStore.account.username?.let { Username(it).hash }.takeIf { Recipient.self().usernameSyncMessagesCapability.isSupported }, keyTransparencyStore = KeyTransparencyStore ) - Log.i(TAG, "Key transparency complete, result: $result") + Log.i(TAG, "Key transparency complete, result: $result. Included username in check: ${Recipient.self().usernameSyncMessagesCapability.isSupported}") return when (result) { is RequestResult.Success -> { SignalStore.misc.hasKeyTransparencyFailure = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index ca62bd0067..795b2c9b0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -224,6 +224,7 @@ public final class JobManagerFactories { put(MultiDeviceStickerPackSyncJob.KEY, new MultiDeviceStickerPackSyncJob.Factory()); put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory()); put(MultiDeviceSubscriptionSyncRequestJob.KEY, new MultiDeviceSubscriptionSyncRequestJob.Factory()); + put(MultiDeviceUsernameChangeSyncJob.KEY, new MultiDeviceUsernameChangeSyncJob.Factory()); put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory()); put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory()); put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceUsernameChangeSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceUsernameChangeSyncJob.kt new file mode 100644 index 0000000000..ac634f6b2a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceUsernameChangeSyncJob.kt @@ -0,0 +1,95 @@ +package org.thoughtcrime.securesms.jobs + +import androidx.annotation.WorkerThread +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.pad +import org.thoughtcrime.securesms.recipients.Recipient +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException +import org.whispersystems.signalservice.internal.push.Content +import org.whispersystems.signalservice.internal.push.SyncMessage +import java.io.IOException +import java.util.Optional +import kotlin.time.Duration.Companion.days + +/** + * Sends a sync message to alert linked devices of a username change so they can reset KT. + */ +class MultiDeviceUsernameChangeSyncJob private constructor( + parameters: Parameters +) : Job(parameters) { + + companion object { + const val KEY = "MultiDeviceUsernameChangeSyncJob" + private val TAG = Log.tag(MultiDeviceUsernameChangeSyncJob::class.java) + + @WorkerThread + @JvmStatic + fun enqueueUsernameChangeSync() { + if (!SignalStore.account.isMultiDevice) { + return + } + + AppDependencies.jobManager.add( + MultiDeviceUsernameChangeSyncJob( + parameters = Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(Parameters.UNLIMITED) + .setLifespan(1.days.inWholeMilliseconds) + .build() + ) + ) + } + } + + override fun serialize(): ByteArray? = null + + override fun getFactoryKey(): String = KEY + + override fun run(): Result { + if (!Recipient.self().isRegistered) { + Log.w(TAG, "Not registered") + return Result.failure() + } + + if (!SignalStore.account.isMultiDevice) { + Log.w(TAG, "Not multi-device") + return Result.failure() + } + + val syncMessageContent = Content( + syncMessage = SyncMessage.Builder() + .pad() + .usernameChange(SyncMessage.UsernameChange()) + .build() + ) + + return try { + Log.d(TAG, "Sending username change sync") + val success = AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessageContent, true, Optional.empty()).isSuccess + if (success) { + Result.success() + } else { + Log.w(TAG, "Unsuccessful username change send. Retrying.") + Result.retry(defaultBackoff()) + } + } catch (e: IOException) { + Log.w(TAG, "Unable to send username change sync due to io exception", e) + Result.retry(defaultBackoff()) + } catch (e: UntrustedIdentityException) { + Log.w(TAG, "Unable to send username change sync due to untrusted exception", e) + Result.failure() + } + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceUsernameChangeSyncJob { + return MultiDeviceUsernameChangeSyncJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index f467e1bccd..34b52749fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -14,6 +14,7 @@ import org.signal.core.util.Util import org.signal.core.util.UuidUtil import org.signal.core.util.isNotEmpty import org.signal.core.util.orNull +import org.signal.libsignal.net.KeyTransparency import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.InvalidKeyException @@ -42,6 +43,7 @@ import org.thoughtcrime.securesms.database.PaymentMetaDataUtil import org.thoughtcrime.securesms.database.SentStorySyncManifest import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.database.model.KeyTransparencyStore import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -58,6 +60,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.PollTerminate import org.thoughtcrime.securesms.database.model.toBodyRangeList import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.dependencies.KeyTransparencyApi import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.GroupChangeBusyException import org.thoughtcrime.securesms.groups.GroupId @@ -188,6 +191,7 @@ object SyncMessageProcessor { syncMessage.deleteForMe != null -> handleSynchronizeDeleteForMe(context, syncMessage.deleteForMe!!, envelope.clientTimestamp!!, earlyMessageCacheEntry) syncMessage.attachmentBackfillRequest != null -> handleSynchronizeAttachmentBackfillRequest(syncMessage.attachmentBackfillRequest!!, envelope.clientTimestamp!!) syncMessage.attachmentBackfillResponse != null -> warn(envelope.clientTimestamp!!, "Contains a backfill response, but we don't handle these!") + syncMessage.usernameChange != null -> handleSynchronizeUsernameChange(envelope.clientTimestamp!!) else -> warn(envelope.clientTimestamp!!, "Contains no known sync types...") } } @@ -2026,6 +2030,12 @@ object SyncMessageProcessor { return threadId } + private fun handleSynchronizeUsernameChange(timestamp: Long) { + log(timestamp, "[handleSynchronizeUsernameChange] Synchronize username change. Resetting KT.") + + KeyTransparencyApi.reset(aci = SignalStore.account.requireAci().libSignalAci, field = KeyTransparency.AccountDataField.USERNAME_HASH, keyTransparencyStore = KeyTransparencyStore) + } + private fun ConversationIdentifier.toRecipientId(): RecipientId? { val threadServiceId = ServiceId.parseOrNull(this.threadServiceId, this.threadServiceIdBinary) return when { 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 97ef69271a..46e510e843 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 @@ -20,9 +20,13 @@ import org.signal.network.NetworkResult import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkResetResult import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.MultiDeviceUsernameChangeSyncJob import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.net.SignalNetwork +import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.confirmUsernameAndCreateNewLink +import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.reserveUsername +import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.updateUsernameDisplayForCurrentLink import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.NetworkUtil @@ -444,6 +448,9 @@ object UsernameRepository { SignalStore.account.usernameSyncErrorCount = 0 SignalStore.misc.needsUsernameRestore = false + if (Recipient.self().usernameSyncMessagesCapability.isSupported) { + MultiDeviceUsernameChangeSyncJob.enqueueUsernameChangeSync() + } SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Successfully updated username.") @@ -477,6 +484,9 @@ object UsernameRepository { SignalStore.account.usernameSyncErrorCount = 0 SignalStore.misc.needsUsernameRestore = false + if (Recipient.self().usernameSyncMessagesCapability.isSupported) { + MultiDeviceUsernameChangeSyncJob.enqueueUsernameChangeSync() + } SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username.") @@ -534,6 +544,10 @@ object UsernameRepository { SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC SignalStore.account.usernameSyncErrorCount = 0 SignalStore.misc.needsUsernameRestore = false + + if (Recipient.self().usernameSyncMessagesCapability.isSupported) { + MultiDeviceUsernameChangeSyncJob.enqueueUsernameChangeSync() + } SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() Log.i(TAG, "[deleteUsername] Successfully deleted the username.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index b45ca61cfd..f4fc8f8b3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -352,6 +352,9 @@ class Recipient( sealedSenderAccessModeValue } + /** The user's capability to receive username sync messages */ + val usernameSyncMessagesCapability: Capability = capabilities.usernameSyncMessages + /** The wallpaper to render as the chat background, if present. */ val wallpaper: ChatWallpaper? get() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt index 60ec5d4011..947df4ff47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationNetworkController.kt @@ -731,7 +731,8 @@ class AppRegistrationNetworkController( storage, versionedExpirationTimer, attachmentBackfill, - spqr + spqr, + usernameChangeSyncMessage ) } diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/ApiPlugin.kt b/app/src/spinner/java/org/thoughtcrime/securesms/ApiPlugin.kt index 03c491b387..77a1775a89 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/ApiPlugin.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/ApiPlugin.kt @@ -368,7 +368,7 @@ class ApiPlugin : Plugin { } SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true)) SignalDatabase.recipients.setProfileSharing(recipientId, profileSharing) if (registered) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 3b15f01e20..2acf4a2e0c 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -122,7 +122,8 @@ object RecipientDatabaseTestUtils { notificationChannel = notificationChannel, sealedSenderAccessMode = sealedSenderAccessMode, capabilities = RecipientRecord.Capabilities( - rawBits = capabilities + rawBits = capabilities, + usernameSyncMessages = Recipient.Capability.SUPPORTED ), storageId = storageId, mentionSetting = mentionSetting, diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt b/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt index ec3a3431f7..9eb3ec641e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt @@ -184,7 +184,7 @@ class RecipientTestRule : TestRule { val id = SignalDatabase.recipients.getOrInsertFromServiceId(aci) SignalDatabase.recipients.setProfileName(id, profileName) SignalDatabase.recipients.setProfileKeyIfAbsent(id, ProfileKey(Random.nextBytes(32))) - SignalDatabase.recipients.setCapabilities(id, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setCapabilities(id, SignalServiceProfile.Capabilities(true, true, true)) SignalDatabase.recipients.setProfileSharing(id, profileSharing) SignalDatabase.recipients.markRegistered(id, aci) return id diff --git a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt index 856ec4515e..27ce8b70db 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoNetworkController.kt @@ -885,7 +885,8 @@ class DemoNetworkController( storage = !RegistrationPreferences.pinsOptedOut, versionedExpirationTimer = true, attachmentBackfill = true, - spqr = true + spqr = true, + usernameChangeSyncMessage = true ), name = null, pniRegistrationId = RegistrationPreferences.pniRegistrationId, @@ -1127,7 +1128,8 @@ class DemoNetworkController( storage, versionedExpirationTimer, attachmentBackfill, - spqr + spqr, + usernameChangeSyncMessage ) } diff --git a/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt b/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt index 4d45027fe2..8e7df9a28d 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt @@ -182,7 +182,8 @@ class PinSettingsViewModel( storage = !newOptedOut, versionedExpirationTimer = true, attachmentBackfill = true, - spqr = true + spqr = true, + usernameChangeSyncMessage = true ), name = null, pniRegistrationId = RegistrationPreferences.pniRegistrationId, diff --git a/feature/registration/src/main/java/org/signal/registration/NetworkController.kt b/feature/registration/src/main/java/org/signal/registration/NetworkController.kt index aa992203e1..54db12c4b7 100644 --- a/feature/registration/src/main/java/org/signal/registration/NetworkController.kt +++ b/feature/registration/src/main/java/org/signal/registration/NetworkController.kt @@ -369,7 +369,8 @@ interface NetworkController { val storage: Boolean, val versionedExpirationTimer: Boolean, val attachmentBackfill: Boolean, - val spqr: Boolean + val spqr: Boolean, + val usernameChangeSyncMessage: Boolean ) } diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 84aa0316f5..ab287b9248 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -369,7 +369,8 @@ class RegistrationRepository(val context: Context, val networkController: Networ storage = true, // True initially -- can turn off later if users opt-out versionedExpirationTimer = true, attachmentBackfill = true, - spqr = true + spqr = true, + usernameChangeSyncMessage = false // TODO(michelle): Turn on once all clients support it ), name = null, pniRegistrationId = keyMaterial.pniRegistrationId, diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt index c8066e51aa..23a23fc167 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt @@ -57,6 +57,7 @@ class AccountAttributes @JsonCreator constructor( @JsonProperty val storage: Boolean, @JsonProperty val versionedExpirationTimer: Boolean, @JsonProperty val attachmentBackfill: Boolean, - @JsonProperty val spqr: Boolean + @JsonProperty val spqr: Boolean, + @JsonProperty val usernameChangeSyncMessage: Boolean ) } diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index 670c1e8c4a..a1c9be9302 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -195,12 +195,16 @@ public class SignalServiceProfile { @JsonProperty("ssre2") private boolean storageServiceEncryptionV2; + @JsonProperty("usernameChangeSyncMessage") + private boolean usernameSyncMessages; + @JsonCreator public Capabilities() {} - public Capabilities(boolean storage, boolean storageServiceEncryptionV2) { + public Capabilities(boolean storage, boolean storageServiceEncryptionV2, boolean usernameSyncMessages) { this.storage = storage; this.storageServiceEncryptionV2 = storageServiceEncryptionV2; + this.usernameSyncMessages = usernameSyncMessages; } public boolean isStorage() { @@ -210,6 +214,10 @@ public class SignalServiceProfile { public boolean isStorageServiceEncryptionV2() { return storageServiceEncryptionV2; } + + public boolean isUsernameSyncMessages() { + return usernameSyncMessages; + } } public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() { diff --git a/lib/libsignal-service/src/main/protowire/SignalService.proto b/lib/libsignal-service/src/main/protowire/SignalService.proto index 60cbd8642d..58d0b0f991 100644 --- a/lib/libsignal-service/src/main/protowire/SignalService.proto +++ b/lib/libsignal-service/src/main/protowire/SignalService.proto @@ -850,6 +850,8 @@ message SyncMessage { } } + message UsernameChange {} + oneof content { Sent sent = 1; Contacts contacts = 2; @@ -870,6 +872,7 @@ message SyncMessage { DeviceNameChange deviceNameChange = 23; AttachmentBackfillRequest attachmentBackfillRequest = 24; AttachmentBackfillResponse attachmentBackfillResponse = 25; + UsernameChange usernameChange = 26; } reserved /*groups*/ 3;