diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt index 1c10fb0e47..0f8b53d52f 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt @@ -448,7 +448,7 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe { // Detected changes SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name") - SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, "previous display name") + SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42") SignalDatabase.messages.insertNumberChangeMessages(alice.id) SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!) SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt index 02daf434d4..4b34ea130c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.FilePointer import org.thoughtcrime.securesms.backup.v2.proto.GroupCall import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall +import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate @@ -147,21 +148,19 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: builder.expiresInMs = 0 } MessageTypes.isProfileChange(record.type) -> { - if (record.body == null) continue - builder.updateMessage = ChatUpdateMessage( - profileChange = try { - val decoded: ByteArray = Base64.decode(record.body!!) - val profileChangeDetails = ProfileChangeDetails.ADAPTER.decode(decoded) - if (profileChangeDetails.profileNameChange != null) { - ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue) - } else { - ProfileChangeChatUpdate() - } - } catch (e: IOException) { - Log.w(TAG, "Profile name change details could not be read", e) - ProfileChangeChatUpdate() - } - ) + val profileChangeDetails = if (record.messageExtras != null) { + record.messageExtras.profileChangeDetails + } else { + Base64.decodeOrNull(record.body)?.let { ProfileChangeDetails.ADAPTER.decode(it) } + } + + builder.updateMessage = if (profileChangeDetails?.profileNameChange != null) { + ChatUpdateMessage(profileChange = ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue)) + } else if (profileChangeDetails?.learnedProfileName != null) { + ChatUpdateMessage(learnedProfileChange = LearnedProfileChatUpdate(e164 = profileChangeDetails.learnedProfileName.e164?.e164ToLong(), username = profileChangeDetails.learnedProfileName.username)) + } else { + continue + } builder.sms = false } MessageTypes.isSessionSwitchoverType(record.type) -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt index abbab849e6..9cfdd2e781 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt @@ -475,8 +475,14 @@ class ChatItemImportInserter( updateMessage.profileChange != null -> { typeFlags = MessageTypes.PROFILE_CHANGE_TYPE val profileChangeDetails = ProfileChangeDetails(profileNameChange = ProfileChangeDetails.StringChange(previous = updateMessage.profileChange.previousName, newValue = updateMessage.profileChange.newName)) - .encode() - put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails)) + val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode() + put(MessageTable.MESSAGE_EXTRAS, Base64.encodeWithPadding(messageExtras)) + } + updateMessage.learnedProfileChange != null -> { + typeFlags = MessageTypes.PROFILE_CHANGE_TYPE + val profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = updateMessage.learnedProfileChange.e164?.toString(), username = updateMessage.learnedProfileChange.username)) + val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode() + put(MessageTable.MESSAGE_EXTRAS, Base64.encodeWithPadding(messageExtras)) } updateMessage.sessionSwitchover != null -> { typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index b082425f8a..b279092ea7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -1118,12 +1118,17 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } } - fun insertLearnedProfileNameChangeMessage(recipient: Recipient, previousDisplayName: String) { + fun insertLearnedProfileNameChangeMessage(recipient: Recipient, e164: String?, username: String?) { + if ((e164 == null && username == null) || (e164 != null && username != null)) { + Log.w(TAG, "Learn profile event expects an e164 or username") + return + } + val threadId: Long? = SignalDatabase.threads.getThreadIdFor(recipient.id) if (threadId != null) { val extras = MessageExtras( - profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.StringChange(previous = previousDisplayName)) + profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = e164, username = username)) ) writableDatabase diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 2ecb6eb3cb..879385041e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -32,6 +32,7 @@ import androidx.core.content.ContextCompat; import com.annimon.stream.Stream; +import org.signal.core.util.Base64; import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.signal.storageservice.protos.groups.local.DecryptedGroup; @@ -57,7 +58,6 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.signal.core.util.Base64; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.GroupUtil; @@ -461,8 +461,19 @@ public abstract class MessageRecord extends DisplayRecord { } return staticUpdateDescription(updateMessage, R.drawable.ic_update_profile_16); + } else if (profileChangeDetails.deprecatedLearnedProfileName != null) { + return staticUpdateDescription(context.getString(R.string.MessageRecord_started_this_chat, profileChangeDetails.deprecatedLearnedProfileName.previous), R.drawable.symbol_thread_16); } else if (profileChangeDetails.learnedProfileName != null) { - return staticUpdateDescription(context.getString(R.string.MessageRecord_started_this_chat, profileChangeDetails.learnedProfileName.previous), R.drawable.symbol_thread_16); + String previouslyKnownAs; + if (!Util.isEmpty(profileChangeDetails.learnedProfileName.e164)) { + previouslyKnownAs = PhoneNumberFormatter.prettyPrint(profileChangeDetails.learnedProfileName.e164); + } else { + previouslyKnownAs = profileChangeDetails.learnedProfileName.username; + } + + if (!Util.isEmpty(previouslyKnownAs)) { + return staticUpdateDescription(context.getString(R.string.MessageRecord_started_this_chat, previouslyKnownAs), R.drawable.symbol_thread_16); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt index a7f35812be..917ec85f13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt @@ -376,8 +376,15 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val !recipient.isGroup && !recipient.isSelf ) { - Log.i(TAG, "Learned profile name for first time, insert event") - SignalDatabase.messages.insertLearnedProfileNameChangeMessage(recipient, recipient.getDisplayName(context)) + val username = SignalDatabase.recipients.getUsername(recipient.id) + val e164 = if (username == null) SignalDatabase.recipients.getE164sForIds(listOf(recipient.id)).firstOrNull() else null + + if (username != null || e164 != null) { + Log.i(TAG, "Learned profile name for first time, inserting event") + SignalDatabase.messages.insertLearnedProfileNameChangeMessage(recipient, e164, username) + } else { + Log.w(TAG, "Learned profile name for first time, but do not have username or e164 for ${recipient.id}") + } } if (remoteProfileName != localProfileName) { diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index e0d3a1882b..1893a075a4 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -648,6 +648,7 @@ message ChatUpdateMessage { SessionSwitchoverChatUpdate sessionSwitchover = 6; IndividualCall individualCall = 7; GroupCall groupCall = 8; + LearnedProfileChatUpdate learnedProfileChange = 9; } } @@ -744,6 +745,13 @@ message ProfileChangeChatUpdate { string newName = 2; } +message LearnedProfileChatUpdate { + oneof previousName { + uint64 e164 = 1; + string username = 2; + } +} + message ThreadMergeChatUpdate { uint64 previousE164 = 1; } diff --git a/app/src/main/protowire/Database.proto b/app/src/main/protowire/Database.proto index 266bd5e895..be715fb09a 100644 --- a/app/src/main/protowire/Database.proto +++ b/app/src/main/protowire/Database.proto @@ -72,8 +72,17 @@ message ProfileChangeDetails { string newValue = 2; } - StringChange profileNameChange = 1; - StringChange learnedProfileName = 2; + message LearnedProfileName { + oneof PreviouslyKnownAs { + string e164 = 1; + string username = 2; + } + } + + StringChange profileNameChange = 1; + // Deprecated - Use learnedProfileName instead + StringChange deprecatedLearnedProfileName = 2; + LearnedProfileName learnedProfileName = 3; } message BodyRangeList { diff --git a/core-util-jvm/src/main/java/org/signal/core/util/Base64.kt b/core-util-jvm/src/main/java/org/signal/core/util/Base64.kt index 7e81ad0082..aeb4bf9a30 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/Base64.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/Base64.kt @@ -71,6 +71,23 @@ object Base64 { } } + /** + * The same as [decode], except that instead of requiring you to handle an exception, this will return null + * if the input is null or cannot be decoded. + */ + @JvmStatic + fun decodeOrNull(value: String?): ByteArray? { + if (value == null) { + return null + } + + return try { + decode(value) + } catch (e: IOException) { + null + } + } + /** * The same as [decode], except that instead of requiring you to handle an exception, this will just crash on invalid base64 strings. * Should only be used if the value is definitely a valid base64 string.