Add support for new backup calls proto and call links.

This commit is contained in:
Clark
2024-04-30 10:05:31 -04:00
committed by Greyson Parrelli
parent 333fa22c96
commit 1223c3c768
12 changed files with 502 additions and 373 deletions
@@ -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<Call>()
val groupCalls = ArrayList<Call>()
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<ChatItem>()
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<Chat>()
val chatItemsImported = ArrayList<ChatItem>()
val chatItemsExported = ArrayList<ChatItem>()
val callsImported = ArrayList<Call>()
val callsExported = ArrayList<Call>()
val callsImported = ArrayList<AdHocCall>()
val callsExported = ArrayList<AdHocCall>()
val stickersImported = ArrayList<StickerPack>()
val stickersExported = ArrayList<StickerPack>()
@@ -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!!)
}
}
@@ -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<Long, Long>()
val chatIdToLocalRecipientId = HashMap<Long, RecipientId>()
val chatIdToBackupRecipientId = HashMap<Long, Long>()
val callIdToType = HashMap<Long, Long>()
}
class BackupMetadata(
@@ -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<BackupRecipient>, 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
}
}
@@ -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<BackupCall?>, Closeable {
class CallLogIterator(private val cursor: Cursor) : Iterator<AdHocCall?>, 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)
)
}
@@ -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)
)
@@ -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(
@@ -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<BackupRecipient
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
hideStory = extras?.hideStory() ?: false,
storySendMode = showAsStoryState.toGroupStorySendMode(),
name = cursor.requireString(GroupTable.TITLE) ?: "",
snapshot = decryptedGroup.toSnapshot()
)
)
@@ -7,29 +7,28 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.database.getCallsForBackup
import org.thoughtcrime.securesms.backup.v2.database.getAdhocCallsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreCallLogFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
typealias BackupCall = org.thoughtcrime.securesms.backup.v2.proto.Call
object AdHocCallBackupProcessor {
object CallLogBackupProcessor {
val TAG = Log.tag(CallLogBackupProcessor::class.java)
val TAG = Log.tag(AdHocCallBackupProcessor::class.java)
fun export(emitter: BackupFrameEmitter) {
SignalDatabase.calls.getCallsForBackup().use { reader ->
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)
}
}
@@ -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
}
@@ -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<CallLink, Cursor> {
object CallLinkDeserializer : Serializer<CallLink, Cursor> {
override fun serialize(data: CallLink): Cursor {
throw UnsupportedOperationException()
}
@@ -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());
+113 -63
View File
@@ -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 {