mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Add remaining non-group update messages for backup.
This commit is contained in:
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -20,8 +21,10 @@ import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
@@ -60,7 +63,8 @@ class BackupTest {
|
||||
|
||||
/** Columns that we don't need to check equality of */
|
||||
private val IGNORED_COLUMNS: Map<String, Set<String>> = mapOf(
|
||||
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID)
|
||||
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID),
|
||||
MessageTable.TABLE_NAME to setOf(MessageTable.FROM_DEVICE_ID)
|
||||
)
|
||||
|
||||
/** Tables we don't need to check equality of */
|
||||
@@ -145,6 +149,88 @@ class BackupTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun individualCallLogs() {
|
||||
backupTest {
|
||||
val aliceId = individualRecipient(
|
||||
aci = ALICE_ACI,
|
||||
pni = ALICE_PNI,
|
||||
e164 = ALICE_E164,
|
||||
givenName = "Alice",
|
||||
familyName = "Smith",
|
||||
username = "alice.99",
|
||||
hidden = false,
|
||||
registeredState = RecipientTable.RegisteredState.REGISTERED,
|
||||
profileKey = ProfileKey(Random.nextBytes(32)),
|
||||
profileSharing = true,
|
||||
hideStory = false
|
||||
)
|
||||
insertOneToOneCallVariations(1, 1, aliceId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertOneToOneCallVariations(callId: Long, timestamp: Long, id: RecipientId): Long {
|
||||
val directions = arrayOf(CallTable.Direction.INCOMING, CallTable.Direction.OUTGOING)
|
||||
val callTypes = arrayOf(CallTable.Type.AUDIO_CALL, CallTable.Type.VIDEO_CALL)
|
||||
val events = arrayOf(
|
||||
CallTable.Event.MISSED,
|
||||
CallTable.Event.OUTGOING_RING,
|
||||
CallTable.Event.ONGOING,
|
||||
CallTable.Event.ACCEPTED,
|
||||
CallTable.Event.NOT_ACCEPTED
|
||||
)
|
||||
var callTimestamp: Long = timestamp
|
||||
var currentCallId = callId
|
||||
for (direction in directions) {
|
||||
for (event in events) {
|
||||
for (type in callTypes) {
|
||||
insertOneToOneCall(callId = currentCallId, callTimestamp, id, type, direction, event)
|
||||
callTimestamp++
|
||||
currentCallId++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentCallId
|
||||
}
|
||||
|
||||
private fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: CallTable.Type, direction: CallTable.Direction, event: CallTable.Event) {
|
||||
val messageType: Long = CallTable.Call.getMessageType(type, direction, event)
|
||||
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val recipient = Recipient.resolved(peer)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val outgoing = direction == CallTable.Direction.OUTGOING
|
||||
|
||||
val messageValues = contentValuesOf(
|
||||
MessageTable.FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else peer.serialize(),
|
||||
MessageTable.FROM_DEVICE_ID to 1,
|
||||
MessageTable.TO_RECIPIENT_ID to if (outgoing) peer.serialize() else Recipient.self().id.serialize(),
|
||||
MessageTable.DATE_RECEIVED to timestamp,
|
||||
MessageTable.DATE_SENT to timestamp,
|
||||
MessageTable.READ to 1,
|
||||
MessageTable.TYPE to messageType,
|
||||
MessageTable.THREAD_ID to threadId
|
||||
)
|
||||
|
||||
val messageId = SignalDatabase.rawDatabase.insert(MessageTable.TABLE_NAME, null, messageValues)
|
||||
|
||||
val values = contentValuesOf(
|
||||
CallTable.CALL_ID to callId,
|
||||
CallTable.MESSAGE_ID to messageId,
|
||||
CallTable.PEER to peer.serialize(),
|
||||
CallTable.TYPE to CallTable.Type.serialize(type),
|
||||
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
|
||||
CallTable.EVENT to CallTable.Event.serialize(event),
|
||||
CallTable.TIMESTAMP to timestamp
|
||||
)
|
||||
|
||||
SignalDatabase.rawDatabase.insert(CallTable.TABLE_NAME, null, values)
|
||||
|
||||
SignalDatabase.threads.update(threadId, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accountData() {
|
||||
val context = ApplicationDependencies.getApplication()
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
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.ChatBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
|
||||
@@ -70,6 +71,11 @@ object BackupRepository {
|
||||
eventTimer.emit("thread")
|
||||
}
|
||||
|
||||
CallLogBackupProcessor.export { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("call")
|
||||
}
|
||||
|
||||
ChatItemBackupProcessor.export { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("message")
|
||||
@@ -131,6 +137,11 @@ object BackupRepository {
|
||||
eventTimer.emit("chat")
|
||||
}
|
||||
|
||||
frame.call != null -> {
|
||||
CallLogBackupProcessor.import(frame.call, backupState)
|
||||
eventTimer.emit("call")
|
||||
}
|
||||
|
||||
frame.chatItem != null -> {
|
||||
chatItemInserter.insert(frame.chatItem)
|
||||
eventTimer.emit("chatItem")
|
||||
@@ -214,4 +225,5 @@ class BackupState {
|
||||
val chatIdToLocalThreadId = HashMap<Long, Long>()
|
||||
val chatIdToLocalRecipientId = HashMap<Long, RecipientId>()
|
||||
val chatIdToBackupRecipientId = HashMap<Long, Long>()
|
||||
val callIdToType = HashMap<Long, Long>()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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.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 {
|
||||
return CallLogIterator(
|
||||
readableDatabase
|
||||
.select()
|
||||
.from(CallTable.TABLE_NAME)
|
||||
.where("${CallTable.EVENT} != ${CallTable.Event.serialize(CallTable.Event.DELETE)}")
|
||||
.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
|
||||
}
|
||||
|
||||
val event = when (call.event) {
|
||||
Call.Event.DELETE -> CallTable.Event.DELETE
|
||||
Call.Event.JOINED -> CallTable.Event.JOINED
|
||||
Call.Event.GENERIC_GROUP_CALL -> CallTable.Event.GENERIC_GROUP_CALL
|
||||
Call.Event.DECLINED -> CallTable.Event.DECLINED
|
||||
Call.Event.ACCEPTED -> CallTable.Event.ACCEPTED
|
||||
Call.Event.MISSED -> CallTable.Event.MISSED
|
||||
Call.Event.OUTGOING_RING -> CallTable.Event.OUTGOING_RING
|
||||
Call.Event.OUTGOING -> CallTable.Event.ONGOING
|
||||
Call.Event.NOT_ACCEPTED -> CallTable.Event.NOT_ACCEPTED
|
||||
Call.Event.UNKNOWN_EVENT -> return
|
||||
}
|
||||
|
||||
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.EVENT to CallTable.Event.serialize(event),
|
||||
CallTable.TIMESTAMP to call.timestamp
|
||||
)
|
||||
|
||||
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
override fun hasNext(): Boolean {
|
||||
return cursor.count > 0 && !cursor.isLast
|
||||
}
|
||||
|
||||
override fun next(): BackupCall? {
|
||||
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(
|
||||
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),
|
||||
event = when (event) {
|
||||
CallTable.Event.ONGOING -> Call.Event.OUTGOING
|
||||
CallTable.Event.OUTGOING_RING -> Call.Event.OUTGOING_RING
|
||||
CallTable.Event.ACCEPTED -> Call.Event.ACCEPTED
|
||||
CallTable.Event.DECLINED -> Call.Event.DECLINED
|
||||
CallTable.Event.GENERIC_GROUP_CALL -> Call.Event.GENERIC_GROUP_CALL
|
||||
CallTable.Event.JOINED -> Call.Event.JOINED
|
||||
CallTable.Event.MISSED -> Call.Event.MISSED
|
||||
CallTable.Event.DELETE -> Call.Event.DELETE
|
||||
CallTable.Event.RINGING -> Call.Event.UNKNOWN_EVENT
|
||||
CallTable.Event.NOT_ACCEPTED -> Call.Event.NOT_ACCEPTED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
@@ -6,42 +6,55 @@
|
||||
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.decodeOrThrow
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
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.GroupCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.RemoteDeletedMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
|
||||
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.GroupReceiptTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -121,6 +134,79 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
)
|
||||
}
|
||||
MessageTypes.isSessionSwitchoverType(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
sessionSwitchover = try {
|
||||
val event = SessionSwitchoverEvent.ADAPTER.decode(decodeOrThrow(record.body!!))
|
||||
SessionSwitchoverChatUpdate(event.e164.e164ToLong()!!)
|
||||
} catch (e: Exception) {
|
||||
SessionSwitchoverChatUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
MessageTypes.isThreadMergeType(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
threadMerge = try {
|
||||
val event = ThreadMergeEvent.ADAPTER.decode(decodeOrThrow(record.body!!))
|
||||
ThreadMergeChatUpdate(event.previousE164.e164ToLong()!!)
|
||||
} catch (e: Exception) {
|
||||
ThreadMergeChatUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
MessageTypes.isCallLog(record.type) -> {
|
||||
val call = calls.getCallByMessageId(record.id)
|
||||
if (call != null) {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
|
||||
} else {
|
||||
when {
|
||||
MessageTypes.isMissedAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL)))
|
||||
}
|
||||
MessageTypes.isMissedVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL)))
|
||||
}
|
||||
MessageTypes.isIncomingAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL)))
|
||||
}
|
||||
MessageTypes.isIncomingVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL)))
|
||||
}
|
||||
MessageTypes.isOutgoingAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL)))
|
||||
}
|
||||
MessageTypes.isOutgoingVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL)))
|
||||
}
|
||||
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()
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
callingMessage = CallChatUpdate(
|
||||
groupCall = GroupCallChatUpdate(
|
||||
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
|
||||
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
|
||||
inCallAcis = joinedMembers
|
||||
)
|
||||
)
|
||||
)
|
||||
} catch (exception: java.lang.Exception) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
record.body == null -> {
|
||||
Log.w(TAG, "Record missing a body, skipping")
|
||||
continue
|
||||
}
|
||||
else -> builder.standardMessage = record.toTextMessage(reactionsById[id])
|
||||
}
|
||||
|
||||
@@ -138,6 +224,16 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
private fun String.e164ToLong(): Long? {
|
||||
val fixed = if (this.startsWith("+")) {
|
||||
this.substring(1)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
return fixed.toLongOrNull()
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toBasicChatItemBuilder(groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?): ChatItem.Builder {
|
||||
val record = this
|
||||
|
||||
|
||||
@@ -10,16 +10,19 @@ import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.toInt
|
||||
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.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
@@ -31,6 +34,8 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -131,7 +136,7 @@ class ChatItemImportInserter(
|
||||
return
|
||||
}
|
||||
|
||||
buffer.messages += chatItem.toMessageContentValues(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
|
||||
buffer.messages += chatItem.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
|
||||
buffer.reactions += chatItem.toReactionContentValues(messageId)
|
||||
buffer.groupReceipts += chatItem.toGroupReceiptContentValues(messageId, chatBackupRecipientId)
|
||||
|
||||
@@ -148,8 +153,18 @@ class ChatItemImportInserter(
|
||||
return false
|
||||
}
|
||||
|
||||
SqlUtil.buildBulkInsert(MessageTable.TABLE_NAME, MESSAGE_COLUMNS, buffer.messages).forEach {
|
||||
db.execSQL(it.where, it.whereArgs)
|
||||
buildBulkInsert(MessageTable.TABLE_NAME, MESSAGE_COLUMNS, buffer.messages).forEach {
|
||||
db.rawQuery("${it.query.where} RETURNING ${MessageTable.ID}", it.query.whereArgs).use { cursor ->
|
||||
var index = 0
|
||||
while (cursor.moveToNext()) {
|
||||
val rowId = cursor.requireLong(MessageTable.ID)
|
||||
val followup = it.inserts[index].followUp
|
||||
if (followup != null) {
|
||||
followup(rowId)
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SqlUtil.buildBulkInsert(ReactionTable.TABLE_NAME, REACTION_COLUMNS, buffer.reactions).forEach {
|
||||
@@ -165,6 +180,33 @@ class ChatItemImportInserter(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun buildBulkInsert(tableName: String, columns: Array<String>, messageInserts: List<MessageInsert>, maxQueryArgs: Int = 999): List<BatchInsert> {
|
||||
val batchSize = maxQueryArgs / columns.size
|
||||
|
||||
return messageInserts
|
||||
.chunked(batchSize)
|
||||
.map { batch: List<MessageInsert> -> BatchInsert(batch, SqlUtil.buildSingleBulkInsert(tableName, columns, batch.map { it.contentValues })) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun ChatItem.toMessageInsert(fromRecipientId: RecipientId, chatRecipientId: RecipientId, threadId: Long): MessageInsert {
|
||||
val contentValues = this.toMessageContentValues(fromRecipientId, chatRecipientId, threadId)
|
||||
|
||||
var followUp: ((Long) -> Unit)? = null
|
||||
if (this.updateMessage != null) {
|
||||
if (this.updateMessage.callingMessage != null && this.updateMessage.callingMessage.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
return MessageInsert(contentValues, followUp)
|
||||
}
|
||||
|
||||
private class BatchInsert(val inserts: List<MessageInsert>, val query: SqlUtil.Query)
|
||||
|
||||
private fun ChatItem.toMessageContentValues(fromRecipientId: RecipientId, chatRecipientId: RecipientId, threadId: Long): ContentValues {
|
||||
val contentValues = ContentValues()
|
||||
|
||||
@@ -338,6 +380,36 @@ class ChatItemImportInserter(
|
||||
.encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails))
|
||||
}
|
||||
updateMessage.sessionSwitchover != null -> {
|
||||
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE
|
||||
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(sessionSwitchoverDetails))
|
||||
}
|
||||
updateMessage.threadMerge != null -> {
|
||||
typeFlags = MessageTypes.THREAD_MERGE_TYPE
|
||||
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_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
|
||||
}
|
||||
}
|
||||
}
|
||||
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
|
||||
this.put(MessageTable.TYPE, typeFlags)
|
||||
}
|
||||
}
|
||||
this.put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or typeFlags)
|
||||
}
|
||||
@@ -431,8 +503,10 @@ class ChatItemImportInserter(
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)
|
||||
|
||||
private class Buffer(
|
||||
val messages: MutableList<ContentValues> = mutableListOf(),
|
||||
val messages: MutableList<MessageInsert> = mutableListOf(),
|
||||
val reactions: MutableList<ContentValues> = mutableListOf(),
|
||||
val groupReceipts: MutableList<ContentValues> = mutableListOf()
|
||||
) {
|
||||
|
||||
@@ -58,7 +58,7 @@ fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
|
||||
${MessageTypes.BASE_SENT_TYPE},
|
||||
${MessageTypes.BASE_SENDING_TYPE},
|
||||
${MessageTypes.BASE_SENT_FAILED_TYPE}
|
||||
)
|
||||
) OR ${MessageTable.IS_CALL_TYPE_CLAUSE}
|
||||
"""
|
||||
)
|
||||
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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.restoreCallLogFromBackup
|
||||
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 CallLogBackupProcessor {
|
||||
|
||||
val TAG = Log.tag(CallLogBackupProcessor::class.java)
|
||||
|
||||
fun export(emitter: BackupFrameEmitter) {
|
||||
SignalDatabase.calls.getCallsForBackup().use { reader ->
|
||||
for (callLog in reader) {
|
||||
if (callLog != null) {
|
||||
emitter.emit(Frame(call = callLog))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun import(call: BackupCall, backupState: BackupState) {
|
||||
SignalDatabase.calls.restoreCallLogFromBackup(call, backupState)
|
||||
}
|
||||
}
|
||||
@@ -49,16 +49,16 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
private val TIME_WINDOW = TimeUnit.HOURS.toMillis(4)
|
||||
|
||||
const val TABLE_NAME = "call"
|
||||
private const val ID = "_id"
|
||||
private const val CALL_ID = "call_id"
|
||||
private const val MESSAGE_ID = "message_id"
|
||||
const val ID = "_id"
|
||||
const val CALL_ID = "call_id"
|
||||
const val MESSAGE_ID = "message_id"
|
||||
const val PEER = "peer"
|
||||
const val TYPE = "type"
|
||||
private const val DIRECTION = "direction"
|
||||
const val DIRECTION = "direction"
|
||||
const val EVENT = "event"
|
||||
const val TIMESTAMP = "timestamp"
|
||||
private const val RINGER = "ringer"
|
||||
private const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
const val RINGER = "ringer"
|
||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
|
||||
//language=sql
|
||||
val CREATE_TABLE = """
|
||||
|
||||
@@ -413,7 +413,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
ORDER BY $DATE_RECEIVED DESC LIMIT 1
|
||||
"""
|
||||
|
||||
private val IS_CALL_TYPE_CLAUSE = """(
|
||||
const val IS_CALL_TYPE_CLAUSE = """(
|
||||
($TYPE = ${MessageTypes.INCOMING_AUDIO_CALL_TYPE})
|
||||
OR
|
||||
($TYPE = ${MessageTypes.INCOMING_VIDEO_CALL_TYPE})
|
||||
|
||||
@@ -192,7 +192,7 @@ message Call {
|
||||
Type type = 3;
|
||||
bool outgoing = 4;
|
||||
uint64 timestamp = 5;
|
||||
uint64 ringerRecipientId = 6;
|
||||
optional uint64 ringerRecipientId = 6;
|
||||
Event event = 7;
|
||||
}
|
||||
|
||||
@@ -505,6 +505,7 @@ message IndividualCallChatUpdate {
|
||||
MISSED_AUDIO_CALL = 5;
|
||||
MISSED_VIDEO_CALL = 6;
|
||||
}
|
||||
Type type = 1;
|
||||
}
|
||||
|
||||
message GroupCallChatUpdate {
|
||||
|
||||
@@ -393,7 +393,7 @@ object SqlUtil {
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun buildSingleBulkInsert(tableName: String, columns: Array<String>, contentValues: List<ContentValues>): Query {
|
||||
fun buildSingleBulkInsert(tableName: String, columns: Array<String>, contentValues: List<ContentValues>): Query {
|
||||
val builder = StringBuilder()
|
||||
builder.append("INSERT INTO ").append(tableName).append(" (")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user