That fuck shit the fascists are using
at master 2133 lines 70 kB view raw
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}