Skip to content

Commit d74b302

Browse files
committed
Add remaining non-group update messages for backup.
1 parent 0200430 commit d74b302

11 files changed

Lines changed: 442 additions & 15 deletions

File tree

app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/BackupTest.kt

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2
77

88
import android.content.ContentValues
99
import android.database.Cursor
10+
import androidx.core.content.contentValuesOf
1011
import net.zetetic.database.sqlcipher.SQLiteDatabase
1112
import org.junit.Before
1213
import org.junit.Test
@@ -20,8 +21,10 @@ import org.signal.core.util.requireLong
2021
import org.signal.core.util.requireString
2122
import org.signal.core.util.select
2223
import org.signal.core.util.toInt
24+
import org.signal.core.util.withinTransaction
2325
import org.signal.libsignal.zkgroup.profiles.ProfileKey
2426
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
27+
import org.thoughtcrime.securesms.database.CallTable
2528
import org.thoughtcrime.securesms.database.EmojiSearchTable
2629
import org.thoughtcrime.securesms.database.MessageTable
2730
import org.thoughtcrime.securesms.database.MessageTypes
@@ -60,7 +63,8 @@ class BackupTest {
6063

6164
/** Columns that we don't need to check equality of */
6265
private val IGNORED_COLUMNS: Map<String, Set<String>> = mapOf(
63-
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID)
66+
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID),
67+
MessageTable.TABLE_NAME to setOf(MessageTable.FROM_DEVICE_ID)
6468
)
6569

6670
/** Tables we don't need to check equality of */
@@ -145,6 +149,88 @@ class BackupTest {
145149
}
146150
}
147151

