diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt index 59134e6eca..f7615b9a4e 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt @@ -21,10 +21,9 @@ import org.signal.libsignal.messagebackup.MessageBackup import org.signal.libsignal.messagebackup.MessageBackupKey import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.thoughtcrime.securesms.backup.v2.proto.AccountData +import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.proto.BodyRange -import org.thoughtcrime.securesms.backup.v2.proto.Call -import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.Chat import org.thoughtcrime.securesms.backup.v2.proto.ChatItem import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage @@ -34,8 +33,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.FilePointer import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.proto.Group -import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate -import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate +import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.Quote @@ -198,8 +196,7 @@ class ImportExportTest { masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), whitelisted = true, hideStory = false, - storySendMode = Group.StorySendMode.ENABLED, - name = "Cool Group $i" + storySendMode = Group.StorySendMode.ENABLED ) ) ) @@ -265,8 +262,7 @@ class ImportExportTest { masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), whitelisted = random.trueWithProbability(0.9f), hideStory = random.trueWithProbability(0.1f), - storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED, - name = "Cool Group $i" + storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED ) ) ) @@ -434,8 +430,7 @@ class ImportExportTest { masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), whitelisted = true, hideStory = true, - storySendMode = Group.StorySendMode.ENABLED, - name = "Cool test group" + storySendMode = Group.StorySendMode.ENABLED ) ), Recipient( @@ -444,8 +439,7 @@ class ImportExportTest { masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), whitelisted = false, hideStory = false, - storySendMode = Group.StorySendMode.DEFAULT, - name = "Cool test group" + storySendMode = Group.StorySendMode.DEFAULT ) ) ) @@ -596,8 +590,7 @@ class ImportExportTest { masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), whitelisted = true, hideStory = true, - storySendMode = Group.StorySendMode.DEFAULT, - name = "Cool test group" + storySendMode = Group.StorySendMode.DEFAULT ) ), Chat( @@ -615,113 +608,64 @@ class ImportExportTest { } @Test - fun calls() { - val individualCalls = ArrayList() - val groupCalls = ArrayList() - val states = arrayOf(Call.State.MISSED, Call.State.COMPLETED, Call.State.DECLINED_BY_USER, Call.State.DECLINED_BY_NOTIFICATION_PROFILE) - val types = arrayOf(Call.Type.VIDEO_CALL, Call.Type.AD_HOC_CALL, Call.Type.AUDIO_CALL) - var id = 1L - var timestamp = 12345L - + fun individualCalls() { + val individualCalls = ArrayList() + val states = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.NOT_ACCEPTED, IndividualCall.State.MISSED, IndividualCall.State.MISSED_NOTIFICATION_PROFILE) + val oldStates = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.MISSED) + val types = arrayOf(IndividualCall.Type.VIDEO_CALL, IndividualCall.Type.AUDIO_CALL) + val directions = arrayOf(IndividualCall.Direction.OUTGOING, IndividualCall.Direction.INCOMING) + var sentTime = 0L + var callId = 1L + val startedAci = TestRecipientUtils.nextAci().toByteString() for (state in states) { for (type in types) { - individualCalls.add( - Call( - callId = id++, - conversationRecipientId = 3, - type = type, - state = state, - timestamp = timestamp++, - ringerRecipientId = 3, - outgoing = true + for (direction in directions) { + // With call id + individualCalls.add( + ChatItem( + chatId = 1, + authorId = selfRecipient.id, + dateSent = sentTime++, + sms = false, + directionless = ChatItem.DirectionlessMessageDetails(), + updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + callId = callId++, + type = type, + state = state, + direction = direction + ) + ) + ) ) - ) - individualCalls.add( - Call( - callId = id++, - conversationRecipientId = 3, - type = type, - state = state, - timestamp = timestamp++, - ringerRecipientId = selfRecipient.id, - outgoing = false - ) - ) + } } - groupCalls.add( - Call( - callId = id++, - conversationRecipientId = 4, - type = Call.Type.GROUP_CALL, - state = state, - timestamp = timestamp++, - ringerRecipientId = 3, - outgoing = true - ) - ) - groupCalls.add( - Call( - callId = id++, - conversationRecipientId = 4, - type = Call.Type.GROUP_CALL, - state = state, - timestamp = timestamp++, - ringerRecipientId = selfRecipient.id, - outgoing = false - ) - ) } - - var sentTime = 0L - val individualCallChatItems = individualCalls.map { call -> - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = sentTime + 1, - dateServerSent = sentTime, - read = true, - sealedSender = true - ), - updateMessage = ChatUpdateMessage( - callingMessage = CallChatUpdate( - callMessage = IndividualCallChatUpdate( - type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL + for (state in oldStates) { + for (type in types) { + for (direction in directions) { + if (state == IndividualCall.State.MISSED && direction == IndividualCall.Direction.OUTGOING) continue + // Without call id + individualCalls.add( + ChatItem( + chatId = 1, + authorId = selfRecipient.id, + dateSent = sentTime++, + sms = false, + directionless = ChatItem.DirectionlessMessageDetails(), + updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + callId = null, + type = type, + state = state, + direction = direction + ) + ) ) ) - ) - ) - }.toTypedArray() - - val startedAci = TestRecipientUtils.nextAci().toByteString() - val groupCallChatItems = groupCalls.map { call -> - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = sentTime + 1, - dateServerSent = sentTime, - read = true, - sealedSender = true - ), - updateMessage = ChatUpdateMessage( - callingMessage = CallChatUpdate( - groupCall = GroupCallChatUpdate( - startedCallAci = startedAci, - startedCallTimestamp = 0, - endedCallTimestamp = 0, - localUserJoined = GroupCallChatUpdate.LocalUserJoined.JOINED, - inCallAcis = emptyList() - ) - ) - ) - ) - }.toTypedArray() - + } + } + } importExport( *standardFrames, Recipient( @@ -748,8 +692,7 @@ class ImportExportTest { masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), whitelisted = true, hideStory = true, - storySendMode = Group.StorySendMode.DEFAULT, - name = "Cool test group" + storySendMode = Group.StorySendMode.DEFAULT ) ), Chat( @@ -763,10 +706,7 @@ class ImportExportTest { dontNotifyForMentionsIfMuted = true, wallpaper = null ), - *individualCalls.toArray(), - *groupCalls.toArray(), - *individualCallChatItems, - *groupCallChatItems + *individualCalls.toArray() ) } @@ -1003,7 +943,7 @@ class ImportExportTest { chatId = 1, authorId = alice.id, dateSent = 101, - expireStartDate = null, + expireStartDate = 0, expiresInMs = TimeUnit.DAYS.toMillis(1), sms = false, incoming = ChatItem.IncomingMessageDetails( @@ -1435,7 +1375,7 @@ class ImportExportTest { is Recipient -> writer.write(Frame(recipient = obj)) is Chat -> writer.write(Frame(chat = obj)) is ChatItem -> writer.write(Frame(chatItem = obj)) - is Call -> writer.write(Frame(call = obj)) + is AdHocCall -> writer.write(Frame(adHocCall = obj)) is StickerPack -> writer.write(Frame(stickerPack = obj)) else -> Assert.fail("invalid object $obj") } @@ -1496,7 +1436,7 @@ class ImportExportTest { is Recipient -> writer.write(Frame(recipient = obj)) is Chat -> writer.write(Frame(chat = obj)) is ChatItem -> writer.write(Frame(chatItem = obj)) - is Call -> writer.write(Frame(call = obj)) + is AdHocCall -> writer.write(Frame(adHocCall = obj)) is StickerPack -> writer.write(Frame(stickerPack = obj)) else -> Assert.fail("invalid object $obj") } @@ -1527,8 +1467,8 @@ class ImportExportTest { val chatsExported = ArrayList() val chatItemsImported = ArrayList() val chatItemsExported = ArrayList() - val callsImported = ArrayList() - val callsExported = ArrayList() + val callsImported = ArrayList() + val callsExported = ArrayList() val stickersImported = ArrayList() val stickersExported = ArrayList() @@ -1538,7 +1478,7 @@ class ImportExportTest { f.recipient != null -> recipientsImported.add(f.recipient!!) f.chat != null -> chatsImported.add(f.chat!!) f.chatItem != null -> chatItemsImported.add(f.chatItem!!) - f.call != null -> callsImported.add(f.call!!) + f.adHocCall != null -> callsImported.add(f.adHocCall!!) f.stickerPack != null -> stickersImported.add(f.stickerPack!!) } } @@ -1549,7 +1489,7 @@ class ImportExportTest { f.recipient != null -> recipientsExported.add(f.recipient!!) f.chat != null -> chatsExported.add(f.chat!!) f.chatItem != null -> chatItemsExported.add(f.chatItem!!) - f.call != null -> callsExported.add(f.call!!) + f.adHocCall != null -> callsExported.add(f.adHocCall!!) f.stickerPack != null -> stickersExported.add(f.stickerPack!!) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 8193585d4b..834f432f43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor -import org.thoughtcrime.securesms.backup.v2.processor.CallLogBackupProcessor +import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallBackupProcessor import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor @@ -109,7 +109,7 @@ object BackupRepository { eventTimer.emit("thread") } - CallLogBackupProcessor.export { frame -> + AdHocCallBackupProcessor.export { frame -> writer.write(frame) eventTimer.emit("call") } @@ -198,8 +198,8 @@ object BackupRepository { eventTimer.emit("chat") } - frame.call != null -> { - CallLogBackupProcessor.import(frame.call, backupState) + frame.adHocCall != null -> { + AdHocCallBackupProcessor.import(frame.adHocCall, backupState) eventTimer.emit("call") } @@ -644,7 +644,6 @@ class BackupState(val backupKey: BackupKey) { val chatIdToLocalThreadId = HashMap() val chatIdToLocalRecipientId = HashMap() val chatIdToBackupRecipientId = HashMap() - val callIdToType = HashMap() } class BackupMetadata( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallLinkTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallLinkTableBackupExtensions.kt new file mode 100644 index 0000000000..0b6923eaa5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallLinkTableBackupExtensions.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.database + +import android.database.Cursor +import okio.ByteString.Companion.toByteString +import org.signal.core.util.select +import org.signal.ringrtc.CallLinkRootKey +import org.signal.ringrtc.CallLinkState +import org.thoughtcrime.securesms.backup.v2.proto.CallLink +import org.thoughtcrime.securesms.database.CallLinkTable +import org.thoughtcrime.securesms.database.RecipientTable +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials +import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId +import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState +import java.io.Closeable +import java.time.Instant + +fun CallLinkTable.getCallLinksForBackup(): BackupCallLinkIterator { + val cursor = readableDatabase + .select() + .from(CallLinkTable.TABLE_NAME) + .run() + + return BackupCallLinkIterator(cursor) +} + +fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId { + return SignalDatabase.callLinks.insertCallLink( + CallLinkTable.CallLink( + recipientId = RecipientId.UNKNOWN, + roomId = CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(callLink.rootKey.toByteArray())), + credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()), + state = SignalCallLinkState( + name = callLink.name, + restrictions = callLink.restrictions.toLocal(), + expiration = Instant.ofEpochMilli(callLink.expirationMs) + ) + ) + ) +} + +/** + * Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s. + * Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources. + */ +class BackupCallLinkIterator(private val cursor: Cursor) : Iterator, Closeable { + override fun hasNext(): Boolean { + return cursor.count > 0 && !cursor.isLast + } + + override fun next(): BackupRecipient { + if (!cursor.moveToNext()) { + throw NoSuchElementException() + } + + val callLink = CallLinkTable.CallLinkDeserializer.deserialize(cursor) + return BackupRecipient( + id = callLink.recipientId.toLong(), + callLink = CallLink( + rootKey = callLink.credentials!!.linkKeyBytes.toByteString(), + adminKey = callLink.credentials.adminPassBytes?.toByteString(), + name = callLink.state.name, + expirationMs = callLink.state.expiration.toEpochMilli(), + restrictions = callLink.state.restrictions.toBackup() + ) + ) + } + + override fun close() { + cursor.close() + } +} + +private fun CallLinkState.Restrictions.toBackup(): CallLink.Restrictions { + return when (this) { + CallLinkState.Restrictions.ADMIN_APPROVAL -> CallLink.Restrictions.ADMIN_APPROVAL + CallLinkState.Restrictions.NONE -> CallLink.Restrictions.NONE + CallLinkState.Restrictions.UNKNOWN -> CallLink.Restrictions.UNKNOWN + } +} + +private fun CallLink.Restrictions.toLocal(): CallLinkState.Restrictions { + return when (this) { + CallLink.Restrictions.ADMIN_APPROVAL -> CallLinkState.Restrictions.ADMIN_APPROVAL + CallLink.Restrictions.NONE -> CallLinkState.Restrictions.NONE + CallLink.Restrictions.UNKNOWN -> CallLinkState.Restrictions.UNKNOWN + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallTableBackupExtensions.kt index 5f3116b0d5..8e2060df94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/CallTableBackupExtensions.kt @@ -8,57 +8,37 @@ package org.thoughtcrime.securesms.backup.v2.database import android.database.Cursor import android.database.sqlite.SQLiteDatabase import androidx.core.content.contentValuesOf -import org.signal.core.util.isNull -import org.signal.core.util.requireInt import org.signal.core.util.requireLong import org.signal.core.util.select import org.thoughtcrime.securesms.backup.v2.BackupState -import org.thoughtcrime.securesms.backup.v2.proto.Call +import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.RecipientTable import java.io.Closeable -typealias BackupCall = org.thoughtcrime.securesms.backup.v2.proto.Call - -fun CallTable.getCallsForBackup(): CallLogIterator { +fun CallTable.getAdhocCallsForBackup(): CallLogIterator { return CallLogIterator( readableDatabase .select() .from(CallTable.TABLE_NAME) - .where("${CallTable.EVENT} != ${CallTable.Event.serialize(CallTable.Event.DELETE)}") + .where("${CallTable.TYPE}=?", CallTable.Type.AD_HOC_CALL) .run() ) } -fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupState) { - val type = when (call.type) { - Call.Type.VIDEO_CALL -> CallTable.Type.VIDEO_CALL - Call.Type.AUDIO_CALL -> CallTable.Type.AUDIO_CALL - Call.Type.AD_HOC_CALL -> CallTable.Type.AD_HOC_CALL - Call.Type.GROUP_CALL -> CallTable.Type.GROUP_CALL - Call.Type.UNKNOWN_TYPE -> return - } - +fun CallTable.restoreCallLogFromBackup(call: AdHocCall, backupState: BackupState) { val event = when (call.state) { - Call.State.MISSED -> CallTable.Event.MISSED - Call.State.COMPLETED -> CallTable.Event.ACCEPTED - Call.State.DECLINED_BY_USER -> CallTable.Event.DECLINED - Call.State.DECLINED_BY_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE - Call.State.UNKNOWN_EVENT -> return + AdHocCall.State.GENERIC -> CallTable.Event.GENERIC_GROUP_CALL + AdHocCall.State.UNKNOWN_STATE -> CallTable.Event.GENERIC_GROUP_CALL } - val direction = if (call.outgoing) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING - - backupState.callIdToType[call.callId] = CallTable.Call.getMessageType(type, direction, event) - val values = contentValuesOf( CallTable.CALL_ID to call.callId, - CallTable.PEER to backupState.backupToLocalRecipientId[call.conversationRecipientId]!!.serialize(), - CallTable.TYPE to CallTable.Type.serialize(type), - CallTable.DIRECTION to CallTable.Direction.serialize(direction), + CallTable.PEER to backupState.backupToLocalRecipientId[call.recipientId]!!.serialize(), + CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL), + CallTable.DIRECTION to CallTable.Direction.serialize(CallTable.Direction.OUTGOING), CallTable.EVENT to CallTable.Event.serialize(event), - CallTable.TIMESTAMP to call.timestamp, - CallTable.RINGER to if (call.ringerRecipientId != null) backupState.backupToLocalRecipientId[call.ringerRecipientId]?.toLong() else null + CallTable.TIMESTAMP to call.startedCallTimestamp ) writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values) @@ -68,49 +48,23 @@ fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupStat * Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s. * Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources. */ -class CallLogIterator(private val cursor: Cursor) : Iterator, Closeable { +class CallLogIterator(private val cursor: Cursor) : Iterator, Closeable { override fun hasNext(): Boolean { return cursor.count > 0 && !cursor.isLast } - override fun next(): BackupCall? { + override fun next(): AdHocCall? { if (!cursor.moveToNext()) { throw NoSuchElementException() } val callId = cursor.requireLong(CallTable.CALL_ID) - val type = CallTable.Type.deserialize(cursor.requireInt(CallTable.TYPE)) - val direction = CallTable.Direction.deserialize(cursor.requireInt(CallTable.DIRECTION)) - val event = CallTable.Event.deserialize(cursor.requireInt(CallTable.EVENT)) - return BackupCall( + return AdHocCall( callId = callId, - conversationRecipientId = cursor.requireLong(CallTable.PEER), - type = when (type) { - CallTable.Type.AUDIO_CALL -> Call.Type.AUDIO_CALL - CallTable.Type.VIDEO_CALL -> Call.Type.VIDEO_CALL - CallTable.Type.AD_HOC_CALL -> Call.Type.AD_HOC_CALL - CallTable.Type.GROUP_CALL -> Call.Type.GROUP_CALL - }, - outgoing = when (direction) { - CallTable.Direction.OUTGOING -> true - else -> false - }, - timestamp = cursor.requireLong(CallTable.TIMESTAMP), - ringerRecipientId = if (cursor.isNull(CallTable.RINGER)) null else cursor.requireLong(CallTable.RINGER), - state = when (event) { - CallTable.Event.ONGOING -> Call.State.COMPLETED - CallTable.Event.OUTGOING_RING -> Call.State.COMPLETED - CallTable.Event.ACCEPTED -> Call.State.COMPLETED - CallTable.Event.DECLINED -> Call.State.DECLINED_BY_USER - CallTable.Event.GENERIC_GROUP_CALL -> Call.State.COMPLETED - CallTable.Event.JOINED -> Call.State.COMPLETED - CallTable.Event.MISSED -> Call.State.MISSED - CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.State.DECLINED_BY_NOTIFICATION_PROFILE - CallTable.Event.DELETE -> Call.State.COMPLETED - CallTable.Event.RINGING -> Call.State.MISSED - CallTable.Event.NOT_ACCEPTED -> Call.State.MISSED - } + recipientId = cursor.requireLong(CallTable.PEER), + state = AdHocCall.State.GENERIC, + startedCallTimestamp = cursor.requireLong(CallTable.TIMESTAMP) ) } 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 f6646be505..65a1d231f6 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 @@ -6,7 +6,6 @@ package org.thoughtcrime.securesms.backup.v2.database import android.database.Cursor -import com.annimon.stream.Stream import okio.ByteString.Companion.toByteString import org.signal.core.util.Base64 import org.signal.core.util.Base64.decode @@ -20,13 +19,12 @@ import org.signal.core.util.requireString import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName -import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.ChatItem import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.FilePointer -import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate -import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate +import org.thoughtcrime.securesms.backup.v2.proto.GroupCall +import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.Quote @@ -39,6 +37,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage import org.thoughtcrime.securesms.backup.v2.proto.Text import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.GroupReceiptTable import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.MessageTypes @@ -66,7 +65,6 @@ import java.io.Closeable import java.io.IOException import java.util.LinkedList import java.util.Queue -import java.util.UUID import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange /** @@ -139,7 +137,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST)) MessageTypes.isExpirationTimerUpdate(record.type) -> { builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt())) - builder.expiresInMs = null + builder.expiresInMs = 0 } MessageTypes.isProfileChange(record.type) -> { if (record.body == null) continue @@ -203,54 +201,112 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: builder.sms = false val call = calls.getCallByMessageId(record.id) if (call != null) { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId)) + if (call.type == CallTable.Type.GROUP_CALL) { + builder.updateMessage = ChatUpdateMessage( + groupCall = GroupCall( + callId = record.id, + state = when (call.event) { + CallTable.Event.MISSED -> GroupCall.State.MISSED + CallTable.Event.ONGOING -> GroupCall.State.GENERIC + CallTable.Event.ACCEPTED -> GroupCall.State.ACCEPTED + CallTable.Event.NOT_ACCEPTED -> GroupCall.State.GENERIC + CallTable.Event.MISSED_NOTIFICATION_PROFILE -> GroupCall.State.MISSED_NOTIFICATION_PROFILE + CallTable.Event.DELETE -> continue + CallTable.Event.GENERIC_GROUP_CALL -> GroupCall.State.GENERIC + CallTable.Event.JOINED -> GroupCall.State.JOINED + CallTable.Event.RINGING -> GroupCall.State.RINGING + CallTable.Event.DECLINED -> GroupCall.State.DECLINED + CallTable.Event.OUTGOING_RING -> GroupCall.State.OUTGOING_RING + }, + ringerRecipientId = call.ringerRecipient?.toLong(), + startedCallAci = if (call.ringerRecipient != null) SignalDatabase.recipients.getRecord(call.ringerRecipient).aci?.toByteString() else null, + startedCallTimestamp = call.timestamp + ) + ) + } else if (call.type != CallTable.Type.AD_HOC_CALL) { + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + callId = call.callId, + type = if (call.type == CallTable.Type.VIDEO_CALL) IndividualCall.Type.VIDEO_CALL else IndividualCall.Type.AUDIO_CALL, + direction = if (call.direction == CallTable.Direction.INCOMING) IndividualCall.Direction.INCOMING else IndividualCall.Direction.OUTGOING, + state = when (call.event) { + CallTable.Event.MISSED -> IndividualCall.State.MISSED + CallTable.Event.MISSED_NOTIFICATION_PROFILE -> IndividualCall.State.MISSED_NOTIFICATION_PROFILE + CallTable.Event.ACCEPTED -> IndividualCall.State.ACCEPTED + CallTable.Event.NOT_ACCEPTED -> IndividualCall.State.NOT_ACCEPTED + else -> IndividualCall.State.UNKNOWN_STATE + }, + startedCallTimestamp = call.timestamp + ) + ) + } else { + continue + } } else { when { MessageTypes.isMissedAudioCall(record.type) -> { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL))) + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + type = IndividualCall.Type.AUDIO_CALL, + state = IndividualCall.State.MISSED, + direction = IndividualCall.Direction.INCOMING + ) + ) } MessageTypes.isMissedVideoCall(record.type) -> { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL))) + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + type = IndividualCall.Type.VIDEO_CALL, + state = IndividualCall.State.MISSED, + direction = IndividualCall.Direction.INCOMING + ) + ) } MessageTypes.isIncomingAudioCall(record.type) -> { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL))) + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + type = IndividualCall.Type.AUDIO_CALL, + state = IndividualCall.State.ACCEPTED, + direction = IndividualCall.Direction.INCOMING + ) + ) } MessageTypes.isIncomingVideoCall(record.type) -> { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL))) + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + type = IndividualCall.Type.VIDEO_CALL, + state = IndividualCall.State.ACCEPTED, + direction = IndividualCall.Direction.INCOMING + ) + ) } MessageTypes.isOutgoingAudioCall(record.type) -> { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL))) + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + type = IndividualCall.Type.AUDIO_CALL, + state = IndividualCall.State.ACCEPTED, + direction = IndividualCall.Direction.OUTGOING + ) + ) } MessageTypes.isOutgoingVideoCall(record.type) -> { - builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL))) + builder.updateMessage = ChatUpdateMessage( + individualCall = IndividualCall( + type = IndividualCall.Type.VIDEO_CALL, + state = IndividualCall.State.ACCEPTED, + direction = IndividualCall.Direction.OUTGOING + ) + ) } MessageTypes.isGroupCall(record.type) -> { try { val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body) - - val joinedMembers = Stream.of(groupCallUpdateDetails.inCallUuids) - .map { uuid: String? -> UuidUtil.parseOrNull(uuid) } - .withoutNulls() - .map { obj: UUID? -> ACI.from(obj!!).toByteString() } - .toList() - - val localUserJoined: GroupCallChatUpdate.LocalUserJoined = if (groupCallUpdateDetails.localUserJoined) { - GroupCallChatUpdate.LocalUserJoined.JOINED - } else if (groupCallUpdateDetails.endedCallTimestamp == 0L) { - GroupCallChatUpdate.LocalUserJoined.UNKNOWN - } else { - GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN - } - builder.updateMessage = ChatUpdateMessage( - callingMessage = CallChatUpdate( - groupCall = GroupCallChatUpdate( - startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(), - startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp, - inCallAcis = joinedMembers, - localUserJoined = localUserJoined, - endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp - ) + groupCall = GroupCall( + state = GroupCall.State.GENERIC, + startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(), + startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp, + endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp ) ) } catch (exception: java.lang.Exception) { @@ -298,12 +354,13 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: chatId = record.threadId authorId = record.fromRecipientId dateSent = record.dateSent - expireStartDate = if (record.expireStarted > 0) record.expireStarted else null - expiresInMs = if (record.expiresIn > 0) record.expiresIn else null + expireStartDate = if (record.expireStarted > 0) record.expireStarted else 0 + expiresInMs = if (record.expiresIn > 0) record.expiresIn else 0 revisions = emptyList() sms = !MessageTypes.isSecureType(record.type) - - if (MessageTypes.isOutgoingMessageType(record.type)) { + if (MessageTypes.isCallLog(record.type)) { + directionless = ChatItem.DirectionlessMessageDetails() + } else if (MessageTypes.isOutgoingMessageType(record.type)) { outgoing = ChatItem.OutgoingMessageDetails( sendStatus = record.toBackupSendStatus(groupReceipts) ) 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 e735f62060..c8f3ee2dea 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 @@ -22,7 +22,8 @@ import org.thoughtcrime.securesms.backup.v2.BackupState import org.thoughtcrime.securesms.backup.v2.proto.BodyRange import org.thoughtcrime.securesms.backup.v2.proto.ChatItem import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage -import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate +import org.thoughtcrime.securesms.backup.v2.proto.GroupCall +import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.Quote import org.thoughtcrime.securesms.backup.v2.proto.Reaction @@ -215,11 +216,53 @@ class ChatItemImportInserter( var followUp: ((Long) -> Unit)? = null if (this.updateMessage != null) { - if (this.updateMessage.callingMessage != null && this.updateMessage.callingMessage.callId != null) { + if (this.updateMessage.individualCall != null && this.updateMessage.individualCall.callId != null) { followUp = { messageRowId -> - val callContentValues = ContentValues() - callContentValues.put(CallTable.MESSAGE_ID, messageRowId) - db.update(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, callContentValues, "${CallTable.CALL_ID} = ?", SqlUtil.buildArgs(this.updateMessage.callingMessage.callId)) + val values = contentValuesOf( + CallTable.CALL_ID to updateMessage.individualCall.callId, + CallTable.MESSAGE_ID to messageRowId, + CallTable.PEER to chatRecipientId.serialize(), + CallTable.TYPE to CallTable.Type.serialize(if (updateMessage.individualCall.type == IndividualCall.Type.VIDEO_CALL) CallTable.Type.VIDEO_CALL else CallTable.Type.AUDIO_CALL), + CallTable.DIRECTION to CallTable.Direction.serialize(if (updateMessage.individualCall.direction == IndividualCall.Direction.OUTGOING) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING), + CallTable.EVENT to CallTable.Event.serialize( + when (updateMessage.individualCall.state) { + IndividualCall.State.MISSED -> CallTable.Event.MISSED + IndividualCall.State.MISSED_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE + IndividualCall.State.ACCEPTED -> CallTable.Event.ACCEPTED + IndividualCall.State.NOT_ACCEPTED -> CallTable.Event.NOT_ACCEPTED + else -> CallTable.Event.MISSED + } + ), + CallTable.TIMESTAMP to updateMessage.individualCall.startedCallTimestamp, + CallTable.READ to CallTable.ReadState.serialize(CallTable.ReadState.UNREAD) + ) + db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values) + } + } else if (this.updateMessage.groupCall != null && this.updateMessage.groupCall.callId != null) { + followUp = { messageRowId -> + val values = contentValuesOf( + CallTable.CALL_ID to updateMessage.groupCall.callId, + CallTable.MESSAGE_ID to messageRowId, + CallTable.PEER to chatRecipientId.serialize(), + CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.GROUP_CALL), + CallTable.DIRECTION to CallTable.Direction.serialize(if (backupState.backupToLocalRecipientId[updateMessage.groupCall.ringerRecipientId] == selfId) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING), + CallTable.EVENT to CallTable.Event.serialize( + when (updateMessage.groupCall.state) { + GroupCall.State.ACCEPTED -> CallTable.Event.ACCEPTED + GroupCall.State.MISSED -> CallTable.Event.MISSED + GroupCall.State.MISSED_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE + GroupCall.State.GENERIC -> CallTable.Event.GENERIC_GROUP_CALL + GroupCall.State.JOINED -> CallTable.Event.JOINED + GroupCall.State.RINGING -> CallTable.Event.RINGING + GroupCall.State.OUTGOING_RING -> CallTable.Event.OUTGOING_RING + GroupCall.State.DECLINED -> CallTable.Event.DECLINED + else -> CallTable.Event.GENERIC_GROUP_CALL + } + ), + CallTable.TIMESTAMP to updateMessage.groupCall.startedCallTimestamp, + CallTable.READ to CallTable.ReadState.serialize(CallTable.ReadState.UNREAD) + ) + db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values) } } } @@ -444,32 +487,22 @@ class ChatItemImportInserter( val threadMergeDetails = ThreadMergeEvent(previousE164 = updateMessage.threadMerge.previousE164.toString()).encode() put(MessageTable.BODY, Base64.encodeWithPadding(threadMergeDetails)) } - updateMessage.callingMessage != null -> { - when { - updateMessage.callingMessage.callId != null -> { - typeFlags = backupState.callIdToType[updateMessage.callingMessage.callId]!! - } - updateMessage.callingMessage.callMessage != null -> { - typeFlags = when (updateMessage.callingMessage.callMessage.type) { - IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL -> MessageTypes.INCOMING_AUDIO_CALL_TYPE - IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL -> MessageTypes.INCOMING_VIDEO_CALL_TYPE - IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE - IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE - IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE - IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE - IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE - IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE - IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags - } - } - updateMessage.callingMessage.groupCall != null -> { - typeFlags = MessageTypes.GROUP_CALL_TYPE - this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.callingMessage.groupCall)) + updateMessage.individualCall != null -> { + if (updateMessage.individualCall.state == IndividualCall.State.MISSED || updateMessage.individualCall.state == IndividualCall.State.MISSED_NOTIFICATION_PROFILE) { + typeFlags = if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) MessageTypes.MISSED_AUDIO_CALL_TYPE else MessageTypes.MISSED_VIDEO_CALL_TYPE + } else { + typeFlags = if (updateMessage.individualCall.direction == IndividualCall.Direction.OUTGOING) { + if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) MessageTypes.OUTGOING_AUDIO_CALL_TYPE else MessageTypes.OUTGOING_VIDEO_CALL_TYPE + } else { + if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) MessageTypes.INCOMING_AUDIO_CALL_TYPE else MessageTypes.INCOMING_VIDEO_CALL_TYPE } } - // Calls don't use the incoming/outgoing flags, so we overwrite the flags here this.put(MessageTable.TYPE, typeFlags) } + updateMessage.groupCall != null -> { + this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall)) + this.put(MessageTable.TYPE, MessageTypes.GROUP_CALL_TYPE) + } updateMessage.groupChange != null -> { put(MessageTable.BODY, "") put( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt index 3aab49c032..f685563864 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt @@ -34,7 +34,6 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember import org.signal.storageservice.protos.groups.local.DecryptedTimer import org.signal.storageservice.protos.groups.local.EnabledState -import org.thoughtcrime.securesms.backup.v2.BackupState import org.thoughtcrime.securesms.backup.v2.proto.AccountData import org.thoughtcrime.securesms.backup.v2.proto.Contact import org.thoughtcrime.securesms.backup.v2.proto.Group @@ -47,6 +46,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName @@ -129,25 +129,6 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator { return BackupGroupIterator(cursor) } -/** - * Takes a [BackupRecipient] and writes it into the database. - */ -fun RecipientTable.restoreRecipientFromBackup(recipient: BackupRecipient, backupState: BackupState): RecipientId? { - // TODO Need to handle groups - // TODO Also, should we move this when statement up to mimic the export? Kinda weird that this calls distributionListTable functions - return when { - recipient.contact != null -> restoreContactFromBackup(recipient.contact) - recipient.group != null -> restoreGroupFromBackup(recipient.group) - recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState) - recipient.self != null -> Recipient.self().id - recipient.releaseNotes != null -> restoreReleaseNotes() - else -> { - Log.w(TAG, "Unrecognized recipient type!") - null - } - } -} - /** * Given [AccountData], this will insert the necessary data for the local user into the [RecipientTable]. */ @@ -187,7 +168,7 @@ fun RecipientTable.clearAllDataForBackupRestore() { ApplicationDependencies.getRecipientCache().clearSelf() } -private fun RecipientTable.restoreContactFromBackup(contact: Contact): RecipientId { +fun RecipientTable.restoreContactFromBackup(contact: Contact): RecipientId { val id = getAndPossiblyMergePnpVerified( aci = ACI.parseOrNull(contact.aci?.toByteArray()), pni = PNI.parseOrNull(contact.pni?.toByteArray()), @@ -218,7 +199,7 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient return id } -private fun RecipientTable.restoreReleaseNotes(): RecipientId { +fun RecipientTable.restoreReleaseNotes(): RecipientId { val releaseChannelId: RecipientId = insertReleaseChannelRecipient() SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelId) @@ -227,12 +208,16 @@ private fun RecipientTable.restoreReleaseNotes(): RecipientId { return releaseChannelId } -private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId { +fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId { val masterKey = GroupMasterKey(group.masterKey.toByteArray()) val groupId = GroupId.v2(masterKey) val operations = ApplicationDependencies.getGroupsV2Operations().forGroup(GroupSecretParams.deriveFromMasterKey(masterKey)) - val decryptedState = group.snapshot!!.toDecryptedGroup(operations) + val decryptedState = if (group.snapshot == null) { + DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) + } else { + group.snapshot.toDecryptedGroup(operations) + } val values = ContentValues().apply { put(RecipientTable.GROUP_ID, groupId.toString()) @@ -296,7 +281,10 @@ private fun Member.Role.toSnapshot(): Group.Member.Role { } } -private fun DecryptedGroup.toSnapshot(): Group.GroupSnapshot { +private fun DecryptedGroup.toSnapshot(): Group.GroupSnapshot? { + if (revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION || revision == GroupsV2StateProcessor.PLACEHOLDER_REVISION) { + return null + } return Group.GroupSnapshot( title = title, avatar = avatar, @@ -479,7 +467,6 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator + SignalDatabase.calls.getAdhocCallsForBackup().use { reader -> for (callLog in reader) { if (callLog != null) { - emitter.emit(Frame(call = callLog)) + emitter.emit(Frame(adHocCall = callLog)) } } } } - fun import(call: BackupCall, backupState: BackupState) { + fun import(call: AdHocCall, backupState: BackupState) { SignalDatabase.calls.restoreCallLogFromBackup(call, backupState) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt index ae8c81b743..8888f99faa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt @@ -10,9 +10,13 @@ import org.thoughtcrime.securesms.backup.v2.BackupState import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.database.BackupRecipient import org.thoughtcrime.securesms.backup.v2.database.getAllForBackup +import org.thoughtcrime.securesms.backup.v2.database.getCallLinksForBackup import org.thoughtcrime.securesms.backup.v2.database.getContactsForBackup import org.thoughtcrime.securesms.backup.v2.database.getGroupsForBackup -import org.thoughtcrime.securesms.backup.v2.database.restoreRecipientFromBackup +import org.thoughtcrime.securesms.backup.v2.database.restoreContactFromBackup +import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup +import org.thoughtcrime.securesms.backup.v2.database.restoreGroupFromBackup +import org.thoughtcrime.securesms.backup.v2.database.restoreReleaseNotes import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter @@ -60,10 +64,26 @@ object RecipientBackupProcessor { state.recipientIds.add(it.id) emitter.emit(Frame(recipient = it)) } + + SignalDatabase.callLinks.getCallLinksForBackup().forEach { + state.recipientIds.add(it.id) + emitter.emit(Frame(recipient = it)) + } } fun import(recipient: BackupRecipient, backupState: BackupState) { - val newId = SignalDatabase.recipients.restoreRecipientFromBackup(recipient, backupState) + val newId = when { + recipient.contact != null -> SignalDatabase.recipients.restoreContactFromBackup(recipient.contact) + recipient.group != null -> SignalDatabase.recipients.restoreGroupFromBackup(recipient.group) + recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState) + recipient.self != null -> Recipient.self().id + recipient.releaseNotes != null -> SignalDatabase.recipients.restoreReleaseNotes() + recipient.callLink != null -> SignalDatabase.callLinks.restoreFromBackup(recipient.callLink) + else -> { + Log.w(TAG, "Unrecognized recipient type!") + null + } + } if (newId != null) { backupState.backupToLocalRecipientId[recipient.id] = newId } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt index 202d350c49..401e44bd14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallLinkTable.kt @@ -91,18 +91,22 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database fun insertCallLink( callLink: CallLink - ) { - writableDatabase.withinTransaction { db -> + ): RecipientId { + val recipientId: RecipientId = writableDatabase.withinTransaction { db -> val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId) db .insertInto(TABLE_NAME) .values(CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId))) .run() + + recipientId } ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(callLink.roomId) ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() + + return recipientId!! } fun updateCallLinkCredentials( @@ -402,7 +406,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database } } - private object CallLinkDeserializer : Serializer { + object CallLinkDeserializer : Serializer { override fun serialize(data: CallLink): Cursor { throw UnsupportedOperationException() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java index 56c58e397f..2264893a89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupCallUpdateDetailsUtil.java @@ -5,7 +5,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.Base64; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupCall; import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.push.ServiceId; @@ -28,7 +28,7 @@ public final class GroupCallUpdateDetailsUtil { /** * Generates a group chat update message body from backup data */ - public static @NonNull String createBodyFromBackup(@NonNull GroupCallChatUpdate groupCallChatUpdate) { + public static @NonNull String createBodyFromBackup(@NonNull GroupCall groupCallChatUpdate) { ServiceId.ACI startedCall = groupCallChatUpdate.startedCallAci != null ? ServiceId.ACI.parseOrNull(groupCallChatUpdate.startedCallAci) : null; GroupCallUpdateDetails details = new GroupCallUpdateDetails.Builder() @@ -36,15 +36,7 @@ public final class GroupCallUpdateDetailsUtil { .startedCallTimestamp(groupCallChatUpdate.startedCallTimestamp) .endedCallTimestamp(groupCallChatUpdate.endedCallTimestamp) .isCallFull(false) - .inCallUuids(groupCallChatUpdate.inCallAcis.stream() - .filter(Objects::nonNull) - .map(ServiceId.ACI::parseOrNull) - .filter(Objects::nonNull) - .map(ServiceId.ACI::toString) - .collect(Collectors.toList()) - ) .isRingingOnLocalDevice(false) - .localUserJoined(groupCallChatUpdate.localUserJoined != GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN) .build(); return Base64.encodeWithPadding(details.encode()); diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index a258d90c37..4386f398cf 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -9,14 +9,26 @@ message BackupInfo { uint64 backupTimeMs = 2; } +// Frames must follow in the following ordering rules: +// +// 1. There is exactly one AccountData and it is the first frame. +// 2. A frame referenced by ID must come before the referencing frame. +// e.g. a Recipient must come before any Chat referencing it. +// 3. All ChatItems must appear in global Chat rendering order. +// (The order in which they were received by the client.) +// +// Recipients, Chats, StickerPacks, and AdHocCalls can be in any order. +// (But must respect rule 2.) +// For example, Chats may all be together at the beginning, +// or may each immediately precede its first ChatItem. message Frame { oneof item { AccountData account = 1; Recipient recipient = 2; Chat chat = 3; ChatItem chatItem = 4; - Call call = 5; - StickerPack stickerPack = 6; + StickerPack stickerPack = 5; + AdHocCall adHocCall = 6; } } @@ -84,6 +96,7 @@ message Recipient { DistributionList distributionList = 4; Self self = 5; ReleaseNotes releaseNotes = 6; + CallLink callLink = 7; } } @@ -120,8 +133,7 @@ message Group { bool whitelisted = 2; bool hideStory = 3; StorySendMode storySendMode = 4; - string name = 5; - GroupSnapshot snapshot = 6; + GroupSnapshot snapshot = 5; // These are simply plaintext copies of the groups proto from Groups.proto. // They should be kept completely in-sync with Groups.proto. @@ -154,7 +166,7 @@ message Group { bytes userId = 1; Role role = 2; bytes profileKey = 3; - bytes presentation = 4; + reserved /*presentation*/ 4; // The field is deprecated in the context of static group state uint32 joinedAtVersion = 5; } @@ -207,6 +219,44 @@ message Chat { FilePointer wallpaper = 9; } +/** + * Call Links have some associated data including a call, but unlike other recipients + * are not tied to threads because they do not have messages associated with them. + * + * note: + * - room id can be derived from the root key + * - the presence of an admin key means this user is a call admin + */ +message CallLink { + enum Restrictions { + UNKNOWN = 0; + NONE = 1; + ADMIN_APPROVAL = 2; + } + + bytes rootKey = 1; + optional bytes adminKey = 2; // Only present if the user is an admin + string name = 3; + Restrictions restrictions = 4; + uint64 expirationMs = 5; +} + +message AdHocCall { + enum State { + UNKNOWN_STATE = 0; + GENERIC = 1; + } + + uint64 callId = 1; + // Refers to a `CallLink` recipient. + uint64 recipientId = 2; + State state = 3; + optional bytes startedCallAci = 4; + uint64 startedCallTimestamp = 5; + // The time the call ended. 0 indicates an unknown time. + uint64 endedCallTimestamp = 6; +} + message DistributionList { enum PrivacyMode { UNKNOWN = 0; @@ -232,32 +282,6 @@ message Identity { bool nonblockingApproval = 6; } -message Call { - enum Type { - UNKNOWN_TYPE = 0; - AUDIO_CALL = 1; - VIDEO_CALL = 2; - GROUP_CALL = 3; - AD_HOC_CALL = 4; - } - - enum State { - UNKNOWN_EVENT = 0; - COMPLETED = 1; // A call that was successfully completed or was accepted and in-progress at the time of the backup. - DECLINED_BY_USER = 2; // An incoming call that was manually declined by the user. - DECLINED_BY_NOTIFICATION_PROFILE = 3; // An incoming call that was automatically declined by an active notification profile. - MISSED = 4; // An incoming call that either expired, was cancelled by the sender, or was auto-rejected due to already being in a different call. - } - - uint64 callId = 1; - uint64 conversationRecipientId = 2; - Type type = 3; - bool outgoing = 4; - uint64 timestamp = 5; - optional uint64 ringerRecipientId = 6; - State state = 7; -} - message ChatItem { message IncomingMessageDetails { uint64 dateReceived = 1; @@ -276,8 +300,8 @@ message ChatItem { uint64 chatId = 1; // conversation id uint64 authorId = 2; // recipient id uint64 dateSent = 3; - optional uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down - optional uint64 expiresInMs = 5; // how long timer of message is (ms) + uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down + uint64 expiresInMs = 5; // how long timer of message is (ms) repeated ChatItem revisions = 6; // ordered from oldest to newest bool sms = 7; @@ -561,46 +585,72 @@ message ChatUpdateMessage { ProfileChangeChatUpdate profileChange = 4; ThreadMergeChatUpdate threadMerge = 5; SessionSwitchoverChatUpdate sessionSwitchover = 6; - CallChatUpdate callingMessage = 7; + IndividualCall individualCall = 7; + GroupCall groupCall = 8; } } -message CallChatUpdate{ - oneof call { - uint64 callId = 1; // maps to id of Call from call log - IndividualCallChatUpdate callMessage = 2; - GroupCallChatUpdate groupCall = 3; - } -} - -message IndividualCallChatUpdate { +message IndividualCall { enum Type { - UNKNOWN = 0; - INCOMING_AUDIO_CALL = 1; - INCOMING_VIDEO_CALL = 2; - OUTGOING_AUDIO_CALL = 3; - OUTGOING_VIDEO_CALL = 4; - MISSED_INCOMING_AUDIO_CALL = 5; - MISSED_INCOMING_VIDEO_CALL = 6; - UNANSWERED_OUTGOING_AUDIO_CALL = 7; - UNANSWERED_OUTGOING_VIDEO_CALL = 8; + UNKNOWN_TYPE = 0; + AUDIO_CALL = 1; + VIDEO_CALL = 2; } - Type type = 1; + enum Direction { + UNKNOWN_DIRECTION = 0; + INCOMING = 1; + OUTGOING = 2; + } + + enum State { + UNKNOWN_STATE = 0; + ACCEPTED = 1; + NOT_ACCEPTED = 2; + // An incoming call that is no longer ongoing, which we neither accepted + // not actively declined. For example, it expired, was canceled by the + // sender, or was rejected due to being in another call. + MISSED = 3; + // We auto-declined an incoming call due to a notification profile. + MISSED_NOTIFICATION_PROFILE = 4; + } + + optional uint64 callId = 1; + Type type = 2; + Direction direction = 3; + State state = 4; + uint64 startedCallTimestamp = 5; } -message GroupCallChatUpdate { - enum LocalUserJoined { - UNKNOWN = 0; - JOINED = 1; - DID_NOT_JOIN = 2; +message GroupCall { + enum State { + UNKNOWN_STATE = 0; + // A group call was started without ringing. + GENERIC = 1; + // We joined a group call that was started without ringing. + JOINED = 2; + // An incoming group call is actively ringing. + RINGING = 3; + // We accepted an incoming group ring. + ACCEPTED = 4; + // We declined an incoming group ring. + DECLINED = 5; + // We missed an incoming group ring, for example because it expired. + MISSED = 6; + // We auto-declined an incoming group ring due to a notification profile. + MISSED_NOTIFICATION_PROFILE = 7; + // An outgoing ring was started. We don't track any state for outgoing rings + // beyond that they started. + OUTGOING_RING = 8; } - optional bytes startedCallAci = 1; - uint64 startedCallTimestamp = 2; - repeated bytes inCallAcis = 3; - uint64 endedCallTimestamp = 4; // 0 indicates we do not know - LocalUserJoined localUserJoined = 5; + optional uint64 callId = 1; + State state = 2; + optional uint64 ringerRecipientId = 3; + optional bytes startedCallAci = 4; + uint64 startedCallTimestamp = 5; + // The time the call ended. 0 indicates an unknown time. + uint64 endedCallTimestamp = 6; } message SimpleChatUpdate {