That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.content.Context
4import android.database.Cursor
5import androidx.annotation.Discouraged
6import androidx.core.content.contentValuesOf
7import org.signal.core.util.IntSerializer
8import org.signal.core.util.Serializer
9import org.signal.core.util.SqlUtil
10import org.signal.core.util.count
11import org.signal.core.util.delete
12import org.signal.core.util.deleteAll
13import org.signal.core.util.flatten
14import org.signal.core.util.insertInto
15import org.signal.core.util.isAbsent
16import org.signal.core.util.logging.Log
17import org.signal.core.util.readToList
18import org.signal.core.util.readToMap
19import org.signal.core.util.readToSingleLong
20import org.signal.core.util.readToSingleObject
21import org.signal.core.util.requireLong
22import org.signal.core.util.requireNonNullString
23import org.signal.core.util.requireObject
24import org.signal.core.util.requireString
25import org.signal.core.util.select
26import org.signal.core.util.toSingleLine
27import org.signal.core.util.update
28import org.signal.core.util.withinTransaction
29import org.signal.ringrtc.CallId
30import org.signal.ringrtc.CallManager.RingUpdate
31import org.tm.archive.calls.log.CallLogFilter
32import org.tm.archive.calls.log.CallLogRow
33import org.tm.archive.database.model.GroupCallUpdateDetailsUtil
34import org.tm.archive.database.model.MessageId
35import org.tm.archive.dependencies.ApplicationDependencies
36import org.tm.archive.jobs.CallSyncEventJob
37import org.tm.archive.recipients.Recipient
38import org.tm.archive.recipients.RecipientId
39import org.tm.archive.service.webrtc.links.CallLinkRoomId
40import org.whispersystems.signalservice.api.push.ServiceId.ACI
41import org.whispersystems.signalservice.internal.push.SyncMessage.CallEvent
42import java.util.UUID
43import java.util.concurrent.TimeUnit
44
45/**
46 * Contains details for each 1:1 call.
47 */
48class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
49
50 companion object {
51 private val TAG = Log.tag(CallTable::class.java)
52 private val TIME_WINDOW = TimeUnit.HOURS.toMillis(4)
53
54 const val TABLE_NAME = "call"
55 const val ID = "_id"
56 const val CALL_ID = "call_id"
57 const val MESSAGE_ID = "message_id"
58 const val PEER = "peer"
59 const val TYPE = "type"
60 const val DIRECTION = "direction"
61 const val EVENT = "event"
62 const val TIMESTAMP = "timestamp"
63 const val RINGER = "ringer"
64 const val DELETION_TIMESTAMP = "deletion_timestamp"
65 const val READ = "read"
66
67 //language=sql
68 const val CREATE_TABLE = """
69 CREATE TABLE $TABLE_NAME (
70 $ID INTEGER PRIMARY KEY,
71 $CALL_ID INTEGER NOT NULL,
72 $MESSAGE_ID INTEGER DEFAULT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE SET NULL,
73 $PEER INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
74 $TYPE INTEGER NOT NULL,
75 $DIRECTION INTEGER NOT NULL,
76 $EVENT INTEGER NOT NULL,
77 $TIMESTAMP INTEGER NOT NULL,
78 $RINGER INTEGER DEFAULT NULL,
79 $DELETION_TIMESTAMP INTEGER DEFAULT 0,
80 $READ INTEGER DEFAULT 1,
81 UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL
82 )
83 """
84
85 val CREATE_INDEXES = arrayOf(
86 "CREATE INDEX call_call_id_index ON $TABLE_NAME ($CALL_ID)",
87 "CREATE INDEX call_message_id_index ON $TABLE_NAME ($MESSAGE_ID)",
88 "CREATE INDEX call_peer_index ON $TABLE_NAME ($PEER)"
89 )
90 }
91
92 fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE) {
93 writableDatabase.update(TABLE_NAME)
94 .values(READ to ReadState.serialize(ReadState.READ))
95 .where("$TIMESTAMP <= ?", timestamp)
96 .run()
97
98 notifyConversationListListeners()
99 }
100
101 fun getUnreadMissedCallCount(): Long {
102 return readableDatabase
103 .count()
104 .from(TABLE_NAME)
105 .where("$EVENT = ? AND $READ = ?", Event.serialize(Event.MISSED), ReadState.serialize(ReadState.UNREAD))
106 .run()
107 .readToSingleLong()
108 }
109
110 fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
111 val messageType: Long = Call.getMessageType(type, direction, event)
112
113 writableDatabase.withinTransaction {
114 val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
115 val values = contentValuesOf(
116 CALL_ID to callId,
117 MESSAGE_ID to result.messageId,
118 PEER to peer.serialize(),
119 TYPE to Type.serialize(type),
120 DIRECTION to Direction.serialize(direction),
121 EVENT to Event.serialize(event),
122 TIMESTAMP to timestamp,
123 READ to ReadState.serialize(ReadState.UNREAD)
124 )
125
126 writableDatabase.insert(TABLE_NAME, null, values)
127 }
128
129 ApplicationDependencies.getMessageNotifier().updateNotification(context)
130 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
131
132 Log.i(TAG, "Inserted call: $callId type: $type direction: $direction event:$event")
133 }
134
135 fun updateOneToOneCall(callId: Long, event: Event): Call? {
136 return writableDatabase.withinTransaction {
137 writableDatabase
138 .update(TABLE_NAME)
139 .values(
140 EVENT to Event.serialize(event),
141 READ to ReadState.serialize(ReadState.UNREAD)
142 )
143 .where("$CALL_ID = ?", callId)
144 .run()
145
146 val call = readableDatabase
147 .select()
148 .from(TABLE_NAME)
149 .where("$CALL_ID = ?", callId)
150 .run()
151 .readToSingleObject(Call.Deserializer)
152
153 if (call != null) {
154 Log.i(TAG, "Updated call: $callId event: $event")
155
156 if (call.messageId == null) {
157 Log.w(TAG, "Call does not have an associated message id! No message to update.")
158 } else {
159 SignalDatabase.messages.updateCallLog(call.messageId, call.messageType)
160 }
161
162 ApplicationDependencies.getMessageNotifier().updateNotification(context)
163 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
164 }
165
166 call
167 }
168 }
169
170 fun getCallById(callId: Long, recipientId: RecipientId): Call? {
171 val query = getCallSelectionQuery(callId, recipientId)
172
173 return readableDatabase
174 .select()
175 .from(TABLE_NAME)
176 .where(query.where, query.whereArgs)
177 .run()
178 .readToSingleObject(Call.Deserializer)
179 }
180
181 fun getCallByMessageId(messageId: Long): Call? {
182 return readableDatabase
183 .select()
184 .from(TABLE_NAME)
185 .where("$MESSAGE_ID = ?", messageId)
186 .run()
187 .readToSingleObject(Call.Deserializer)
188 }
189
190 fun getCalls(messageIds: Collection<Long>): Map<Long, Call> {
191 val queries = SqlUtil.buildCollectionQuery(MESSAGE_ID, messageIds)
192 val maps = queries.map { query ->
193 readableDatabase
194 .select()
195 .from(TABLE_NAME)
196 .where("$EVENT != ${Event.serialize(Event.DELETE)} AND ${query.where}", query.whereArgs)
197 .run()
198 .readToMap { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) }
199 }
200
201 return maps.flatten()
202 }
203
204 /**
205 * @param callRowIds The CallTable.ID collection to query
206 *
207 * @return a map of raw MessageId -> Call
208 */
209 fun getCallsByRowIds(callRowIds: Collection<Long>): Map<Long, Call> {
210 val queries = SqlUtil.buildCollectionQuery(ID, callRowIds)
211
212 val maps = queries.map { query ->
213 readableDatabase
214 .select()
215 .from(TABLE_NAME)
216 .where("$EVENT != ${Event.serialize(Event.DELETE)} AND ${query.where}", query.whereArgs)
217 .run()
218 .readToMap { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) }
219 }
220
221 return maps.flatten()
222 }
223
224 fun getOldestDeletionTimestamp(): Long {
225 return writableDatabase
226 .select(DELETION_TIMESTAMP)
227 .from(TABLE_NAME)
228 .where("$DELETION_TIMESTAMP > 0")
229 .orderBy("$DELETION_TIMESTAMP DESC")
230 .limit(1)
231 .run()
232 .readToSingleLong(0L)
233 }
234
235 fun deleteCallEventsDeletedBefore(threshold: Long): Int {
236 return writableDatabase
237 .delete(TABLE_NAME)
238 .where("$DELETION_TIMESTAMP > 0 AND $DELETION_TIMESTAMP <= ?", threshold)
239 .run()
240 }
241
242 fun getCallLinkRoomIdsFromCallRowIds(callRowIds: Set<Long>): Set<CallLinkRoomId> {
243 return SqlUtil.buildCollectionQuery("$TABLE_NAME.$ID", callRowIds).map { query ->
244 //language=sql
245 val statement = """
246 SELECT ${CallLinkTable.ROOM_ID} FROM $TABLE_NAME
247 INNER JOIN ${CallLinkTable.TABLE_NAME} ON ${CallLinkTable.TABLE_NAME}.${CallLinkTable.RECIPIENT_ID} = $PEER
248 WHERE $TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND ${query.where}
249 """.toSingleLine()
250
251 readableDatabase.query(statement, query.whereArgs).readToList {
252 CallLinkRoomId.DatabaseSerializer.deserialize(it.requireNonNullString(CallLinkTable.ROOM_ID))
253 }
254 }.flatten().toSet()
255 }
256
257 /**
258 * If a call link has been revoked, or if we do not have a CallLink table entry for an AD_HOC_CALL type
259 * event, we mark it deleted.
260 */
261 fun updateAdHocCallEventDeletionTimestamps(skipSync: Boolean = false) {
262 //language=sql
263 val statement = """
264 UPDATE $TABLE_NAME
265 SET $DELETION_TIMESTAMP = ${System.currentTimeMillis()}, $EVENT = ${Event.serialize(Event.DELETE)}
266 WHERE $TYPE = ${Type.serialize(Type.AD_HOC_CALL)}
267 AND (
268 (NOT EXISTS (SELECT 1 FROM ${CallLinkTable.TABLE_NAME} WHERE ${CallLinkTable.RECIPIENT_ID} = $PEER))
269 OR
270 (SELECT ${CallLinkTable.REVOKED} FROM ${CallLinkTable.TABLE_NAME} WHERE ${CallLinkTable.RECIPIENT_ID} = $PEER)
271 )
272 RETURNING *
273 """.toSingleLine()
274
275 val toSync = writableDatabase.query(statement).readToList {
276 Call.deserialize(it)
277 }.toSet()
278
279 if (!skipSync) {
280 CallSyncEventJob.enqueueDeleteSyncEvents(toSync)
281 }
282
283 ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
284 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
285 }
286
287 /**
288 * If a non-ad-hoc call has been deleted from the message database, then we need to
289 * set its deletion_timestamp to now.
290 */
291 fun updateCallEventDeletionTimestamps(skipSync: Boolean = false) {
292 val where = "$TYPE != ? AND $DELETION_TIMESTAMP = 0 AND $MESSAGE_ID IS NULL"
293 val type = Type.serialize(Type.AD_HOC_CALL)
294
295 val toSync = writableDatabase.withinTransaction { db ->
296 val result = db
297 .select()
298 .from(TABLE_NAME)
299 .where(where, type)
300 .run()
301 .readToList {
302 Call.deserialize(it)
303 }
304 .toSet()
305
306 db
307 .update(TABLE_NAME)
308 .values(
309 EVENT to Event.serialize(Event.DELETE),
310 DELETION_TIMESTAMP to System.currentTimeMillis()
311 )
312 .where(where, type)
313 .run()
314
315 result
316 }
317
318 if (!skipSync) {
319 CallSyncEventJob.enqueueDeleteSyncEvents(toSync)
320 }
321
322 ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
323 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
324 }
325
326 // region Group / Ad-Hoc Calling
327 fun deleteGroupCall(call: Call) {
328 checkIsGroupOrAdHocCall(call)
329
330 val filter: SqlUtil.Query = getCallSelectionQuery(call.callId, call.peer)
331
332 writableDatabase.withinTransaction { db ->
333 db
334 .update(TABLE_NAME)
335 .values(
336 EVENT to Event.serialize(Event.DELETE),
337 DELETION_TIMESTAMP to System.currentTimeMillis()
338 )
339 .where(filter.where, filter.whereArgs)
340 .run()
341
342 if (call.messageId != null) {
343 SignalDatabase.messages.deleteMessage(call.messageId)
344 }
345 }
346
347 ApplicationDependencies.getMessageNotifier().updateNotification(context)
348 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
349 Log.d(TAG, "Marked group call event for deletion: ${call.callId}")
350 }
351
352 fun insertDeletedGroupCallFromSyncEvent(
353 callId: Long,
354 recipientId: RecipientId,
355 direction: Direction,
356 timestamp: Long
357 ) {
358 val recipient = Recipient.resolved(recipientId)
359 val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
360
361 writableDatabase
362 .insertInto(TABLE_NAME)
363 .values(
364 CALL_ID to callId,
365 MESSAGE_ID to null,
366 PEER to recipientId.toLong(),
367 EVENT to Event.serialize(Event.DELETE),
368 TYPE to Type.serialize(type),
369 DIRECTION to Direction.serialize(direction),
370 TIMESTAMP to timestamp,
371 DELETION_TIMESTAMP to System.currentTimeMillis()
372 )
373 .run(SQLiteDatabase.CONFLICT_ABORT)
374
375 ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
376 }
377
378 fun acceptIncomingGroupCall(call: Call) {
379 checkIsGroupOrAdHocCall(call)
380
381 val newEvent = when (call.event) {
382 Event.RINGING, Event.MISSED, Event.MISSED_NOTIFICATION_PROFILE, Event.DECLINED -> Event.ACCEPTED
383 Event.GENERIC_GROUP_CALL -> Event.JOINED
384 else -> {
385 Log.d(TAG, "[acceptIncomingGroupCall] Call in state ${call.event} cannot be transitioned by ACCEPTED")
386 return
387 }
388 }
389
390 writableDatabase
391 .update(TABLE_NAME)
392 .values(EVENT to Event.serialize(newEvent))
393 .where("$CALL_ID = ?", call.callId)
394 .run()
395
396 ApplicationDependencies.getMessageNotifier().updateNotification(context)
397 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
398 Log.d(TAG, "[acceptIncomingGroupCall] Transitioned group call ${call.callId} from ${call.event} to $newEvent")
399 }
400
401 fun acceptOutgoingGroupCall(call: Call) {
402 checkIsGroupOrAdHocCall(call)
403
404 val newEvent = when (call.event) {
405 Event.GENERIC_GROUP_CALL, Event.JOINED -> Event.OUTGOING_RING
406 Event.RINGING, Event.MISSED, Event.MISSED_NOTIFICATION_PROFILE, Event.DECLINED, Event.ACCEPTED -> {
407 Log.w(TAG, "[acceptOutgoingGroupCall] This shouldn't have been an outgoing ring because the call already existed!")
408 Event.ACCEPTED
409 }
410
411 else -> {
412 Log.d(TAG, "[acceptOutgoingGroupCall] Call in state ${call.event} cannot be transitioned by ACCEPTED")
413 return
414 }
415 }
416
417 writableDatabase
418 .update(TABLE_NAME)
419 .values(EVENT to Event.serialize(newEvent), DIRECTION to Direction.serialize(Direction.OUTGOING))
420 .where("$CALL_ID = ?", call.callId)
421 .run()
422
423 ApplicationDependencies.getMessageNotifier().updateNotification(context)
424 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
425 Log.d(TAG, "[acceptOutgoingGroupCall] Transitioned group call ${call.callId} from ${call.event} to $newEvent")
426 }
427
428 fun declineIncomingGroupCall(call: Call) {
429 checkIsGroupOrAdHocCall(call)
430 check(call.direction == Direction.INCOMING)
431
432 val newEvent = when (call.event) {
433 Event.GENERIC_GROUP_CALL, Event.RINGING, Event.MISSED, Event.MISSED_NOTIFICATION_PROFILE -> Event.DECLINED
434 Event.JOINED -> Event.ACCEPTED
435 else -> {
436 Log.d(TAG, "Call in state ${call.event} cannot be transitioned by DECLINED")
437 return
438 }
439 }
440
441 writableDatabase
442 .update(TABLE_NAME)
443 .values(EVENT to Event.serialize(newEvent))
444 .where("$CALL_ID = ?", call.callId)
445 .run()
446
447 ApplicationDependencies.getMessageNotifier().updateNotification(context)
448 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
449 Log.d(TAG, "Transitioned group call ${call.callId} from ${call.event} to $newEvent")
450 }
451
452 fun insertAcceptedGroupCall(
453 callId: Long,
454 recipientId: RecipientId,
455 direction: Direction,
456 timestamp: Long
457 ) {
458 val recipient = Recipient.resolved(recipientId)
459 val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
460 val event = if (direction == Direction.OUTGOING) Event.OUTGOING_RING else Event.JOINED
461 val ringer = if (direction == Direction.OUTGOING) Recipient.self().id.toLong() else null
462
463 writableDatabase.withinTransaction { db ->
464 val messageId: MessageId? = if (type == Type.GROUP_CALL) {
465 SignalDatabase.messages.insertGroupCall(
466 groupRecipientId = recipientId,
467 sender = Recipient.self().id,
468 timestamp,
469 "",
470 emptyList(),
471 false
472 )
473 } else {
474 null
475 }
476
477 db
478 .insertInto(TABLE_NAME)
479 .values(
480 CALL_ID to callId,
481 MESSAGE_ID to messageId?.id,
482 PEER to recipientId.toLong(),
483 EVENT to Event.serialize(event),
484 TYPE to Type.serialize(type),
485 DIRECTION to Direction.serialize(direction),
486 TIMESTAMP to timestamp,
487 RINGER to ringer
488 )
489 .run(SQLiteDatabase.CONFLICT_ABORT)
490 }
491
492 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
493 }
494
495 fun insertDeclinedGroupCall(
496 callId: Long,
497 recipientId: RecipientId,
498 timestamp: Long
499 ) {
500 val recipient = Recipient.resolved(recipientId)
501 val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
502
503 writableDatabase.withinTransaction { db ->
504 val messageId: MessageId? = if (type == Type.GROUP_CALL) {
505 SignalDatabase.messages.insertGroupCall(
506 groupRecipientId = recipientId,
507 sender = Recipient.self().id,
508 timestamp,
509 "",
510 emptyList(),
511 false
512 )
513 } else {
514 null
515 }
516
517 db
518 .insertInto(TABLE_NAME)
519 .values(
520 CALL_ID to callId,
521 MESSAGE_ID to messageId?.id,
522 PEER to recipientId.toLong(),
523 EVENT to Event.serialize(Event.DECLINED),
524 TYPE to Type.serialize(type),
525 DIRECTION to Direction.serialize(Direction.INCOMING),
526 TIMESTAMP to timestamp,
527 RINGER to null
528 )
529 .run(SQLiteDatabase.CONFLICT_ABORT)
530 }
531
532 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
533 }
534
535 fun insertOrUpdateGroupCallFromExternalEvent(
536 groupRecipientId: RecipientId,
537 sender: RecipientId,
538 timestamp: Long,
539 messageGroupCallEraId: String?
540 ) {
541 insertOrUpdateGroupCallFromLocalEvent(
542 groupRecipientId,
543 sender,
544 timestamp,
545 messageGroupCallEraId,
546 emptyList(),
547 false
548 )
549 }
550
551 fun insertOrUpdateGroupCallFromLocalEvent(
552 groupRecipientId: RecipientId,
553 sender: RecipientId,
554 timestamp: Long,
555 peekGroupCallEraId: String?,
556 peekJoinedUuids: Collection<UUID>,
557 isCallFull: Boolean
558 ) {
559 val recipient = Recipient.resolved(groupRecipientId)
560 if (recipient.isCallLink) {
561 handleCallLinkUpdate(recipient, timestamp, peekGroupCallEraId)
562 } else {
563 handleGroupUpdate(recipient, sender, timestamp, peekGroupCallEraId, peekJoinedUuids, isCallFull)
564 }
565 }
566
567 private fun handleGroupUpdate(
568 groupRecipient: Recipient,
569 sender: RecipientId,
570 timestamp: Long,
571 peekGroupCallEraId: String?,
572 peekJoinedUuids: Collection<UUID>,
573 isCallFull: Boolean
574 ) {
575 check(groupRecipient.isPushV2Group)
576 writableDatabase.withinTransaction {
577 if (peekGroupCallEraId.isNullOrEmpty()) {
578 Log.w(TAG, "Dropping local call event with null era id.")
579 return@withinTransaction
580 }
581
582 val callId = CallId.fromEra(peekGroupCallEraId).longValue()
583 val call = getCallById(callId, groupRecipient.id)
584 val messageId: MessageId = if (call != null) {
585 if (call.event == Event.DELETE) {
586 Log.d(TAG, "Dropping group call update for deleted call.")
587 return@withinTransaction
588 }
589
590 if (call.type != Type.GROUP_CALL) {
591 Log.d(TAG, "Dropping unsupported update message for non-group-call call.")
592 return@withinTransaction
593 }
594
595 if (call.messageId == null) {
596 Log.d(TAG, "Dropping group call update for call without an attached message.")
597 return@withinTransaction
598 }
599
600 SignalDatabase.messages.updateGroupCall(
601 call.messageId,
602 peekGroupCallEraId,
603 peekJoinedUuids,
604 isCallFull
605 )
606 } else {
607 SignalDatabase.messages.insertGroupCall(
608 groupRecipient.id,
609 sender,
610 timestamp,
611 peekGroupCallEraId,
612 peekJoinedUuids,
613 isCallFull
614 )
615 }
616
617 insertCallEventFromGroupUpdate(
618 callId,
619 messageId,
620 sender,
621 groupRecipient.id,
622 timestamp
623 )
624 }
625 }
626
627 private fun handleCallLinkUpdate(
628 callLinkRecipient: Recipient,
629 timestamp: Long,
630 peekGroupCallEraId: String?
631 ) {
632 check(callLinkRecipient.isCallLink)
633
634 val callId = peekGroupCallEraId?.let { CallId.fromEra(it).longValue() } ?: return
635
636 writableDatabase.withinTransaction { db ->
637 db.delete(TABLE_NAME)
638 .where("$PEER = ?", callLinkRecipient.id.serialize())
639 .run()
640
641 db.insertInto(TABLE_NAME)
642 .values(
643 CALL_ID to callId,
644 MESSAGE_ID to null,
645 PEER to callLinkRecipient.id.toLong(),
646 EVENT to Event.serialize(Event.GENERIC_GROUP_CALL),
647 TYPE to Type.serialize(Type.AD_HOC_CALL),
648 DIRECTION to Direction.serialize(Direction.OUTGOING),
649 TIMESTAMP to timestamp,
650 RINGER to null
651 ).run(SQLiteDatabase.CONFLICT_ABORT)
652 }
653
654 Log.d(TAG, "Inserted new call event for call link. Call Id: $callId")
655 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
656 }
657
658 private fun insertCallEventFromGroupUpdate(
659 callId: Long,
660 messageId: MessageId?,
661 sender: RecipientId,
662 groupRecipientId: RecipientId,
663 timestamp: Long
664 ) {
665 if (messageId != null) {
666 val call = getCallById(callId, groupRecipientId)
667 if (call == null) {
668 val direction = if (sender == Recipient.self().id) Direction.OUTGOING else Direction.INCOMING
669
670 writableDatabase
671 .insertInto(TABLE_NAME)
672 .values(
673 CALL_ID to callId,
674 MESSAGE_ID to messageId.id,
675 PEER to groupRecipientId.toLong(),
676 EVENT to Event.serialize(Event.GENERIC_GROUP_CALL),
677 TYPE to Type.serialize(Type.GROUP_CALL),
678 DIRECTION to Direction.serialize(direction),
679 TIMESTAMP to timestamp,
680 RINGER to null
681 )
682 .run(SQLiteDatabase.CONFLICT_ABORT)
683
684 Log.d(TAG, "Inserted new call event from group call update message. Call Id: $callId")
685 } else {
686 if (timestamp < call.timestamp) {
687 setTimestamp(callId, groupRecipientId, timestamp)
688 Log.d(TAG, "Updated call event timestamp for call id $callId")
689 }
690
691 if (call.messageId == null) {
692 setMessageId(callId, messageId)
693 Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
694 }
695 }
696 } else {
697 Log.d(TAG, "Skipping call event processing for null era id.")
698 }
699
700 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
701 }
702
703 /**
704 * Since this does not alter the call table, we can simply pass this directly through to the old handler.
705 */
706 fun updateGroupCallFromPeek(
707 threadId: Long,
708 peekGroupCallEraId: String?,
709 peekJoinedUuids: Collection<UUID>,
710 isCallFull: Boolean
711 ): Boolean {
712 val sameEraId = SignalDatabase.messages.updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
713 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
714 return sameEraId
715 }
716
717 fun insertOrUpdateGroupCallFromRingState(
718 ringId: Long,
719 groupRecipientId: RecipientId,
720 ringerRecipient: RecipientId,
721 dateReceived: Long,
722 ringState: RingUpdate
723 ) {
724 handleGroupRingState(ringId, groupRecipientId, ringerRecipient, dateReceived, ringState)
725 }
726
727 @JvmOverloads
728 fun insertOrUpdateGroupCallFromRingState(
729 ringId: Long,
730 groupRecipientId: RecipientId,
731 ringerAci: ACI,
732 dateReceived: Long,
733 ringState: RingUpdate,
734 dueToNotificationProfile: Boolean = false
735 ) {
736 val ringerRecipient = Recipient.externalPush(ringerAci)
737 handleGroupRingState(ringId, groupRecipientId, ringerRecipient.id, dateReceived, ringState, dueToNotificationProfile)
738 }
739
740 fun isRingCancelled(ringId: Long, groupRecipientId: RecipientId): Boolean {
741 val call = getCallById(ringId, groupRecipientId) ?: return false
742 return call.event != Event.RINGING && call.event != Event.GENERIC_GROUP_CALL
743 }
744
745 private fun handleGroupRingState(
746 ringId: Long,
747 groupRecipientId: RecipientId,
748 ringerRecipient: RecipientId,
749 dateReceived: Long,
750 ringState: RingUpdate,
751 dueToNotificationProfile: Boolean = false
752 ) {
753 writableDatabase.withinTransaction {
754 Log.d(TAG, "Processing group ring state update for $ringId in state $ringState")
755
756 val call = getCallById(ringId, groupRecipientId)
757 if (call != null) {
758 if (call.event == Event.DELETE) {
759 Log.d(TAG, "Ignoring ring request for $ringId since its event has been deleted.")
760 return@withinTransaction
761 }
762
763 when (ringState) {
764 RingUpdate.REQUESTED -> {
765 when (call.event) {
766 Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.RINGING, ringerRecipient)
767 Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient)
768 else -> Log.w(TAG, "Received a REQUESTED ring event while in ${call.event}. Ignoring.")
769 }
770 }
771
772 RingUpdate.EXPIRED_REQUEST, RingUpdate.CANCELLED_BY_RINGER -> {
773 when (call.event) {
774 Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, if (dueToNotificationProfile) Event.MISSED_NOTIFICATION_PROFILE else Event.MISSED, ringerRecipient)
775 Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient)
776 Event.OUTGOING_RING -> Log.w(TAG, "Received an expiration or cancellation while in OUTGOING_RING state. Ignoring.")
777 else -> Unit
778 }
779 }
780
781 RingUpdate.BUSY_LOCALLY -> {
782 when (call.event) {
783 Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
784 Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
785 else -> {
786 updateEventFromRingState(ringId, call.event, ringerRecipient)
787 Log.w(TAG, "Received a busy event we can't process. Updating ringer only.")
788 }
789 }
790 }
791
792 RingUpdate.BUSY_ON_ANOTHER_DEVICE -> {
793 when (call.event) {
794 Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
795 Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
796 else -> Log.w(TAG, "Received a busy event we can't process. Ignoring.")
797 }
798 }
799
800 RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> {
801 updateEventFromRingState(ringId, Event.ACCEPTED)
802 }
803
804 RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> {
805 when (call.event) {
806 Event.RINGING, Event.MISSED, Event.MISSED_NOTIFICATION_PROFILE, Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.DECLINED)
807 Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
808 Event.OUTGOING_RING -> Log.w(TAG, "Received DECLINED_ON_ANOTHER_DEVICE while in OUTGOING_RING state.")
809 else -> Unit
810 }
811 }
812 }
813 } else {
814 val event: Event = when (ringState) {
815 RingUpdate.REQUESTED -> Event.RINGING
816 RingUpdate.EXPIRED_REQUEST -> if (dueToNotificationProfile) Event.MISSED_NOTIFICATION_PROFILE else Event.MISSED
817 RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> {
818 Log.w(TAG, "Missed original ring request for $ringId")
819 Event.ACCEPTED
820 }
821
822 RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> {
823 Log.w(TAG, "Missed original ring request for $ringId")
824 Event.DECLINED
825 }
826
827 RingUpdate.BUSY_LOCALLY, RingUpdate.BUSY_ON_ANOTHER_DEVICE -> {
828 Log.w(TAG, "Missed original ring request for $ringId")
829 Event.MISSED
830 }
831
832 RingUpdate.CANCELLED_BY_RINGER -> {
833 Log.w(TAG, "Missed original ring request for $ringId")
834 Event.MISSED
835 }
836 }
837
838 createEventFromRingState(ringId, groupRecipientId, ringerRecipient, event, dateReceived)
839 }
840 }
841
842 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
843 }
844
845 private fun updateEventFromRingState(
846 callId: Long,
847 event: Event,
848 ringerRecipient: RecipientId
849 ) {
850 writableDatabase
851 .update(TABLE_NAME)
852 .values(
853 EVENT to Event.serialize(event),
854 RINGER to ringerRecipient.serialize()
855 )
856 .where("$CALL_ID = ?", callId)
857 .run()
858
859 Log.d(TAG, "Updated ring state to $event")
860 }
861
862 private fun updateEventFromRingState(
863 callId: Long,
864 event: Event
865 ) {
866 writableDatabase
867 .update(TABLE_NAME)
868 .values(
869 EVENT to Event.serialize(event)
870 )
871 .where("$CALL_ID = ?", callId)
872 .run()
873
874 Log.d(TAG, "Updated ring state to $event")
875 }
876
877 private fun createEventFromRingState(
878 callId: Long,
879 groupRecipientId: RecipientId,
880 ringerRecipient: RecipientId,
881 event: Event,
882 timestamp: Long
883 ) {
884 val direction = if (ringerRecipient == Recipient.self().id) Direction.OUTGOING else Direction.INCOMING
885
886 val recipient = Recipient.resolved(groupRecipientId)
887 check(recipient.isPushV2Group)
888
889 writableDatabase.withinTransaction { db ->
890 val messageId = SignalDatabase.messages.insertGroupCall(
891 groupRecipientId = groupRecipientId,
892 sender = ringerRecipient,
893 timestamp = timestamp,
894 eraId = "",
895 joinedUuids = emptyList(),
896 isCallFull = false
897 )
898
899 db
900 .insertInto(TABLE_NAME)
901 .values(
902 CALL_ID to callId,
903 MESSAGE_ID to messageId.id,
904 PEER to groupRecipientId.toLong(),
905 EVENT to Event.serialize(event),
906 TYPE to Type.serialize(Type.GROUP_CALL),
907 DIRECTION to Direction.serialize(direction),
908 TIMESTAMP to timestamp,
909 RINGER to ringerRecipient.toLong()
910 )
911 .run(SQLiteDatabase.CONFLICT_ABORT)
912 }
913
914 Log.d(TAG, "Inserted a new group ring event for $callId with event $event")
915 }
916
917 fun setTimestamp(callId: Long, recipientId: RecipientId, timestamp: Long) {
918 writableDatabase.withinTransaction { db ->
919 val call = getCallById(callId, recipientId)
920 if (call == null || call.event == Event.DELETE) {
921 Log.d(TAG, "Refusing to update deleted call event.")
922 return@withinTransaction
923 }
924
925 db
926 .update(TABLE_NAME)
927 .values(TIMESTAMP to timestamp)
928 .where("$CALL_ID = ?", callId)
929 .run()
930
931 if (call.messageId != null) {
932 SignalDatabase.messages.updateCallTimestamps(call.messageId, timestamp)
933 }
934 }
935
936 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
937 }
938
939 private fun setMessageId(callId: Long, messageId: MessageId) {
940 writableDatabase
941 .update(TABLE_NAME)
942 .values(MESSAGE_ID to messageId.id)
943 .where("$CALL_ID = ?", callId)
944 .run()
945 }
946
947 /**
948 * Gets the most recent timestamp from the [TIMESTAMP] column
949 */
950 fun getLatestCall(): Call? {
951 val statement = """
952 SELECT * FROM $TABLE_NAME ORDER BY $TIMESTAMP DESC LIMIT 1
953 """.trimIndent()
954
955 return readableDatabase.query(statement).readToSingleObject { Call.deserialize(it) }
956 }
957
958 fun deleteNonAdHocCallEventsOnOrBefore(timestamp: Long) {
959 val messageIdsOnOrBeforeTimestamp = """
960 SELECT $MESSAGE_ID FROM $TABLE_NAME WHERE $TIMESTAMP <= $timestamp AND $MESSAGE_ID IS NOT NULL
961 """.trimIndent()
962
963 writableDatabase.withinTransaction { db ->
964 db.delete(MessageTable.TABLE_NAME)
965 .where("${MessageTable.ID} IN ($messageIdsOnOrBeforeTimestamp)")
966 .run()
967
968 updateCallEventDeletionTimestamps(skipSync = true)
969 }
970 }
971
972 fun deleteNonAdHocCallEvents(callRowIds: Set<Long>) {
973 val messageIds = getMessageIds(callRowIds)
974 SignalDatabase.messages.deleteCallUpdates(messageIds)
975 updateCallEventDeletionTimestamps()
976 }
977
978 fun deleteAllNonAdHocCallEventsExcept(callRowIds: Set<Long>, missedOnly: Boolean) {
979 val callFilter = if (missedOnly) {
980 "($EVENT = ${Event.serialize(Event.MISSED)} OR $EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND $DELETION_TIMESTAMP = 0"
981 } else {
982 "$DELETION_TIMESTAMP = 0"
983 }
984
985 if (callRowIds.isEmpty()) {
986 val threadIds = writableDatabase.withinTransaction { db ->
987 val ids = db.select(MessageTable.THREAD_ID)
988 .from(MessageTable.TABLE_NAME)
989 .where(
990 """
991 ${MessageTable.ID} IN (
992 SELECT $MESSAGE_ID FROM $TABLE_NAME
993 WHERE $callFilter
994 )
995 """.toSingleLine()
996 )
997 .run()
998 .readToList { it.requireLong(MessageTable.THREAD_ID) }
999
1000 db.delete(MessageTable.TABLE_NAME)
1001 .where(
1002 """
1003 ${MessageTable.ID} IN (
1004 SELECT $MESSAGE_ID FROM $TABLE_NAME
1005 WHERE $callFilter
1006 )
1007 """.toSingleLine()
1008 )
1009 .run()
1010
1011 ids.toSet()
1012 }
1013
1014 threadIds.forEach {
1015 SignalDatabase.threads.update(
1016 threadId = it,
1017 unarchive = false,
1018 allowDeletion = true
1019 )
1020 }
1021
1022 notifyConversationListeners(threadIds)
1023 notifyConversationListListeners()
1024 updateCallEventDeletionTimestamps()
1025 } else {
1026 writableDatabase.withinTransaction { db ->
1027 SqlUtil.buildCollectionQuery(
1028 column = ID,
1029 values = callRowIds,
1030 prefix = "$callFilter AND",
1031 collectionOperator = SqlUtil.CollectionOperator.NOT_IN
1032 ).forEach { query ->
1033 val messageIds = db.select(MESSAGE_ID)
1034 .from(TABLE_NAME)
1035 .where(query.where, query.whereArgs)
1036 .run()
1037 .readToList { it.requireLong(MESSAGE_ID) }
1038 .toSet()
1039 SignalDatabase.messages.deleteCallUpdates(messageIds)
1040 updateCallEventDeletionTimestamps()
1041 }
1042 }
1043 }
1044 }
1045
1046 @Discouraged("Using this method is generally considered an error. Utilize other deletion methods instead of this.")
1047 fun deleteAllCalls() {
1048 Log.w(TAG, "Deleting all calls from the local database.")
1049 writableDatabase.deleteAll(TABLE_NAME)
1050 }
1051
1052 private fun getCallSelectionQuery(callId: Long, recipientId: RecipientId): SqlUtil.Query {
1053 return SqlUtil.Query("$CALL_ID = ? AND $PEER = ?", SqlUtil.buildArgs(callId, recipientId))
1054 }
1055
1056 private fun getMessageIds(callRowIds: Set<Long>): Set<Long> {
1057 val queries = SqlUtil.buildCollectionQuery(
1058 ID,
1059 callRowIds,
1060 "$MESSAGE_ID NOT NULL AND"
1061 )
1062
1063 return queries.map { query ->
1064 readableDatabase.select(MESSAGE_ID).from(TABLE_NAME).where(query.where, query.whereArgs).run().readToList {
1065 it.requireLong(MESSAGE_ID)
1066 }
1067 }.flatten().toSet()
1068 }
1069
1070 private fun checkIsGroupOrAdHocCall(call: Call) {
1071 check(call.type == Type.GROUP_CALL || call.type == Type.AD_HOC_CALL)
1072 }
1073
1074 // endregion
1075
1076 private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
1077 val filterClause: SqlUtil.Query = when (filter) {
1078 CallLogFilter.ALL -> SqlUtil.buildQuery("$DELETION_TIMESTAMP = 0")
1079 CallLogFilter.MISSED -> SqlUtil.buildQuery("($EVENT = ${Event.serialize(Event.MISSED)} OR $EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND $DELETION_TIMESTAMP = 0")
1080 CallLogFilter.AD_HOC -> SqlUtil.buildQuery("$TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND $DELETION_TIMESTAMP = 0")
1081 }
1082
1083 val queryClause: SqlUtil.Query = if (!searchTerm.isNullOrEmpty()) {
1084 val glob = SqlUtil.buildCaseInsensitiveGlobPattern(searchTerm)
1085 val selection =
1086 """
1087 ${RecipientTable.TABLE_NAME}.${RecipientTable.BLOCKED} = ? AND ${RecipientTable.TABLE_NAME}.${RecipientTable.HIDDEN} = ? AND
1088 (
1089 sort_name GLOB ? OR
1090 ${RecipientTable.TABLE_NAME}.${RecipientTable.USERNAME} GLOB ? OR
1091 ${RecipientTable.TABLE_NAME}.${RecipientTable.E164} GLOB ? OR
1092 ${RecipientTable.TABLE_NAME}.${RecipientTable.EMAIL} GLOB ?
1093 )
1094 """
1095 SqlUtil.buildQuery(selection, 0, 0, glob, glob, glob, glob)
1096 } else {
1097 SqlUtil.buildQuery(
1098 """
1099 ${RecipientTable.TABLE_NAME}.${RecipientTable.BLOCKED} = ? AND ${RecipientTable.TABLE_NAME}.${RecipientTable.HIDDEN} = ?
1100 """,
1101 0,
1102 0
1103 )
1104 }
1105
1106 val offsetLimit = if (limit > 0) {
1107 "LIMIT $offset,$limit"
1108 } else {
1109 ""
1110 }
1111
1112 val projection = if (isCount) {
1113 "COUNT(*),"
1114 } else {
1115 "p.$ID, p.$TIMESTAMP, $EVENT, $DIRECTION, $PEER, p.$TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, children, in_period, ${MessageTable.BODY},"
1116 }
1117
1118 val eventTypeSubQuery = """
1119 ($TABLE_NAME.$EVENT = c.$EVENT AND ($TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)})) OR
1120 (
1121 $TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED)} AND
1122 c.$EVENT != ${Event.serialize(Event.MISSED)} AND
1123 $TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND
1124 c.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}
1125 )
1126 """
1127
1128 //language=sql
1129 val statement = """
1130 SELECT $projection
1131 LOWER(
1132 COALESCE(
1133 NULLIF(${GroupTable.TABLE_NAME}.${GroupTable.TITLE}, ''),
1134 NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''),
1135 NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''),
1136 NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
1137 NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_GIVEN_NAME}, ''),
1138 NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.USERNAME}, '')
1139 )
1140 ) AS sort_name
1141 FROM (
1142 WITH cte AS (
1143 SELECT
1144 $ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, $TYPE, $CALL_ID, $MESSAGE_ID, $RINGER,
1145 (
1146 SELECT
1147 $ID
1148 FROM
1149 $TABLE_NAME
1150 WHERE
1151 $TABLE_NAME.$DIRECTION = c.$DIRECTION
1152 AND $TABLE_NAME.$PEER = c.$PEER
1153 AND $TABLE_NAME.$TIMESTAMP - $TIME_WINDOW <= c.$TIMESTAMP
1154 AND $TABLE_NAME.$TIMESTAMP >= c.$TIMESTAMP
1155 AND ($eventTypeSubQuery)
1156 AND ${filterClause.where}
1157 ORDER BY
1158 $TIMESTAMP DESC
1159 ) as parent,
1160 (
1161 SELECT
1162 group_concat($ID)
1163 FROM
1164 $TABLE_NAME
1165 WHERE
1166 $TABLE_NAME.$DIRECTION = c.$DIRECTION
1167 AND $TABLE_NAME.$PEER = c.$PEER
1168 AND c.$TIMESTAMP - $TIME_WINDOW <= $TABLE_NAME.$TIMESTAMP
1169 AND c.$TIMESTAMP >= $TABLE_NAME.$TIMESTAMP
1170 AND ($eventTypeSubQuery)
1171 AND ${filterClause.where}
1172 ) as children,
1173 (
1174 SELECT
1175 group_concat($ID)
1176 FROM
1177 $TABLE_NAME
1178 WHERE
1179 c.$TIMESTAMP - $TIME_WINDOW <= $TABLE_NAME.$TIMESTAMP
1180 AND c.$TIMESTAMP >= $TABLE_NAME.$TIMESTAMP
1181 AND ${filterClause.where}
1182 ) as in_period
1183 FROM
1184 $TABLE_NAME c
1185 WHERE ${filterClause.where}
1186 ORDER BY
1187 $TIMESTAMP DESC
1188 )
1189 SELECT
1190 *,
1191 CASE
1192 WHEN LAG (parent, 1, 0) OVER (
1193 ORDER BY
1194 $TIMESTAMP DESC
1195 ) != parent THEN $ID
1196 ELSE parent
1197 END true_parent
1198 FROM
1199 cte
1200 ) p
1201 INNER JOIN ${RecipientTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = $PEER
1202 LEFT JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $MESSAGE_ID
1203 LEFT JOIN ${GroupTable.TABLE_NAME} ON ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
1204 WHERE true_parent = p.$ID ${if (queryClause.where.isNotEmpty()) "AND ${queryClause.where}" else ""}
1205 ORDER BY p.$TIMESTAMP DESC
1206 $offsetLimit
1207 """
1208
1209 return readableDatabase.query(
1210 statement,
1211 queryClause.whereArgs
1212 )
1213 }
1214
1215 fun getLatestRingingCalls(): List<Call> {
1216 return readableDatabase.select()
1217 .from(TABLE_NAME)
1218 .where("$EVENT = ?", Event.serialize(Event.RINGING))
1219 .limit(10)
1220 .orderBy(TIMESTAMP)
1221 .run()
1222 .readToList {
1223 Call.deserialize(it)
1224 }
1225 }
1226
1227 fun markRingingCallsAsMissed() {
1228 writableDatabase.update(TABLE_NAME)
1229 .values(EVENT to Event.serialize(Event.MISSED))
1230 .where("$EVENT = ?", Event.serialize(Event.RINGING))
1231 .run()
1232 }
1233
1234 fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int {
1235 return getCallsCursor(true, 0, 0, searchTerm, filter).use {
1236 it.moveToFirst()
1237 it.getInt(0)
1238 }
1239 }
1240
1241 fun getCalls(offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): List<CallLogRow.Call> {
1242 return getCallsCursor(false, offset, limit, searchTerm, filter).readToList { cursor ->
1243 val call = Call.deserialize(cursor)
1244 val groupCallDetails = GroupCallUpdateDetailsUtil.parse(cursor.requireString(MessageTable.BODY))
1245
1246 val children = cursor.requireNonNullString("children")
1247 .split(',')
1248 .map { it.toLong() }
1249 .toSet()
1250
1251 val inPeriod = cursor.requireNonNullString("in_period")
1252 .split(',')
1253 .map { it.toLong() }
1254 .sortedDescending()
1255 .toSet()
1256
1257 val actualChildren = inPeriod.takeWhile { children.contains(it) }
1258 val peer = Recipient.resolved(call.peer)
1259
1260 val canUserBeginCall = if (peer.isGroup) {
1261 val record = SignalDatabase.groups.getGroup(peer.id)
1262
1263 !record.isAbsent() &&
1264 record.get().isActive &&
1265 (!record.get().isAnnouncementGroup || record.get().memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR)
1266 } else {
1267 true
1268 }
1269
1270 CallLogRow.Call(
1271 record = call,
1272 date = call.timestamp,
1273 peer = peer,
1274 groupCallState = CallLogRow.GroupCallState.fromDetails(groupCallDetails),
1275 children = actualChildren.toSet(),
1276 searchQuery = searchTerm,
1277 callLinkPeekInfo = ApplicationDependencies.getSignalCallManager().peekInfoSnapshot[peer.id],
1278 canUserBeginCall = canUserBeginCall
1279 )
1280 }
1281 }
1282
1283 override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
1284 writableDatabase
1285 .update(TABLE_NAME)
1286 .values(PEER to toId.serialize())
1287 .where("$PEER = ?", fromId)
1288 .run()
1289 }
1290
1291 data class Call(
1292 val callId: Long,
1293 val peer: RecipientId,
1294 val type: Type,
1295 val direction: Direction,
1296 val event: Event,
1297 val messageId: Long?,
1298 val timestamp: Long,
1299 val ringerRecipient: RecipientId?
1300 ) {
1301 val messageType: Long = getMessageType(type, direction, event)
1302
1303 companion object Deserializer : Serializer<Call, Cursor> {
1304 fun getMessageType(type: Type, direction: Direction, event: Event): Long {
1305 if (type == Type.GROUP_CALL || type == Type.AD_HOC_CALL) {
1306 return MessageTypes.GROUP_CALL_TYPE
1307 }
1308
1309 return if (direction == Direction.INCOMING && event.isMissedCall()) {
1310 if (type == Type.VIDEO_CALL) MessageTypes.MISSED_VIDEO_CALL_TYPE else MessageTypes.MISSED_AUDIO_CALL_TYPE
1311 } else if (direction == Direction.INCOMING) {
1312 if (type == Type.VIDEO_CALL) MessageTypes.INCOMING_VIDEO_CALL_TYPE else MessageTypes.INCOMING_AUDIO_CALL_TYPE
1313 } else {
1314 if (type == Type.VIDEO_CALL) MessageTypes.OUTGOING_VIDEO_CALL_TYPE else MessageTypes.OUTGOING_AUDIO_CALL_TYPE
1315 }
1316 }
1317
1318 override fun serialize(data: Call): Cursor {
1319 throw UnsupportedOperationException()
1320 }
1321
1322 override fun deserialize(data: Cursor): Call {
1323 return Call(
1324 callId = data.requireLong(CALL_ID),
1325 peer = RecipientId.from(data.requireLong(PEER)),
1326 type = data.requireObject(TYPE, Type.Serializer),
1327 direction = data.requireObject(DIRECTION, Direction.Serializer),
1328 event = data.requireObject(EVENT, Event.Serializer),
1329 messageId = data.requireLong(MESSAGE_ID).takeIf { it > 0L },
1330 timestamp = data.requireLong(TIMESTAMP),
1331 ringerRecipient = data.requireLong(RINGER).let {
1332 if (it > 0) {
1333 RecipientId.from(it)
1334 } else {
1335 null
1336 }
1337 }
1338 )
1339 }
1340 }
1341 }
1342
1343 enum class Type(private val code: Int) {
1344 AUDIO_CALL(0),
1345 VIDEO_CALL(1),
1346 GROUP_CALL(3),
1347 AD_HOC_CALL(4);
1348
1349 companion object Serializer : IntSerializer<Type> {
1350 override fun serialize(data: Type): Int = data.code
1351
1352 override fun deserialize(data: Int): Type {
1353 return when (data) {
1354 AUDIO_CALL.code -> AUDIO_CALL
1355 VIDEO_CALL.code -> VIDEO_CALL
1356 GROUP_CALL.code -> GROUP_CALL
1357 AD_HOC_CALL.code -> AD_HOC_CALL
1358 else -> throw IllegalArgumentException("Unknown type $data")
1359 }
1360 }
1361
1362 @JvmStatic
1363 fun from(type: CallEvent.Type?): Type? {
1364 return when (type) {
1365 null, CallEvent.Type.UNKNOWN_TYPE -> null
1366 CallEvent.Type.AUDIO_CALL -> AUDIO_CALL
1367 CallEvent.Type.VIDEO_CALL -> VIDEO_CALL
1368 CallEvent.Type.GROUP_CALL -> GROUP_CALL
1369 CallEvent.Type.AD_HOC_CALL -> AD_HOC_CALL
1370 }
1371 }
1372 }
1373 }
1374
1375 enum class Direction(private val code: Int) {
1376 INCOMING(0),
1377 OUTGOING(1);
1378
1379 companion object Serializer : IntSerializer<Direction> {
1380 override fun serialize(data: Direction): Int = data.code
1381
1382 override fun deserialize(data: Int): Direction {
1383 return when (data) {
1384 INCOMING.code -> INCOMING
1385 OUTGOING.code -> OUTGOING
1386 else -> throw IllegalArgumentException("Unknown type $data")
1387 }
1388 }
1389
1390 @JvmStatic
1391 fun from(direction: CallEvent.Direction?): Direction? {
1392 return when (direction) {
1393 null, CallEvent.Direction.UNKNOWN_DIRECTION -> null
1394 CallEvent.Direction.INCOMING -> INCOMING
1395 CallEvent.Direction.OUTGOING -> OUTGOING
1396 }
1397 }
1398 }
1399 }
1400
1401 enum class ReadState(private val code: Int) {
1402 UNREAD(0),
1403 READ(1);
1404
1405 companion object Serializer : IntSerializer<ReadState> {
1406 override fun serialize(data: ReadState): Int {
1407 return data.code
1408 }
1409
1410 override fun deserialize(data: Int): ReadState {
1411 return ReadState.values().first { it.code == data }
1412 }
1413 }
1414 }
1415
1416 enum class Event(private val code: Int) {
1417 /**
1418 * 1:1 Calls only.
1419 */
1420 ONGOING(0),
1421
1422 /**
1423 * 1:1 and Group Calls.
1424 *
1425 * Group calls: You accepted a ring.
1426 */
1427 ACCEPTED(1),
1428
1429 /**
1430 * 1:1 Calls only.
1431 */
1432 NOT_ACCEPTED(2),
1433
1434 /**
1435 * 1:1 and Group/Ad-Hoc Calls.
1436 *
1437 * Group calls: The remote ring has expired or was cancelled by the ringer.
1438 */
1439 MISSED(3),
1440
1441 /**
1442 * 1:1 and Group/Ad-Hoc Calls.
1443 *
1444 * Call was auto-declined due to a notification profile.
1445 */
1446 MISSED_NOTIFICATION_PROFILE(10),
1447
1448 /**
1449 * 1:1 and Group/Ad-Hoc Calls.
1450 */
1451 DELETE(4),
1452
1453 /**
1454 * Group/Ad-Hoc Calls only.
1455 *
1456 * Initial state.
1457 */
1458 GENERIC_GROUP_CALL(5),
1459
1460 /**
1461 * Group Calls: User has joined the group call.
1462 */
1463 JOINED(6),
1464
1465 /**
1466 * Group Calls: If a ring was requested by another user.
1467 */
1468 RINGING(7),
1469
1470 /**
1471 * Group Calls: If you declined a ring.
1472 */
1473 DECLINED(8),
1474
1475 /**
1476 * Group Calls: If you are ringing a group.
1477 */
1478 OUTGOING_RING(9); // Next is 11
1479
1480 fun isMissedCall(): Boolean {
1481 return this == MISSED || this == MISSED_NOTIFICATION_PROFILE
1482 }
1483
1484 companion object Serializer : IntSerializer<Event> {
1485 override fun serialize(data: Event): Int = data.code
1486
1487 override fun deserialize(data: Int): Event {
1488 return values().firstOrNull {
1489 it.code == data
1490 } ?: throw IllegalArgumentException("Unknown event $data")
1491 }
1492
1493 @JvmStatic
1494 fun from(event: CallEvent.Event?): Event? {
1495 return when (event) {
1496 null, CallEvent.Event.UNKNOWN_ACTION -> null
1497 CallEvent.Event.ACCEPTED -> ACCEPTED
1498 CallEvent.Event.NOT_ACCEPTED -> NOT_ACCEPTED
1499 CallEvent.Event.DELETE -> DELETE
1500 }
1501 }
1502 }
1503 }
1504}