152+
@Test
153+
fun individualCallLogs() {
154+
backupTest {
155+
val aliceId = individualRecipient(
156+
aci = ALICE_ACI,
157+
pni = ALICE_PNI,
158+
e164 = ALICE_E164,
159+
givenName = "Alice",
160+
familyName = "Smith",
161+
username = "alice.99",
162+
hidden = false,
163+
registeredState = RecipientTable.RegisteredState.REGISTERED,
164+
profileKey = ProfileKey(Random.nextBytes(32)),
165+
profileSharing = true,
166+
hideStory = false
167+
)
168+
insertOneToOneCallVariations(1, 1, aliceId)
169+
}
170+
}
171+
172+
private fun insertOneToOneCallVariations(callId: Long, timestamp: Long, id: RecipientId): Long {
173+
val directions = arrayOf(CallTable.Direction.INCOMING, CallTable.Direction.OUTGOING)
174+
val callTypes = arrayOf(CallTable.Type.AUDIO_CALL, CallTable.Type.VIDEO_CALL)
175+
val events = arrayOf(
176+
CallTable.Event.MISSED,
177+
CallTable.Event.OUTGOING_RING,
178+
CallTable.Event.ONGOING,
179+
CallTable.Event.ACCEPTED,
180+
CallTable.Event.NOT_ACCEPTED
181+
)
182+
var callTimestamp: Long = timestamp
183+
var currentCallId = callId
184+
for (direction in directions) {
185+
for (event in events) {
186+
for (type in callTypes) {
187+
insertOneToOneCall(callId = currentCallId, callTimestamp, id, type, direction, event)
188+
callTimestamp++
189+
currentCallId++
190+
}
191+
}
192+
}
193+
194+
return currentCallId
195+
}
196+
197+
private fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: CallTable.Type, direction: CallTable.Direction, event: CallTable.Event) {
198+
val messageType: Long = CallTable.Call.getMessageType(type, direction, event)
199+
200+
SignalDatabase.rawDatabase.withinTransaction {
201+
val recipient = Recipient.resolved(peer)
202+
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
203+
val outgoing = direction == CallTable.Direction.OUTGOING
204+
205+
val messageValues = contentValuesOf(
206+
MessageTable.FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else peer.serialize(),
207+
MessageTable.FROM_DEVICE_ID to 1,
208+
MessageTable.TO_RECIPIENT_ID to if (outgoing) peer.serialize() else Recipient.self().id.serialize(),
209+
MessageTable.DATE_RECEIVED to timestamp,
210+
MessageTable.DATE_SENT to timestamp,
211+
MessageTable.READ to 1,
212+
MessageTable.TYPE to messageType,
213+
MessageTable.THREAD_ID to threadId
214+
)
215+
216+
val messageId = SignalDatabase.rawDatabase.insert(MessageTable.TABLE_NAME, null, messageValues)
217+
218+
val values = contentValuesOf(
219+
CallTable.CALL_ID to callId,
220+
CallTable.MESSAGE_ID to messageId,
221+
CallTable.PEER to peer.serialize(),
222+
CallTable.TYPE to CallTable.Type.serialize(type),
223+
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
224+
CallTable.EVENT to CallTable.Event.serialize(event),
225+
CallTable.TIMESTAMP to timestamp
226+
)
227+
228+
SignalDatabase.rawDatabase.insert(CallTable.TABLE_NAME, null, values)
229+
230+
SignalDatabase.threads.update(threadId, true)
231+
}
232+
}
233+
148234
@Test
149235
fun accountData() {
150236
val context = ApplicationDependencies.getApplication()

app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
1212
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
1313
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
1414
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
15+
import org.thoughtcrime.securesms.backup.v2.processor.CallLogBackupProcessor
1516
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
1617
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
1718
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
@@ -70,6 +71,11 @@ object BackupRepository {
7071
eventTimer.emit("thread")
7172
}
7273

74+
CallLogBackupProcessor.export { frame ->
75+
writer.write(frame)
76+
eventTimer.emit("call")
77+
}
78+
7379
ChatItemBackupProcessor.export { frame ->
7480
writer.write(frame)
7581
eventTimer.emit("message")
@@ -131,6 +137,11 @@ object BackupRepository {
131137
eventTimer.emit("chat")
132138
}
133139

140+
frame.call != null -> {
141+
CallLogBackupProcessor.import(frame.call, backupState)
142+
eventTimer.emit("call")
143+
}
144+
134145
frame.chatItem != null -> {
135146
chatItemInserter.insert(frame.chatItem)
136147
eventTimer.emit("chatItem")
@@ -214,4 +225,5 @@ class BackupState {
214225
val chatIdToLocalThreadId = HashMap<Long, Long>()
215226
val chatIdToLocalRecipientId = HashMap<Long, RecipientId>()
216227
val chatIdToBackupRecipientId = HashMap<Long, Long>()
228+
val callIdToType = HashMap<Long, Long>()
217229
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2023 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.thoughtcrime.securesms.backup.v2.database
7+
8+
import android.database.Cursor
9+
import android.database.sqlite.SQLiteDatabase
10+
import androidx.core.content.contentValuesOf
11+
import org.signal.core.util.isNull
12+
import org.signal.core.util.requireInt
13+
import org.signal.core.util.requireLong
14+
import org.signal.core.util.select
15+
import org.thoughtcrime.securesms.backup.v2.BackupState
16+
import org.thoughtcrime.securesms.backup.v2.proto.Call
17+
import org.thoughtcrime.securesms.database.CallTable
18+
import org.thoughtcrime.securesms.database.RecipientTable
19+
import java.io.Closeable
20+
21+
typealias BackupCall = org.thoughtcrime.securesms.backup.v2.proto.Call
22+
23+
fun CallTable.getCallsForBackup(): CallLogIterator {
24+
return CallLogIterator(
25+
readableDatabase
26+
.select()
27+
.from(CallTable.TABLE_NAME)
28+
.where("${CallTable.EVENT} != ${CallTable.Event.serialize(CallTable.Event.DELETE)}")
29+
.run()
30+
)
31+
}
32+
33+
fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupState) {
34+
val type = when (call.type) {
35+
Call.Type.VIDEO_CALL -> CallTable.Type.VIDEO_CALL
36+
Call.Type.AUDIO_CALL -> CallTable.Type.AUDIO_CALL
37+
Call.Type.AD_HOC_CALL -> CallTable.Type.AD_HOC_CALL
38+
Call.Type.GROUP_CALL -> CallTable.Type.GROUP_CALL
39+
Call.Type.UNKNOWN_TYPE -> return
40+
}
41+
42+
val event = when (call.event) {
43+
Call.Event.DELETE -> CallTable.Event.DELETE
44+
Call.Event.JOINED -> CallTable.Event.JOINED
45+
Call.Event.GENERIC_GROUP_CALL -> CallTable.Event.GENERIC_GROUP_CALL
46+
Call.Event.DECLINED -> CallTable.Event.DECLINED
47+
Call.Event.ACCEPTED -> CallTable.Event.ACCEPTED
48+
Call.Event.MISSED -> CallTable.Event.MISSED
49+
Call.Event.OUTGOING_RING -> CallTable.Event.OUTGOING_RING
50+
Call.Event.OUTGOING -> CallTable.Event.ONGOING
51+
Call.Event.NOT_ACCEPTED -> CallTable.Event.NOT_ACCEPTED
52+
Call.Event.UNKNOWN_EVENT -> return
53+
}
54+
55+
val direction = if (call.outgoing) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING
56+
57+
backupState.callIdToType[call.callId] = CallTable.Call.getMessageType(type, direction, event)
58+
59+
val values = contentValuesOf(
60+
CallTable.CALL_ID to call.callId,
61+
CallTable.PEER to backupState.backupToLocalRecipientId[call.conversationRecipientId]!!.serialize(),
62+
CallTable.TYPE to CallTable.Type.serialize(type),
63+
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
64+
CallTable.EVENT to CallTable.Event.serialize(event),
65+
CallTable.TIMESTAMP to call.timestamp
66+
)
67+
68+
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
69+
}
70+
71+
/**
72+
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
73+
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
74+
*/
75+
class CallLogIterator(private val cursor: Cursor) : Iterator<BackupCall?>, Closeable {
76+
override fun hasNext(): Boolean {
77+
return cursor.count > 0 && !cursor.isLast
78+
}
79+
80+
override fun next(): BackupCall? {
81+
if (!cursor.moveToNext()) {
82+
throw NoSuchElementException()
83+
}
84+
85+
val callId = cursor.requireLong(CallTable.CALL_ID)
86+
val type = CallTable.Type.deserialize(cursor.requireInt(CallTable.TYPE))
87+
val direction = CallTable.Direction.deserialize(cursor.requireInt(CallTable.DIRECTION))
88+
val event = CallTable.Event.deserialize(cursor.requireInt(CallTable.EVENT))
89+
90+
return BackupCall(
91+
callId = callId,
92+
conversationRecipientId = cursor.requireLong(CallTable.PEER),
93+
type = when (type) {
94+
CallTable.Type.AUDIO_CALL -> Call.Type.AUDIO_CALL
95+
CallTable.Type.VIDEO_CALL -> Call.Type.VIDEO_CALL
96+
CallTable.Type.AD_HOC_CALL -> Call.Type.AD_HOC_CALL
97+
CallTable.Type.GROUP_CALL -> Call.Type.GROUP_CALL
98+
},
99+
outgoing = when (direction) {
100+
CallTable.Direction.OUTGOING -> true
101+
else -> false
102+
},
103+
timestamp = cursor.requireLong(CallTable.TIMESTAMP),
104+
ringerRecipientId = if (cursor.isNull(CallTable.RINGER)) null else cursor.requireLong(CallTable.RINGER),
105+
event = when (event) {
106+
CallTable.Event.ONGOING -> Call.Event.OUTGOING
107+
CallTable.Event.OUTGOING_RING -> Call.Event.OUTGOING_RING
108+
CallTable.Event.ACCEPTED -> Call.Event.ACCEPTED
109+
CallTable.Event.DECLINED -> Call.Event.DECLINED
110+
CallTable.Event.GENERIC_GROUP_CALL -> Call.Event.GENERIC_GROUP_CALL
111+
CallTable.Event.JOINED -> Call.Event.JOINED
112+
CallTable.Event.MISSED -> Call.Event.MISSED
113+
CallTable.Event.DELETE -> Call.Event.DELETE
114+
CallTable.Event.RINGING -> Call.Event.UNKNOWN_EVENT
115+
CallTable.Event.NOT_ACCEPTED -> Call.Event.NOT_ACCEPTED
116+
}
117+
)
118+
}
119+
120+
override fun close() {
121+
cursor.close()
122+
}
123+
}

0 commit comments

Comments
 (0)