That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.annotation.SuppressLint
4import android.content.ContentValues
5import android.content.Context
6import android.database.Cursor
7import android.database.MergeCursor
8import android.net.Uri
9import androidx.core.content.contentValuesOf
10import com.fasterxml.jackson.annotation.JsonProperty
11import org.json.JSONObject
12import org.jsoup.helper.StringUtil
13import org.signal.core.util.CursorUtil
14import org.signal.core.util.SqlUtil
15import org.signal.core.util.Stopwatch
16import org.signal.core.util.delete
17import org.signal.core.util.exists
18import org.signal.core.util.logging.Log
19import org.signal.core.util.or
20import org.signal.core.util.readToList
21import org.signal.core.util.readToSingleLong
22import org.signal.core.util.requireBoolean
23import org.signal.core.util.requireInt
24import org.signal.core.util.requireLong
25import org.signal.core.util.requireString
26import org.signal.core.util.select
27import org.signal.core.util.toInt
28import org.signal.core.util.update
29import org.signal.core.util.updateAll
30import org.signal.core.util.withinTransaction
31import org.signal.libsignal.zkgroup.InvalidInputException
32import org.signal.libsignal.zkgroup.groups.GroupMasterKey
33import org.tm.archive.conversationlist.model.ConversationFilter
34import org.tm.archive.database.MessageTable.MarkedMessageInfo
35import org.tm.archive.database.SignalDatabase.Companion.attachments
36import org.tm.archive.database.SignalDatabase.Companion.calls
37import org.tm.archive.database.SignalDatabase.Companion.drafts
38import org.tm.archive.database.SignalDatabase.Companion.groupReceipts
39import org.tm.archive.database.SignalDatabase.Companion.mentions
40import org.tm.archive.database.SignalDatabase.Companion.messageLog
41import org.tm.archive.database.SignalDatabase.Companion.messages
42import org.tm.archive.database.SignalDatabase.Companion.recipients
43import org.tm.archive.database.ThreadBodyUtil.ThreadBody
44import org.tm.archive.database.model.MessageRecord
45import org.tm.archive.database.model.MmsMessageRecord
46import org.tm.archive.database.model.ThreadRecord
47import org.tm.archive.database.model.databaseprotos.BodyRangeList
48import org.tm.archive.database.model.databaseprotos.MessageExtras
49import org.tm.archive.database.model.serialize
50import org.tm.archive.dependencies.ApplicationDependencies
51import org.tm.archive.groups.BadGroupIdException
52import org.tm.archive.groups.GroupId
53import org.tm.archive.jobs.OptimizeMessageSearchIndexJob
54import org.tm.archive.keyvalue.SignalStore
55import org.tm.archive.mms.SlideDeck
56import org.tm.archive.mms.StickerSlide
57import org.tm.archive.notifications.v2.ConversationId
58import org.tm.archive.recipients.Recipient
59import org.tm.archive.recipients.RecipientDetails
60import org.tm.archive.recipients.RecipientId
61import org.tm.archive.recipients.RecipientUtil
62import org.tm.archive.storage.StorageSyncHelper
63import org.tm.archive.util.ConversationUtil
64import org.tm.archive.util.JsonUtils
65import org.tm.archive.util.JsonUtils.SaneJSONObject
66import org.tm.archive.util.LRUCache
67import org.tm.archive.util.TextSecurePreferences
68import org.tm.archive.util.isScheduled
69import org.whispersystems.signalservice.api.storage.SignalAccountRecord
70import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation
71import org.whispersystems.signalservice.api.storage.SignalContactRecord
72import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
73import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
74import java.io.Closeable
75import java.io.IOException
76import java.util.Collections
77import java.util.LinkedList
78import java.util.Optional
79import kotlin.math.max
80import kotlin.math.min
81
82@SuppressLint("RecipientIdDatabaseReferenceUsage", "ThreadIdDatabaseReferenceUsage") // Handles remapping in a unique way
83class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
84
85 companion object {
86 private val TAG = Log.tag(ThreadTable::class.java)
87
88 const val TABLE_NAME = "thread"
89 const val ID = "_id"
90 const val DATE = "date"
91 const val MEANINGFUL_MESSAGES = "meaningful_messages"
92 const val RECIPIENT_ID = "recipient_id"
93 const val READ = "read"
94 const val UNREAD_COUNT = "unread_count"
95 const val TYPE = "type"
96 const val ERROR = "error"
97 const val SNIPPET = "snippet"
98 const val SNIPPET_TYPE = "snippet_type"
99 const val SNIPPET_URI = "snippet_uri"
100 const val SNIPPET_CONTENT_TYPE = "snippet_content_type"
101 const val SNIPPET_EXTRAS = "snippet_extras"
102 const val SNIPPET_MESSAGE_EXTRAS = "snippet_message_extras"
103 const val ARCHIVED = "archived"
104 const val STATUS = "status"
105 const val HAS_DELIVERY_RECEIPT = "has_delivery_receipt"
106 const val HAS_READ_RECEIPT = "has_read_receipt"
107 const val EXPIRES_IN = "expires_in"
108 const val LAST_SEEN = "last_seen"
109 const val HAS_SENT = "has_sent"
110 const val LAST_SCROLLED = "last_scrolled"
111 const val PINNED = "pinned"
112 const val UNREAD_SELF_MENTION_COUNT = "unread_self_mention_count"
113 const val ACTIVE = "active"
114
115 const val MAX_CACHE_SIZE = 1000
116
117 @JvmField
118 val CREATE_TABLE = """
119 CREATE TABLE $TABLE_NAME (
120 $ID INTEGER PRIMARY KEY AUTOINCREMENT,
121 $DATE INTEGER DEFAULT 0,
122 $MEANINGFUL_MESSAGES INTEGER DEFAULT 0,
123 $RECIPIENT_ID INTEGER NOT NULL UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
124 $READ INTEGER DEFAULT ${ReadStatus.READ.serialize()},
125 $TYPE INTEGER DEFAULT 0,
126 $ERROR INTEGER DEFAULT 0,
127 $SNIPPET TEXT,
128 $SNIPPET_TYPE INTEGER DEFAULT 0,
129 $SNIPPET_URI TEXT DEFAULT NULL,
130 $SNIPPET_CONTENT_TYPE TEXT DEFAULT NULL,
131 $SNIPPET_EXTRAS TEXT DEFAULT NULL,
132 $UNREAD_COUNT INTEGER DEFAULT 0,
133 $ARCHIVED INTEGER DEFAULT 0,
134 $STATUS INTEGER DEFAULT 0,
135 $HAS_DELIVERY_RECEIPT INTEGER DEFAULT 0,
136 $HAS_READ_RECEIPT INTEGER DEFAULT 0,
137 $EXPIRES_IN INTEGER DEFAULT 0,
138 $LAST_SEEN INTEGER DEFAULT 0,
139 $HAS_SENT INTEGER DEFAULT 0,
140 $LAST_SCROLLED INTEGER DEFAULT 0,
141 $PINNED INTEGER DEFAULT 0,
142 $UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0,
143 $ACTIVE INTEGER DEFAULT 0,
144 $SNIPPET_MESSAGE_EXTRAS BLOB DEFAULT NULL
145 )
146 """
147
148 @JvmField
149 val CREATE_INDEXS = arrayOf(
150 "CREATE INDEX IF NOT EXISTS thread_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID, $ACTIVE);",
151 "CREATE INDEX IF NOT EXISTS archived_count_index ON $TABLE_NAME ($ACTIVE, $ARCHIVED, $MEANINGFUL_MESSAGES, $PINNED);",
152 "CREATE INDEX IF NOT EXISTS thread_pinned_index ON $TABLE_NAME ($PINNED);",
153 "CREATE INDEX IF NOT EXISTS thread_read ON $TABLE_NAME ($READ);",
154 "CREATE INDEX IF NOT EXISTS thread_active ON $TABLE_NAME ($ACTIVE);"
155 )
156
157 private val THREAD_PROJECTION = arrayOf(
158 ID,
159 DATE,
160 MEANINGFUL_MESSAGES,
161 RECIPIENT_ID,
162 SNIPPET,
163 READ,
164 UNREAD_COUNT,
165 TYPE,
166 ERROR,
167 SNIPPET_TYPE,
168 SNIPPET_URI,
169 SNIPPET_CONTENT_TYPE,
170 SNIPPET_EXTRAS,
171 SNIPPET_MESSAGE_EXTRAS,
172 ARCHIVED,
173 STATUS,
174 HAS_DELIVERY_RECEIPT,
175 EXPIRES_IN,
176 LAST_SEEN,
177 HAS_READ_RECEIPT,
178 LAST_SCROLLED,
179 PINNED,
180 UNREAD_SELF_MENTION_COUNT
181 )
182
183 private val TYPED_THREAD_PROJECTION: List<String> = THREAD_PROJECTION
184 .map { columnName: String -> "$TABLE_NAME.$columnName" }
185 .toList()
186
187 private val COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION: List<String> = TYPED_THREAD_PROJECTION + RecipientTable.TYPED_RECIPIENT_PROJECTION_NO_ID + GroupTable.TYPED_GROUP_PROJECTION
188
189 const val NO_TRIM_BEFORE_DATE_SET: Long = 0
190 const val NO_TRIM_MESSAGE_COUNT_SET = Int.MAX_VALUE
191 }
192
193 private val threadIdCache = LRUCache<RecipientId, Long>(MAX_CACHE_SIZE)
194
195 private fun createThreadForRecipient(recipientId: RecipientId, group: Boolean, distributionType: Int): Long {
196 if (recipientId.isUnknown) {
197 throw AssertionError("Cannot create a thread for an unknown recipient!")
198 }
199
200 val date = System.currentTimeMillis()
201 val contentValues = contentValuesOf(
202 DATE to date - date % 1000,
203 RECIPIENT_ID to recipientId.serialize(),
204 MEANINGFUL_MESSAGES to 0
205 )
206
207 if (group) {
208 contentValues.put(TYPE, distributionType)
209 }
210
211 val result = writableDatabase.insert(TABLE_NAME, null, contentValues)
212 Recipient.live(recipientId).refresh()
213 return result
214 }
215
216 private fun updateThread(
217 threadId: Long,
218 meaningfulMessages: Boolean,
219 body: String?,
220 attachment: Uri?,
221 contentType: String?,
222 extra: Extra?,
223 date: Long,
224 status: Int,
225 deliveryReceiptCount: Int,
226 type: Long,
227 unarchive: Boolean,
228 expiresIn: Long,
229 readReceiptCount: Int,
230 unreadCount: Int,
231 unreadMentionCount: Int,
232 messageExtras: MessageExtras?
233 ) {
234 var extraSerialized: String? = null
235
236 if (extra != null) {
237 extraSerialized = try {
238 JsonUtils.toJson(extra)
239 } catch (e: IOException) {
240 throw AssertionError(e)
241 }
242 }
243
244 val contentValues = contentValuesOf(
245 DATE to date - date % 1000,
246 SNIPPET to body,
247 SNIPPET_URI to attachment?.toString(),
248 SNIPPET_TYPE to type,
249 SNIPPET_CONTENT_TYPE to contentType,
250 SNIPPET_EXTRAS to extraSerialized,
251 MEANINGFUL_MESSAGES to if (meaningfulMessages) 1 else 0,
252 STATUS to status,
253 HAS_DELIVERY_RECEIPT to deliveryReceiptCount,
254 HAS_READ_RECEIPT to readReceiptCount,
255 EXPIRES_IN to expiresIn,
256 ACTIVE to 1,
257 UNREAD_COUNT to unreadCount,
258 UNREAD_SELF_MENTION_COUNT to unreadMentionCount,
259 SNIPPET_MESSAGE_EXTRAS to messageExtras?.encode()
260 )
261
262 writableDatabase
263 .update(TABLE_NAME)
264 .values(contentValues)
265 .where("$ID = ?", threadId)
266 .run()
267
268 if (unarchive && allowedToUnarchive(threadId)) {
269 val archiveValues = contentValuesOf(ARCHIVED to 0)
270 val query = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(threadId), archiveValues)
271 if (writableDatabase.update(TABLE_NAME, archiveValues, query.where, query.whereArgs) > 0) {
272 StorageSyncHelper.scheduleSyncForDataChange()
273 }
274 }
275 }
276
277 private fun allowedToUnarchive(threadId: Long): Boolean {
278 if (!SignalStore.settings().shouldKeepMutedChatsArchived()) {
279 return true
280 }
281
282 val threadRecipientId: RecipientId? = getRecipientIdForThreadId(threadId)
283
284 return threadRecipientId == null || !recipients.isMuted(threadRecipientId)
285 }
286
287 fun updateSnippetUriSilently(threadId: Long, attachment: Uri?) {
288 writableDatabase
289 .update(TABLE_NAME)
290 .values(SNIPPET_URI to attachment?.toString())
291 .where("$ID = ?", threadId)
292 .run()
293 }
294
295 fun updateSnippet(threadId: Long, snippet: String?, attachment: Uri?, date: Long, type: Long, unarchive: Boolean) {
296 if (isSilentType(type)) {
297 return
298 }
299
300 val contentValues = contentValuesOf(
301 DATE to date - date % 1000,
302 SNIPPET to snippet,
303 SNIPPET_URI to attachment?.toString(),
304 SNIPPET_TYPE to type,
305 SNIPPET_CONTENT_TYPE to null,
306 SNIPPET_EXTRAS to null
307 )
308
309 if (unarchive && allowedToUnarchive(threadId)) {
310 contentValues.put(ARCHIVED, 0)
311 }
312
313 writableDatabase
314 .update(TABLE_NAME)
315 .values(contentValues)
316 .where("$ID = ?", threadId)
317 .run()
318
319 notifyConversationListListeners()
320 }
321
322 fun trimAllThreads(length: Int, trimBeforeDate: Long) {
323 if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
324 return
325 }
326
327 readableDatabase
328 .select(ID)
329 .from(TABLE_NAME)
330 .run()
331 .use { cursor ->
332 while (cursor.moveToNext()) {
333 trimThreadInternal(cursor.requireLong(ID), length, trimBeforeDate)
334 }
335 }
336
337 val deletes = writableDatabase.withinTransaction {
338 messages.deleteAbandonedMessages()
339 attachments.trimAllAbandonedAttachments()
340 groupReceipts.deleteAbandonedRows()
341 mentions.deleteAbandonedMentions()
342 return@withinTransaction attachments.deleteAbandonedAttachmentFiles()
343 }
344
345 if (deletes > 0) {
346 Log.i(TAG, "Trim all threads caused $deletes attachments to be deleted.")
347 }
348
349 notifyAttachmentListeners()
350 notifyStickerPackListeners()
351 OptimizeMessageSearchIndexJob.enqueue()
352 }
353
354 fun trimThread(threadId: Long, length: Int, trimBeforeDate: Long) {
355 if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
356 return
357 }
358
359 val deletes = writableDatabase.withinTransaction {
360 trimThreadInternal(threadId, length, trimBeforeDate)
361 messages.deleteAbandonedMessages()
362 attachments.trimAllAbandonedAttachments()
363 groupReceipts.deleteAbandonedRows()
364 mentions.deleteAbandonedMentions()
365 return@withinTransaction attachments.deleteAbandonedAttachmentFiles()
366 }
367
368 if (deletes > 0) {
369 Log.i(TAG, "Trim thread $threadId caused $deletes attachments to be deleted.")
370 }
371
372 notifyAttachmentListeners()
373 notifyStickerPackListeners()
374 OptimizeMessageSearchIndexJob.enqueue()
375 }
376
377 private fun trimThreadInternal(threadId: Long, length: Int, trimBeforeDate: Long) {
378 if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
379 return
380 }
381
382 val finalTrimBeforeDate = if (length != NO_TRIM_MESSAGE_COUNT_SET && length > 0) {
383 messages.getConversation(threadId).use { cursor ->
384 if (cursor.count > length) {
385 cursor.moveToPosition(length - 1)
386 max(trimBeforeDate, cursor.requireLong(MessageTable.DATE_RECEIVED))
387 } else {
388 trimBeforeDate
389 }
390 }
391 } else {
392 trimBeforeDate
393 }
394
395 if (finalTrimBeforeDate != NO_TRIM_BEFORE_DATE_SET) {
396 Log.i(TAG, "Trimming thread: $threadId before: $finalTrimBeforeDate")
397
398 val deletes = messages.deleteMessagesInThreadBeforeDate(threadId, finalTrimBeforeDate)
399 if (deletes > 0) {
400 Log.i(TAG, "Trimming deleted $deletes messages thread: $threadId")
401 setLastScrolled(threadId, 0)
402 update(threadId, false)
403 notifyConversationListeners(threadId)
404 SignalDatabase.calls.updateCallEventDeletionTimestamps()
405 } else {
406 Log.i(TAG, "Trimming deleted no messages thread: $threadId")
407 }
408 }
409 }
410
411 fun setAllThreadsRead(): List<MarkedMessageInfo> {
412 writableDatabase
413 .updateAll(TABLE_NAME)
414 .values(
415 READ to ReadStatus.READ.serialize(),
416 UNREAD_COUNT to 0,
417 UNREAD_SELF_MENTION_COUNT to 0
418 )
419 .run()
420
421 val messageRecords: List<MarkedMessageInfo> = messages.setAllMessagesRead()
422
423 messages.setAllReactionsSeen()
424 notifyConversationListListeners()
425
426 return messageRecords
427 }
428
429 fun hasCalledSince(recipient: Recipient, timestamp: Long): Boolean {
430 return hasReceivedAnyCallsSince(getOrCreateThreadIdFor(recipient), timestamp)
431 }
432
433 fun hasReceivedAnyCallsSince(threadId: Long, timestamp: Long): Boolean {
434 return messages.hasReceivedAnyCallsSince(threadId, timestamp)
435 }
436
437 fun setEntireThreadRead(threadId: Long): List<MarkedMessageInfo> {
438 setRead(threadId, false)
439 return messages.setEntireThreadRead(threadId)
440 }
441
442 fun setRead(threadId: Long, lastSeen: Boolean): List<MarkedMessageInfo> {
443 return setReadSince(Collections.singletonMap(threadId, -1L), lastSeen)
444 }
445
446 fun setRead(conversationId: ConversationId, lastSeen: Boolean): List<MarkedMessageInfo> {
447 return if (conversationId.groupStoryId == null) {
448 setRead(conversationId.threadId, lastSeen)
449 } else {
450 setGroupStoryReadSince(conversationId.threadId, conversationId.groupStoryId, System.currentTimeMillis())
451 }
452 }
453
454 fun setReadSince(conversationId: ConversationId, lastSeen: Boolean, sinceTimestamp: Long): List<MarkedMessageInfo> {
455 return if (conversationId.groupStoryId != null) {
456 setGroupStoryReadSince(conversationId.threadId, conversationId.groupStoryId, sinceTimestamp)
457 } else {
458 setReadSince(conversationId.threadId, lastSeen, sinceTimestamp)
459 }
460 }
461
462 fun setReadSince(threadId: Long, lastSeen: Boolean, sinceTimestamp: Long): List<MarkedMessageInfo> {
463 return setReadSince(Collections.singletonMap(threadId, sinceTimestamp), lastSeen)
464 }
465
466 fun setRead(threadIds: Collection<Long>, lastSeen: Boolean): List<MarkedMessageInfo> {
467 return setReadSince(threadIds.associateWith { -1L }, lastSeen)
468 }
469
470 private fun setGroupStoryReadSince(threadId: Long, groupStoryId: Long, sinceTimestamp: Long): List<MarkedMessageInfo> {
471 return messages.setGroupStoryMessagesReadSince(threadId, groupStoryId, sinceTimestamp)
472 }
473
474 fun setReadSince(threadIdToSinceTimestamp: Map<Long, Long>, lastSeen: Boolean): List<MarkedMessageInfo> {
475 val messageRecords: MutableList<MarkedMessageInfo> = LinkedList()
476 var needsSync = false
477
478 writableDatabase.withinTransaction { db ->
479 for ((threadId, sinceTimestamp) in threadIdToSinceTimestamp) {
480 val previous = getThreadRecord(threadId)
481
482 messageRecords += messages.setMessagesReadSince(threadId, sinceTimestamp)
483
484 messages.setReactionsSeen(threadId, sinceTimestamp)
485
486 val unreadCount = messages.getUnreadCount(threadId)
487 val unreadMentionsCount = messages.getUnreadMentionCount(threadId)
488
489 val contentValues = contentValuesOf(
490 READ to ReadStatus.READ.serialize(),
491 UNREAD_COUNT to unreadCount,
492 UNREAD_SELF_MENTION_COUNT to unreadMentionsCount
493 )
494
495 if (lastSeen) {
496 contentValues.put(LAST_SEEN, if (sinceTimestamp == -1L) System.currentTimeMillis() else sinceTimestamp)
497 }
498
499 db.update(TABLE_NAME)
500 .values(contentValues)
501 .where("$ID = ?", threadId)
502 .run()
503
504 if (previous != null && previous.isForcedUnread) {
505 recipients.markNeedsSync(previous.recipient.id)
506 needsSync = true
507 }
508 }
509 }
510
511 notifyVerboseConversationListeners(threadIdToSinceTimestamp.keys)
512 notifyConversationListListeners()
513
514 if (needsSync) {
515 StorageSyncHelper.scheduleSyncForDataChange()
516 }
517
518 return messageRecords
519 }
520
521 fun setForcedUnread(threadIds: Collection<Long>) {
522 var recipientIds: List<RecipientId> = emptyList()
523
524 writableDatabase.withinTransaction { db ->
525 val query = SqlUtil.buildSingleCollectionQuery(ID, threadIds)
526 val contentValues = contentValuesOf(READ to ReadStatus.FORCED_UNREAD.serialize())
527 db.update(TABLE_NAME, contentValues, query.where, query.whereArgs)
528
529 recipientIds = getRecipientIdsForThreadIds(threadIds)
530 recipients.markNeedsSyncWithoutRefresh(recipientIds)
531 }
532
533 for (id in recipientIds) {
534 Recipient.live(id).refresh()
535 }
536
537 StorageSyncHelper.scheduleSyncForDataChange()
538 notifyConversationListListeners()
539 }
540
541 fun getUnreadThreadCount(): Long {
542 return getUnreadThreadIdAggregate(SqlUtil.COUNT) { cursor ->
543 if (cursor.moveToFirst()) {
544 cursor.getLong(0)
545 } else {
546 0L
547 }
548 }
549 }
550
551 /**
552 * Returns the number of unread messages across all threads.
553 * Threads that are forced-unread count as 1.
554 */
555 fun getUnreadMessageCount(): Long {
556 val allCount: Long = readableDatabase
557 .select("SUM($UNREAD_COUNT)")
558 .from(TABLE_NAME)
559 .where("$ARCHIVED = ?", 0)
560 .run()
561 .use { cursor ->
562 if (cursor.moveToFirst()) {
563 cursor.getLong(0)
564 } else {
565 0
566 }
567 }
568
569 val forcedUnreadCount: Long = readableDatabase
570 .select("COUNT(*)")
571 .from(TABLE_NAME)
572 .where("$READ = ? AND $ARCHIVED = ?", ReadStatus.FORCED_UNREAD.serialize(), 0)
573 .run()
574 .use { cursor ->
575 if (cursor.moveToFirst()) {
576 cursor.getLong(0)
577 } else {
578 0
579 }
580 }
581
582 return allCount + forcedUnreadCount
583 }
584
585 /**
586 * Returns the number of unread messages in a given thread.
587 */
588 fun getUnreadMessageCount(threadId: Long): Long {
589 return readableDatabase
590 .select(UNREAD_COUNT)
591 .from(TABLE_NAME)
592 .where("$ID = ?", threadId)
593 .run()
594 .use { cursor ->
595 if (cursor.moveToFirst()) {
596 CursorUtil.requireLong(cursor, UNREAD_COUNT)
597 } else {
598 0L
599 }
600 }
601 }
602
603 fun getUnreadThreadIdList(): String? {
604 return getUnreadThreadIdAggregate(arrayOf("GROUP_CONCAT($ID)")) { cursor ->
605 if (cursor.moveToFirst()) {
606 cursor.getString(0)
607 } else {
608 null
609 }
610 }
611 }
612
613 private fun <T> getUnreadThreadIdAggregate(aggregator: Array<String>, mapCursorToType: (Cursor) -> T): T {
614 return readableDatabase
615 .select(*aggregator)
616 .from(TABLE_NAME)
617 .where("$READ != ${ReadStatus.READ.serialize()} AND $ARCHIVED = 0 AND $MEANINGFUL_MESSAGES != 0")
618 .run()
619 .use(mapCursorToType)
620 }
621
622 fun incrementUnread(threadId: Long, unreadAmount: Int, unreadSelfMentionAmount: Int) {
623 writableDatabase.execSQL(
624 """
625 UPDATE $TABLE_NAME
626 SET $READ = ${ReadStatus.UNREAD.serialize()},
627 $UNREAD_COUNT = $UNREAD_COUNT + ?,
628 $UNREAD_SELF_MENTION_COUNT = $UNREAD_SELF_MENTION_COUNT + ?,
629 $LAST_SCROLLED = ?
630 WHERE $ID = ?
631 """,
632 SqlUtil.buildArgs(unreadAmount, unreadSelfMentionAmount, 0, threadId)
633 )
634 }
635
636 fun setDistributionType(threadId: Long, distributionType: Int) {
637 writableDatabase
638 .update(TABLE_NAME)
639 .values(TYPE to distributionType)
640 .where("$ID = ?", threadId)
641 .run()
642
643 notifyConversationListListeners()
644 }
645
646 fun getDistributionType(threadId: Long): Int {
647 return readableDatabase
648 .select(TYPE)
649 .from(TABLE_NAME)
650 .where("$ID = ?", threadId)
651 .run()
652 .use { cursor ->
653 if (cursor.moveToFirst()) {
654 cursor.requireInt(TYPE)
655 } else {
656 DistributionTypes.DEFAULT
657 }
658 }
659 }
660
661 fun containsId(threadId: Long): Boolean {
662 return readableDatabase
663 .exists(TABLE_NAME)
664 .where("$ID = ?", threadId)
665 .run()
666 }
667
668 fun getFilteredConversationList(filter: List<RecipientId>, unreadOnly: Boolean): Cursor? {
669 if (filter.isEmpty()) {
670 return null
671 }
672
673 val db = databaseHelper.signalReadableDatabase
674 val splitRecipientIds: List<List<RecipientId>> = filter.chunked(900)
675 val cursors: MutableList<Cursor> = LinkedList()
676
677 for (recipientIds in splitRecipientIds) {
678 var selection = "($TABLE_NAME.$RECIPIENT_ID = ?"
679 val selectionArgs = arrayOfNulls<String>(recipientIds.size)
680
681 for (i in 0 until recipientIds.size - 1) {
682 selection += " OR $TABLE_NAME.$RECIPIENT_ID = ?"
683 }
684
685 var i = 0
686 for (recipientId in recipientIds) {
687 selectionArgs[i] = recipientId.serialize()
688 i++
689 }
690
691 selection += if (unreadOnly) {
692 ") AND $TABLE_NAME.$READ != ${ReadStatus.READ.serialize()}"
693 } else {
694 ")"
695 }
696
697 val query = createQuery(selection, "$DATE DESC", 0, 0)
698 cursors.add(db.rawQuery(query, selectionArgs))
699 }
700
701 return if (cursors.size > 1) {
702 MergeCursor(cursors.toTypedArray())
703 } else {
704 cursors[0]
705 }
706 }
707
708 fun getRecentConversationList(limit: Int, includeInactiveGroups: Boolean, hideV1Groups: Boolean): Cursor {
709 return getRecentConversationList(
710 limit = limit,
711 includeInactiveGroups = includeInactiveGroups,
712 individualsOnly = false,
713 groupsOnly = false,
714 hideV1Groups = hideV1Groups,
715 hideSms = false,
716 hideSelf = false
717 )
718 }
719
720 fun getRecentConversationList(limit: Int, includeInactiveGroups: Boolean, individualsOnly: Boolean, groupsOnly: Boolean, hideV1Groups: Boolean, hideSms: Boolean, hideSelf: Boolean): Cursor {
721 var where = ""
722
723 if (!includeInactiveGroups) {
724 where += "$MEANINGFUL_MESSAGES != 0 AND (${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} IS NULL OR ${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} = 1)"
725 } else {
726 where += "$MEANINGFUL_MESSAGES != 0"
727 }
728
729 if (groupsOnly) {
730 where += " AND ${RecipientTable.TABLE_NAME}.${RecipientTable.GROUP_ID} NOT NULL"
731 }
732
733 if (individualsOnly) {
734 where += " AND ${RecipientTable.TABLE_NAME}.${RecipientTable.GROUP_ID} IS NULL"
735 }
736
737 if (hideV1Groups) {
738 where += " AND ${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} != ${RecipientTable.RecipientType.GV1.id}"
739 }
740
741 if (hideSms) {
742 where += """ AND (
743 ${RecipientTable.TABLE_NAME}.${RecipientTable.REGISTERED} = ${RecipientTable.RegisteredState.REGISTERED.id}
744 OR
745 (
746 ${RecipientTable.TABLE_NAME}.${RecipientTable.GROUP_ID} NOT NULL
747 AND ${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} != ${RecipientTable.RecipientType.MMS.id}
748 )
749 )"""
750 }
751
752 if (hideSelf) {
753 where += " AND $TABLE_NAME.$RECIPIENT_ID != ${Recipient.self().id.toLong()}"
754 }
755
756 where += " AND $ARCHIVED = 0"
757 where += " AND ${RecipientTable.TABLE_NAME}.${RecipientTable.BLOCKED} = 0"
758
759 if (SignalStore.releaseChannelValues().releaseChannelRecipientId != null) {
760 where += " AND $TABLE_NAME.$RECIPIENT_ID != ${SignalStore.releaseChannelValues().releaseChannelRecipientId!!.toLong()}"
761 }
762
763 val query = createQuery(
764 where = where,
765 offset = 0,
766 limit = limit.toLong(),
767 preferPinned = true
768 )
769
770 return readableDatabase.rawQuery(query, null)
771 }
772
773 fun getRecentPushConversationList(limit: Int, includeInactiveGroups: Boolean): Cursor {
774 val activeGroupQuery = if (!includeInactiveGroups) " AND " + GroupTable.TABLE_NAME + "." + GroupTable.ACTIVE + " = 1" else ""
775 val where = """
776 $MEANINGFUL_MESSAGES != 0
777 AND (
778 ${RecipientTable.REGISTERED} = ${RecipientTable.RegisteredState.REGISTERED.id}
779 OR (
780 ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID} NOT NULL
781 AND ${GroupTable.TABLE_NAME}.${GroupTable.MMS} = 0
782 $activeGroupQuery
783 )
784 )
785 """
786
787 val query = createQuery(
788 where = where,
789 offset = 0,
790 limit = limit.toLong(),
791 preferPinned = true
792 )
793
794 return readableDatabase.rawQuery(query, null)
795 }
796
797 fun isArchived(recipientId: RecipientId): Boolean {
798 return readableDatabase
799 .select(ARCHIVED)
800 .from(TABLE_NAME)
801 .where("$RECIPIENT_ID = ?", recipientId)
802 .run()
803 .use { cursor ->
804 if (cursor.moveToFirst()) {
805 cursor.requireBoolean(ARCHIVED)
806 } else {
807 false
808 }
809 }
810 }
811
812 fun setArchived(threadIds: Set<Long>, archive: Boolean) {
813 var recipientIds: List<RecipientId> = emptyList()
814
815 writableDatabase.withinTransaction { db ->
816 for (threadId in threadIds) {
817 val values = ContentValues().apply {
818 if (archive) {
819 put(PINNED, "0")
820 put(ARCHIVED, "1")
821 } else {
822 put(ARCHIVED, "0")
823 }
824 }
825
826 db.update(TABLE_NAME)
827 .values(values)
828 .where("$ID = ?", threadId)
829 .run()
830 }
831
832 recipientIds = getRecipientIdsForThreadIds(threadIds)
833 recipients.markNeedsSyncWithoutRefresh(recipientIds)
834 }
835
836 for (id in recipientIds) {
837 Recipient.live(id).refresh()
838 }
839 notifyConversationListListeners()
840 StorageSyncHelper.scheduleSyncForDataChange()
841 }
842
843 fun getArchivedRecipients(): Set<RecipientId> {
844 return getArchivedConversationList(ConversationFilter.OFF).readToList { cursor ->
845 RecipientId.from(cursor.requireLong(RECIPIENT_ID))
846 }.toSet()
847 }
848
849 fun getInboxPositions(): Map<RecipientId, Int> {
850 val query = createQuery("$MEANINGFUL_MESSAGES != ?", 0)
851 val positions: MutableMap<RecipientId, Int> = mutableMapOf()
852
853 readableDatabase.rawQuery(query, arrayOf("0")).use { cursor ->
854 var i = 0
855 while (cursor != null && cursor.moveToNext()) {
856 val recipientId = RecipientId.from(cursor.requireLong(RECIPIENT_ID))
857 positions[recipientId] = i
858 i++
859 }
860 }
861
862 return positions
863 }
864
865 fun getArchivedConversationList(conversationFilter: ConversationFilter, offset: Long = 0, limit: Long = 0): Cursor {
866 val filterQuery = conversationFilter.toQuery()
867 val query = createQuery("$ARCHIVED = ? AND $MEANINGFUL_MESSAGES != 0 $filterQuery", offset, limit, preferPinned = false)
868 return readableDatabase.rawQuery(query, arrayOf("1"))
869 }
870
871 fun getUnarchivedConversationList(conversationFilter: ConversationFilter, pinned: Boolean, offset: Long, limit: Long): Cursor {
872 val filterQuery = conversationFilter.toQuery()
873 val where = if (pinned) {
874 "$ARCHIVED = 0 AND $PINNED != 0 $filterQuery"
875 } else {
876 "$ARCHIVED = 0 AND $PINNED = 0 AND $MEANINGFUL_MESSAGES != 0 $filterQuery"
877 }
878
879 val query = if (pinned) {
880 createQuery(where, PINNED + " ASC", offset, limit)
881 } else {
882 createQuery(where, offset, limit, preferPinned = false)
883 }
884
885 return readableDatabase.rawQuery(query, null)
886 }
887
888 fun getArchivedConversationListCount(conversationFilter: ConversationFilter): Int {
889 val filterQuery = conversationFilter.toQuery()
890 return readableDatabase
891 .select("COUNT(*)")
892 .from(TABLE_NAME)
893 .where("$ACTIVE = 1 AND $ARCHIVED = 1 AND $MEANINGFUL_MESSAGES != 0 $filterQuery")
894 .run()
895 .use { cursor ->
896 if (cursor.moveToFirst()) {
897 cursor.getInt(0)
898 } else {
899 0
900 }
901 }
902 }
903
904 fun getPinnedConversationListCount(conversationFilter: ConversationFilter): Int {
905 val filterQuery = conversationFilter.toQuery()
906 return readableDatabase
907 .select("COUNT(*)")
908 .from(TABLE_NAME)
909 .where("$ACTIVE = 1 AND $ARCHIVED = 0 AND $PINNED != 0 $filterQuery")
910 .run()
911 .use { cursor ->
912 if (cursor.moveToFirst()) {
913 cursor.getInt(0)
914 } else {
915 0
916 }
917 }
918 }
919
920 fun getUnarchivedConversationListCount(conversationFilter: ConversationFilter): Int {
921 val filterQuery = conversationFilter.toQuery()
922 return readableDatabase
923 .select("COUNT(*)")
924 .from(TABLE_NAME)
925 .where("$ACTIVE = 1 AND $ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0) $filterQuery")
926 .run()
927 .use { cursor ->
928 if (cursor.moveToFirst()) {
929 cursor.getInt(0)
930 } else {
931 0
932 }
933 }
934 }
935
936 /**
937 * @return Pinned recipients, in order from top to bottom.
938 */
939 fun getPinnedRecipientIds(): List<RecipientId> {
940 return readableDatabase
941 .select(ID, RECIPIENT_ID)
942 .from(TABLE_NAME)
943 .where("$PINNED > 0")
944 .run()
945 .readToList { cursor ->
946 RecipientId.from(cursor.requireLong(RECIPIENT_ID))
947 }
948 }
949
950 /**
951 * @return Pinned thread ids, in order from top to bottom.
952 */
953 fun getPinnedThreadIds(): List<Long> {
954 return readableDatabase
955 .select(ID)
956 .from(TABLE_NAME)
957 .where("$PINNED > 0")
958 .run()
959 .readToList { cursor ->
960 cursor.requireLong(ID)
961 }
962 }
963
964 fun restorePins(threadIds: Collection<Long>) {
965 Log.d(TAG, "Restoring pinned threads " + StringUtil.join(threadIds, ","))
966 pinConversations(threadIds, true)
967 }
968
969 fun pinConversations(threadIds: Collection<Long>) {
970 Log.d(TAG, "Pinning threads " + StringUtil.join(threadIds, ","))
971 pinConversations(threadIds, false)
972 }
973
974 private fun pinConversations(threadIds: Collection<Long>, clearFirst: Boolean) {
975 writableDatabase.withinTransaction { db ->
976 if (clearFirst) {
977 db.update(TABLE_NAME)
978 .values(PINNED to 0)
979 .where("$PINNED > 0")
980 .run()
981 }
982
983 var pinnedCount = getPinnedConversationListCount(ConversationFilter.OFF)
984
985 for (threadId in threadIds) {
986 pinnedCount++
987 db.update(TABLE_NAME)
988 .values(PINNED to pinnedCount, ACTIVE to 1)
989 .where("$ID = ?", threadId)
990 .run()
991 }
992 }
993
994 notifyConversationListListeners()
995 recipients.markNeedsSync(Recipient.self().id)
996 StorageSyncHelper.scheduleSyncForDataChange()
997 }
998
999 fun unpinConversations(threadIds: Collection<Long>) {
1000 writableDatabase.withinTransaction { db ->
1001 val query: SqlUtil.Query = SqlUtil.buildSingleCollectionQuery(ID, threadIds)
1002 db.update(TABLE_NAME)
1003 .values(PINNED to 0)
1004 .where(query.where, *query.whereArgs)
1005 .run()
1006
1007 getPinnedThreadIds().forEachIndexed { index: Int, threadId: Long ->
1008 db.update(TABLE_NAME)
1009 .values(PINNED to index + 1)
1010 .where("$ID = ?", threadId)
1011 .run()
1012 }
1013 }
1014
1015 notifyConversationListListeners()
1016 recipients.markNeedsSync(Recipient.self().id)
1017 StorageSyncHelper.scheduleSyncForDataChange()
1018 }
1019
1020 fun archiveConversation(threadId: Long) {
1021 setArchived(setOf(threadId), archive = true)
1022 }
1023
1024 fun unarchiveConversation(threadId: Long) {
1025 setArchived(setOf(threadId), archive = false)
1026 }
1027
1028 fun setLastSeen(threadId: Long) {
1029 writableDatabase
1030 .update(TABLE_NAME)
1031 .values(LAST_SEEN to System.currentTimeMillis())
1032 .where("$ID = ?", threadId)
1033 .run()
1034
1035 notifyConversationListListeners()
1036 }
1037
1038 fun setLastScrolled(threadId: Long, lastScrolledTimestamp: Long) {
1039 writableDatabase
1040 .update(TABLE_NAME)
1041 .values(LAST_SCROLLED to lastScrolledTimestamp)
1042 .where("$ID = ?", threadId)
1043 .run()
1044 }
1045
1046 fun getConversationMetadata(threadId: Long): ConversationMetadata {
1047 return readableDatabase
1048 .select(UNREAD_COUNT, LAST_SEEN, HAS_SENT, LAST_SCROLLED)
1049 .from(TABLE_NAME)
1050 .where("$ID = ?", threadId)
1051 .run()
1052 .use { cursor ->
1053 if (cursor.moveToFirst()) {
1054 ConversationMetadata(
1055 lastSeen = cursor.requireLong(LAST_SEEN),
1056 hasSent = cursor.requireBoolean(HAS_SENT),
1057 lastScrolled = cursor.requireLong(LAST_SCROLLED),
1058 unreadCount = cursor.requireInt(UNREAD_COUNT)
1059 )
1060 } else {
1061 ConversationMetadata(
1062 lastSeen = -1L,
1063 hasSent = false,
1064 lastScrolled = -1,
1065 unreadCount = 0
1066 )
1067 }
1068 }
1069 }
1070
1071 fun deleteConversation(threadId: Long) {
1072 val recipientIdForThreadId = getRecipientIdForThreadId(threadId)
1073
1074 writableDatabase.withinTransaction { db ->
1075 messages.deleteThread(threadId)
1076 drafts.clearDrafts(threadId)
1077 db.deactivateThread(threadId)
1078 synchronized(threadIdCache) {
1079 threadIdCache.remove(recipientIdForThreadId)
1080 }
1081 }
1082
1083 notifyConversationListListeners()
1084 notifyConversationListeners(threadId)
1085 ApplicationDependencies.getDatabaseObserver().notifyConversationDeleteListeners(threadId)
1086 ConversationUtil.clearShortcuts(context, setOf(recipientIdForThreadId))
1087 }
1088
1089 fun deleteConversations(selectedConversations: Set<Long>) {
1090 val recipientIds = getRecipientIdsForThreadIds(selectedConversations)
1091
1092 val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(ID, selectedConversations)
1093 writableDatabase.withinTransaction { db ->
1094 for (query in queries) {
1095 db.deactivateThread(query)
1096 }
1097
1098 messages.deleteAbandonedMessages()
1099 attachments.trimAllAbandonedAttachments()
1100 groupReceipts.deleteAbandonedRows()
1101 mentions.deleteAbandonedMentions()
1102 drafts.clearDrafts(selectedConversations)
1103 attachments.deleteAbandonedAttachmentFiles()
1104 synchronized(threadIdCache) {
1105 for (recipientId in recipientIds) {
1106 threadIdCache.remove(recipientId)
1107 }
1108 }
1109 }
1110
1111 notifyConversationListListeners()
1112 notifyConversationListeners(selectedConversations)
1113 ApplicationDependencies.getDatabaseObserver().notifyConversationDeleteListeners(selectedConversations)
1114 ConversationUtil.clearShortcuts(context, recipientIds)
1115 }
1116
1117 @SuppressLint("DiscouragedApi")
1118 fun deleteAllConversations() {
1119 writableDatabase.withinTransaction { db ->
1120 messageLog.deleteAll()
1121 messages.deleteAllThreads()
1122 drafts.clearAllDrafts()
1123 db.deactivateThreads()
1124 calls.deleteAllCalls()
1125 synchronized(threadIdCache) {
1126 threadIdCache.clear()
1127 }
1128 }
1129
1130 notifyConversationListListeners()
1131 ConversationUtil.clearAllShortcuts(context)
1132 }
1133
1134 fun getThreadIdIfExistsFor(recipientId: RecipientId): Long {
1135 return getThreadIdFor(recipientId) ?: -1
1136 }
1137
1138 fun getOrCreateValidThreadId(recipient: Recipient, candidateId: Long): Long {
1139 return getOrCreateValidThreadId(recipient, candidateId, DistributionTypes.DEFAULT)
1140 }
1141
1142 fun getOrCreateValidThreadId(recipient: Recipient, candidateId: Long, distributionType: Int): Long {
1143 return if (candidateId != -1L) {
1144 val remapped = RemappedRecords.getInstance().getThread(candidateId)
1145 if (remapped.isPresent) {
1146 Log.i(TAG, "Using remapped threadId: " + candidateId + " -> " + remapped.get())
1147 remapped.get()
1148 } else {
1149 if (areThreadIdAndRecipientAssociated(candidateId, recipient)) {
1150 candidateId
1151 } else {
1152 throw IllegalArgumentException()
1153 }
1154 }
1155 } else {
1156 getOrCreateThreadIdFor(recipient, distributionType)
1157 }
1158 }
1159
1160 fun getOrCreateThreadIdFor(recipient: Recipient): Long {
1161 return getOrCreateThreadIdFor(recipient, DistributionTypes.DEFAULT)
1162 }
1163
1164 fun getOrCreateThreadIdFor(recipient: Recipient, distributionType: Int): Long {
1165 return getOrCreateThreadIdFor(recipient.id, recipient.isGroup, distributionType)
1166 }
1167
1168 fun getOrCreateThreadIdFor(recipientId: RecipientId, isGroup: Boolean, distributionType: Int = DistributionTypes.DEFAULT): Long {
1169 return getOrCreateThreadIdResultFor(recipientId, isGroup, distributionType).threadId
1170 }
1171
1172 fun getOrCreateThreadIdResultFor(recipientId: RecipientId, isGroup: Boolean, distributionType: Int = DistributionTypes.DEFAULT): ThreadIdResult {
1173 return writableDatabase.withinTransaction {
1174 val threadId = getThreadIdFor(recipientId)
1175 if (threadId != null) {
1176 ThreadIdResult(
1177 threadId = threadId,
1178 newlyCreated = false
1179 )
1180 } else {
1181 ThreadIdResult(
1182 threadId = createThreadForRecipient(recipientId, isGroup, distributionType),
1183 newlyCreated = true
1184 )
1185 }
1186 }
1187 }
1188
1189 fun areThreadIdAndRecipientAssociated(threadId: Long, recipient: Recipient): Boolean {
1190 return readableDatabase
1191 .exists(TABLE_NAME)
1192 .where("$ID = ? AND $RECIPIENT_ID = ?", threadId, recipient.id)
1193 .run()
1194 }
1195
1196 fun getThreadIdFor(recipientId: RecipientId): Long? {
1197 var threadId: Long? = synchronized(threadIdCache) {
1198 threadIdCache[recipientId]
1199 }
1200 if (threadId == null) {
1201 threadId = readableDatabase
1202 .select(ID)
1203 .from(TABLE_NAME)
1204 .where("$RECIPIENT_ID = ?", recipientId)
1205 .run()
1206 .use { cursor ->
1207 if (cursor.moveToFirst()) {
1208 cursor.requireLong(ID)
1209 } else {
1210 null
1211 }
1212 }
1213 if (threadId != null) {
1214 synchronized(threadIdCache) {
1215 threadIdCache[recipientId] = threadId
1216 }
1217 }
1218 }
1219 return threadId
1220 }
1221
1222 fun getRecipientIdForThreadId(threadId: Long): RecipientId? {
1223 return readableDatabase
1224 .select(RECIPIENT_ID)
1225 .from(TABLE_NAME)
1226 .where("$ID = ?", threadId)
1227 .run()
1228 .use { cursor ->
1229 if (cursor.moveToFirst()) {
1230 return RecipientId.from(cursor.requireLong(RECIPIENT_ID))
1231 } else {
1232 null
1233 }
1234 }
1235 }
1236
1237 fun getRecipientForThreadId(threadId: Long): Recipient? {
1238 val id: RecipientId = getRecipientIdForThreadId(threadId) ?: return null
1239 return Recipient.resolved(id)
1240 }
1241
1242 fun getRecipientIdsForThreadIds(threadIds: Collection<Long>): List<RecipientId> {
1243 val query = SqlUtil.buildSingleCollectionQuery(ID, threadIds)
1244
1245 return readableDatabase
1246 .select(RECIPIENT_ID)
1247 .from(TABLE_NAME)
1248 .where(query.where, *query.whereArgs)
1249 .run()
1250 .readToList { cursor ->
1251 RecipientId.from(cursor.requireLong(RECIPIENT_ID))
1252 }
1253 }
1254
1255 fun hasThread(recipientId: RecipientId): Boolean {
1256 return getThreadIdIfExistsFor(recipientId) > -1
1257 }
1258
1259 fun hasActiveThread(recipientId: RecipientId): Boolean {
1260 return readableDatabase
1261 .exists(TABLE_NAME)
1262 .where("$RECIPIENT_ID = ? AND $ACTIVE = 1", recipientId)
1263 .run()
1264 }
1265
1266 fun updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId: Long) {
1267 writableDatabase
1268 .update(TABLE_NAME)
1269 .values(
1270 LAST_SEEN to System.currentTimeMillis(),
1271 HAS_SENT to 1,
1272 LAST_SCROLLED to 0
1273 )
1274 .where("$ID = ?", threadId)
1275 .run()
1276 }
1277
1278 fun updateReadState(threadId: Long) {
1279 val previous = getThreadRecord(threadId)
1280 val unreadCount = messages.getUnreadCount(threadId)
1281 val unreadMentionsCount = messages.getUnreadMentionCount(threadId)
1282
1283 writableDatabase
1284 .update(TABLE_NAME)
1285 .values(
1286 READ to if (unreadCount == 0) ReadStatus.READ.serialize() else ReadStatus.UNREAD.serialize(),
1287 UNREAD_COUNT to unreadCount,
1288 UNREAD_SELF_MENTION_COUNT to unreadMentionsCount
1289 )
1290 .where("$ID = ?", threadId)
1291 .run()
1292
1293 notifyConversationListListeners()
1294
1295 if (previous != null && previous.isForcedUnread) {
1296 recipients.markNeedsSync(previous.recipient.id)
1297 StorageSyncHelper.scheduleSyncForDataChange()
1298 }
1299 }
1300
1301 fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalContactRecord) {
1302 applyStorageSyncUpdate(recipientId, record.isArchived, record.isForcedUnread)
1303 }
1304
1305 fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalGroupV1Record) {
1306 applyStorageSyncUpdate(recipientId, record.isArchived, record.isForcedUnread)
1307 }
1308
1309 fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalGroupV2Record) {
1310 applyStorageSyncUpdate(recipientId, record.isArchived, record.isForcedUnread)
1311 }
1312
1313 fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalAccountRecord) {
1314 writableDatabase.withinTransaction { db ->
1315 applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived, record.isNoteToSelfForcedUnread)
1316
1317 db.updateAll(TABLE_NAME)
1318 .values(PINNED to 0)
1319 .run()
1320
1321 var pinnedPosition = 1
1322
1323 for (pinned: PinnedConversation in record.pinnedConversations) {
1324 val pinnedRecipient: Recipient? = if (pinned.contact.isPresent) {
1325 Recipient.externalPush(pinned.contact.get())
1326 } else if (pinned.groupV1Id.isPresent) {
1327 try {
1328 Recipient.externalGroupExact(GroupId.v1(pinned.groupV1Id.get()))
1329 } catch (e: BadGroupIdException) {
1330 Log.w(TAG, "Failed to parse pinned groupV1 ID!", e)
1331 null
1332 }
1333 } else if (pinned.groupV2MasterKey.isPresent) {
1334 try {
1335 Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(pinned.groupV2MasterKey.get())))
1336 } catch (e: InvalidInputException) {
1337 Log.w(TAG, "Failed to parse pinned groupV2 master key!", e)
1338 null
1339 }
1340 } else {
1341 Log.w(TAG, "Empty pinned conversation on the AccountRecord?")
1342 null
1343 }
1344
1345 if (pinnedRecipient != null) {
1346 db.update(TABLE_NAME)
1347 .values(PINNED to pinnedPosition, ACTIVE to 1)
1348 .where("$RECIPIENT_ID = ?", pinnedRecipient.id)
1349 .run()
1350 }
1351
1352 pinnedPosition++
1353 }
1354 }
1355
1356 notifyConversationListListeners()
1357 }
1358
1359 private fun applyStorageSyncUpdate(recipientId: RecipientId, archived: Boolean, forcedUnread: Boolean) {
1360 val values = ContentValues()
1361 values.put(ARCHIVED, if (archived) 1 else 0)
1362
1363 val threadId: Long? = getThreadIdFor(recipientId)
1364
1365 if (forcedUnread) {
1366 values.put(READ, ReadStatus.FORCED_UNREAD.serialize())
1367 } else if (threadId != null) {
1368 val unreadCount = messages.getUnreadCount(threadId)
1369 val unreadMentionsCount = messages.getUnreadMentionCount(threadId)
1370
1371 values.put(READ, if (unreadCount == 0) ReadStatus.READ.serialize() else ReadStatus.UNREAD.serialize())
1372 values.put(UNREAD_COUNT, unreadCount)
1373 values.put(UNREAD_SELF_MENTION_COUNT, unreadMentionsCount)
1374 }
1375
1376 writableDatabase
1377 .update(TABLE_NAME)
1378 .values(values)
1379 .where("$RECIPIENT_ID = ?", recipientId)
1380 .run()
1381
1382 if (threadId != null) {
1383 notifyConversationListeners(threadId)
1384 }
1385 }
1386
1387 /**
1388 * Set a thread as active prior to an [update] call. Useful when a thread is for sure active but
1389 * hasn't had the update call yet. e.g., inserting a message in a new thread.
1390 */
1391 fun markAsActiveEarly(threadId: Long) {
1392 writableDatabase
1393 .update(TABLE_NAME)
1394 .values(ACTIVE to 1)
1395 .where("$ID = ?", threadId)
1396 .run()
1397 }
1398
1399 fun update(threadId: Long, unarchive: Boolean): Boolean {
1400 return update(
1401 threadId = threadId,
1402 unarchive = unarchive,
1403 allowDeletion = true,
1404 notifyListeners = true
1405 )
1406 }
1407
1408 fun updateSilently(threadId: Long, unarchive: Boolean): Boolean {
1409 return update(
1410 threadId = threadId,
1411 unarchive = unarchive,
1412 allowDeletion = true,
1413 notifyListeners = false
1414 )
1415 }
1416
1417 fun update(threadId: Long, unarchive: Boolean, allowDeletion: Boolean): Boolean {
1418 return update(
1419 threadId = threadId,
1420 unarchive = unarchive,
1421 allowDeletion = allowDeletion,
1422 notifyListeners = true
1423 )
1424 }
1425
1426 /**
1427 * Updates the thread with the receipt status of the message provided, but only if that message is the most recent meaningful message.
1428 * The idea here is that if it _is_ the most meaningful message, we can set the new status. If it's not, there's no need to update
1429 * the thread at all.
1430 */
1431 fun updateReceiptStatus(messageId: Long, threadId: Long, stopwatch: Stopwatch? = null) {
1432 val status = messages.getReceiptStatusIfItsTheMostRecentMeaningfulMessage(messageId, threadId)
1433 stopwatch?.split("thread-query")
1434
1435 if (status != null) {
1436 Log.d(TAG, "Updating receipt status for thread $threadId")
1437 writableDatabase
1438 .update(TABLE_NAME)
1439 .values(
1440 HAS_DELIVERY_RECEIPT to status.hasDeliveryReceipt.toInt(),
1441 HAS_READ_RECEIPT to status.hasReadReceipt.toInt(),
1442 STATUS to when {
1443 MessageTypes.isFailedMessageType(status.type) -> MessageTable.Status.STATUS_FAILED
1444 MessageTypes.isSentType(status.type) -> MessageTable.Status.STATUS_COMPLETE
1445 MessageTypes.isPendingMessageType(status.type) -> MessageTable.Status.STATUS_PENDING
1446 else -> MessageTable.Status.STATUS_NONE
1447 }
1448 )
1449 .where("$ID = ?", threadId)
1450 .run()
1451 } else {
1452 Log.d(TAG, "Receipt was for an old message, not updating thread.")
1453 }
1454 stopwatch?.split("thread-update")
1455 }
1456
1457 private fun update(threadId: Long, unarchive: Boolean, allowDeletion: Boolean, notifyListeners: Boolean): Boolean {
1458 if (threadId == -1L) {
1459 Log.d(TAG, "Skipping update for threadId -1")
1460 return false
1461 }
1462
1463 return writableDatabase.withinTransaction {
1464 val meaningfulMessages = messages.hasMeaningfulMessage(threadId)
1465
1466 val isPinned by lazy { getPinnedThreadIds().contains(threadId) }
1467 val shouldDelete by lazy { allowDeletion && !isPinned && !messages.containsStories(threadId) }
1468
1469 if (!meaningfulMessages) {
1470 if (shouldDelete) {
1471 Log.d(TAG, "Deleting thread $threadId because it has no meaningful messages.")
1472 deleteConversation(threadId)
1473 return@withinTransaction true
1474 } else if (!isPinned) {
1475 return@withinTransaction false
1476 }
1477 }
1478
1479 val record: MessageRecord? = try {
1480 messages.getConversationSnippet(threadId)
1481 } catch (e: NoSuchMessageException) {
1482 val scheduledMessage: MessageRecord? = messages.getScheduledMessagesInThread(threadId).lastOrNull()
1483
1484 if (scheduledMessage != null) {
1485 Log.i(TAG, "Using scheduled message for conversation snippet")
1486 }
1487 scheduledMessage
1488 }
1489
1490 if (record == null) {
1491 Log.w(TAG, "Failed to get a conversation snippet for thread $threadId")
1492 if (shouldDelete) {
1493 deleteConversation(threadId)
1494 } else if (isPinned) {
1495 updateThread(
1496 threadId = threadId,
1497 meaningfulMessages = meaningfulMessages,
1498 body = null,
1499 attachment = null,
1500 contentType = null,
1501 extra = null,
1502 date = 0,
1503 status = 0,
1504 deliveryReceiptCount = 0,
1505 type = 0,
1506 unarchive = unarchive,
1507 expiresIn = 0,
1508 readReceiptCount = 0,
1509 unreadCount = 0,
1510 unreadMentionCount = 0,
1511 messageExtras = null
1512 )
1513 }
1514 return@withinTransaction true
1515 }
1516
1517 if (hasMoreRecentDraft(threadId, record.timestamp)) {
1518 return@withinTransaction false
1519 }
1520
1521 val threadBody: ThreadBody = ThreadBodyUtil.getFormattedBodyFor(context, record)
1522 val unreadCount: Int = messages.getUnreadCount(threadId)
1523 val unreadMentionCount: Int = messages.getUnreadMentionCount(threadId)
1524
1525 updateThread(
1526 threadId = threadId,
1527 meaningfulMessages = meaningfulMessages,
1528 body = threadBody.body.toString(),
1529 attachment = getAttachmentUriFor(record),
1530 contentType = getContentTypeFor(record),
1531 extra = getExtrasFor(record, threadBody),
1532 date = record.timestamp,
1533 status = record.deliveryStatus,
1534 deliveryReceiptCount = record.hasDeliveryReceipt().toInt(),
1535 type = record.type,
1536 unarchive = unarchive,
1537 expiresIn = record.expiresIn,
1538 readReceiptCount = record.hasReadReceipt().toInt(),
1539 unreadCount = unreadCount,
1540 unreadMentionCount = unreadMentionCount,
1541 messageExtras = record.messageExtras
1542 )
1543
1544 if (notifyListeners) {
1545 notifyConversationListListeners()
1546 }
1547 return@withinTransaction false
1548 }
1549 }
1550
1551 private fun hasMoreRecentDraft(threadId: Long, timestamp: Long): Boolean {
1552 val drafts: DraftTable.Drafts = SignalDatabase.drafts.getDrafts(threadId)
1553 if (drafts.isNotEmpty()) {
1554 val threadRecord: ThreadRecord? = getThreadRecord(threadId)
1555 if (threadRecord != null &&
1556 threadRecord.type == MessageTypes.BASE_DRAFT_TYPE &&
1557 threadRecord.date > timestamp
1558 ) {
1559 return true
1560 }
1561 }
1562 return false
1563 }
1564
1565 fun updateSnippetTypeSilently(threadId: Long) {
1566 if (threadId == -1L) {
1567 return
1568 }
1569
1570 val type: Long = try {
1571 messages.getConversationSnippetType(threadId)
1572 } catch (e: NoSuchMessageException) {
1573 Log.w(TAG, "Unable to find snippet message for thread $threadId")
1574 return
1575 }
1576
1577 writableDatabase
1578 .update(TABLE_NAME)
1579 .values(SNIPPET_TYPE to type)
1580 .where("$ID = ?", threadId)
1581 .run()
1582 }
1583
1584 fun getThreadRecordFor(recipient: Recipient): ThreadRecord {
1585 return getThreadRecord(getOrCreateThreadIdFor(recipient))!!
1586 }
1587
1588 fun getAllThreadRecipients(): Set<RecipientId> {
1589 return readableDatabase
1590 .select(RECIPIENT_ID)
1591 .from(TABLE_NAME)
1592 .run()
1593 .readToList { cursor ->
1594 RecipientId.from(cursor.requireLong(RECIPIENT_ID))
1595 }
1596 .toSet()
1597 }
1598
1599 fun merge(primaryRecipientId: RecipientId, secondaryRecipientId: RecipientId): MergeResult {
1600 check(databaseHelper.signalWritableDatabase.inTransaction()) { "Must be in a transaction!" }
1601 Log.w(TAG, "Merging threads. Primary: $primaryRecipientId, Secondary: $secondaryRecipientId", true)
1602
1603 val primaryThreadId: Long? = getThreadIdFor(primaryRecipientId)
1604 val secondaryThreadId: Long? = getThreadIdFor(secondaryRecipientId)
1605
1606 return if (primaryThreadId != null && secondaryThreadId == null) {
1607 Log.w(TAG, "[merge] Only had a thread for primary. Returning that.", true)
1608 MergeResult(threadId = primaryThreadId, previousThreadId = -1, neededMerge = false)
1609 } else if (primaryThreadId == null && secondaryThreadId != null) {
1610 Log.w(TAG, "[merge] Only had a thread for secondary. Updating it to have the recipientId of the primary.", true)
1611 writableDatabase
1612 .update(TABLE_NAME)
1613 .values(RECIPIENT_ID to primaryRecipientId.serialize())
1614 .where("$ID = ?", secondaryThreadId)
1615 .run()
1616 synchronized(threadIdCache) {
1617 threadIdCache.remove(secondaryRecipientId)
1618 }
1619 MergeResult(threadId = secondaryThreadId, previousThreadId = -1, neededMerge = false)
1620 } else if (primaryThreadId == null && secondaryThreadId == null) {
1621 Log.w(TAG, "[merge] No thread for either.")
1622 MergeResult(threadId = -1, previousThreadId = -1, neededMerge = false)
1623 } else {
1624 Log.w(TAG, "[merge] Had a thread for both. Deleting the secondary and merging the attributes together.", true)
1625 check(primaryThreadId != null)
1626 check(secondaryThreadId != null)
1627
1628 for (table in threadIdDatabaseTables) {
1629 table.remapThread(secondaryThreadId, primaryThreadId)
1630 }
1631
1632 writableDatabase
1633 .delete(TABLE_NAME)
1634 .where("$ID = ?", secondaryThreadId)
1635 .run()
1636
1637 synchronized(threadIdCache) {
1638 threadIdCache.remove(secondaryRecipientId)
1639 }
1640
1641 val primaryExpiresIn = getExpiresIn(primaryThreadId)
1642 val secondaryExpiresIn = getExpiresIn(secondaryThreadId)
1643
1644 val values = ContentValues()
1645 values.put(ACTIVE, true)
1646
1647 if (primaryExpiresIn != secondaryExpiresIn) {
1648 if (primaryExpiresIn == 0L) {
1649 values.put(EXPIRES_IN, secondaryExpiresIn)
1650 } else if (secondaryExpiresIn == 0L) {
1651 values.put(EXPIRES_IN, primaryExpiresIn)
1652 } else {
1653 values.put(EXPIRES_IN, min(primaryExpiresIn, secondaryExpiresIn))
1654 }
1655 }
1656
1657 writableDatabase
1658 .update(TABLE_NAME)
1659 .values(values)
1660 .where("$ID = ?", primaryThreadId)
1661 .run()
1662
1663 RemappedRecords.getInstance().addThread(secondaryThreadId, primaryThreadId)
1664
1665 MergeResult(threadId = primaryThreadId, previousThreadId = secondaryThreadId, neededMerge = true)
1666 }
1667 }
1668
1669 fun getThreadRecord(threadId: Long?): ThreadRecord? {
1670 if (threadId == null) {
1671 return null
1672 }
1673
1674 val query = createQuery("$TABLE_NAME.$ID = ?", 1)
1675
1676 return readableDatabase.rawQuery(query, SqlUtil.buildArgs(threadId)).use { cursor ->
1677 if (cursor.moveToFirst()) {
1678 readerFor(cursor).getCurrent()
1679 } else {
1680 null
1681 }
1682 }
1683 }
1684
1685 private fun getExpiresIn(threadId: Long): Long {
1686 return readableDatabase
1687 .select(EXPIRES_IN)
1688 .from(TABLE_NAME)
1689 .where("$ID = $threadId")
1690 .run()
1691 .readToSingleLong()
1692 }
1693
1694 private fun SQLiteDatabase.deactivateThreads() {
1695 deactivateThread(query = null)
1696 }
1697
1698 private fun SQLiteDatabase.deactivateThread(threadId: Long) {
1699 deactivateThread(SqlUtil.Query("$ID = ?", SqlUtil.buildArgs(threadId)))
1700 }
1701
1702 private fun SQLiteDatabase.deactivateThread(query: SqlUtil.Query?) {
1703 val contentValues = contentValuesOf(
1704 DATE to 0,
1705 MEANINGFUL_MESSAGES to 0,
1706 READ to ReadStatus.READ.serialize(),
1707 TYPE to 0,
1708 ERROR to 0,
1709 SNIPPET to null,
1710 SNIPPET_TYPE to 0,
1711 SNIPPET_URI to null,
1712 SNIPPET_CONTENT_TYPE to null,
1713 SNIPPET_EXTRAS to null,
1714 SNIPPET_MESSAGE_EXTRAS to null,
1715 UNREAD_COUNT to 0,
1716 ARCHIVED to 0,
1717 STATUS to 0,
1718 HAS_DELIVERY_RECEIPT to 0,
1719 HAS_READ_RECEIPT to 0,
1720 EXPIRES_IN to 0,
1721 LAST_SEEN to 0,
1722 HAS_SENT to 0,
1723 LAST_SCROLLED to 0,
1724 PINNED to 0,
1725 UNREAD_SELF_MENTION_COUNT to 0,
1726 ACTIVE to 0
1727 )
1728
1729 if (query != null) {
1730 writableDatabase
1731 .update(TABLE_NAME)
1732 .values(contentValues)
1733 .where(query.where, query.whereArgs)
1734 .run()
1735 } else {
1736 writableDatabase
1737 .updateAll(TABLE_NAME)
1738 .values(contentValues)
1739 .run()
1740 }
1741 }
1742
1743 private fun getAttachmentUriFor(record: MessageRecord): Uri? {
1744 if (!record.isMms || record.isMmsNotification || record.isGroupAction) {
1745 return null
1746 }
1747
1748 val slideDeck: SlideDeck = (record as MmsMessageRecord).slideDeck
1749 val thumbnail = Optional.ofNullable(slideDeck.thumbnailSlide)
1750 .or(Optional.ofNullable(slideDeck.stickerSlide))
1751 .orElse(null)
1752
1753 return if (thumbnail != null && !(record as MmsMessageRecord).isViewOnce) {
1754 thumbnail.uri
1755 } else {
1756 null
1757 }
1758 }
1759
1760 private fun getContentTypeFor(record: MessageRecord): String? {
1761 if (record.isMms) {
1762 val slideDeck = (record as MmsMessageRecord).slideDeck
1763 if (slideDeck.slides.isNotEmpty()) {
1764 return slideDeck.slides[0].contentType
1765 }
1766 }
1767 return null
1768 }
1769
1770 private fun getExtrasFor(record: MessageRecord, body: ThreadBody): Extra? {
1771 val threadRecipient = getRecipientForThreadId(record.threadId)
1772 val messageRequestAccepted = RecipientUtil.isMessageRequestAccepted(record.threadId, threadRecipient)
1773 val isHidden = threadRecipient?.isHidden ?: false
1774 val authorId = record.fromRecipient.id
1775
1776 if (!messageRequestAccepted && threadRecipient != null) {
1777 if (threadRecipient.isPushGroup) {
1778 if (threadRecipient.isPushV2Group) {
1779 val inviteAddState = record.gv2AddInviteState
1780 if (inviteAddState != null) {
1781 val from = RecipientId.from(inviteAddState.addedOrInvitedBy)
1782 return if (inviteAddState.isInvited) {
1783 Log.i(TAG, "GV2 invite message request from $from")
1784 Extra.forGroupV2invite(from, authorId)
1785 } else {
1786 Log.i(TAG, "GV2 message request from $from")
1787 Extra.forGroupMessageRequest(from, authorId)
1788 }
1789 }
1790
1791 Log.w(TAG, "Falling back to unknown message request state for GV2 message")
1792 return Extra.forMessageRequest(authorId)
1793 } else {
1794 val recipientId = messages.getGroupAddedBy(record.threadId)
1795 if (recipientId != null) {
1796 return Extra.forGroupMessageRequest(recipientId, authorId)
1797 }
1798 }
1799 } else {
1800 return Extra.forMessageRequest(authorId, isHidden)
1801 }
1802 }
1803
1804 val extras: Extra? = if (record.isScheduled()) {
1805 Extra.forScheduledMessage(authorId)
1806 } else if (record.isRemoteDelete) {
1807 Extra.forRemoteDelete(authorId)
1808 } else if (record.isViewOnce) {
1809 Extra.forViewOnce(authorId)
1810 } else if (record.isMms && (record as MmsMessageRecord).slideDeck.stickerSlide != null) {
1811 val slide: StickerSlide = record.slideDeck.stickerSlide!!
1812 Extra.forSticker(slide.emoji, authorId)
1813 } else if (record.isMms && (record as MmsMessageRecord).slideDeck.slides.size > 1) {
1814 Extra.forAlbum(authorId)
1815 } else if (threadRecipient != null && threadRecipient.isGroup) {
1816 Extra.forDefault(authorId)
1817 } else {
1818 null
1819 }
1820
1821 return if (record.messageRanges != null) {
1822 val bodyRanges = record.requireMessageRanges().adjustBodyRanges(body.bodyAdjustments)!!
1823 extras?.copy(bodyRanges = bodyRanges.serialize()) ?: Extra.forBodyRanges(bodyRanges, authorId)
1824 } else {
1825 extras
1826 }
1827 }
1828
1829 private fun createQuery(where: String, limit: Long): String {
1830 return createQuery(
1831 where = where,
1832 offset = 0,
1833 limit = limit,
1834 preferPinned = false
1835 )
1836 }
1837
1838 private fun createQuery(where: String, offset: Long, limit: Long, preferPinned: Boolean): String {
1839 val orderBy = if (preferPinned) {
1840 "$TABLE_NAME.$PINNED DESC, $TABLE_NAME.$DATE DESC"
1841 } else {
1842 "$TABLE_NAME.$DATE DESC"
1843 }
1844
1845 return createQuery(
1846 where = where,
1847 orderBy = orderBy,
1848 offset = offset,
1849 limit = limit
1850 )
1851 }
1852
1853 fun clearCache() {
1854 threadIdCache.clear()
1855 }
1856
1857 private fun createQuery(where: String, orderBy: String, offset: Long, limit: Long): String {
1858 val projection = COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION.joinToString(separator = ",")
1859
1860 //language=sql
1861 var query = """
1862 SELECT $projection, ${GroupTable.MEMBER_GROUP_CONCAT}
1863 FROM $TABLE_NAME
1864 LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
1865 LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
1866 LEFT OUTER JOIN (
1867 SELECT group_id, GROUP_CONCAT(${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID}) as ${GroupTable.MEMBER_GROUP_CONCAT}
1868 FROM ${GroupTable.MembershipTable.TABLE_NAME}
1869 ) as MembershipAlias ON MembershipAlias.${GroupTable.MembershipTable.GROUP_ID} = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID}
1870 WHERE $TABLE_NAME.$ACTIVE = 1 AND $where
1871 ORDER BY $orderBy
1872 """
1873
1874 if (limit > 0) {
1875 query += " LIMIT $limit"
1876 }
1877
1878 if (offset > 0) {
1879 query += " OFFSET $offset"
1880 }
1881
1882 return query
1883 }
1884
1885 private fun isSilentType(type: Long): Boolean {
1886 return MessageTypes.isProfileChange(type) ||
1887 MessageTypes.isGroupV1MigrationEvent(type) ||
1888 MessageTypes.isChangeNumber(type) ||
1889 MessageTypes.isBoostRequest(type) ||
1890 MessageTypes.isGroupV2LeaveOnly(type) ||
1891 MessageTypes.isThreadMergeType(type)
1892 }
1893
1894 fun readerFor(cursor: Cursor): Reader {
1895 return Reader(cursor)
1896 }
1897
1898 private fun ConversationFilter.toQuery(): String {
1899 return when (this) {
1900 ConversationFilter.OFF -> ""
1901 //language=sql
1902 ConversationFilter.UNREAD -> " AND ($UNREAD_COUNT > 0 OR $READ == ${ReadStatus.FORCED_UNREAD.serialize()})"
1903 ConversationFilter.MUTED -> error("This filter selection isn't supported yet.")
1904 ConversationFilter.GROUPS -> error("This filter selection isn't supported yet.")
1905 }
1906 }
1907
1908 object DistributionTypes {
1909 const val DEFAULT = 2
1910 const val BROADCAST = 1
1911 const val CONVERSATION = 2
1912 const val ARCHIVE = 3
1913 const val INBOX_ZERO = 4
1914 }
1915
1916 inner class Reader(cursor: Cursor) : StaticReader(cursor, context)
1917
1918 open class StaticReader(private val cursor: Cursor, private val context: Context) : Closeable {
1919 fun getNext(): ThreadRecord? {
1920 return if (!cursor.moveToNext()) {
1921 null
1922 } else {
1923 getCurrent()
1924 }
1925 }
1926
1927 open fun getCurrent(): ThreadRecord? {
1928 val recipientId = RecipientId.from(cursor.requireLong(RECIPIENT_ID))
1929 val recipientSettings = RecipientTableCursorUtil.getRecord(context, cursor, RECIPIENT_ID)
1930
1931 val recipient: Recipient = if (recipientSettings.groupId != null) {
1932 GroupTable.Reader(cursor).getCurrent()?.let { group ->
1933 val details = RecipientDetails.forGroup(
1934 groupRecord = group,
1935 recipientRecord = recipientSettings
1936 )
1937 Recipient(recipientId, details, false)
1938 } ?: Recipient.live(recipientId).get()
1939 } else {
1940 val details = RecipientDetails.forIndividual(context, recipientSettings)
1941 Recipient(recipientId, details, true)
1942 }
1943
1944 val hasReadReceipt = TextSecurePreferences.isReadReceiptsEnabled(context) && cursor.requireBoolean(HAS_READ_RECEIPT)
1945 val extraString = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_EXTRAS))
1946 val messageExtraBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(SNIPPET_MESSAGE_EXTRAS))
1947 val messageExtras = if (messageExtraBytes != null) MessageExtras.ADAPTER.decode(messageExtraBytes) else null
1948 val extra: Extra? = if (extraString != null) {
1949 try {
1950 val jsonObject = SaneJSONObject(JSONObject(extraString))
1951 Extra(
1952 isViewOnce = jsonObject.getBoolean("isRevealable"),
1953 isSticker = jsonObject.getBoolean("isSticker"),
1954 stickerEmoji = jsonObject.getString("stickerEmoji"),
1955 isAlbum = jsonObject.getBoolean("isAlbum"),
1956 isRemoteDelete = jsonObject.getBoolean("isRemoteDelete"),
1957 isMessageRequestAccepted = jsonObject.getBoolean("isMessageRequestAccepted"),
1958 isGv2Invite = jsonObject.getBoolean("isGv2Invite"),
1959 groupAddedBy = jsonObject.getString("groupAddedBy"),
1960 individualRecipientId = jsonObject.getString("individualRecipientId")!!,
1961 bodyRanges = jsonObject.getString("bodyRanges"),
1962 isScheduled = jsonObject.getBoolean("isScheduled"),
1963 isRecipientHidden = jsonObject.getBoolean("isRecipientHidden")
1964 )
1965 } catch (exception: Exception) {
1966 null
1967 }
1968 } else {
1969 null
1970 }
1971
1972 return ThreadRecord.Builder(cursor.requireLong(ID))
1973 .setRecipient(recipient)
1974 .setType(cursor.requireInt(SNIPPET_TYPE).toLong())
1975 .setDistributionType(cursor.requireInt(TYPE))
1976 .setBody(cursor.requireString(SNIPPET) ?: "")
1977 .setDate(cursor.requireLong(DATE))
1978 .setArchived(cursor.requireBoolean(ARCHIVED))
1979 .setDeliveryStatus(cursor.requireInt(STATUS).toLong())
1980 .setHasDeliveryReceipt(cursor.requireBoolean(HAS_DELIVERY_RECEIPT))
1981 .setHasReadReceipt(hasReadReceipt)
1982 .setExpiresIn(cursor.requireLong(EXPIRES_IN))
1983 .setLastSeen(cursor.requireLong(LAST_SEEN))
1984 .setSnippetUri(getSnippetUri(cursor))
1985 .setContentType(cursor.requireString(SNIPPET_CONTENT_TYPE))
1986 .setMeaningfulMessages(cursor.requireLong(MEANINGFUL_MESSAGES) > 0)
1987 .setUnreadCount(cursor.requireInt(UNREAD_COUNT))
1988 .setForcedUnread(cursor.requireInt(READ) == ReadStatus.FORCED_UNREAD.serialize())
1989 .setPinned(cursor.requireBoolean(PINNED))
1990 .setUnreadSelfMentionsCount(cursor.requireInt(UNREAD_SELF_MENTION_COUNT))
1991 .setExtra(extra)
1992 .setSnippetMessageExtras(messageExtras)
1993 .build()
1994 }
1995
1996 private fun getSnippetUri(cursor: Cursor?): Uri? {
1997 return if (cursor!!.isNull(cursor.getColumnIndexOrThrow(SNIPPET_URI))) {
1998 null
1999 } else {
2000 try {
2001 Uri.parse(cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_URI)))
2002 } catch (e: IllegalArgumentException) {
2003 Log.w(TAG, e)
2004 null
2005 }
2006 }
2007 }
2008
2009 override fun close() {
2010 cursor.close()
2011 }
2012 }
2013
2014 data class Extra(
2015 @field:JsonProperty
2016 @param:JsonProperty("isRevealable")
2017 val isViewOnce: Boolean = false,
2018 @field:JsonProperty
2019 @param:JsonProperty("isSticker")
2020 val isSticker: Boolean = false,
2021 @field:JsonProperty
2022 @param:JsonProperty("stickerEmoji")
2023 val stickerEmoji: String? = null,
2024 @field:JsonProperty
2025 @param:JsonProperty("isAlbum")
2026 val isAlbum: Boolean = false,
2027 @field:JsonProperty
2028 @param:JsonProperty("isRemoteDelete")
2029 val isRemoteDelete: Boolean = false,
2030 @field:JsonProperty
2031 @param:JsonProperty("isMessageRequestAccepted")
2032 val isMessageRequestAccepted: Boolean = true,
2033 @field:JsonProperty
2034 @param:JsonProperty("isGv2Invite")
2035 val isGv2Invite: Boolean = false,
2036 @field:JsonProperty
2037 @param:JsonProperty("groupAddedBy")
2038 val groupAddedBy: String? = null,
2039 @field:JsonProperty
2040 @param:JsonProperty("individualRecipientId")
2041 private val individualRecipientId: String,
2042 @field:JsonProperty
2043 @param:JsonProperty("bodyRanges")
2044 val bodyRanges: String? = null,
2045 @field:JsonProperty
2046 @param:JsonProperty("isScheduled")
2047 val isScheduled: Boolean = false,
2048 @field:JsonProperty
2049 @param:JsonProperty("isRecipientHidden")
2050 val isRecipientHidden: Boolean = false
2051 ) {
2052
2053 fun getIndividualRecipientId(): String {
2054 return individualRecipientId
2055 }
2056
2057 companion object {
2058 fun forViewOnce(individualRecipient: RecipientId): Extra {
2059 return Extra(isViewOnce = true, individualRecipientId = individualRecipient.serialize())
2060 }
2061
2062 fun forSticker(emoji: String?, individualRecipient: RecipientId): Extra {
2063 return Extra(isSticker = true, stickerEmoji = emoji, individualRecipientId = individualRecipient.serialize())
2064 }
2065
2066 fun forAlbum(individualRecipient: RecipientId): Extra {
2067 return Extra(isAlbum = true, individualRecipientId = individualRecipient.serialize())
2068 }
2069
2070 fun forRemoteDelete(individualRecipient: RecipientId): Extra {
2071 return Extra(isRemoteDelete = true, individualRecipientId = individualRecipient.serialize())
2072 }
2073
2074 fun forMessageRequest(individualRecipient: RecipientId, isHidden: Boolean = false): Extra {
2075 return Extra(isMessageRequestAccepted = false, individualRecipientId = individualRecipient.serialize(), isRecipientHidden = isHidden)
2076 }
2077
2078 fun forGroupMessageRequest(recipientId: RecipientId, individualRecipient: RecipientId): Extra {
2079 return Extra(isMessageRequestAccepted = false, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
2080 }
2081
2082 fun forGroupV2invite(recipientId: RecipientId, individualRecipient: RecipientId): Extra {
2083 return Extra(isGv2Invite = true, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
2084 }
2085
2086 fun forDefault(individualRecipient: RecipientId): Extra {
2087 return Extra(individualRecipientId = individualRecipient.serialize())
2088 }
2089
2090 fun forBodyRanges(bodyRanges: BodyRangeList, individualRecipient: RecipientId): Extra {
2091 return Extra(individualRecipientId = individualRecipient.serialize(), bodyRanges = bodyRanges.serialize())
2092 }
2093
2094 fun forScheduledMessage(individualRecipient: RecipientId): Extra {
2095 return Extra(individualRecipientId = individualRecipient.serialize(), isScheduled = true)
2096 }
2097 }
2098 }
2099
2100 internal enum class ReadStatus(private val value: Int) {
2101 READ(1), UNREAD(0), FORCED_UNREAD(2);
2102
2103 fun serialize(): Int {
2104 return value
2105 }
2106
2107 companion object {
2108 fun deserialize(value: Int): ReadStatus {
2109 for (status in values()) {
2110 if (status.value == value) {
2111 return status
2112 }
2113 }
2114 throw IllegalArgumentException("No matching status for value $value")
2115 }
2116 }
2117 }
2118
2119 data class ConversationMetadata(
2120 val lastSeen: Long,
2121 @get:JvmName("hasSent")
2122 val hasSent: Boolean,
2123 val lastScrolled: Long,
2124 val unreadCount: Int
2125 )
2126
2127 data class MergeResult(val threadId: Long, val previousThreadId: Long, val neededMerge: Boolean)
2128
2129 data class ThreadIdResult(
2130 val threadId: Long,
2131 val newlyCreated: Boolean
2132 )
2133}