That fuck shit the fascists are using
at master 5264 lines 191 kB view raw
1/** 2 * Copyright (C) 2011 Whisper Systems 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http:></http:>//www.gnu.org/licenses/>. 16 */ 17package org.tm.archive.database 18 19import android.content.ContentValues 20import android.content.Context 21import android.database.Cursor 22import android.text.SpannableString 23import android.text.TextUtils 24import androidx.annotation.VisibleForTesting 25import androidx.core.content.contentValuesOf 26import org.json.JSONArray 27import org.json.JSONException 28import org.json.JSONObject 29import org.signal.core.util.Base64 30import org.signal.core.util.CursorUtil 31import org.signal.core.util.SqlUtil 32import org.signal.core.util.SqlUtil.appendArg 33import org.signal.core.util.SqlUtil.buildArgs 34import org.signal.core.util.SqlUtil.buildCustomCollectionQuery 35import org.signal.core.util.SqlUtil.buildSingleCollectionQuery 36import org.signal.core.util.SqlUtil.buildTrueUpdateQuery 37import org.signal.core.util.SqlUtil.getNextAutoIncrementId 38import org.signal.core.util.Stopwatch 39import org.signal.core.util.count 40import org.signal.core.util.delete 41import org.signal.core.util.deleteAll 42import org.signal.core.util.exists 43import org.signal.core.util.forEach 44import org.signal.core.util.insertInto 45import org.signal.core.util.logging.Log 46import org.signal.core.util.readToList 47import org.signal.core.util.readToSet 48import org.signal.core.util.readToSingleInt 49import org.signal.core.util.readToSingleLong 50import org.signal.core.util.readToSingleObject 51import org.signal.core.util.requireBlob 52import org.signal.core.util.requireBoolean 53import org.signal.core.util.requireInt 54import org.signal.core.util.requireLong 55import org.signal.core.util.requireLongOrNull 56import org.signal.core.util.requireNonNullString 57import org.signal.core.util.requireString 58import org.signal.core.util.select 59import org.signal.core.util.toInt 60import org.signal.core.util.toOptional 61import org.signal.core.util.toSingleLine 62import org.signal.core.util.update 63import org.signal.core.util.withinTransaction 64import org.signal.libsignal.protocol.IdentityKey 65import org.signal.libsignal.protocol.util.Pair 66import org.tm.archive.attachments.Attachment 67import org.tm.archive.attachments.AttachmentId 68import org.tm.archive.attachments.DatabaseAttachment 69import org.tm.archive.attachments.DatabaseAttachment.DisplayOrderComparator 70import org.tm.archive.contactshare.Contact 71import org.tm.archive.conversation.MessageStyler 72import org.tm.archive.database.EarlyDeliveryReceiptCache.Receipt 73import org.tm.archive.database.MentionUtil.UpdatedBodyAndMentions 74import org.tm.archive.database.SignalDatabase.Companion.attachments 75import org.tm.archive.database.SignalDatabase.Companion.calls 76import org.tm.archive.database.SignalDatabase.Companion.distributionLists 77import org.tm.archive.database.SignalDatabase.Companion.groupReceipts 78import org.tm.archive.database.SignalDatabase.Companion.groups 79import org.tm.archive.database.SignalDatabase.Companion.mentions 80import org.tm.archive.database.SignalDatabase.Companion.messageLog 81import org.tm.archive.database.SignalDatabase.Companion.messages 82import org.tm.archive.database.SignalDatabase.Companion.reactions 83import org.tm.archive.database.SignalDatabase.Companion.recipients 84import org.tm.archive.database.SignalDatabase.Companion.storySends 85import org.tm.archive.database.SignalDatabase.Companion.threads 86import org.tm.archive.database.documents.Document 87import org.tm.archive.database.documents.IdentityKeyMismatch 88import org.tm.archive.database.documents.IdentityKeyMismatchSet 89import org.tm.archive.database.documents.NetworkFailure 90import org.tm.archive.database.documents.NetworkFailureSet 91import org.tm.archive.database.model.GroupCallUpdateDetailsUtil 92import org.tm.archive.database.model.Mention 93import org.tm.archive.database.model.MessageExportStatus 94import org.tm.archive.database.model.MessageId 95import org.tm.archive.database.model.MessageRecord 96import org.tm.archive.database.model.MmsMessageRecord 97import org.tm.archive.database.model.ParentStoryId 98import org.tm.archive.database.model.ParentStoryId.DirectReply 99import org.tm.archive.database.model.ParentStoryId.GroupReply 100import org.tm.archive.database.model.Quote 101import org.tm.archive.database.model.StoryResult 102import org.tm.archive.database.model.StoryType 103import org.tm.archive.database.model.StoryType.Companion.fromCode 104import org.tm.archive.database.model.StoryViewState 105import org.tm.archive.database.model.databaseprotos.BodyRangeList 106import org.tm.archive.database.model.databaseprotos.GiftBadge 107import org.tm.archive.database.model.databaseprotos.GroupCallUpdateDetails 108import org.tm.archive.database.model.databaseprotos.MessageExportState 109import org.tm.archive.database.model.databaseprotos.MessageExtras 110import org.tm.archive.database.model.databaseprotos.ProfileChangeDetails 111import org.tm.archive.database.model.databaseprotos.SessionSwitchoverEvent 112import org.tm.archive.database.model.databaseprotos.ThreadMergeEvent 113import org.tm.archive.dependencies.ApplicationDependencies 114import org.tm.archive.groups.GroupMigrationMembershipChange 115import org.tm.archive.jobs.OptimizeMessageSearchIndexJob 116import org.tm.archive.jobs.ThreadUpdateJob 117import org.tm.archive.jobs.TrimThreadJob 118import org.tm.archive.keyvalue.SignalStore 119import org.tm.archive.linkpreview.LinkPreview 120import org.tm.archive.mms.IncomingMessage 121import org.tm.archive.mms.MessageGroupContext 122import org.tm.archive.mms.MmsException 123import org.tm.archive.mms.OutgoingMessage 124import org.tm.archive.mms.QuoteModel 125import org.tm.archive.mms.SlideDeck 126import org.tm.archive.notifications.v2.DefaultMessageNotifier.StickyThread 127import org.tm.archive.recipients.Recipient 128import org.tm.archive.recipients.RecipientId 129import org.tm.archive.revealable.ViewOnceExpirationInfo 130import org.tm.archive.revealable.ViewOnceUtil 131import org.tm.archive.sms.GroupV2UpdateMessageUtil 132import org.tm.archive.stories.Stories.isFeatureEnabled 133import org.tm.archive.util.JsonUtils 134import org.tm.archive.util.MediaUtil 135import org.tm.archive.util.MessageConstraintsUtil 136import org.tm.archive.util.TextSecurePreferences 137import org.tm.archive.util.Util 138import org.tm.archive.util.isStory 139import org.whispersystems.signalservice.api.push.ServiceId 140import org.whispersystems.signalservice.internal.push.SyncMessage 141import java.io.Closeable 142import java.io.IOException 143import java.util.LinkedList 144import java.util.Optional 145import java.util.UUID 146import java.util.function.Function 147import kotlin.math.max 148import kotlin.math.min 149 150open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), MessageTypes, RecipientIdDatabaseReference, ThreadIdDatabaseReference { 151 152 companion object { 153 private val TAG = Log.tag(MessageTable::class.java) 154 const val TABLE_NAME = "message" 155 const val ID = "_id" 156 const val DATE_SENT = "date_sent" 157 const val DATE_RECEIVED = "date_received" 158 const val TYPE = "type" 159 const val DATE_SERVER = "date_server" 160 const val THREAD_ID = "thread_id" 161 const val READ = "read" 162 const val BODY = "body" 163 const val FROM_RECIPIENT_ID = "from_recipient_id" 164 const val FROM_DEVICE_ID = "from_device_id" 165 const val TO_RECIPIENT_ID = "to_recipient_id" 166 const val HAS_DELIVERY_RECEIPT = "has_delivery_receipt" 167 const val HAS_READ_RECEIPT = "has_read_receipt" 168 const val VIEWED_COLUMN = "viewed" 169 const val MISMATCHED_IDENTITIES = "mismatched_identities" 170 const val SMS_SUBSCRIPTION_ID = "subscription_id" 171 const val EXPIRES_IN = "expires_in" 172 const val EXPIRE_STARTED = "expire_started" 173 const val NOTIFIED = "notified" 174 const val NOTIFIED_TIMESTAMP = "notified_timestamp" 175 const val UNIDENTIFIED = "unidentified" 176 const val REACTIONS_UNREAD = "reactions_unread" 177 const val REACTIONS_LAST_SEEN = "reactions_last_seen" 178 const val REMOTE_DELETED = "remote_deleted" 179 const val SERVER_GUID = "server_guid" 180 const val RECEIPT_TIMESTAMP = "receipt_timestamp" 181 const val EXPORT_STATE = "export_state" 182 const val EXPORTED = "exported" 183 const val MMS_CONTENT_LOCATION = "ct_l" 184 const val MMS_EXPIRY = "exp" 185 const val MMS_MESSAGE_TYPE = "m_type" 186 const val MMS_MESSAGE_SIZE = "m_size" 187 const val MMS_STATUS = "st" 188 const val MMS_TRANSACTION_ID = "tr_id" 189 const val NETWORK_FAILURES = "network_failures" 190 const val QUOTE_ID = "quote_id" 191 const val QUOTE_AUTHOR = "quote_author" 192 const val QUOTE_BODY = "quote_body" 193 const val QUOTE_MISSING = "quote_missing" 194 const val QUOTE_BODY_RANGES = "quote_mentions" 195 const val QUOTE_TYPE = "quote_type" 196 const val SHARED_CONTACTS = "shared_contacts" 197 const val LINK_PREVIEWS = "link_previews" 198 const val MENTIONS_SELF = "mentions_self" 199 const val MESSAGE_RANGES = "message_ranges" 200 const val VIEW_ONCE = "view_once" 201 const val STORY_TYPE = "story_type" 202 const val PARENT_STORY_ID = "parent_story_id" 203 const val SCHEDULED_DATE = "scheduled_date" 204 const val LATEST_REVISION_ID = "latest_revision_id" 205 const val ORIGINAL_MESSAGE_ID = "original_message_id" 206 const val REVISION_NUMBER = "revision_number" 207 const val MESSAGE_EXTRAS = "message_extras" 208 209 const val QUOTE_NOT_PRESENT_ID = 0L 210 const val QUOTE_TARGET_MISSING_ID = -1L 211 212 const val CREATE_TABLE = """ 213 CREATE TABLE $TABLE_NAME ( 214 $ID INTEGER PRIMARY KEY AUTOINCREMENT, 215 $DATE_SENT INTEGER NOT NULL, 216 $DATE_RECEIVED INTEGER NOT NULL, 217 $DATE_SERVER INTEGER DEFAULT -1, 218 $THREAD_ID INTEGER NOT NULL REFERENCES ${ThreadTable.TABLE_NAME} (${ThreadTable.ID}) ON DELETE CASCADE, 219 $FROM_RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, 220 $FROM_DEVICE_ID INTEGER, 221 $TO_RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, 222 $TYPE INTEGER NOT NULL, 223 $BODY TEXT, 224 $READ INTEGER DEFAULT 0, 225 $MMS_CONTENT_LOCATION TEXT, 226 $MMS_EXPIRY INTEGER, 227 $MMS_MESSAGE_TYPE INTEGER, 228 $MMS_MESSAGE_SIZE INTEGER, 229 $MMS_STATUS INTEGER, 230 $MMS_TRANSACTION_ID TEXT, 231 $SMS_SUBSCRIPTION_ID INTEGER DEFAULT -1, 232 $RECEIPT_TIMESTAMP INTEGER DEFAULT -1, 233 $HAS_DELIVERY_RECEIPT INTEGER DEFAULT 0, 234 $HAS_READ_RECEIPT INTEGER DEFAULT 0, 235 $VIEWED_COLUMN INTEGER DEFAULT 0, 236 $MISMATCHED_IDENTITIES TEXT DEFAULT NULL, 237 $NETWORK_FAILURES TEXT DEFAULT NULL, 238 $EXPIRES_IN INTEGER DEFAULT 0, 239 $EXPIRE_STARTED INTEGER DEFAULT 0, 240 $NOTIFIED INTEGER DEFAULT 0, 241 $QUOTE_ID INTEGER DEFAULT 0, 242 $QUOTE_AUTHOR INTEGER DEFAULT 0, 243 $QUOTE_BODY TEXT DEFAULT NULL, 244 $QUOTE_MISSING INTEGER DEFAULT 0, 245 $QUOTE_BODY_RANGES BLOB DEFAULT NULL, 246 $QUOTE_TYPE INTEGER DEFAULT 0, 247 $SHARED_CONTACTS TEXT DEFAULT NULL, 248 $UNIDENTIFIED INTEGER DEFAULT 0, 249 $LINK_PREVIEWS TEXT DEFAULT NULL, 250 $VIEW_ONCE INTEGER DEFAULT 0, 251 $REACTIONS_UNREAD INTEGER DEFAULT 0, 252 $REACTIONS_LAST_SEEN INTEGER DEFAULT -1, 253 $REMOTE_DELETED INTEGER DEFAULT 0, 254 $MENTIONS_SELF INTEGER DEFAULT 0, 255 $NOTIFIED_TIMESTAMP INTEGER DEFAULT 0, 256 $SERVER_GUID TEXT DEFAULT NULL, 257 $MESSAGE_RANGES BLOB DEFAULT NULL, 258 $STORY_TYPE INTEGER DEFAULT 0, 259 $PARENT_STORY_ID INTEGER DEFAULT 0, 260 $EXPORT_STATE BLOB DEFAULT NULL, 261 $EXPORTED INTEGER DEFAULT 0, 262 $SCHEDULED_DATE INTEGER DEFAULT -1, 263 $LATEST_REVISION_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE, 264 $ORIGINAL_MESSAGE_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE, 265 $REVISION_NUMBER INTEGER DEFAULT 0, 266 $MESSAGE_EXTRAS BLOB DEFAULT NULL 267 ) 268 """ 269 270 private const val INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID = "message_thread_story_parent_story_scheduled_date_latest_revision_id_index" 271 private const val INDEX_DATE_SENT_FROM_TO_THREAD = "message_date_sent_from_to_thread_index" 272 private const val INDEX_THREAD_COUNT = "message_thread_count_index" 273 274 @JvmField 275 val CREATE_INDEXS = arrayOf( 276 "CREATE INDEX IF NOT EXISTS message_read_and_notified_and_thread_id_index ON $TABLE_NAME ($READ, $NOTIFIED, $THREAD_ID)", 277 "CREATE INDEX IF NOT EXISTS message_type_index ON $TABLE_NAME ($TYPE)", 278 "CREATE INDEX IF NOT EXISTS $INDEX_DATE_SENT_FROM_TO_THREAD ON $TABLE_NAME ($DATE_SENT, $FROM_RECIPIENT_ID, $TO_RECIPIENT_ID, $THREAD_ID)", 279 "CREATE INDEX IF NOT EXISTS message_date_server_index ON $TABLE_NAME ($DATE_SERVER)", 280 "CREATE INDEX IF NOT EXISTS message_reactions_unread_index ON $TABLE_NAME ($REACTIONS_UNREAD);", 281 "CREATE INDEX IF NOT EXISTS message_story_type_index ON $TABLE_NAME ($STORY_TYPE);", 282 "CREATE INDEX IF NOT EXISTS message_parent_story_id_index ON $TABLE_NAME ($PARENT_STORY_ID);", 283 "CREATE INDEX IF NOT EXISTS $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED, $STORY_TYPE, $PARENT_STORY_ID, $SCHEDULED_DATE, $LATEST_REVISION_ID);", 284 "CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_latest_revision_id_index ON $TABLE_NAME ($QUOTE_ID, $QUOTE_AUTHOR, $SCHEDULED_DATE, $LATEST_REVISION_ID);", 285 "CREATE INDEX IF NOT EXISTS message_exported_index ON $TABLE_NAME ($EXPORTED);", 286 "CREATE INDEX IF NOT EXISTS message_id_type_payment_transactions_index ON $TABLE_NAME ($ID,$TYPE) WHERE $TYPE & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} != 0;", 287 "CREATE INDEX IF NOT EXISTS message_original_message_id_index ON $TABLE_NAME ($ORIGINAL_MESSAGE_ID);", 288 "CREATE INDEX IF NOT EXISTS message_latest_revision_id_index ON $TABLE_NAME ($LATEST_REVISION_ID)", 289 "CREATE INDEX IF NOT EXISTS message_from_recipient_id_index ON $TABLE_NAME ($FROM_RECIPIENT_ID)", 290 "CREATE INDEX IF NOT EXISTS message_to_recipient_id_index ON $TABLE_NAME ($TO_RECIPIENT_ID)", 291 "CREATE UNIQUE INDEX IF NOT EXISTS message_unique_sent_from_thread ON $TABLE_NAME ($DATE_SENT, $FROM_RECIPIENT_ID, $THREAD_ID)", 292 // This index is created specifically for getting the number of messages in a thread and therefore needs to be kept in sync with that query 293 "CREATE INDEX IF NOT EXISTS $INDEX_THREAD_COUNT ON $TABLE_NAME ($THREAD_ID) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL" 294 ) 295 296 private val MMS_PROJECTION_BASE = arrayOf( 297 "$TABLE_NAME.$ID AS $ID", 298 THREAD_ID, 299 DATE_SENT, 300 DATE_RECEIVED, 301 DATE_SERVER, 302 TYPE, 303 READ, 304 MMS_CONTENT_LOCATION, 305 MMS_EXPIRY, 306 MMS_MESSAGE_SIZE, 307 MMS_STATUS, 308 MMS_TRANSACTION_ID, 309 BODY, 310 FROM_RECIPIENT_ID, 311 FROM_DEVICE_ID, 312 TO_RECIPIENT_ID, 313 HAS_DELIVERY_RECEIPT, 314 HAS_READ_RECEIPT, 315 MISMATCHED_IDENTITIES, 316 NETWORK_FAILURES, 317 SMS_SUBSCRIPTION_ID, 318 EXPIRES_IN, 319 EXPIRE_STARTED, 320 NOTIFIED, 321 QUOTE_ID, 322 QUOTE_AUTHOR, 323 QUOTE_BODY, 324 QUOTE_TYPE, 325 QUOTE_MISSING, 326 QUOTE_BODY_RANGES, 327 SHARED_CONTACTS, 328 LINK_PREVIEWS, 329 UNIDENTIFIED, 330 VIEW_ONCE, 331 REACTIONS_UNREAD, 332 REACTIONS_LAST_SEEN, 333 REMOTE_DELETED, 334 MENTIONS_SELF, 335 NOTIFIED_TIMESTAMP, 336 VIEWED_COLUMN, 337 RECEIPT_TIMESTAMP, 338 MESSAGE_RANGES, 339 STORY_TYPE, 340 PARENT_STORY_ID, 341 SCHEDULED_DATE, 342 LATEST_REVISION_ID, 343 ORIGINAL_MESSAGE_ID, 344 REVISION_NUMBER, 345 MESSAGE_EXTRAS 346 ) 347 348 private val MMS_PROJECTION: Array<String> = MMS_PROJECTION_BASE + "NULL AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}" 349 350 private val MMS_PROJECTION_WITH_ATTACHMENTS: Array<String> = MMS_PROJECTION_BASE + 351 """ 352 json_group_array( 353 json_object( 354 '${AttachmentTable.ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ID}, 355 '${AttachmentTable.MESSAGE_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}, 356 '${AttachmentTable.DATA_SIZE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE}, 357 '${AttachmentTable.FILE_NAME}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME}, 358 '${AttachmentTable.DATA_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE}, 359 '${AttachmentTable.CONTENT_TYPE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CONTENT_TYPE}, 360 '${AttachmentTable.CDN_NUMBER}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER}, 361 '${AttachmentTable.REMOTE_LOCATION}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_LOCATION}, 362 '${AttachmentTable.FAST_PREFLIGHT_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FAST_PREFLIGHT_ID}, 363 '${AttachmentTable.VOICE_NOTE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.VOICE_NOTE}, 364 '${AttachmentTable.BORDERLESS}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.BORDERLESS}, 365 '${AttachmentTable.VIDEO_GIF}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.VIDEO_GIF}, 366 '${AttachmentTable.WIDTH}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.WIDTH}, 367 '${AttachmentTable.HEIGHT}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.HEIGHT}, 368 '${AttachmentTable.QUOTE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE}, 369 '${AttachmentTable.REMOTE_KEY}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_KEY}, 370 '${AttachmentTable.TRANSFER_STATE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.TRANSFER_STATE}, 371 '${AttachmentTable.CAPTION}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CAPTION}, 372 '${AttachmentTable.STICKER_PACK_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_ID}, 373 '${AttachmentTable.STICKER_PACK_KEY}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_KEY}, 374 '${AttachmentTable.STICKER_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_ID}, 375 '${AttachmentTable.STICKER_EMOJI}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_EMOJI}, 376 '${AttachmentTable.BLUR_HASH}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.BLUR_HASH}, 377 '${AttachmentTable.TRANSFORM_PROPERTIES}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.TRANSFORM_PROPERTIES}, 378 '${AttachmentTable.DISPLAY_ORDER}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DISPLAY_ORDER}, 379 '${AttachmentTable.UPLOAD_TIMESTAMP}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP} 380 ) 381 ) AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS} 382 """.toSingleLine() 383 384 private const val IS_STORY_CLAUSE = "$STORY_TYPE > 0 AND $REMOTE_DELETED = 0" 385 private const val RAW_ID_WHERE = "$TABLE_NAME.$ID = ?" 386 387 private val SNIPPET_QUERY = 388 """ 389 SELECT 390 $ID, 391 $TYPE, 392 $DATE_RECEIVED 393 FROM 394 $TABLE_NAME 395 WHERE 396 $THREAD_ID = ? AND 397 $TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND 398 $STORY_TYPE = 0 AND 399 $PARENT_STORY_ID <= 0 AND 400 $SCHEDULED_DATE = -1 AND 401 $LATEST_REVISION_ID IS NULL AND 402 $TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT} = 0 AND 403 $TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT} = 0 AND 404 $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND 405 $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND 406 $TYPE NOT IN ( 407 ${MessageTypes.PROFILE_CHANGE_TYPE}, 408 ${MessageTypes.GV1_MIGRATION_TYPE}, 409 ${MessageTypes.CHANGE_NUMBER_TYPE}, 410 ${MessageTypes.BOOST_REQUEST_TYPE}, 411 ${MessageTypes.SMS_EXPORT_TYPE} 412 ) 413 ORDER BY $DATE_RECEIVED DESC LIMIT 1 414 """ 415 416 const val IS_CALL_TYPE_CLAUSE = """( 417 ($TYPE = ${MessageTypes.INCOMING_AUDIO_CALL_TYPE}) 418 OR 419 ($TYPE = ${MessageTypes.INCOMING_VIDEO_CALL_TYPE}) 420 OR 421 ($TYPE = ${MessageTypes.OUTGOING_AUDIO_CALL_TYPE}) 422 OR 423 ($TYPE = ${MessageTypes.OUTGOING_VIDEO_CALL_TYPE}) 424 OR 425 ($TYPE = ${MessageTypes.MISSED_AUDIO_CALL_TYPE}) 426 OR 427 ($TYPE = ${MessageTypes.MISSED_VIDEO_CALL_TYPE}) 428 OR 429 ($TYPE = ${MessageTypes.GROUP_CALL_TYPE}) 430 )""" 431 432 private val outgoingTypeClause: String by lazy { 433 MessageTypes.OUTGOING_MESSAGE_TYPES 434 .map { "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK} = $it)" } 435 .joinToString(" OR ") 436 } 437 438 @JvmStatic 439 fun mmsReaderFor(cursor: Cursor): MmsReader { 440 return MmsReader(cursor) 441 } 442 443 private fun getSharedContacts(cursor: Cursor, attachments: List<DatabaseAttachment>): List<Contact> { 444 val serializedContacts: String? = cursor.requireString(SHARED_CONTACTS) 445 446 if (serializedContacts.isNullOrEmpty()) { 447 return emptyList() 448 } 449 450 val attachmentIdMap: Map<AttachmentId, DatabaseAttachment> = attachments.associateBy { it.attachmentId } 451 452 try { 453 val contacts: MutableList<Contact> = LinkedList() 454 val jsonContacts = JSONArray(serializedContacts) 455 456 for (i in 0 until jsonContacts.length()) { 457 val contact: Contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString()) 458 459 if (contact.avatar != null && contact.avatar!!.attachmentId != null) { 460 val attachment = attachmentIdMap[contact.avatar!!.attachmentId] 461 462 val updatedAvatar = Contact.Avatar( 463 contact.avatar!!.attachmentId, 464 attachment, 465 contact.avatar!!.isProfile 466 ) 467 468 contacts += Contact(contact, updatedAvatar) 469 } else { 470 contacts += contact 471 } 472 } 473 474 return contacts 475 } catch (e: JSONException) { 476 Log.w(TAG, "Failed to parse shared contacts.", e) 477 } catch (e: IOException) { 478 Log.w(TAG, "Failed to parse shared contacts.", e) 479 } 480 481 return emptyList() 482 } 483 484 private fun getLinkPreviews(cursor: Cursor, attachments: List<DatabaseAttachment>): List<LinkPreview> { 485 val serializedPreviews: String? = cursor.requireString(LINK_PREVIEWS) 486 487 if (serializedPreviews.isNullOrEmpty()) { 488 return emptyList() 489 } 490 491 val attachmentIdMap: Map<AttachmentId, DatabaseAttachment> = attachments.associateBy { it.attachmentId } 492 493 try { 494 val previews: MutableList<LinkPreview> = LinkedList() 495 val jsonPreviews = JSONArray(serializedPreviews) 496 497 for (i in 0 until jsonPreviews.length()) { 498 val preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString()) 499 500 if (preview.attachmentId != null) { 501 val attachment = attachmentIdMap[preview.attachmentId] 502 503 if (attachment != null) { 504 previews += LinkPreview(preview.url, preview.title, preview.description, preview.date, attachment) 505 } else { 506 previews += preview 507 } 508 } else { 509 previews += preview 510 } 511 } 512 513 return previews 514 } catch (e: JSONException) { 515 Log.w(TAG, "Failed to parse shared contacts.", e) 516 } catch (e: IOException) { 517 Log.w(TAG, "Failed to parse shared contacts.", e) 518 } 519 520 return emptyList() 521 } 522 523 private fun parseQuoteMentions(cursor: Cursor): List<Mention> { 524 val data: ByteArray? = cursor.requireBlob(QUOTE_BODY_RANGES) 525 526 val bodyRanges: BodyRangeList? = if (data != null) { 527 try { 528 BodyRangeList.ADAPTER.decode(data) 529 } catch (e: IOException) { 530 Log.w(TAG, "Unable to parse quote body ranges", e) 531 null 532 } 533 } else { 534 null 535 } 536 537 return MentionUtil.bodyRangeListToMentions(bodyRanges) 538 } 539 540 private fun parseQuoteBodyRanges(cursor: Cursor): BodyRangeList? { 541 val data: ByteArray? = cursor.requireBlob(QUOTE_BODY_RANGES) 542 543 if (data != null) { 544 try { 545 val bodyRanges = BodyRangeList 546 .ADAPTER.decode(data) 547 .ranges 548 .filter { bodyRange -> bodyRange.mentionUuid == null } 549 550 return BodyRangeList(ranges = bodyRanges) 551 } catch (e: IOException) { 552 Log.w(TAG, "Unable to parse quote body ranges", e) 553 } 554 } 555 return null 556 } 557 } 558 559 private val earlyDeliveryReceiptCache = EarlyDeliveryReceiptCache() 560 561 private fun getOldestGroupUpdateSender(threadId: Long, minimumDateReceived: Long): RecipientId? { 562 val type = MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_UPDATE_BIT or MessageTypes.BASE_INBOX_TYPE 563 564 return readableDatabase 565 .select(FROM_RECIPIENT_ID) 566 .from(TABLE_NAME) 567 .where("$THREAD_ID = ? AND $TYPE & ? AND $DATE_RECEIVED >= ?", threadId.toString(), type.toString(), minimumDateReceived.toString()) 568 .limit(1) 569 .run() 570 .readToSingleObject { RecipientId.from(it.requireLong(FROM_RECIPIENT_ID)) } 571 } 572 573 fun getExpirationStartedMessages(): Cursor { 574 val where = "$EXPIRE_STARTED > 0" 575 return rawQueryWithAttachments(where, null) 576 } 577 578 fun getMessageCursor(messageId: Long): Cursor { 579 return internalGetMessage(messageId) 580 } 581 582 fun hasReceivedAnyCallsSince(threadId: Long, timestamp: Long): Boolean { 583 return readableDatabase 584 .exists(TABLE_NAME) 585 .where( 586 "$THREAD_ID = ? AND $DATE_RECEIVED > ? AND ($TYPE = ? OR $TYPE = ? OR $TYPE = ? OR $TYPE =?)", 587 threadId, 588 timestamp, 589 MessageTypes.INCOMING_AUDIO_CALL_TYPE, 590 MessageTypes.INCOMING_VIDEO_CALL_TYPE, 591 MessageTypes.MISSED_AUDIO_CALL_TYPE, 592 MessageTypes.MISSED_VIDEO_CALL_TYPE 593 ) 594 .run() 595 } 596 597 fun markAsInvalidVersionKeyExchange(id: Long) { 598 updateTypeBitmask(id, 0, MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT) 599 } 600 601 fun markAsUnsupportedProtocolVersion(id: Long) { 602 updateTypeBitmask(id, MessageTypes.BASE_TYPE_MASK, MessageTypes.UNSUPPORTED_MESSAGE_TYPE) 603 } 604 605 fun markAsInvalidMessage(id: Long) { 606 updateTypeBitmask(id, MessageTypes.BASE_TYPE_MASK, MessageTypes.INVALID_MESSAGE_TYPE) 607 } 608 609 fun markAsLegacyVersion(id: Long) { 610 updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT) 611 } 612 613 open fun markSmsStatus(id: Long, status: Int) {//*TM_SA*/ make fun open 614 Log.i(TAG, "Updating ID: $id to status: $status") 615 616 writableDatabase 617 .update(TABLE_NAME) 618 .values(MMS_STATUS to status) 619 .where("$ID = ?", id) 620 .run() 621 622 val threadId = getThreadIdForMessage(id) 623 threads.update(threadId, false) 624 notifyConversationListeners(threadId) 625 } 626 627 protected open fun updateTypeBitmask(id: Long, maskOff: Long, maskOn: Long) {//*TM_SA*/ make fun protected open 628 writableDatabase.withinTransaction { db -> 629 db.execSQL( 630 """ 631 UPDATE $TABLE_NAME 632 SET $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn ) 633 WHERE $ID = ? 634 """, 635 buildArgs(id) 636 ) 637 638 val threadId = getThreadIdForMessage(id) 639 threads.updateSnippetTypeSilently(threadId) 640 } 641 642 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(id)) 643 ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() 644 } 645 646 private fun updateMessageBodyAndType(messageId: Long, body: String, maskOff: Long, maskOn: Long): InsertResult { 647 writableDatabase.execSQL( 648 """ 649 UPDATE $TABLE_NAME 650 SET 651 $BODY = ?, 652 $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn) 653 WHERE $ID = ? 654 """, 655 arrayOf(body, messageId.toString() + "") 656 ) 657 658 val threadId = getThreadIdForMessage(messageId) 659 threads.update(threadId, true) 660 notifyConversationListeners(threadId) 661 662 return InsertResult( 663 messageId = messageId, 664 threadId = threadId, 665 threadWasNewlyCreated = false 666 ) 667 } 668 669 fun updateBundleMessageBody(messageId: Long, body: String): InsertResult { 670 val type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT 671 return updateMessageBodyAndType(messageId, body, MessageTypes.TOTAL_MASK, type) 672 } 673 674 fun getViewedIncomingMessages(threadId: Long): List<MarkedMessageInfo> { 675 return readableDatabase 676 .select(ID, FROM_RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE) 677 .from(TABLE_NAME) 678 .where("$THREAD_ID = ? AND $VIEWED_COLUMN > 0 AND $TYPE & ${MessageTypes.BASE_INBOX_TYPE} = ${MessageTypes.BASE_INBOX_TYPE}", threadId) 679 .run() 680 .readToList { it.toMarkedMessageInfo(outgoing = false) } 681 } 682 683 fun setIncomingMessageViewed(messageId: Long): MarkedMessageInfo? { 684 val results = setIncomingMessagesViewed(listOf(messageId)) 685 return if (results.isEmpty()) { 686 null 687 } else { 688 results[0] 689 } 690 } 691 692 open fun setIncomingMessagesViewed(messageIds: List<Long>): List<MarkedMessageInfo> {//*TM_SA*/ make fun open 693 if (messageIds.isEmpty()) { 694 return emptyList() 695 } 696 697 val results: List<MarkedMessageInfo> = readableDatabase 698 .select(ID, FROM_RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE) 699 .from(TABLE_NAME) 700 .where("$ID IN (${Util.join(messageIds, ",")}) AND $VIEWED_COLUMN = 0") 701 .run() 702 .readToList { cursor -> 703 val type = cursor.requireLong(TYPE) 704 705 if (MessageTypes.isSecureType(type) && MessageTypes.isInboxType(type)) { 706 cursor.toMarkedMessageInfo(outgoing = false) 707 } else { 708 null 709 } 710 } 711 .filterNotNull() 712 713 val currentTime = System.currentTimeMillis() 714 SqlUtil 715 .buildCollectionQuery(ID, results.map { it.messageId.id }) 716 .forEach { query -> 717 writableDatabase 718 .update(TABLE_NAME) 719 .values( 720 VIEWED_COLUMN to 1, 721 RECEIPT_TIMESTAMP to currentTime 722 ) 723 .where(query.where, query.whereArgs) 724 .run() 725 } 726 727 val threadsUpdated: Set<Long> = results 728 .map { it.threadId } 729 .toSet() 730 731 val storyRecipientsUpdated: Set<RecipientId> = results 732 .filter { it.storyType.isStory } 733 .mapNotNull { threads.getRecipientIdForThreadId(it.threadId) } 734 .toSet() 735 736 notifyConversationListeners(threadsUpdated) 737 notifyConversationListListeners() 738 ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(storyRecipientsUpdated) 739 740 return results 741 } 742 743 fun setOutgoingGiftsRevealed(messageIds: List<Long>): List<MarkedMessageInfo> { 744 val results: List<MarkedMessageInfo> = readableDatabase 745 .select(ID, TO_RECIPIENT_ID, DATE_SENT, THREAD_ID, STORY_TYPE) 746 .from(TABLE_NAME) 747 .where("""$ID IN (${Util.join(messageIds, ",")}) AND ($outgoingTypeClause) AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} = ${MessageTypes.SPECIAL_TYPE_GIFT_BADGE}) AND $VIEWED_COLUMN = 0""") 748 .run() 749 .readToList { it.toMarkedMessageInfo(outgoing = true) } 750 751 val currentTime = System.currentTimeMillis() 752 SqlUtil 753 .buildCollectionQuery(ID, results.map { it.messageId.id }) 754 .forEach { query -> 755 writableDatabase 756 .update(TABLE_NAME) 757 .values( 758 VIEWED_COLUMN to 1, 759 RECEIPT_TIMESTAMP to currentTime 760 ) 761 .where(query.where, query.whereArgs) 762 .run() 763 } 764 765 val threadsUpdated = results 766 .map { it.threadId } 767 .toSet() 768 769 notifyConversationListeners(threadsUpdated) 770 return results 771 } 772 773 open fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {//*TM_SA*/ make fun open 774 val recipient = Recipient.resolved(recipientId) 775 val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup) 776 val threadId = threadIdResult.threadId 777 778 val values = contentValuesOf( 779 FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else recipientId.serialize(), 780 FROM_DEVICE_ID to 1, 781 TO_RECIPIENT_ID to if (outgoing) recipientId.serialize() else Recipient.self().id.serialize(), 782 DATE_RECEIVED to System.currentTimeMillis(), 783 DATE_SENT to timestamp, 784 READ to 1, 785 TYPE to type, 786 THREAD_ID to threadId 787 ) 788 789 val messageId = writableDatabase.insert(TABLE_NAME, null, values) 790 791 threads.update(threadId, true) 792 793 notifyConversationListeners(threadId) 794 TrimThreadJob.enqueueAsync(threadId) 795 796 return InsertResult( 797 messageId = messageId, 798 threadId = threadId, 799 threadWasNewlyCreated = threadIdResult.newlyCreated 800 ) 801 } 802 803 open fun updateCallLog(messageId: Long, type: Long) {//*TM_SA*/ make fun open 804 writableDatabase 805 .update(TABLE_NAME) 806 .values( 807 TYPE to type, 808 READ to 1 809 ) 810 .where("$ID = ?", messageId) 811 .run() 812 813 val threadId = getThreadIdForMessage(messageId) 814 815 threads.update(threadId, true) 816 817 notifyConversationListeners(threadId) 818 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 819 } 820 821 open fun insertGroupCall( 822 groupRecipientId: RecipientId, 823 sender: RecipientId, 824 timestamp: Long, 825 eraId: String, 826 joinedUuids: Collection<UUID>, 827 isCallFull: Boolean 828 ): MessageId {//*TM_SA*/ make fun open 829 val recipient = Recipient.resolved(groupRecipientId) 830 val threadId = threads.getOrCreateThreadIdFor(recipient) 831 val messageId: MessageId = writableDatabase.withinTransaction { db -> 832 val self = Recipient.self() 833 val markRead = joinedUuids.contains(self.requireServiceId().rawUuid) || self.id == sender 834 val updateDetails: ByteArray = GroupCallUpdateDetails( 835 eraId = eraId, 836 startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(), 837 startedCallTimestamp = timestamp, 838 inCallUuids = joinedUuids.map { it.toString() }, 839 isCallFull = isCallFull 840 ).encode() 841 842 val values = contentValuesOf( 843 FROM_RECIPIENT_ID to sender.serialize(), 844 FROM_DEVICE_ID to 1, 845 TO_RECIPIENT_ID to groupRecipientId.serialize(), 846 DATE_RECEIVED to timestamp, 847 DATE_SENT to timestamp, 848 READ to if (markRead) 1 else 0, 849 BODY to Base64.encodeWithPadding(updateDetails), 850 TYPE to MessageTypes.GROUP_CALL_TYPE, 851 THREAD_ID to threadId 852 ) 853 854 val messageId = MessageId(db.insert(TABLE_NAME, null, values)) 855 threads.incrementUnread(threadId, 1, 0) 856 threads.update(threadId, true) 857 858 messageId 859 } 860 861 notifyConversationListeners(threadId) 862 TrimThreadJob.enqueueAsync(threadId) 863 864 return messageId 865 } 866 867 /** 868 * Updates the timestamps associated with the given message id to the given ts 869 */ 870 fun updateCallTimestamps(messageId: Long, timestamp: Long) { 871 val message = try { 872 getMessageRecord(messageId = messageId) 873 } catch (e: NoSuchMessageException) { 874 error("Message $messageId does not exist") 875 } 876 877 val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body) 878 val contentValues = contentValuesOf( 879 BODY to Base64.encodeWithPadding(updateDetail.newBuilder().startedCallTimestamp(timestamp).build().encode()), 880 DATE_SENT to timestamp, 881 DATE_RECEIVED to timestamp 882 ) 883 884 val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues) 885 val updated = writableDatabase.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0 886 887 if (updated) { 888 notifyConversationListeners(message.threadId) 889 } 890 } 891 892 open fun updateGroupCall(//*TM_SA*/ make fun open 893 messageId: Long, 894 eraId: String, 895 joinedUuids: Collection<UUID>, 896 isCallFull: Boolean 897 ): MessageId { 898 writableDatabase.withinTransaction { db -> 899 val message = try { 900 getMessageRecord(messageId = messageId) 901 } catch (e: NoSuchMessageException) { 902 error("Message $messageId does not exist.") 903 } 904 905 val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body) 906 val containsSelf = joinedUuids.contains(SignalStore.account().requireAci().rawUuid) 907 val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId) 908 val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList() 909 val contentValues = contentValuesOf( 910 BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull) 911 ) 912 913 if (sameEraId && containsSelf) { 914 contentValues.put(READ, 1) 915 } 916 917 val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues) 918 val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0 919 920 if (updated) { 921 notifyConversationListeners(message.threadId) 922 } 923 } 924 925 return MessageId(messageId) 926 } 927 928 fun updatePreviousGroupCall(threadId: Long, peekGroupCallEraId: String?, peekJoinedUuids: Collection<UUID>, isCallFull: Boolean): Boolean { 929 return writableDatabase.withinTransaction { db -> 930 val cursor = db 931 .select(*MMS_PROJECTION) 932 .from(TABLE_NAME) 933 .where("$TYPE = ? AND $THREAD_ID = ?", MessageTypes.GROUP_CALL_TYPE, threadId) 934 .orderBy("$DATE_RECEIVED DESC") 935 .limit(1) 936 .run() 937 938 MmsReader(cursor).use { reader -> 939 val record = reader.getNext() ?: return@withinTransaction false 940 val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body) 941 val containsSelf = peekJoinedUuids.contains(SignalStore.account().requireAci().rawUuid) 942 val sameEraId = groupCallUpdateDetails.eraId == peekGroupCallEraId && !Util.isEmpty(peekGroupCallEraId) 943 944 val inCallUuids = if (sameEraId) { 945 peekJoinedUuids.map { it.toString() }.toList() 946 } else { 947 emptyList() 948 } 949 950 val contentValues = contentValuesOf( 951 BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull) 952 ) 953 954 if (sameEraId && containsSelf) { 955 contentValues.put(READ, 1) 956 } 957 958 val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(record.id), contentValues) 959 val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0 960 961 if (updated) { 962 notifyConversationListeners(threadId) 963 } 964 965 sameEraId 966 } 967 } 968 } 969 970 open fun insertEditMessageInbox(mediaMessage: IncomingMessage, targetMessage: MmsMessageRecord): Optional<InsertResult> {//*TM_SA*/ make fun open 971 val insertResult = insertMessageInbox(retrieved = mediaMessage, editedMessage = targetMessage, notifyObservers = false) 972 973 if (insertResult.isPresent) { 974 val (messageId) = insertResult.get() 975 976 if (targetMessage.expireStarted > 0) { 977 markExpireStarted(messageId, targetMessage.expireStarted) 978 } 979 980 writableDatabase.update(TABLE_NAME) 981 .values(LATEST_REVISION_ID to messageId) 982 .where("$ID = ? OR $LATEST_REVISION_ID = ?", targetMessage.id, targetMessage.id) 983 .run() 984 985 reactions.moveReactionsToNewMessage(newMessageId = messageId, previousId = targetMessage.id) 986 987 notifyConversationListeners(targetMessage.threadId) 988 } 989 990 return insertResult 991 } 992 993 fun insertProfileNameChangeMessages(recipient: Recipient, newProfileName: String, previousProfileName: String) { 994 writableDatabase.withinTransaction { db -> 995 val groupRecords = groups.getGroupsContainingMember(recipient.id, false) 996 997 val extras = MessageExtras( 998 profileChangeDetails = ProfileChangeDetails(profileNameChange = ProfileChangeDetails.StringChange(previous = previousProfileName, newValue = newProfileName)) 999 ) 1000 1001 val threadIdsToUpdate = mutableListOf<Long?>().apply { 1002 add(threads.getThreadIdFor(recipient.id)) 1003 addAll( 1004 groupRecords 1005 .filter { it.isActive } 1006 .map { threads.getThreadIdFor(it.recipientId) } 1007 ) 1008 } 1009 1010 threadIdsToUpdate 1011 .filterNotNull() 1012 .forEach { threadId -> 1013 val values = contentValuesOf( 1014 FROM_RECIPIENT_ID to recipient.id.serialize(), 1015 FROM_DEVICE_ID to 1, 1016 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1017 DATE_RECEIVED to System.currentTimeMillis(), 1018 DATE_SENT to System.currentTimeMillis(), 1019 READ to 1, 1020 TYPE to MessageTypes.PROFILE_CHANGE_TYPE, 1021 THREAD_ID to threadId, 1022 MESSAGE_EXTRAS to extras.encode() 1023 ) 1024 db.insert(TABLE_NAME, null, values) 1025 notifyConversationListeners(threadId) 1026 TrimThreadJob.enqueueAsync(threadId) 1027 } 1028 } 1029 } 1030 1031 fun insertLearnedProfileNameChangeMessage(recipient: Recipient, previousDisplayName: String) { 1032 val threadId: Long? = SignalDatabase.threads.getThreadIdFor(recipient.id) 1033 1034 if (threadId != null) { 1035 val extras = MessageExtras( 1036 profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.StringChange(previous = previousDisplayName)) 1037 ) 1038 1039 writableDatabase 1040 .insertInto(TABLE_NAME) 1041 .values( 1042 FROM_RECIPIENT_ID to recipient.id.serialize(), 1043 FROM_DEVICE_ID to 1, 1044 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1045 DATE_RECEIVED to System.currentTimeMillis(), 1046 DATE_SENT to System.currentTimeMillis(), 1047 READ to 1, 1048 TYPE to MessageTypes.PROFILE_CHANGE_TYPE, 1049 THREAD_ID to threadId, 1050 MESSAGE_EXTRAS to extras.encode() 1051 ) 1052 .run() 1053 1054 notifyConversationListeners(threadId) 1055 } 1056 } 1057 1058 fun insertGroupV1MigrationEvents(recipientId: RecipientId, threadId: Long, membershipChange: GroupMigrationMembershipChange) { 1059 insertGroupV1MigrationNotification(recipientId, threadId) 1060 if (!membershipChange.isEmpty) { 1061 insertGroupV1MigrationMembershipChanges(recipientId, threadId, membershipChange) 1062 } 1063 notifyConversationListeners(threadId) 1064 TrimThreadJob.enqueueAsync(threadId) 1065 } 1066 1067 private fun insertGroupV1MigrationNotification(recipientId: RecipientId, threadId: Long) { 1068 insertGroupV1MigrationMembershipChanges(recipientId, threadId, GroupMigrationMembershipChange.empty()) 1069 } 1070 1071 private fun insertGroupV1MigrationMembershipChanges(recipientId: RecipientId, threadId: Long, membershipChange: GroupMigrationMembershipChange) { 1072 val values = contentValuesOf( 1073 FROM_RECIPIENT_ID to recipientId.serialize(), 1074 FROM_DEVICE_ID to 1, 1075 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1076 DATE_RECEIVED to System.currentTimeMillis(), 1077 DATE_SENT to System.currentTimeMillis(), 1078 READ to 1, 1079 TYPE to MessageTypes.GV1_MIGRATION_TYPE, 1080 THREAD_ID to threadId 1081 ) 1082 1083 if (!membershipChange.isEmpty) { 1084 values.put(BODY, membershipChange.serialize()) 1085 } 1086 1087 databaseHelper.signalWritableDatabase.insert(TABLE_NAME, null, values) 1088 } 1089 1090 fun insertNumberChangeMessages(recipientId: RecipientId) { 1091 val groupRecords = groups.getGroupsContainingMember(recipientId, false) 1092 1093 writableDatabase.withinTransaction { db -> 1094 val threadIdsToUpdate = mutableListOf<Long?>().apply { 1095 add(threads.getThreadIdFor(recipientId)) 1096 addAll( 1097 groupRecords 1098 .filter { it.isActive } 1099 .map { threads.getThreadIdFor(it.recipientId) } 1100 ) 1101 } 1102 1103 threadIdsToUpdate 1104 .filterNotNull() 1105 .forEach { threadId: Long -> 1106 val values = contentValuesOf( 1107 FROM_RECIPIENT_ID to recipientId.serialize(), 1108 FROM_DEVICE_ID to 1, 1109 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1110 DATE_RECEIVED to System.currentTimeMillis(), 1111 DATE_SENT to System.currentTimeMillis(), 1112 READ to 1, 1113 TYPE to MessageTypes.CHANGE_NUMBER_TYPE, 1114 THREAD_ID to threadId, 1115 BODY to null 1116 ) 1117 1118 db.insert(TABLE_NAME, null, values) 1119 threads.update(threadId, true) 1120 1121 TrimThreadJob.enqueueAsync(threadId) 1122 notifyConversationListeners(threadId) 1123 } 1124 } 1125 } 1126 1127 fun insertBoostRequestMessage(recipientId: RecipientId, threadId: Long) { 1128 writableDatabase 1129 .insertInto(TABLE_NAME) 1130 .values( 1131 FROM_RECIPIENT_ID to recipientId.serialize(), 1132 FROM_DEVICE_ID to 1, 1133 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1134 DATE_RECEIVED to System.currentTimeMillis(), 1135 DATE_SENT to System.currentTimeMillis(), 1136 READ to 1, 1137 TYPE to MessageTypes.BOOST_REQUEST_TYPE, 1138 THREAD_ID to threadId, 1139 BODY to null 1140 ) 1141 .run() 1142 } 1143 1144 fun insertThreadMergeEvent(recipientId: RecipientId, threadId: Long, event: ThreadMergeEvent) { 1145 writableDatabase 1146 .insertInto(TABLE_NAME) 1147 .values( 1148 FROM_RECIPIENT_ID to recipientId.serialize(), 1149 FROM_DEVICE_ID to 1, 1150 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1151 DATE_RECEIVED to System.currentTimeMillis(), 1152 DATE_SENT to System.currentTimeMillis(), 1153 READ to 1, 1154 TYPE to MessageTypes.THREAD_MERGE_TYPE, 1155 THREAD_ID to threadId, 1156 BODY to Base64.encodeWithPadding(event.encode()) 1157 ) 1158 .run() 1159 ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId) 1160 } 1161 1162 fun insertSessionSwitchoverEvent(recipientId: RecipientId, threadId: Long, event: SessionSwitchoverEvent) { 1163 writableDatabase 1164 .insertInto(TABLE_NAME) 1165 .values( 1166 FROM_RECIPIENT_ID to recipientId.serialize(), 1167 FROM_DEVICE_ID to 1, 1168 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1169 DATE_RECEIVED to System.currentTimeMillis(), 1170 DATE_SENT to System.currentTimeMillis(), 1171 READ to 1, 1172 TYPE to MessageTypes.SESSION_SWITCHOVER_TYPE, 1173 THREAD_ID to threadId, 1174 BODY to Base64.encodeWithPadding(event.encode()) 1175 ) 1176 .run() 1177 ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId) 1178 } 1179 1180 fun insertSmsExportMessage(recipientId: RecipientId, threadId: Long) { 1181 val updated = writableDatabase.withinTransaction { db -> 1182 if (messages.hasSmsExportMessage(threadId)) { 1183 false 1184 } else { 1185 db.insertInto(TABLE_NAME) 1186 .values( 1187 FROM_RECIPIENT_ID to recipientId.serialize(), 1188 FROM_DEVICE_ID to 1, 1189 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 1190 DATE_RECEIVED to System.currentTimeMillis(), 1191 DATE_SENT to System.currentTimeMillis(), 1192 READ to 1, 1193 TYPE to MessageTypes.SMS_EXPORT_TYPE, 1194 THREAD_ID to threadId, 1195 BODY to null 1196 ) 1197 .run() 1198 true 1199 } 1200 } 1201 1202 if (updated) { 1203 ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId) 1204 } 1205 } 1206 1207 fun endTransaction(database: SQLiteDatabase) { 1208 database.endTransaction() 1209 } 1210 1211 fun ensureMigration() { 1212 databaseHelper.signalWritableDatabase 1213 } 1214 1215 fun isStory(messageId: Long): Boolean { 1216 return readableDatabase 1217 .exists(TABLE_NAME) 1218 .where("$IS_STORY_CLAUSE AND $ID = ?", messageId) 1219 .run() 1220 } 1221 1222 fun getOutgoingStoriesTo(recipientId: RecipientId): Reader { 1223 val recipient = Recipient.resolved(recipientId) 1224 val threadId: Long? = if (recipient.isGroup) { 1225 threads.getThreadIdFor(recipientId) 1226 } else { 1227 null 1228 } 1229 1230 var where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause)" 1231 val whereArgs: Array<String> 1232 1233 if (threadId == null) { 1234 where += " AND $FROM_RECIPIENT_ID = ?" 1235 whereArgs = buildArgs(recipientId) 1236 } else { 1237 where += " AND $THREAD_ID = ?" 1238 whereArgs = buildArgs(threadId) 1239 } 1240 1241 return MmsReader(rawQueryWithAttachments(where, whereArgs)) 1242 } 1243 1244 fun getAllOutgoingStories(reverse: Boolean, limit: Int): Reader { 1245 val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause)" 1246 return MmsReader(rawQueryWithAttachments(where, null, reverse, limit.toLong())) 1247 } 1248 1249 fun markAllIncomingStoriesRead(): List<MarkedMessageInfo> { 1250 val where = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $READ = 0" 1251 val markedMessageInfos = setMessagesRead(where, null) 1252 notifyConversationListListeners() 1253 return markedMessageInfos 1254 } 1255 1256 fun markAllCallEventsRead(): List<MarkedMessageInfo> { 1257 val where = "$IS_CALL_TYPE_CLAUSE AND $READ = 0" 1258 val markedMessageInfos = setMessagesRead(where, null) 1259 notifyConversationListListeners() 1260 return markedMessageInfos 1261 } 1262 1263 fun markAllFailedStoriesNotified() { 1264 val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}" 1265 1266 writableDatabase 1267 .update("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 1268 .values(NOTIFIED to 1) 1269 .where(where) 1270 .run() 1271 notifyConversationListListeners() 1272 } 1273 1274 fun markOnboardingStoryRead() { 1275 val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId ?: return 1276 val where = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $READ = 0 AND $FROM_RECIPIENT_ID = ?" 1277 val markedMessageInfos = setMessagesRead(where, buildArgs(recipientId)) 1278 1279 if (markedMessageInfos.isNotEmpty()) { 1280 notifyConversationListListeners() 1281 } 1282 } 1283 1284 fun getAllStoriesFor(recipientId: RecipientId, limit: Int): Reader { 1285 val threadId = threads.getThreadIdIfExistsFor(recipientId) 1286 val where = "$IS_STORY_CLAUSE AND $THREAD_ID = ?" 1287 val whereArgs = buildArgs(threadId) 1288 val cursor = rawQueryWithAttachments(where, whereArgs, false, limit.toLong()) 1289 return MmsReader(cursor) 1290 } 1291 1292 fun getUnreadStories(recipientId: RecipientId, limit: Int): Reader { 1293 val threadId = threads.getThreadIdIfExistsFor(recipientId) 1294 val query = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $THREAD_ID = ? AND $VIEWED_COLUMN = ?" 1295 val args = buildArgs(threadId, 0) 1296 return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong())) 1297 } 1298 1299 fun getUnreadMissedCallCount(): Long { 1300 return readableDatabase 1301 .select("COUNT(*)") 1302 .from(TABLE_NAME) 1303 .where( 1304 "($TYPE = ? OR $TYPE = ?) AND $READ = ?", 1305 MessageTypes.MISSED_AUDIO_CALL_TYPE, 1306 MessageTypes.MISSED_VIDEO_CALL_TYPE, 1307 0 1308 ) 1309 .run() 1310 .readToSingleLong(0L) 1311 } 1312 1313 fun getParentStoryIdForGroupReply(messageId: Long): GroupReply? { 1314 return readableDatabase 1315 .select(PARENT_STORY_ID) 1316 .from(TABLE_NAME) 1317 .where("$ID = ?", messageId) 1318 .run() 1319 .readToSingleObject { cursor -> 1320 val parentStoryId: ParentStoryId? = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID)) 1321 if (parentStoryId != null && parentStoryId.isGroupReply()) { 1322 parentStoryId as GroupReply 1323 } else { 1324 null 1325 } 1326 } 1327 } 1328 1329 fun getStoryViewState(recipientId: RecipientId): StoryViewState { 1330 if (!isFeatureEnabled()) { 1331 return StoryViewState.NONE 1332 } 1333 val threadId = threads.getThreadIdIfExistsFor(recipientId) 1334 return getStoryViewState(threadId) 1335 } 1336 1337 /** 1338 * Synchronizes whether we've viewed a recipient's story based on incoming sync messages. 1339 */ 1340 fun updateViewedStories(targetTimestamps: Set<Long>) { 1341 val timestamps: String = targetTimestamps 1342 .joinToString(",") 1343 1344 writableDatabase.withinTransaction { db -> 1345 db.select(FROM_RECIPIENT_ID) 1346 .from(TABLE_NAME) 1347 .where("$IS_STORY_CLAUSE AND $DATE_SENT IN ($timestamps) AND NOT ($outgoingTypeClause) AND $VIEWED_COLUMN > 0") 1348 .run() 1349 .readToList { cursor -> RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)) } 1350 .forEach { id -> recipients.updateLastStoryViewTimestamp(id) } 1351 } 1352 } 1353 1354 @VisibleForTesting 1355 fun getStoryViewState(threadId: Long): StoryViewState { 1356 val hasStories = readableDatabase 1357 .exists(TABLE_NAME) 1358 .where("$IS_STORY_CLAUSE AND $THREAD_ID = ?", threadId) 1359 .run() 1360 1361 if (!hasStories) { 1362 return StoryViewState.NONE 1363 } 1364 1365 val hasUnviewedStories = readableDatabase 1366 .exists(TABLE_NAME) 1367 .where("$IS_STORY_CLAUSE AND $THREAD_ID = ? AND $VIEWED_COLUMN = ? AND NOT ($outgoingTypeClause)", threadId, 0) 1368 .run() 1369 1370 return if (hasUnviewedStories) { 1371 StoryViewState.UNVIEWED 1372 } else { 1373 StoryViewState.VIEWED 1374 } 1375 } 1376 1377 fun isOutgoingStoryAlreadyInDatabase(recipientId: RecipientId, sentTimestamp: Long): Boolean { 1378 return readableDatabase 1379 .exists(TABLE_NAME) 1380 .where("$TO_RECIPIENT_ID = ? AND $STORY_TYPE > 0 AND $DATE_SENT = ? AND ($outgoingTypeClause)", recipientId, sentTimestamp) 1381 .run() 1382 } 1383 1384 @Throws(NoSuchMessageException::class) 1385 fun getStoryId(authorId: RecipientId, sentTimestamp: Long): MessageId { 1386 return readableDatabase 1387 .select(ID) 1388 .from(TABLE_NAME) 1389 .where("$IS_STORY_CLAUSE AND $DATE_SENT = ? AND $FROM_RECIPIENT_ID = ?", sentTimestamp, authorId) 1390 .run() 1391 .readToSingleObject { cursor -> 1392 MessageId(CursorUtil.requireLong(cursor, ID)) 1393 } ?: throw NoSuchMessageException("No story sent at $sentTimestamp") 1394 } 1395 1396 fun getUnreadStoryThreadRecipientIds(): List<RecipientId> { 1397 val query = """ 1398 SELECT DISTINCT ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} 1399 FROM $TABLE_NAME 1400 JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} 1401 WHERE 1402 $IS_STORY_CLAUSE AND 1403 ($outgoingTypeClause) = 0 AND 1404 $VIEWED_COLUMN = 0 AND 1405 $TABLE_NAME.$READ = 0 1406 """ 1407 1408 return readableDatabase 1409 .rawQuery(query, null) 1410 .readToList { RecipientId.from(it.getLong(0)) } 1411 } 1412 1413 fun hasFailedOutgoingStory(): Boolean { 1414 val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause) AND $NOTIFIED = 0 AND ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_FAILED_TYPE}" 1415 return readableDatabase.exists(TABLE_NAME).where(where).run() 1416 } 1417 1418 fun getOrderedStoryRecipientsAndIds(isOutgoingOnly: Boolean): List<StoryResult> { 1419 val query = """ 1420 SELECT 1421 $TABLE_NAME.$DATE_SENT AS sent_timestamp, 1422 $TABLE_NAME.$ID AS mms_id, 1423 ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}, 1424 ($outgoingTypeClause) AS is_outgoing, 1425 $VIEWED_COLUMN, 1426 $TABLE_NAME.$DATE_SENT, 1427 $RECEIPT_TIMESTAMP, 1428 ($outgoingTypeClause) = 0 AND $VIEWED_COLUMN = 0 AS is_unread 1429 FROM $TABLE_NAME 1430 JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} 1431 WHERE 1432 $STORY_TYPE > 0 AND 1433 $REMOTE_DELETED = 0 1434 ${if (isOutgoingOnly) " AND is_outgoing != 0" else ""} 1435 ORDER BY 1436 is_unread DESC, 1437 CASE 1438 WHEN is_outgoing = 0 AND $VIEWED_COLUMN = 0 THEN $TABLE_NAME.$DATE_SENT 1439 WHEN is_outgoing = 0 AND $VIEWED_COLUMN > 0 THEN $RECEIPT_TIMESTAMP 1440 WHEN is_outgoing = 1 THEN $TABLE_NAME.$DATE_SENT 1441 END DESC 1442 """ 1443 1444 return readableDatabase 1445 .rawQuery(query, null) 1446 .readToList { cursor -> 1447 StoryResult( 1448 RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)), 1449 CursorUtil.requireLong(cursor, "mms_id"), 1450 CursorUtil.requireLong(cursor, "sent_timestamp"), 1451 CursorUtil.requireBoolean(cursor, "is_outgoing") 1452 ) 1453 } 1454 } 1455 1456 fun getStoryReplies(parentStoryId: Long): Cursor { 1457 val where = "$PARENT_STORY_ID = ?" 1458 val whereArgs = buildArgs(parentStoryId) 1459 return rawQueryWithAttachments(where, whereArgs, false, 0) 1460 } 1461 1462 fun getNumberOfStoryReplies(parentStoryId: Long): Int { 1463 return readableDatabase 1464 .select("COUNT(*)") 1465 .from(TABLE_NAME) 1466 .where("$PARENT_STORY_ID = ?", parentStoryId) 1467 .run() 1468 .readToSingleInt() 1469 } 1470 1471 fun containsStories(threadId: Long): Boolean { 1472 return readableDatabase 1473 .exists(TABLE_NAME) 1474 .where("$THREAD_ID = ? AND $STORY_TYPE > 0", threadId) 1475 .run() 1476 } 1477 1478 fun hasSelfReplyInStory(parentStoryId: Long): Boolean { 1479 return readableDatabase 1480 .exists(TABLE_NAME) 1481 .where("$PARENT_STORY_ID = ? AND ($outgoingTypeClause)", -parentStoryId) 1482 .run() 1483 } 1484 1485 fun hasGroupReplyOrReactionInStory(parentStoryId: Long): Boolean { 1486 return hasSelfReplyInStory(-parentStoryId) 1487 } 1488 1489 fun getOldestStorySendTimestamp(hasSeenReleaseChannelStories: Boolean): Long? { 1490 val releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories) 1491 1492 return readableDatabase 1493 .select(DATE_SENT) 1494 .from(TABLE_NAME) 1495 .where("$IS_STORY_CLAUSE AND $THREAD_ID != ?", releaseChannelThreadId) 1496 .limit(1) 1497 .orderBy("$DATE_SENT ASC") 1498 .run() 1499 .readToSingleObject { it.getLong(0) } 1500 } 1501 1502 @VisibleForTesting 1503 fun deleteGroupStoryReplies(parentStoryId: Long) { 1504 writableDatabase 1505 .delete(TABLE_NAME) 1506 .where("$PARENT_STORY_ID = ?", parentStoryId) 1507 .run() 1508 } 1509 1510 fun deleteStoriesOlderThan(timestamp: Long, hasSeenReleaseChannelStories: Boolean): Int { 1511 return writableDatabase.withinTransaction { db -> 1512 val releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories) 1513 val storiesBeforeTimestampWhere = "$IS_STORY_CLAUSE AND $DATE_SENT < ? AND $THREAD_ID != ?" 1514 val sharedArgs = buildArgs(timestamp, releaseChannelThreadId) 1515 1516 val deleteStoryRepliesQuery = """ 1517 DELETE FROM $TABLE_NAME 1518 WHERE 1519 $PARENT_STORY_ID > 0 AND 1520 $PARENT_STORY_ID IN ( 1521 SELECT $ID 1522 FROM $TABLE_NAME 1523 WHERE $storiesBeforeTimestampWhere 1524 ) 1525 """ 1526 1527 val disassociateQuoteQuery = """ 1528 UPDATE $TABLE_NAME 1529 SET 1530 $QUOTE_MISSING = 1, 1531 $QUOTE_BODY = '' 1532 WHERE 1533 $PARENT_STORY_ID < 0 AND 1534 ABS($PARENT_STORY_ID) IN ( 1535 SELECT $ID 1536 FROM $TABLE_NAME 1537 WHERE $storiesBeforeTimestampWhere 1538 ) 1539 """ 1540 1541 db.execSQL(deleteStoryRepliesQuery, sharedArgs) 1542 db.execSQL(disassociateQuoteQuery, sharedArgs) 1543 1544 db.select(FROM_RECIPIENT_ID) 1545 .from(TABLE_NAME) 1546 .where(storiesBeforeTimestampWhere, sharedArgs) 1547 .run() 1548 .readToList { RecipientId.from(it.requireLong(FROM_RECIPIENT_ID)) } 1549 .forEach { id -> ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(id) } 1550 1551 val deletedStoryCount = db.select(ID) 1552 .from(TABLE_NAME) 1553 .where(storiesBeforeTimestampWhere, sharedArgs) 1554 .run() 1555 .use { cursor -> 1556 while (cursor.moveToNext()) { 1557 deleteMessage(cursor.requireLong(ID)) 1558 } 1559 1560 cursor.count 1561 } 1562 1563 if (deletedStoryCount > 0) { 1564 OptimizeMessageSearchIndexJob.enqueue() 1565 } 1566 1567 deletedStoryCount 1568 } 1569 } 1570 1571 /** 1572 * Delete all the stories received from the recipient in 1:1 stories 1573 */ 1574 fun deleteStoriesForRecipient(recipientId: RecipientId): Int { 1575 return writableDatabase.withinTransaction { db -> 1576 val threadId = threads.getThreadIdFor(recipientId) ?: return@withinTransaction 0 1577 val storesInRecipientThread = "$IS_STORY_CLAUSE AND $THREAD_ID = ?" 1578 val sharedArgs = buildArgs(threadId) 1579 1580 val deleteStoryRepliesQuery = """ 1581 DELETE FROM $TABLE_NAME 1582 WHERE 1583 $PARENT_STORY_ID > 0 AND 1584 $PARENT_STORY_ID IN ( 1585 SELECT $ID 1586 FROM $TABLE_NAME 1587 WHERE $storesInRecipientThread 1588 ) 1589 """ 1590 1591 val disassociateQuoteQuery = """ 1592 UPDATE $TABLE_NAME 1593 SET 1594 $QUOTE_MISSING = 1, 1595 $QUOTE_BODY = '' 1596 WHERE 1597 $PARENT_STORY_ID < 0 AND 1598 ABS($PARENT_STORY_ID) IN ( 1599 SELECT $ID 1600 FROM $TABLE_NAME 1601 WHERE $storesInRecipientThread 1602 ) 1603 """ 1604 1605 db.execSQL(deleteStoryRepliesQuery, sharedArgs) 1606 db.execSQL(disassociateQuoteQuery, sharedArgs) 1607 1608 ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(recipientId) 1609 1610 val deletedStoryCount = db.select(ID) 1611 .from(TABLE_NAME) 1612 .where(storesInRecipientThread, sharedArgs) 1613 .run() 1614 .use { cursor -> 1615 while (cursor.moveToNext()) { 1616 deleteMessage(cursor.requireLong(ID)) 1617 } 1618 1619 cursor.count 1620 } 1621 1622 if (deletedStoryCount > 0) { 1623 OptimizeMessageSearchIndexJob.enqueue() 1624 } 1625 1626 deletedStoryCount 1627 } 1628 } 1629 1630 private fun disassociateStoryQuotes(storyId: Long) { 1631 writableDatabase 1632 .update(TABLE_NAME) 1633 .values( 1634 QUOTE_MISSING to 1, 1635 QUOTE_BODY to null 1636 ) 1637 .where("$PARENT_STORY_ID = ?", DirectReply(storyId).serialize()) 1638 .run() 1639 } 1640 1641 fun isGroupQuitMessage(messageId: Long): Boolean { 1642 val type = MessageTypes.getOutgoingEncryptedMessageType() or MessageTypes.GROUP_LEAVE_BIT 1643 1644 return readableDatabase 1645 .exists(TABLE_NAME) 1646 .where("$ID = ? AND $TYPE & $type = $type AND $TYPE & ${MessageTypes.GROUP_V2_BIT} = 0", messageId) 1647 .run() 1648 } 1649 1650 fun getLatestGroupQuitTimestamp(threadId: Long, quitTimeBarrier: Long): Long { 1651 val type = MessageTypes.getOutgoingEncryptedMessageType() or MessageTypes.GROUP_LEAVE_BIT 1652 1653 return readableDatabase 1654 .select(DATE_SENT) 1655 .from(TABLE_NAME) 1656 .where("$THREAD_ID = ? AND $TYPE & $type = $type AND $TYPE & ${MessageTypes.GROUP_V2_BIT} = 0 AND $DATE_SENT < ?", threadId, quitTimeBarrier) 1657 .orderBy("$DATE_SENT DESC") 1658 .limit(1) 1659 .run() 1660 .readToSingleLong(-1) 1661 } 1662 1663 fun getScheduledMessageCountForThread(threadId: Long): Int { 1664 return readableDatabase 1665 .select("COUNT(*)") 1666 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 1667 .where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ?", threadId, 0, 0, -1) 1668 .run() 1669 .readToSingleInt() 1670 } 1671 1672 fun getMessageCountForThread(threadId: Long): Int { 1673 return readableDatabase 1674 .select("COUNT(*)") 1675 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_COUNT") 1676 .where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL") 1677 .run() 1678 .readToSingleInt() 1679 } 1680 1681 fun canSetUniversalTimer(threadId: Long): Boolean { 1682 if (threadId == -1L) { 1683 return true 1684 } 1685 1686 val meaningfulQuery = buildMeaningfulMessagesQuery(threadId) 1687 val isNotJoinedType = SqlUtil.buildQuery("$TYPE & ${MessageTypes.BASE_TYPE_MASK} != ${MessageTypes.JOINED_TYPE}") 1688 1689 val query = meaningfulQuery and isNotJoinedType 1690 val hasMeaningfulMessages = readableDatabase 1691 .exists("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 1692 .where(query.where, query.whereArgs) 1693 .run() 1694 1695 return !hasMeaningfulMessages 1696 } 1697 1698 fun hasMeaningfulMessage(threadId: Long): Boolean { 1699 if (threadId == -1L) { 1700 return false 1701 } 1702 1703 val query = buildMeaningfulMessagesQuery(threadId) 1704 return readableDatabase 1705 .exists("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 1706 .where(query.where, query.whereArgs) 1707 .run() 1708 } 1709 1710 /** 1711 * Returns the receipt status of the most recent meaningful message in the thread if it matches the provided message ID. 1712 * If the ID doesn't match or otherwise can't be found, it will return null. 1713 * 1714 * This is a very specific method for use with [ThreadTable.updateReceiptStatus] to improve the perfomance of 1715 * processing receipts. 1716 */ 1717 fun getReceiptStatusIfItsTheMostRecentMeaningfulMessage(messageId: Long, threadId: Long): MessageReceiptStatus? { 1718 val query = buildMeaningfulMessagesQuery(threadId) 1719 1720 return readableDatabase 1721 .select(ID, HAS_DELIVERY_RECEIPT, HAS_READ_RECEIPT, TYPE) 1722 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 1723 .where(query.where, query.whereArgs) 1724 .orderBy("$DATE_RECEIVED DESC") 1725 .limit(1) 1726 .run() 1727 .use { cursor -> 1728 if (cursor.moveToFirst()) { 1729 if (cursor.requireLong(ID) != messageId) { 1730 return null 1731 } 1732 1733 return MessageReceiptStatus( 1734 hasDeliveryReceipt = cursor.requireBoolean(HAS_DELIVERY_RECEIPT), 1735 hasReadReceipt = cursor.requireBoolean(HAS_READ_RECEIPT), 1736 type = cursor.requireLong(TYPE) 1737 ) 1738 } else { 1739 null 1740 } 1741 } 1742 } 1743 1744 private fun buildMeaningfulMessagesQuery(threadId: Long): SqlUtil.Query { 1745 val query = """ 1746 $THREAD_ID = $threadId AND 1747 $STORY_TYPE = 0 AND 1748 $LATEST_REVISION_ID IS NULL AND 1749 $PARENT_STORY_ID <= 0 AND 1750 ( 1751 NOT $TYPE & ${MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING} AND 1752 $TYPE != ${MessageTypes.PROFILE_CHANGE_TYPE} AND 1753 $TYPE != ${MessageTypes.CHANGE_NUMBER_TYPE} AND 1754 $TYPE != ${MessageTypes.SMS_EXPORT_TYPE} AND 1755 $TYPE != ${MessageTypes.BOOST_REQUEST_TYPE} AND 1756 $TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND 1757 $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND 1758 $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} 1759 ) 1760 """ 1761 1762 return SqlUtil.buildQuery(query) 1763 } 1764 1765 fun setNetworkFailures(messageId: Long, failures: Set<NetworkFailure?>?) { 1766 try { 1767 setDocument(databaseHelper.signalWritableDatabase, messageId, NETWORK_FAILURES, NetworkFailureSet(failures)) 1768 } catch (e: IOException) { 1769 Log.w(TAG, e) 1770 } 1771 } 1772 1773 fun getThreadIdForMessage(id: Long): Long { 1774 return readableDatabase 1775 .select(THREAD_ID) 1776 .from(TABLE_NAME) 1777 .where("$ID = ?", id) 1778 .run() 1779 .readToSingleLong(-1) 1780 } 1781 1782 private fun getThreadIdFor(retrieved: IncomingMessage): ThreadTable.ThreadIdResult { 1783 return if (retrieved.groupId != null) { 1784 val groupRecipientId = recipients.getOrInsertFromPossiblyMigratedGroupId(retrieved.groupId) 1785 val groupRecipients = Recipient.resolved(groupRecipientId) 1786 threads.getOrCreateThreadIdResultFor(groupRecipients.id, isGroup = true) 1787 } else { 1788 val sender = Recipient.resolved(retrieved.from) 1789 threads.getOrCreateThreadIdResultFor(sender.id, isGroup = false) 1790 } 1791 } 1792 1793 private fun rawQueryWithAttachments(where: String, arguments: Array<String>?, reverse: Boolean = false, limit: Long = 0): Cursor { 1794 return rawQueryWithAttachments(MMS_PROJECTION_WITH_ATTACHMENTS, where, arguments, reverse, limit) 1795 } 1796 1797 private fun rawQueryWithAttachments(projection: Array<String>, where: String, arguments: Array<String>?, reverse: Boolean, limit: Long): Cursor { 1798 val database = databaseHelper.signalReadableDatabase 1799 var rawQueryString = """ 1800 SELECT 1801 ${Util.join(projection, ",")} 1802 FROM 1803 $TABLE_NAME LEFT OUTER JOIN ${AttachmentTable.TABLE_NAME} ON ($TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}) 1804 WHERE 1805 $where 1806 GROUP BY 1807 $TABLE_NAME.$ID 1808 """.toSingleLine() 1809 1810 if (reverse) { 1811 rawQueryString += " ORDER BY $TABLE_NAME.$ID DESC" 1812 } 1813 1814 if (limit > 0) { 1815 rawQueryString += " LIMIT $limit" 1816 } 1817 1818 return database.rawQuery(rawQueryString, arguments) 1819 } 1820 1821 private fun internalGetMessage(messageId: Long): Cursor { 1822 return rawQueryWithAttachments(RAW_ID_WHERE, buildArgs(messageId)) 1823 } 1824 1825 @Throws(NoSuchMessageException::class) 1826 fun getMessageRecord(messageId: Long): MessageRecord { 1827 rawQueryWithAttachments(RAW_ID_WHERE, arrayOf(messageId.toString() + "")).use { cursor -> 1828 return MmsReader(cursor).getNext() ?: throw NoSuchMessageException("No message for ID: $messageId") 1829 } 1830 } 1831 1832 fun getMessageRecordOrNull(messageId: Long): MessageRecord? { 1833 rawQueryWithAttachments(RAW_ID_WHERE, buildArgs(messageId)).use { cursor -> 1834 return MmsReader(cursor).firstOrNull() 1835 } 1836 } 1837 1838 private fun getOriginalEditedMessageRecord(messageId: Long): Long { 1839 return readableDatabase.select(ID) 1840 .from(TABLE_NAME) 1841 .where("$TABLE_NAME.$LATEST_REVISION_ID = ?", messageId) 1842 .orderBy("$ID DESC") 1843 .limit(1) 1844 .run() 1845 .readToSingleLong(0) 1846 } 1847 1848 fun getMessages(messageIds: Collection<Long?>): MmsReader { 1849 val ids = TextUtils.join(",", messageIds) 1850 return mmsReaderFor(rawQueryWithAttachments("$TABLE_NAME.$ID IN ($ids)", null)) 1851 } 1852 1853 fun getMessageEditHistory(id: Long): MmsReader { 1854 val cursor = readableDatabase.select(*MMS_PROJECTION) 1855 .from(TABLE_NAME) 1856 .where("$TABLE_NAME.$ID = ? OR $TABLE_NAME.$ORIGINAL_MESSAGE_ID = ?", id, id) 1857 .orderBy("$TABLE_NAME.$DATE_SENT ASC") 1858 .run() 1859 1860 return mmsReaderFor(cursor) 1861 } 1862 1863 private fun getPreviousEditIds(id: Long): List<Long> { 1864 return readableDatabase 1865 .select(ID) 1866 .from(TABLE_NAME) 1867 .where("$LATEST_REVISION_ID = ?", id) 1868 .orderBy("$DATE_SENT ASC") 1869 .run() 1870 .readToList { 1871 it.requireLong(ID) 1872 } 1873 } 1874 1875 private fun updateMailboxBitmask(id: Long, maskOff: Long, maskOn: Long, threadId: Optional<Long>) { 1876 writableDatabase.withinTransaction { db -> 1877 db.execSQL( 1878 """ 1879 UPDATE $TABLE_NAME 1880 SET $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn ) 1881 WHERE $ID = ? 1882 """, 1883 buildArgs(id) 1884 ) 1885 1886 if (threadId.isPresent) { 1887 threads.updateSnippetTypeSilently(threadId.get()) 1888 } 1889 } 1890 } 1891 1892 open fun markAsOutbox(messageId: Long) {//*TM_SA*/ make fun open 1893 val threadId = getThreadIdForMessage(messageId) 1894 updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_OUTBOX_TYPE, Optional.of(threadId)) 1895 } 1896 1897 fun markAsForcedSms(messageId: Long) { 1898 val threadId = getThreadIdForMessage(messageId) 1899 updateMailboxBitmask(messageId, MessageTypes.PUSH_MESSAGE_BIT, MessageTypes.MESSAGE_FORCE_SMS_BIT, Optional.of(threadId)) 1900 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 1901 } 1902 1903 fun markAsRateLimited(messageId: Long) { 1904 val threadId = getThreadIdForMessage(messageId) 1905 updateMailboxBitmask(messageId, 0, MessageTypes.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId)) 1906 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 1907 } 1908 1909 fun clearRateLimitStatus(ids: Collection<Long>) { 1910 writableDatabase.withinTransaction { 1911 for (id in ids) { 1912 val threadId = getThreadIdForMessage(id) 1913 updateMailboxBitmask(id, MessageTypes.MESSAGE_RATE_LIMITED_BIT, 0, Optional.of(threadId)) 1914 } 1915 } 1916 } 1917 1918 fun markAsPendingInsecureSmsFallback(messageId: Long) { 1919 val threadId = getThreadIdForMessage(messageId) 1920 updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK, Optional.of(threadId)) 1921 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 1922 } 1923 1924 open fun markAsSending(messageId: Long) {//*TM_SA*/ make fun open 1925 val threadId = getThreadIdForMessage(messageId) 1926 updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENDING_TYPE, Optional.of(threadId)) 1927 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 1928 ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() 1929 } 1930 1931 open fun markAsSentFailed(messageId: Long) {//*TM_SA*/ make fun open 1932 val threadId = getThreadIdForMessage(messageId) 1933 updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENT_FAILED_TYPE, Optional.of(threadId)) 1934 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 1935 ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() 1936 } 1937 1938 open fun markAsSent(messageId: Long, secure: Boolean) {//*TM_SA*/ make fun open 1939 val threadId = getThreadIdForMessage(messageId) 1940 updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENT_TYPE or if (secure) MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.SECURE_MESSAGE_BIT else 0, Optional.of(threadId)) 1941 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 1942 ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() 1943 } 1944 1945 fun markAsRemoteDelete(targetMessage: MessageRecord) { 1946 writableDatabase.withinTransaction { db -> 1947 if (targetMessage.isEditMessage) { 1948 val latestRevisionId = (targetMessage as? MmsMessageRecord)?.latestRevisionId?.id ?: targetMessage.id 1949 markAsRemoteDeleteInternal(latestRevisionId) 1950 getPreviousEditIds(latestRevisionId).map { id -> 1951 db.update(TABLE_NAME) 1952 .values( 1953 ORIGINAL_MESSAGE_ID to null, 1954 LATEST_REVISION_ID to null 1955 ) 1956 .where("$ID = ?", id) 1957 .run() 1958 deleteMessage(id) 1959 } 1960 } else { 1961 markAsRemoteDeleteInternal(targetMessage.id) 1962 } 1963 } 1964 } 1965 1966 fun markAsRemoteDelete(messageId: Long) { 1967 val targetMessage: MessageRecord = getMessageRecord(messageId) 1968 markAsRemoteDelete(targetMessage) 1969 } 1970 1971 protected open fun markAsRemoteDeleteInternal(messageId: Long) {//*TM_SA*/ make fun protected open 1972 var deletedAttachments = false 1973 writableDatabase.withinTransaction { db -> 1974 db.update(TABLE_NAME) 1975 .values( 1976 REMOTE_DELETED to 1, 1977 BODY to null, 1978 QUOTE_BODY to null, 1979 QUOTE_AUTHOR to null, 1980 QUOTE_TYPE to null, 1981 QUOTE_ID to null, 1982 LINK_PREVIEWS to null, 1983 SHARED_CONTACTS to null, 1984 ORIGINAL_MESSAGE_ID to null, 1985 LATEST_REVISION_ID to null 1986 ) 1987 .where("$ID = ?", messageId) 1988 .run() 1989 1990 deletedAttachments = attachments.deleteAttachmentsForMessage(messageId) 1991 mentions.deleteMentionsForMessage(messageId) 1992 messageLog.deleteAllRelatedToMessage(messageId) 1993 reactions.deleteReactions(MessageId(messageId)) 1994 deleteGroupStoryReplies(messageId) 1995 disassociateStoryQuotes(messageId) 1996 1997 val threadId = getThreadIdForMessage(messageId) 1998 threads.update(threadId, false) 1999 } 2000 2001 OptimizeMessageSearchIndexJob.enqueue() 2002 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 2003 ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() 2004 2005 if (deletedAttachments) { 2006 ApplicationDependencies.getDatabaseObserver().notifyAttachmentObservers() 2007 } 2008 } 2009 2010 open fun markDownloadState(messageId: Long, state: Long) {//*TM_SA*/ make fun open 2011 writableDatabase 2012 .update(TABLE_NAME) 2013 .values(MMS_STATUS to state) 2014 .where("$ID = ?", messageId) 2015 .run() 2016 2017 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 2018 } 2019 2020 fun clearScheduledStatus(threadId: Long, messageId: Long, expiresIn: Long): Boolean { 2021 val rowsUpdated = writableDatabase 2022 .update(TABLE_NAME) 2023 .values( 2024 SCHEDULED_DATE to -1, 2025 DATE_SENT to System.currentTimeMillis(), 2026 DATE_RECEIVED to System.currentTimeMillis(), 2027 EXPIRES_IN to expiresIn 2028 ) 2029 .where("$ID = ? AND $SCHEDULED_DATE != ?", messageId, -1) 2030 .run() 2031 2032 ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, MessageId(messageId)) 2033 ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId) 2034 2035 return rowsUpdated > 0 2036 } 2037 2038 fun rescheduleMessage(threadId: Long, messageId: Long, time: Long) { 2039 val rowsUpdated = writableDatabase 2040 .update(TABLE_NAME) 2041 .values(SCHEDULED_DATE to time) 2042 .where("$ID = ? AND $SCHEDULED_DATE != ?", messageId, -1) 2043 .run() 2044 2045 ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId) 2046 ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary() 2047 2048 if (rowsUpdated == 0) { 2049 Log.w(TAG, "Failed to reschedule messageId=$messageId to new time $time. may have been sent already") 2050 } 2051 } 2052 2053 fun markAsInsecure(messageId: Long) { 2054 updateMailboxBitmask(messageId, MessageTypes.SECURE_MESSAGE_BIT, 0, Optional.empty()) 2055 } 2056 2057 fun markUnidentified(messageId: Long, unidentified: Boolean) { 2058 writableDatabase 2059 .update(TABLE_NAME) 2060 .values(UNIDENTIFIED to if (unidentified) 1 else 0) 2061 .where("$ID = ?", messageId) 2062 .run() 2063 } 2064 2065 @JvmOverloads 2066 fun markExpireStarted(id: Long, startedTimestamp: Long = System.currentTimeMillis()) { 2067 markExpireStarted(setOf(id to startedTimestamp)) 2068 } 2069 2070 fun markExpireStarted(ids: Collection<kotlin.Pair<Long, Long>>) { 2071 writableDatabase.withinTransaction { db -> 2072 for ((id, startedAtTimestamp) in ids) { 2073 db.update(TABLE_NAME) 2074 .values(EXPIRE_STARTED to startedAtTimestamp) 2075 .where("$ID = ? AND ($EXPIRE_STARTED = 0 OR $EXPIRE_STARTED > ?)", id, startedAtTimestamp) 2076 .run() 2077 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(id)) 2078 } 2079 } 2080 } 2081 2082 fun markAsNotified(id: Long) { 2083 writableDatabase 2084 .update(TABLE_NAME) 2085 .values( 2086 NOTIFIED to 1, 2087 REACTIONS_LAST_SEEN to System.currentTimeMillis() 2088 ) 2089 .where("$ID = ?", id) 2090 .run() 2091 } 2092 2093 fun markAsNotNotified(id: Long) { 2094 writableDatabase 2095 .update(TABLE_NAME) 2096 .values(NOTIFIED to 0) 2097 .where("$ID = ?", id) 2098 .run() 2099 } 2100 2101 fun setAllEditMessageRevisionsRead(messageId: Long): List<MarkedMessageInfo> { 2102 var query = """ 2103 ( 2104 $ORIGINAL_MESSAGE_ID = ? OR 2105 $ID = ? 2106 ) AND 2107 ( 2108 $READ = 0 OR 2109 ( 2110 $REACTIONS_UNREAD = 1 AND 2111 ($outgoingTypeClause) 2112 ) 2113 ) 2114 """ 2115 2116 val args = mutableListOf(messageId.toString(), messageId.toString()) 2117 2118 return setMessagesRead(query, args.toTypedArray()) 2119 } 2120 2121 fun setMessagesReadSince(threadId: Long, sinceTimestamp: Long): List<MarkedMessageInfo> { 2122 var query = """ 2123 $THREAD_ID = ? AND 2124 $STORY_TYPE = 0 AND 2125 $PARENT_STORY_ID <= 0 AND 2126 $LATEST_REVISION_ID IS NULL AND 2127 ( 2128 $READ = 0 OR 2129 ( 2130 $REACTIONS_UNREAD = 1 AND 2131 ($outgoingTypeClause) 2132 ) 2133 ) 2134 """ 2135 2136 val args = mutableListOf(threadId.toString()) 2137 2138 if (sinceTimestamp >= 0L) { 2139 query += " AND $DATE_RECEIVED <= ?" 2140 args += sinceTimestamp.toString() 2141 } 2142 2143 return setMessagesRead(query, args.toTypedArray()) 2144 } 2145 2146 fun setGroupStoryMessagesReadSince(threadId: Long, groupStoryId: Long, sinceTimestamp: Long): List<MarkedMessageInfo> { 2147 var query = """ 2148 $THREAD_ID = ? AND 2149 $STORY_TYPE = 0 AND 2150 $PARENT_STORY_ID = ? AND 2151 ( 2152 $READ = 0 OR 2153 ( 2154 $REACTIONS_UNREAD = 1 AND 2155 ($outgoingTypeClause) 2156 ) 2157 ) 2158 """ 2159 2160 val args = mutableListOf(threadId.toString(), groupStoryId.toString()) 2161 2162 if (sinceTimestamp >= 0L) { 2163 query += " AND $DATE_RECEIVED <= ?" 2164 args += sinceTimestamp.toString() 2165 } 2166 2167 return setMessagesRead(query, args.toTypedArray()) 2168 } 2169 2170 fun getStoryTypes(messageIds: List<MessageId>): List<StoryType> { 2171 if (messageIds.isEmpty()) { 2172 return emptyList() 2173 } 2174 2175 val rawMessageIds: List<Long> = messageIds.map { it.id } 2176 val storyTypes: MutableMap<Long, StoryType> = mutableMapOf() 2177 2178 SqlUtil.buildCollectionQuery(ID, rawMessageIds).forEach { query -> 2179 readableDatabase 2180 .select(ID, STORY_TYPE) 2181 .from(TABLE_NAME) 2182 .where(query.where, query.whereArgs) 2183 .run() 2184 .use { cursor -> 2185 while (cursor.moveToNext()) { 2186 storyTypes[cursor.requireLong(ID)] = fromCode(cursor.requireInt(STORY_TYPE)) 2187 } 2188 } 2189 } 2190 2191 return rawMessageIds.map { id: Long -> 2192 if (storyTypes.containsKey(id)) { 2193 storyTypes[id]!! 2194 } else { 2195 StoryType.NONE 2196 } 2197 } 2198 } 2199 2200 fun setEntireThreadRead(threadId: Long): List<MarkedMessageInfo> { 2201 return setMessagesRead("$THREAD_ID = ? AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0", buildArgs(threadId)) 2202 } 2203 2204 fun setAllMessagesRead(): List<MarkedMessageInfo> { 2205 return setMessagesRead("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($READ = 0 OR ($REACTIONS_UNREAD = 1 AND ($outgoingTypeClause)))", null) 2206 } 2207 2208 protected open fun setMessagesRead(where: String, arguments: Array<String>?): List<MarkedMessageInfo> {//*TM_SA*/ make fun protected open 2209 val releaseChannelId = SignalStore.releaseChannelValues().releaseChannelRecipientId 2210 return writableDatabase.rawQuery( 2211 """ 2212 UPDATE $TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID 2213 SET $READ = 1, $REACTIONS_UNREAD = 0, $REACTIONS_LAST_SEEN = ${System.currentTimeMillis()} 2214 WHERE $where 2215 RETURNING $ID, $FROM_RECIPIENT_ID, $DATE_SENT, $TYPE, $EXPIRES_IN, $EXPIRE_STARTED, $THREAD_ID, $STORY_TYPE 2216 """, 2217 arguments ?: emptyArray() 2218 ).readToList { cursor -> 2219 val threadId = cursor.requireLong(THREAD_ID) 2220 val recipientId = RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)) 2221 val dateSent = cursor.requireLong(DATE_SENT) 2222 val messageId = cursor.requireLong(ID) 2223 val expiresIn = cursor.requireLong(EXPIRES_IN) 2224 val expireStarted = cursor.requireLong(EXPIRE_STARTED) 2225 val syncMessageId = SyncMessageId(recipientId, dateSent) 2226 val expirationInfo = ExpirationInfo(messageId, expiresIn, expireStarted, true) 2227 val storyType = fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)) 2228 2229 if (recipientId != releaseChannelId) { 2230 MarkedMessageInfo(threadId, syncMessageId, MessageId(messageId), expirationInfo, storyType) 2231 } else { 2232 null 2233 } 2234 } 2235 .filterNotNull() 2236 } 2237 2238 fun getOldestUnreadMentionDetails(threadId: Long): Pair<RecipientId, Long>? { 2239 return readableDatabase 2240 .select(FROM_RECIPIENT_ID, DATE_RECEIVED) 2241 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 2242 .where("$THREAD_ID = ? AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $LATEST_REVISION_ID IS NULL AND $READ = 0 AND $MENTIONS_SELF = 1", threadId) 2243 .orderBy("$DATE_RECEIVED ASC") 2244 .limit(1) 2245 .run() 2246 .readToSingleObject { cursor -> 2247 Pair( 2248 RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)), 2249 cursor.requireLong(DATE_RECEIVED) 2250 ) 2251 } 2252 } 2253 2254 fun getUnreadMentionCount(threadId: Long): Int { 2255 return readableDatabase 2256 .count() 2257 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 2258 .where("$THREAD_ID = ? AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $LATEST_REVISION_ID IS NULL AND $READ = 0 AND $MENTIONS_SELF = 1", threadId) 2259 .run() 2260 .readToSingleInt() 2261 } 2262 2263 /** 2264 * Trims data related to expired messages. Only intended to be run after a backup restore. 2265 */ 2266 fun trimEntriesForExpiredMessages() { 2267 val messageDeleteCount = writableDatabase 2268 .delete(TABLE_NAME) 2269 .where("$EXPIRE_STARTED > 0 AND $EXPIRES_IN > 0 AND ($EXPIRE_STARTED + $EXPIRES_IN) < ${System.currentTimeMillis()}") 2270 .run() 2271 2272 Log.d(TAG, "Deleted $messageDeleteCount expired messages after backup.") 2273 2274 writableDatabase 2275 .delete(GroupReceiptTable.TABLE_NAME) 2276 .where("${GroupReceiptTable.MMS_ID} NOT IN (SELECT $ID FROM $TABLE_NAME)") 2277 .run() 2278 2279 readableDatabase 2280 .select(AttachmentTable.ID) 2281 .from(AttachmentTable.TABLE_NAME) 2282 .where("${AttachmentTable.MESSAGE_ID} NOT IN (SELECT $ID FROM $TABLE_NAME)") 2283 .run() 2284 .forEach { cursor -> 2285 attachments.deleteAttachment(AttachmentId(cursor.requireLong(AttachmentTable.ID))) 2286 } 2287 2288 mentions.deleteAbandonedMentions() 2289 2290 readableDatabase 2291 .select(ThreadTable.ID) 2292 .from(ThreadTable.TABLE_NAME) 2293 .where("${ThreadTable.EXPIRES_IN} > 0") 2294 .run() 2295 .forEach { cursor -> 2296 val id = cursor.requireLong(ThreadTable.ID) 2297 threads.setLastScrolled(id, 0) 2298 threads.update(id, false) 2299 } 2300 } 2301 2302 @Throws(MmsException::class, NoSuchMessageException::class) 2303 fun getOutgoingMessage(messageId: Long): OutgoingMessage { 2304 return rawQueryWithAttachments(RAW_ID_WHERE, arrayOf(messageId.toString())).readToSingleObject { cursor -> 2305 val associatedAttachments = attachments.getAttachmentsForMessage(messageId) 2306 val mentions = mentions.getMentionsForMessage(messageId) 2307 val outboxType = cursor.requireLong(TYPE) 2308 val body = cursor.requireString(BODY) 2309 val timestamp = cursor.requireLong(DATE_SENT) 2310 val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID) 2311 val expiresIn = cursor.requireLong(EXPIRES_IN) 2312 val viewOnce = cursor.requireLong(VIEW_ONCE) == 1L 2313 val threadId = cursor.requireLong(THREAD_ID) 2314 val threadRecipient = Recipient.resolved(threads.getRecipientIdForThreadId(threadId)!!) 2315 val distributionType = threads.getDistributionType(threadId) 2316 val storyType = StoryType.fromCode(cursor.requireInt(STORY_TYPE)) 2317 val parentStoryId = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID)) 2318 val messageRangesData = cursor.requireBlob(MESSAGE_RANGES) 2319 val scheduledDate = cursor.requireLong(SCHEDULED_DATE) 2320 val messageExtrasBytes = cursor.requireBlob(MESSAGE_EXTRAS) 2321 val messageExtras = if (messageExtrasBytes != null) MessageExtras.ADAPTER.decode(messageExtrasBytes) else null 2322 2323 val quoteId = cursor.requireLong(QUOTE_ID) 2324 val quoteAuthor = cursor.requireLong(QUOTE_AUTHOR) 2325 val quoteText = cursor.requireString(QUOTE_BODY) 2326 val quoteType = cursor.requireInt(QUOTE_TYPE) 2327 val quoteMissing = cursor.requireBoolean(QUOTE_MISSING) 2328 val quoteAttachments: List<Attachment> = associatedAttachments.filter { it.quote }.toList() 2329 val quoteMentions: List<Mention> = parseQuoteMentions(cursor) 2330 val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor) 2331 val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) { 2332 QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges) 2333 } else { 2334 null 2335 } 2336 2337 val contacts: List<Contact> = getSharedContacts(cursor, associatedAttachments) 2338 val contactAttachments: Set<Attachment> = contacts.mapNotNull { it.avatarAttachment }.toSet() 2339 val previews: List<LinkPreview> = getLinkPreviews(cursor, associatedAttachments) 2340 val previewAttachments: Set<Attachment> = previews.filter { it.thumbnail.isPresent }.map { it.thumbnail.get() }.toSet() 2341 val attachments: List<Attachment> = associatedAttachments 2342 .filterNot { it.quote } 2343 .filterNot { contactAttachments.contains(it) } 2344 .filterNot { previewAttachments.contains(it) } 2345 .sortedWith(DisplayOrderComparator()) 2346 2347 val mismatchDocument = cursor.requireString(MISMATCHED_IDENTITIES) 2348 val mismatches: Set<IdentityKeyMismatch> = if (!TextUtils.isEmpty(mismatchDocument)) { 2349 try { 2350 JsonUtils.fromJson(mismatchDocument, IdentityKeyMismatchSet::class.java).items.toSet() 2351 } catch (e: IOException) { 2352 Log.w(TAG, e) 2353 setOf() 2354 } 2355 } else { 2356 setOf() 2357 } 2358 2359 val networkDocument = cursor.requireString(NETWORK_FAILURES) 2360 val networkFailures: Set<NetworkFailure> = if (!TextUtils.isEmpty(networkDocument)) { 2361 try { 2362 JsonUtils.fromJson(networkDocument, NetworkFailureSet::class.java).items.toSet() 2363 } catch (e: IOException) { 2364 Log.w(TAG, e) 2365 setOf() 2366 } 2367 } else { 2368 setOf() 2369 } 2370 2371 if (body != null && (MessageTypes.isGroupQuit(outboxType) || MessageTypes.isGroupUpdate(outboxType))) { 2372 OutgoingMessage.groupUpdateMessage( 2373 threadRecipient = threadRecipient, 2374 groupContext = if (messageExtras != null) MessageGroupContext(messageExtras, MessageTypes.isGroupV2(outboxType)) else MessageGroupContext(body, MessageTypes.isGroupV2(outboxType)), 2375 avatar = attachments, 2376 sentTimeMillis = timestamp, 2377 expiresIn = 0, 2378 viewOnce = false, 2379 quote = quote, 2380 contacts = contacts, 2381 previews = previews, 2382 mentions = mentions 2383 ) 2384 } else if (MessageTypes.isExpirationTimerUpdate(outboxType)) { 2385 OutgoingMessage.expirationUpdateMessage( 2386 threadRecipient = threadRecipient, 2387 sentTimeMillis = timestamp, 2388 expiresIn = expiresIn 2389 ) 2390 } else if (MessageTypes.isPaymentsNotification(outboxType)) { 2391 OutgoingMessage.paymentNotificationMessage( 2392 threadRecipient = threadRecipient, 2393 paymentUuid = body!!, 2394 sentTimeMillis = timestamp, 2395 expiresIn = expiresIn 2396 ) 2397 } else if (MessageTypes.isPaymentsRequestToActivate(outboxType)) { 2398 OutgoingMessage.requestToActivatePaymentsMessage( 2399 threadRecipient = threadRecipient, 2400 sentTimeMillis = timestamp, 2401 expiresIn = expiresIn 2402 ) 2403 } else if (MessageTypes.isPaymentsActivated(outboxType)) { 2404 OutgoingMessage.paymentsActivatedMessage( 2405 threadRecipient = threadRecipient, 2406 sentTimeMillis = timestamp, 2407 expiresIn = expiresIn 2408 ) 2409 } else if (MessageTypes.isReportedSpam(outboxType)) { 2410 OutgoingMessage.reportSpamMessage( 2411 threadRecipient = threadRecipient, 2412 sentTimeMillis = timestamp, 2413 expiresIn = expiresIn 2414 ) 2415 } else if (MessageTypes.isMessageRequestAccepted(outboxType)) { 2416 OutgoingMessage.messageRequestAcceptMessage( 2417 threadRecipient = threadRecipient, 2418 sentTimeMillis = timestamp, 2419 expiresIn = expiresIn 2420 ) 2421 } else { 2422 val giftBadge: GiftBadge? = if (body != null && MessageTypes.isGiftBadge(outboxType)) { 2423 GiftBadge.ADAPTER.decode(Base64.decode(body)) 2424 } else { 2425 null 2426 } 2427 2428 val messageRanges: BodyRangeList? = if (messageRangesData != null) { 2429 try { 2430 BodyRangeList.ADAPTER.decode(messageRangesData) 2431 } catch (e: IOException) { 2432 Log.w(TAG, "Error parsing message ranges", e) 2433 null 2434 } 2435 } else { 2436 null 2437 } 2438 2439 val editedMessage = getOriginalEditedMessageRecord(messageId) 2440 2441 OutgoingMessage( 2442 recipient = threadRecipient, 2443 body = body, 2444 attachments = attachments, 2445 timestamp = timestamp, 2446 expiresIn = expiresIn, 2447 viewOnce = viewOnce, 2448 distributionType = distributionType, 2449 storyType = storyType, 2450 parentStoryId = parentStoryId, 2451 isStoryReaction = MessageTypes.isStoryReaction(outboxType), 2452 quote = quote, 2453 contacts = contacts, 2454 previews = previews, 2455 mentions = mentions, 2456 networkFailures = networkFailures, 2457 mismatches = mismatches, 2458 giftBadge = giftBadge, 2459 isSecure = MessageTypes.isSecureType(outboxType), 2460 bodyRanges = messageRanges, 2461 scheduledDate = scheduledDate, 2462 messageToEdit = editedMessage 2463 ) 2464 } 2465 } ?: throw NoSuchMessageException("No record found for id: $messageId") 2466 } 2467 2468 @JvmOverloads 2469 @Throws(MmsException::class) 2470 open fun insertMessageInbox(//*TM_SA*/ make fun open 2471 retrieved: IncomingMessage, 2472 candidateThreadId: Long = -1, 2473 editedMessage: MmsMessageRecord? = null, 2474 notifyObservers: Boolean = true 2475 ): Optional<InsertResult> { 2476 val type = retrieved.toMessageType() 2477 2478 val threadIdResult = if (candidateThreadId == -1L || retrieved.isGroupMessage) { 2479 getThreadIdFor(retrieved) 2480 } else { 2481 ThreadTable.ThreadIdResult(threadId = candidateThreadId, newlyCreated = false) 2482 } 2483 val threadId = threadIdResult.threadId 2484 2485 if (retrieved.type == MessageType.GROUP_UPDATE && retrieved.groupContext?.let { GroupV2UpdateMessageUtil.isJoinRequestCancel(it) } == true) { 2486 val result = collapseJoinRequestEventsIfPossible(threadId, retrieved) 2487 if (result.isPresent) { 2488 Log.d(TAG, "[insertMessageInbox] Collapsed join request events.") 2489 return result 2490 } 2491 } 2492 2493 val silent = MessageTypes.isGroupUpdate(type) || 2494 retrieved.type == MessageType.IDENTITY_DEFAULT || 2495 retrieved.type == MessageType.IDENTITY_VERIFIED || 2496 retrieved.type == MessageType.IDENTITY_UPDATE 2497 2498 val read = silent || retrieved.type == MessageType.EXPIRATION_UPDATE 2499 2500 val contentValues = contentValuesOf( 2501 DATE_SENT to retrieved.sentTimeMillis, 2502 DATE_SERVER to retrieved.serverTimeMillis, 2503 FROM_RECIPIENT_ID to retrieved.from.serialize(), 2504 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 2505 TYPE to type, 2506 THREAD_ID to threadId, 2507 MMS_STATUS to MmsStatus.DOWNLOAD_INITIALIZED, 2508 DATE_RECEIVED to retrieved.receivedTimeMillis, 2509 SMS_SUBSCRIPTION_ID to retrieved.subscriptionId, 2510 EXPIRES_IN to retrieved.expiresIn, 2511 VIEW_ONCE to if (retrieved.isViewOnce) 1 else 0, 2512 STORY_TYPE to retrieved.storyType.code, 2513 PARENT_STORY_ID to if (retrieved.parentStoryId != null) retrieved.parentStoryId.serialize() else 0, 2514 READ to read.toInt(), 2515 UNIDENTIFIED to retrieved.isUnidentified, 2516 SERVER_GUID to retrieved.serverGuid, 2517 LATEST_REVISION_ID to null, 2518 ORIGINAL_MESSAGE_ID to editedMessage?.getOriginalOrOwnMessageId()?.id, 2519 REVISION_NUMBER to (editedMessage?.revisionNumber?.inc() ?: 0), 2520 MESSAGE_EXTRAS to (retrieved.messageExtras?.encode()) 2521 ) 2522 2523 val quoteAttachments: MutableList<Attachment> = mutableListOf() 2524 if (retrieved.quote != null) { 2525 contentValues.put(QUOTE_ID, retrieved.quote.id) 2526 contentValues.put(QUOTE_BODY, retrieved.quote.text) 2527 contentValues.put(QUOTE_AUTHOR, retrieved.quote.author.serialize()) 2528 contentValues.put(QUOTE_TYPE, retrieved.quote.type.code) 2529 contentValues.put(QUOTE_MISSING, if (retrieved.quote.isOriginalMissing) 1 else 0) 2530 2531 val quoteBodyRanges: BodyRangeList.Builder = retrieved.quote.bodyRanges?.newBuilder() ?: BodyRangeList.Builder() 2532 val mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.quote.mentions) 2533 2534 if (mentionsList != null) { 2535 quoteBodyRanges.ranges += mentionsList.ranges 2536 } 2537 2538 if (quoteBodyRanges.ranges.isNotEmpty()) { 2539 contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().encode()) 2540 } 2541 2542 quoteAttachments += retrieved.quote.attachments 2543 } 2544 2545 val (messageId, insertedAttachments) = insertMediaMessage( 2546 threadId = threadId, 2547 body = retrieved.body, 2548 attachments = retrieved.attachments, 2549 quoteAttachments = quoteAttachments, 2550 sharedContacts = retrieved.sharedContacts, 2551 linkPreviews = retrieved.linkPreviews, 2552 mentions = retrieved.mentions, 2553 messageRanges = retrieved.messageRanges, 2554 contentValues = contentValues, 2555 insertListener = null, 2556 updateThread = retrieved.storyType === StoryType.NONE && !silent, 2557 unarchive = true 2558 ) 2559 2560 if (messageId < 0) { 2561 Log.w(TAG, "Failed to insert media message (${retrieved.sentTimeMillis}, ${retrieved.from}, ThreadId::$threadId})! Likely a duplicate.") 2562 return Optional.empty() 2563 } 2564 2565 if (editedMessage != null) { 2566 if (retrieved.quote != null && editedMessage.quote != null) { 2567 writableDatabase.execSQL( 2568 """ 2569 WITH o as (SELECT $QUOTE_ID, $QUOTE_AUTHOR, $QUOTE_BODY, $QUOTE_TYPE, $QUOTE_MISSING, $QUOTE_BODY_RANGES FROM $TABLE_NAME WHERE $ID = ${editedMessage.id}) 2570 UPDATE $TABLE_NAME 2571 SET $QUOTE_ID = old.$QUOTE_ID, $QUOTE_AUTHOR = old.$QUOTE_AUTHOR, $QUOTE_BODY = old.$QUOTE_BODY, $QUOTE_TYPE = old.$QUOTE_TYPE, $QUOTE_MISSING = old.$QUOTE_MISSING, $QUOTE_BODY_RANGES = old.$QUOTE_BODY_RANGES 2572 FROM o old 2573 WHERE $TABLE_NAME.$ID = $messageId 2574 """ 2575 ) 2576 } 2577 } 2578 2579 if (retrieved.attachments.isEmpty() && editedMessage?.id != null && attachments.getAttachmentsForMessage(editedMessage.id).isNotEmpty()) { 2580 val linkPreviewAttachmentIds = editedMessage.linkPreviews.mapNotNull { it.attachmentId?.id }.toSet() 2581 attachments.duplicateAttachmentsForMessage(messageId, editedMessage.id, linkPreviewAttachmentIds) 2582 } 2583 2584 val isNotStoryGroupReply = retrieved.parentStoryId == null || !retrieved.parentStoryId.isGroupReply() 2585 2586 if (!MessageTypes.isPaymentsActivated(type) && 2587 !MessageTypes.isPaymentsRequestToActivate(type) && 2588 !MessageTypes.isReportedSpam(type) && 2589 !MessageTypes.isMessageRequestAccepted(type) && 2590 !MessageTypes.isExpirationTimerUpdate(type) && 2591 !retrieved.storyType.isStory && 2592 isNotStoryGroupReply && 2593 !silent 2594 ) { 2595 val incrementUnreadMentions = retrieved.mentions.isNotEmpty() && retrieved.mentions.any { it.recipientId == Recipient.self().id } 2596 threads.incrementUnread(threadId, 1, if (incrementUnreadMentions) 1 else 0) 2597 ThreadUpdateJob.enqueue(threadId) 2598 } 2599 2600 if (notifyObservers) { 2601 notifyConversationListeners(threadId) 2602 } 2603 2604 if (retrieved.storyType.isStory) { 2605 ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(threads.getRecipientIdForThreadId(threadId)!!) 2606 } 2607 2608 return Optional.of( 2609 InsertResult( 2610 messageId = messageId, 2611 threadId = threadId, 2612 threadWasNewlyCreated = threadIdResult.newlyCreated, 2613 insertedAttachments = insertedAttachments 2614 ) 2615 ) 2616 } 2617 2618 fun insertChatSessionRefreshedMessage(recipientId: RecipientId, senderDeviceId: Long, sentTimestamp: Long): InsertResult { 2619 val recipient = Recipient.resolved(recipientId) 2620 val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup) 2621 val threadId = threadIdResult.threadId 2622 var type = MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT 2623 type = type and MessageTypes.TOTAL_MASK - MessageTypes.ENCRYPTION_MASK or MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT 2624 2625 val messageId = writableDatabase 2626 .insertInto(TABLE_NAME) 2627 .values( 2628 FROM_RECIPIENT_ID to recipientId.serialize(), 2629 FROM_DEVICE_ID to senderDeviceId, 2630 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 2631 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 2632 DATE_RECEIVED to System.currentTimeMillis(), 2633 DATE_SENT to sentTimestamp, 2634 DATE_SERVER to -1, 2635 READ to 0, 2636 TYPE to type, 2637 THREAD_ID to threadId 2638 ) 2639 .run() 2640 2641 threads.incrementUnread(threadId, 1, 0) 2642 threads.update(threadId, true) 2643 2644 notifyConversationListeners(threadId) 2645 TrimThreadJob.enqueueAsync(threadId) 2646 2647 return InsertResult( 2648 messageId = messageId, 2649 threadId = threadId, 2650 threadWasNewlyCreated = threadIdResult.newlyCreated 2651 ) 2652 } 2653 2654 fun insertBadDecryptMessage(recipientId: RecipientId, senderDevice: Int, sentTimestamp: Long, receivedTimestamp: Long, threadId: Long) { 2655 writableDatabase 2656 .insertInto(TABLE_NAME) 2657 .values( 2658 FROM_RECIPIENT_ID to recipientId.serialize(), 2659 FROM_DEVICE_ID to senderDevice, 2660 TO_RECIPIENT_ID to Recipient.self().id.serialize(), 2661 DATE_SENT to sentTimestamp, 2662 DATE_RECEIVED to receivedTimestamp, 2663 DATE_SERVER to -1, 2664 READ to 0, 2665 TYPE to MessageTypes.BAD_DECRYPT_TYPE, 2666 THREAD_ID to threadId 2667 ) 2668 .run() 2669 2670 threads.incrementUnread(threadId, 1, 0) 2671 threads.update(threadId, true) 2672 2673 notifyConversationListeners(threadId) 2674 TrimThreadJob.enqueueAsync(threadId) 2675 } 2676 2677 fun markGiftRedemptionCompleted(messageId: Long) { 2678 markGiftRedemptionState(messageId, GiftBadge.RedemptionState.REDEEMED) 2679 } 2680 2681 fun markGiftRedemptionStarted(messageId: Long) { 2682 markGiftRedemptionState(messageId, GiftBadge.RedemptionState.STARTED) 2683 } 2684 2685 fun markGiftRedemptionFailed(messageId: Long) { 2686 markGiftRedemptionState(messageId, GiftBadge.RedemptionState.FAILED) 2687 } 2688 2689 private fun markGiftRedemptionState(messageId: Long, redemptionState: GiftBadge.RedemptionState) { 2690 var updated = false 2691 var threadId: Long = -1 2692 2693 writableDatabase.withinTransaction { db -> 2694 db.select(BODY, THREAD_ID) 2695 .from(TABLE_NAME) 2696 .where("($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} = ${MessageTypes.SPECIAL_TYPE_GIFT_BADGE}) AND $ID = ?", messageId) 2697 .run() 2698 .use { cursor -> 2699 if (cursor.moveToFirst()) { 2700 val giftBadge = GiftBadge.ADAPTER.decode(Base64.decode(cursor.requireNonNullString(BODY))) 2701 val updatedBadge = giftBadge.newBuilder().redemptionState(redemptionState).build() 2702 2703 updated = db 2704 .update(TABLE_NAME) 2705 .values(BODY to Base64.encodeWithPadding(updatedBadge.encode())) 2706 .where("$ID = ?", messageId) 2707 .run() > 0 2708 2709 threadId = cursor.requireLong(THREAD_ID) 2710 } 2711 } 2712 } 2713 2714 if (updated) { 2715 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId)) 2716 notifyConversationListeners(threadId) 2717 } 2718 } 2719 2720 @Throws(MmsException::class) 2721 fun insertMessageOutbox( 2722 message: OutgoingMessage, 2723 threadId: Long, 2724 forceSms: Boolean, 2725 insertListener: InsertListener? 2726 ): Long { 2727 return insertMessageOutbox( 2728 message = message, 2729 threadId = threadId, 2730 forceSms = forceSms, 2731 defaultReceiptStatus = GroupReceiptTable.STATUS_UNDELIVERED, 2732 insertListener = insertListener 2733 ) 2734 } 2735 2736 @Throws(MmsException::class) 2737 open fun insertMessageOutbox(//*TM_SA*/ make fun open 2738 message: OutgoingMessage, 2739 threadId: Long, 2740 forceSms: Boolean, 2741 defaultReceiptStatus: Int, 2742 insertListener: InsertListener? 2743 ): Long { 2744 var type = MessageTypes.BASE_SENDING_TYPE 2745 var hasSpecialType = false 2746 2747 if (message.isSecure) { 2748 type = type or (MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT) 2749 } 2750 2751 if (forceSms) { 2752 type = type or MessageTypes.MESSAGE_FORCE_SMS_BIT 2753 } 2754 2755 if (message.isSecure) { 2756 type = type or (MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT) 2757 } else if (message.isEndSession) { 2758 type = type or MessageTypes.END_SESSION_BIT 2759 } 2760 2761 if (message.isIdentityVerified) { 2762 type = type or MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT 2763 } else if (message.isIdentityDefault) { 2764 type = type or MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT 2765 } 2766 2767 if (message.isGroup) { 2768 if (message.isV2Group) { 2769 type = type or (MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT) 2770 2771 if (message.isJustAGroupLeave) { 2772 type = type or MessageTypes.GROUP_LEAVE_BIT 2773 } 2774 } else { 2775 val properties = message.requireGroupV1Properties() 2776 2777 if (properties.isUpdate) { 2778 type = type or MessageTypes.GROUP_UPDATE_BIT 2779 } else if (properties.isQuit) { 2780 type = type or MessageTypes.GROUP_LEAVE_BIT 2781 } 2782 } 2783 } 2784 2785 if (message.isExpirationUpdate) { 2786 type = type or MessageTypes.EXPIRATION_TIMER_UPDATE_BIT 2787 } 2788 2789 if (message.isStoryReaction) { 2790 type = type or MessageTypes.SPECIAL_TYPE_STORY_REACTION 2791 hasSpecialType = true 2792 } 2793 2794 if (message.giftBadge != null) { 2795 if (hasSpecialType) { 2796 throw MmsException("Cannot insert message with multiple special types.") 2797 } 2798 type = type or MessageTypes.SPECIAL_TYPE_GIFT_BADGE 2799 hasSpecialType = true 2800 } 2801 2802 if (message.isPaymentsNotification) { 2803 if (hasSpecialType) { 2804 throw MmsException("Cannot insert message with multiple special types.") 2805 } 2806 type = type or MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION 2807 hasSpecialType = true 2808 } 2809 2810 if (message.isRequestToActivatePayments) { 2811 if (hasSpecialType) { 2812 throw MmsException("Cannot insert message with multiple special types.") 2813 } 2814 type = type or MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST 2815 hasSpecialType = true 2816 } 2817 2818 if (message.isPaymentsActivated) { 2819 if (hasSpecialType) { 2820 throw MmsException("Cannot insert message with multiple special types.") 2821 } 2822 type = type or MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED 2823 hasSpecialType = true 2824 } 2825 2826 if (message.isReportSpam) { 2827 if (hasSpecialType) { 2828 throw MmsException("Cannot insert message with multiple special types.") 2829 } 2830 type = type or MessageTypes.SPECIAL_TYPE_REPORTED_SPAM 2831 hasSpecialType = true 2832 } 2833 2834 if (message.isMessageRequestAccept) { 2835 if (hasSpecialType) { 2836 throw MmsException("Cannot insert message with multiple special types.") 2837 } 2838 type = type or MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED 2839 hasSpecialType = true 2840 } 2841 2842 val earlyDeliveryReceipts: Map<RecipientId, Receipt> = earlyDeliveryReceiptCache.remove(message.sentTimeMillis) 2843 2844 if (earlyDeliveryReceipts.isNotEmpty()) { 2845 Log.w(TAG, "Found early delivery receipts for " + message.sentTimeMillis + ". Applying them.") 2846 } 2847 2848 var editedMessage: MessageRecord? = null 2849 if (message.isMessageEdit) { 2850 try { 2851 editedMessage = getMessageRecord(message.messageToEdit) 2852 if (!MessageConstraintsUtil.isValidEditMessageSend(editedMessage)) { 2853 throw MmsException("Message is not valid to edit") 2854 } 2855 } catch (e: NoSuchMessageException) { 2856 throw MmsException("Unable to locate edited message", e) 2857 } 2858 } 2859 2860 val contentValues = ContentValues() 2861 contentValues.put(DATE_SENT, message.sentTimeMillis) 2862 contentValues.put(TYPE, type) 2863 contentValues.put(THREAD_ID, threadId) 2864 contentValues.put(READ, 1) 2865 contentValues.put(DATE_RECEIVED, editedMessage?.dateReceived ?: System.currentTimeMillis()) 2866 contentValues.put(SMS_SUBSCRIPTION_ID, message.subscriptionId) 2867 contentValues.put(EXPIRES_IN, editedMessage?.expiresIn ?: message.expiresIn) 2868 contentValues.put(VIEW_ONCE, message.isViewOnce) 2869 contentValues.put(FROM_RECIPIENT_ID, Recipient.self().id.serialize()) 2870 contentValues.put(FROM_DEVICE_ID, SignalStore.account().deviceId) 2871 contentValues.put(TO_RECIPIENT_ID, message.threadRecipient.id.serialize()) 2872 contentValues.put(HAS_DELIVERY_RECEIPT, earlyDeliveryReceipts.values.sumOf { it.count }) 2873 contentValues.put(RECEIPT_TIMESTAMP, earlyDeliveryReceipts.values.map { it.timestamp }.maxOrNull() ?: -1L) 2874 contentValues.put(STORY_TYPE, message.storyType.code) 2875 contentValues.put(PARENT_STORY_ID, if (message.parentStoryId != null) message.parentStoryId.serialize() else 0) 2876 contentValues.put(SCHEDULED_DATE, message.scheduledDate) 2877 contentValues.putNull(LATEST_REVISION_ID) 2878 contentValues.put(MESSAGE_EXTRAS, message.messageExtras?.encode()) 2879 2880 if (editedMessage != null) { 2881 contentValues.put(ORIGINAL_MESSAGE_ID, editedMessage.getOriginalOrOwnMessageId().id) 2882 contentValues.put(REVISION_NUMBER, editedMessage.revisionNumber + 1) 2883 } else { 2884 contentValues.putNull(ORIGINAL_MESSAGE_ID) 2885 } 2886 2887 if (message.threadRecipient.isSelf && hasAudioAttachment(message.attachments)) { 2888 contentValues.put(VIEWED_COLUMN, 1L) 2889 } 2890 2891 val quoteAttachments: MutableList<Attachment> = mutableListOf() 2892 2893 if (message.outgoingQuote != null) { 2894 val updated = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.outgoingQuote.text, message.outgoingQuote.mentions) 2895 2896 contentValues.put(QUOTE_ID, message.outgoingQuote.id) 2897 contentValues.put(QUOTE_AUTHOR, message.outgoingQuote.author.serialize()) 2898 contentValues.put(QUOTE_BODY, updated.bodyAsString) 2899 contentValues.put(QUOTE_TYPE, message.outgoingQuote.type.code) 2900 contentValues.put(QUOTE_MISSING, if (message.outgoingQuote.isOriginalMissing) 1 else 0) 2901 2902 val adjustedQuoteBodyRanges = message.outgoingQuote.bodyRanges.adjustBodyRanges(updated.bodyAdjustments) 2903 val quoteBodyRanges: BodyRangeList.Builder = if (adjustedQuoteBodyRanges != null) { 2904 adjustedQuoteBodyRanges.newBuilder() 2905 } else { 2906 BodyRangeList.Builder() 2907 } 2908 2909 val mentionsList = MentionUtil.mentionsToBodyRangeList(updated.mentions) 2910 if (mentionsList != null) { 2911 quoteBodyRanges.ranges += mentionsList.ranges 2912 } 2913 2914 if (quoteBodyRanges.ranges.isNotEmpty()) { 2915 contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().encode()) 2916 } 2917 2918 if (editedMessage == null) { 2919 quoteAttachments += message.outgoingQuote.attachments 2920 } 2921 } 2922 2923 val updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.body, message.mentions) 2924 val bodyRanges = message.bodyRanges.adjustBodyRanges(updatedBodyAndMentions.bodyAdjustments) 2925 val (messageId, insertedAttachments) = insertMediaMessage( 2926 threadId = threadId, 2927 body = updatedBodyAndMentions.bodyAsString, 2928 attachments = message.attachments, 2929 quoteAttachments = quoteAttachments, 2930 sharedContacts = message.sharedContacts, 2931 linkPreviews = message.linkPreviews, 2932 mentions = updatedBodyAndMentions.mentions, 2933 messageRanges = bodyRanges, 2934 contentValues = contentValues, 2935 insertListener = insertListener, 2936 updateThread = false, 2937 unarchive = false 2938 ) 2939 2940 if (messageId < 0) { 2941 throw MmsException("Failed to insert message! Likely a duplicate.") 2942 } 2943 2944 if (message.threadRecipient.isGroup) { 2945 val members: MutableSet<RecipientId> = mutableSetOf() 2946 2947 if (message.isGroupUpdate && message.isV2Group) { 2948 members += message.requireGroupV2Properties().allActivePendingAndRemovedMembers 2949 .distinct() 2950 .map { serviceId -> RecipientId.from(serviceId) } 2951 .toList() 2952 2953 members -= Recipient.self().id 2954 } else { 2955 members += groups.getGroupMembers(message.threadRecipient.requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF).map { it.id } 2956 } 2957 2958 groupReceipts.insert(members, messageId, defaultReceiptStatus, message.sentTimeMillis) 2959 2960 for (recipientId in earlyDeliveryReceipts.keys) { 2961 groupReceipts.update(recipientId, messageId, GroupReceiptTable.STATUS_DELIVERED, -1) 2962 } 2963 } else if (message.threadRecipient.isDistributionList) { 2964 val members = distributionLists.getMembers(message.threadRecipient.requireDistributionListId()) 2965 2966 groupReceipts.insert(members, messageId, defaultReceiptStatus, message.sentTimeMillis) 2967 2968 for (recipientId in earlyDeliveryReceipts.keys) { 2969 groupReceipts.update(recipientId, messageId, GroupReceiptTable.STATUS_DELIVERED, -1) 2970 } 2971 } 2972 2973 if (message.messageToEdit > 0) { 2974 writableDatabase.update(TABLE_NAME) 2975 .values(LATEST_REVISION_ID to messageId) 2976 .where("$ID_WHERE OR $LATEST_REVISION_ID = ?", message.messageToEdit, message.messageToEdit) 2977 .run() 2978 2979 val textAttachments = (editedMessage as? MmsMessageRecord)?.slideDeck?.asAttachments()?.filter { it.contentType == MediaUtil.LONG_TEXT }?.mapNotNull { (it as? DatabaseAttachment)?.attachmentId?.id } ?: emptyList() 2980 val linkPreviewAttachments = (editedMessage as? MmsMessageRecord)?.linkPreviews?.mapNotNull { it.attachmentId?.id } ?: emptyList() 2981 val excludeIds = HashSet<Long>() 2982 excludeIds += textAttachments 2983 excludeIds += linkPreviewAttachments 2984 attachments.duplicateAttachmentsForMessage(messageId, message.messageToEdit, excludeIds) 2985 2986 reactions.moveReactionsToNewMessage(messageId, message.messageToEdit) 2987 } 2988 2989 threads.updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId) 2990 2991 if (!message.storyType.isStory) { 2992 if (message.outgoingQuote == null && editedMessage == null) { 2993 ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, MessageId(messageId)) 2994 } else { 2995 ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId) 2996 } 2997 2998 if (message.scheduledDate != -1L) { 2999 ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId) 3000 } 3001 } else { 3002 ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.threadRecipient.id) 3003 } 3004 3005 if (!message.isIdentityVerified && !message.isIdentityDefault) { 3006 ThreadUpdateJob.enqueue(threadId) 3007 } 3008 3009 TrimThreadJob.enqueueAsync(threadId) 3010 3011 return messageId 3012 } 3013 3014 private fun hasAudioAttachment(attachments: List<Attachment>): Boolean { 3015 return attachments.any { MediaUtil.isAudio(it) } 3016 } 3017 3018 @Throws(MmsException::class) 3019 private fun insertMediaMessage( 3020 threadId: Long, 3021 body: String?, 3022 attachments: List<Attachment>, 3023 quoteAttachments: List<Attachment>, 3024 sharedContacts: List<Contact>, 3025 linkPreviews: List<LinkPreview>, 3026 mentions: List<Mention>, 3027 messageRanges: BodyRangeList?, 3028 contentValues: ContentValues, 3029 insertListener: InsertListener?, 3030 updateThread: Boolean, 3031 unarchive: Boolean 3032 ): kotlin.Pair<Long, Map<Attachment, AttachmentId>?> { 3033 val mentionsSelf = mentions.any { Recipient.resolved(it.recipientId).isSelf } 3034 val allAttachments: MutableList<Attachment> = mutableListOf() 3035 3036 allAttachments += attachments 3037 allAttachments += sharedContacts.mapNotNull { it.avatarAttachment } 3038 allAttachments += linkPreviews.mapNotNull { it.thumbnail.orElse(null) } 3039 3040 contentValues.put(BODY, body) 3041 contentValues.put(MENTIONS_SELF, if (mentionsSelf) 1 else 0) 3042 if (messageRanges != null) { 3043 contentValues.put(MESSAGE_RANGES, messageRanges.encode()) 3044 } 3045 3046 val (messageId, insertedAttachments) = writableDatabase.withinTransaction { db -> 3047 val messageId = db.insert(TABLE_NAME, null, contentValues) 3048 if (messageId < 0) { 3049 Log.w(TAG, "Tried to insert media message but failed. Assuming duplicate.") 3050 return@withinTransaction -1L to null 3051 } 3052 3053 threads.markAsActiveEarly(threadId) 3054 SignalDatabase.mentions.insert(threadId, messageId, mentions) 3055 3056 val insertedAttachments = SignalDatabase.attachments.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments) 3057 val serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts) 3058 val serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews) 3059 3060 if (!TextUtils.isEmpty(serializedContacts)) { 3061 val rows = db 3062 .update(TABLE_NAME) 3063 .values(SHARED_CONTACTS to serializedContacts) 3064 .where("$ID = ?", messageId) 3065 .run() 3066 3067 if (rows <= 0) { 3068 Log.w(TAG, "Failed to update message with shared contact data.") 3069 } 3070 } 3071 3072 if (!TextUtils.isEmpty(serializedPreviews)) { 3073 val rows = db 3074 .update(TABLE_NAME) 3075 .values(LINK_PREVIEWS to serializedPreviews) 3076 .where("$ID = ?", messageId) 3077 .run() 3078 3079 if (rows <= 0) { 3080 Log.w(TAG, "Failed to update message with link preview data.") 3081 } 3082 } 3083 3084 messageId to insertedAttachments 3085 } 3086 3087 if (messageId < 0) { 3088 return messageId to insertedAttachments 3089 } 3090 3091 insertListener?.onComplete() 3092 3093 val contentValuesThreadId = contentValues.getAsLong(THREAD_ID) 3094 3095 if (updateThread) { 3096 threads.setLastScrolled(contentValuesThreadId, 0) 3097 threads.update(threadId, unarchive) 3098 } 3099 3100 return messageId to insertedAttachments 3101 } 3102 3103 /** 3104 * Deletes the call updates specified in the messageIds set. 3105 */ 3106 fun deleteCallUpdates(messageIds: Set<Long>): Int { 3107 return deleteCallUpdatesInternal(messageIds, SqlUtil.CollectionOperator.IN) 3108 } 3109 3110 private fun deleteCallUpdatesInternal( 3111 messageIds: Set<Long>, 3112 collectionOperator: SqlUtil.CollectionOperator 3113 ): Int { 3114 var rowsDeleted = 0 3115 val threadIds: Set<Long> = writableDatabase.withinTransaction { 3116 SqlUtil.buildCollectionQuery( 3117 column = ID, 3118 values = messageIds, 3119 prefix = "$IS_CALL_TYPE_CLAUSE AND ", 3120 collectionOperator = collectionOperator 3121 ).map { query -> 3122 val threadSet = writableDatabase.select(THREAD_ID) 3123 .from(TABLE_NAME) 3124 .where(query.where, query.whereArgs) 3125 .run() 3126 .readToSet { cursor -> 3127 cursor.requireLong(THREAD_ID) 3128 } 3129 3130 val rows = writableDatabase 3131 .delete(TABLE_NAME) 3132 .where(query.where, query.whereArgs) 3133 .run() 3134 3135 if (rows <= 0) { 3136 Log.w(TAG, "Failed to delete some rows during call update deletion.") 3137 } 3138 3139 rowsDeleted += rows 3140 threadSet 3141 }.flatten().toSet() 3142 } 3143 3144 threadIds.forEach { 3145 threads.update( 3146 threadId = it, 3147 unarchive = false, 3148 allowDeletion = true 3149 ) 3150 } 3151 3152 notifyConversationListeners(threadIds) 3153 notifyConversationListListeners() 3154 return rowsDeleted 3155 } 3156 3157 fun deleteMessage(messageId: Long): Boolean { 3158 val threadId = getThreadIdForMessage(messageId) 3159 return deleteMessage(messageId, threadId) 3160 } 3161 3162 protected open fun deleteMessage(messageId: Long, threadId: Long = getThreadIdForMessage(messageId), notify: Boolean = true, updateThread: Boolean = true): Boolean {//*TM_SA*/ make fun protected open 3163 Log.d(TAG, "deleteMessage($messageId)") 3164 3165 attachments.deleteAttachmentsForMessage(messageId) 3166 groupReceipts.deleteRowsForMessage(messageId) 3167 mentions.deleteMentionsForMessage(messageId) 3168 3169 writableDatabase 3170 .delete(TABLE_NAME) 3171 .where("$ID = ?", messageId) 3172 .run() 3173 3174 calls.updateCallEventDeletionTimestamps() 3175 threads.setLastScrolled(threadId, 0) 3176 3177 val threadDeleted = if (updateThread) { 3178 threads.update(threadId, false) 3179 } else { 3180 false 3181 } 3182 3183 if (notify) { 3184 notifyConversationListeners(threadId) 3185 notifyStickerListeners() 3186 notifyStickerPackListeners() 3187 OptimizeMessageSearchIndexJob.enqueue() 3188 } 3189 3190 return threadDeleted 3191 } 3192 3193 fun deleteScheduledMessage(messageId: Long) { 3194 Log.d(TAG, "deleteScheduledMessage($messageId)") 3195 3196 val threadId = getThreadIdForMessage(messageId) 3197 3198 writableDatabase.withinTransaction { db -> 3199 val rowsUpdated = db 3200 .update(TABLE_NAME) 3201 .values( 3202 SCHEDULED_DATE to -1, 3203 DATE_SENT to System.currentTimeMillis(), 3204 DATE_RECEIVED to System.currentTimeMillis() 3205 ) 3206 .where("$ID = ? AND $SCHEDULED_DATE != ?", messageId, -1) 3207 .run() 3208 3209 if (rowsUpdated > 0) { 3210 deleteMessage(messageId, threadId) 3211 } else { 3212 Log.w(TAG, "tried to delete scheduled message but it may have already been sent") 3213 } 3214 } 3215 3216 ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary() 3217 ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId) 3218 } 3219 3220 fun deleteScheduledMessages(recipientId: RecipientId) { 3221 Log.d(TAG, "deleteScheduledMessages($recipientId)") 3222 3223 val threadId: Long = threads.getThreadIdFor(recipientId) ?: return Log.i(TAG, "No thread exists for $recipientId") 3224 3225 writableDatabase.withinTransaction { 3226 val scheduledMessages = getScheduledMessagesInThread(threadId) 3227 for (record in scheduledMessages) { 3228 deleteScheduledMessage(record.id) 3229 } 3230 } 3231 } 3232 3233 fun deleteThread(threadId: Long) { 3234 Log.d(TAG, "deleteThread($threadId)") 3235 deleteThreads(setOf(threadId)) 3236 } 3237 3238 private fun getSerializedSharedContacts(insertedAttachmentIds: Map<Attachment, AttachmentId>, contacts: List<Contact>): String? { 3239 if (contacts.isEmpty()) { 3240 return null 3241 } 3242 3243 val sharedContactJson = JSONArray() 3244 3245 for (contact in contacts) { 3246 try { 3247 val attachmentId: AttachmentId? = if (contact.avatarAttachment != null) { 3248 insertedAttachmentIds[contact.avatarAttachment] 3249 } else { 3250 null 3251 } 3252 3253 val updatedAvatar = Contact.Avatar( 3254 attachmentId, 3255 contact.avatarAttachment, 3256 contact.avatar != null && contact.avatar!!.isProfile 3257 ) 3258 3259 val updatedContact = Contact(contact, updatedAvatar) 3260 3261 sharedContactJson.put(JSONObject(updatedContact.serialize())) 3262 } catch (e: JSONException) { 3263 Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e) 3264 } catch (e: IOException) { 3265 Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e) 3266 } 3267 } 3268 3269 return sharedContactJson.toString() 3270 } 3271 3272 private fun getSerializedLinkPreviews(insertedAttachmentIds: Map<Attachment, AttachmentId>, previews: List<LinkPreview>): String? { 3273 if (previews.isEmpty()) { 3274 return null 3275 } 3276 3277 val linkPreviewJson = JSONArray() 3278 3279 for (preview in previews) { 3280 try { 3281 val attachmentId: AttachmentId? = if (preview.thumbnail.isPresent) { 3282 insertedAttachmentIds[preview.thumbnail.get()] 3283 } else { 3284 null 3285 } 3286 3287 val updatedPreview = LinkPreview(preview.url, preview.title, preview.description, preview.date, attachmentId) 3288 linkPreviewJson.put(JSONObject(updatedPreview.serialize())) 3289 } catch (e: JSONException) { 3290 Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e) 3291 } catch (e: IOException) { 3292 Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e) 3293 } 3294 } 3295 3296 return linkPreviewJson.toString() 3297 } 3298 3299 fun isSent(messageId: Long): Boolean { 3300 val type = readableDatabase 3301 .select(TYPE) 3302 .from(TABLE_NAME) 3303 .where("$ID = ?", messageId) 3304 .run() 3305 .readToSingleLong() 3306 3307 return MessageTypes.isSentType(type) 3308 } 3309 3310 fun getProfileChangeDetailsRecords(threadId: Long, afterTimestamp: Long): List<MessageRecord> { 3311 val cursor = readableDatabase 3312 .select(*MMS_PROJECTION) 3313 .from(TABLE_NAME) 3314 .where("$THREAD_ID = ? AND $DATE_RECEIVED >= ? AND $TYPE = ?", threadId, afterTimestamp, MessageTypes.PROFILE_CHANGE_TYPE) 3315 .orderBy("$ID DESC") 3316 .run() 3317 3318 return mmsReaderFor(cursor).use { reader -> 3319 reader.filterNotNull() 3320 } 3321 } 3322 3323 fun getAllRateLimitedMessageIds(): Set<Long> { 3324 val db = databaseHelper.signalReadableDatabase 3325 val where = "(" + TYPE + " & " + MessageTypes.TOTAL_MASK + " & " + MessageTypes.MESSAGE_RATE_LIMITED_BIT + ") > 0" 3326 val ids: MutableSet<Long> = HashSet() 3327 db.query(TABLE_NAME, arrayOf(ID), where, null, null, null, null).use { cursor -> 3328 while (cursor.moveToNext()) { 3329 ids.add(CursorUtil.requireLong(cursor, ID)) 3330 } 3331 } 3332 return ids 3333 } 3334 3335 fun getUnexportedInsecureMessages(limit: Int): Cursor { 3336 return rawQueryWithAttachments( 3337 projection = appendArg(MMS_PROJECTION_WITH_ATTACHMENTS, EXPORT_STATE), 3338 where = "${getInsecureMessageClause()} AND NOT $EXPORTED", 3339 arguments = null, 3340 reverse = false, 3341 limit = limit.toLong() 3342 ) 3343 } 3344 3345 fun getUnexportedInsecureMessagesEstimatedSize(): Long { 3346 val bodyTextSize: Long = readableDatabase 3347 .select("SUM(LENGTH($BODY))") 3348 .from(TABLE_NAME) 3349 .where("${getInsecureMessageClause()} AND $EXPORTED < ?", MessageExportStatus.EXPORTED) 3350 .run() 3351 .readToSingleLong() 3352 3353 val fileSize: Long = readableDatabase.rawQuery( 3354 """ 3355 SELECT 3356 SUM(${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE}) AS s 3357 FROM 3358 $TABLE_NAME INNER JOIN ${AttachmentTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} 3359 WHERE 3360 ${getInsecureMessageClause()} AND $EXPORTED < ${MessageExportStatus.EXPORTED.serialize()} 3361 """, 3362 null 3363 ).readToSingleLong() 3364 3365 return bodyTextSize + fileSize 3366 } 3367 3368 fun deleteExportedMessages() { 3369 writableDatabase.withinTransaction { db -> 3370 val threadsToUpdate: List<Long> = db 3371 .query(TABLE_NAME, arrayOf(THREAD_ID), "$EXPORTED = ?", buildArgs(MessageExportStatus.EXPORTED), THREAD_ID, null, null, null) 3372 .readToList { it.requireLong(THREAD_ID) } 3373 3374 db.delete(TABLE_NAME) 3375 .where("$EXPORTED = ?", MessageExportStatus.EXPORTED) 3376 .run() 3377 3378 for (threadId in threadsToUpdate) { 3379 threads.update(threadId, false) 3380 } 3381 3382 attachments.deleteAbandonedAttachmentFiles() 3383 } 3384 3385 OptimizeMessageSearchIndexJob.enqueue() 3386 } 3387 3388 private fun deleteThreads(threadIds: Set<Long>) { 3389 Log.d(TAG, "deleteThreads(count: ${threadIds.size})") 3390 3391 writableDatabase.withinTransaction { db -> 3392 SqlUtil.buildCollectionQuery(THREAD_ID, threadIds).forEach { query -> 3393 db.select(ID) 3394 .from(TABLE_NAME) 3395 .where(query.where, query.whereArgs) 3396 .run() 3397 .forEach { cursor -> 3398 deleteMessage(cursor.requireLong(ID), notify = false, updateThread = false) 3399 } 3400 } 3401 } 3402 3403 notifyConversationListeners(threadIds) 3404 notifyStickerListeners() 3405 notifyStickerPackListeners() 3406 OptimizeMessageSearchIndexJob.enqueue() 3407 } 3408 3409 fun deleteMessagesInThreadBeforeDate(threadId: Long, date: Long): Int { 3410 return writableDatabase 3411 .delete(TABLE_NAME) 3412 .where("$THREAD_ID = ? AND $DATE_RECEIVED < $date", threadId) 3413 .run() 3414 } 3415 3416 fun deleteAbandonedMessages(): Int { 3417 val deletes = writableDatabase 3418 .delete(TABLE_NAME) 3419 .where("$THREAD_ID NOT IN (SELECT _id FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.ACTIVE} = 1)") 3420 .run() 3421 3422 if (deletes > 0) { 3423 Log.i(TAG, "Deleted $deletes abandoned messages") 3424 calls.updateCallEventDeletionTimestamps() 3425 } 3426 3427 return deletes 3428 } 3429 3430 fun deleteRemotelyDeletedStory(messageId: Long) { 3431 if (readableDatabase.exists(TABLE_NAME).where("$ID = ? AND $REMOTE_DELETED = ?", messageId, 1).run()) { 3432 deleteMessage(messageId) 3433 } else { 3434 Log.i(TAG, "Unable to delete remotely deleted story: $messageId") 3435 } 3436 } 3437 3438 private fun getMessagesInThreadAfterInclusive(threadId: Long, timestamp: Long, limit: Long): List<MessageRecord> { 3439 val where = "$TABLE_NAME.$THREAD_ID = ? AND $TABLE_NAME.$DATE_RECEIVED >= ? AND $TABLE_NAME.$SCHEDULED_DATE = -1 AND $TABLE_NAME.$LATEST_REVISION_ID IS NULL" 3440 val args = buildArgs(threadId, timestamp) 3441 3442 return mmsReaderFor(rawQueryWithAttachments(where, args, false, limit)).use { reader -> 3443 reader.filterNotNull() 3444 } 3445 } 3446 3447 fun deleteAllThreads() { 3448 Log.d(TAG, "deleteAllThreads()") 3449 3450 attachments.deleteAllAttachments() 3451 groupReceipts.deleteAllRows() 3452 mentions.deleteAllMentions() 3453 writableDatabase.deleteAll(TABLE_NAME) 3454 calls.updateCallEventDeletionTimestamps() 3455 3456 OptimizeMessageSearchIndexJob.enqueue() 3457 } 3458 3459 fun getNearestExpiringViewOnceMessage(): ViewOnceExpirationInfo? { 3460 val query = """ 3461 SELECT 3462 $TABLE_NAME.$ID, 3463 $VIEW_ONCE, 3464 $DATE_RECEIVED 3465 FROM 3466 $TABLE_NAME INNER JOIN ${AttachmentTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} 3467 WHERE 3468 $VIEW_ONCE > 0 AND 3469 (${AttachmentTable.DATA_FILE} NOT NULL OR ${AttachmentTable.TRANSFER_STATE} != ?) 3470 """ 3471 3472 val args = buildArgs(AttachmentTable.TRANSFER_PROGRESS_DONE) 3473 3474 var info: ViewOnceExpirationInfo? = null 3475 var nearestExpiration = Long.MAX_VALUE 3476 3477 readableDatabase.rawQuery(query, args).forEach { cursor -> 3478 val id = cursor.requireLong(ID) 3479 val dateReceived = cursor.requireLong(DATE_RECEIVED) 3480 val expiresAt = dateReceived + ViewOnceUtil.MAX_LIFESPAN 3481 3482 if (info == null || expiresAt < nearestExpiration) { 3483 info = ViewOnceExpirationInfo(id, dateReceived) 3484 nearestExpiration = expiresAt 3485 } 3486 } 3487 3488 return info 3489 } 3490 3491 /** 3492 * The number of change number messages in the thread. 3493 * Currently only used for tests. 3494 */ 3495 @VisibleForTesting 3496 fun getChangeNumberMessageCount(recipientId: RecipientId): Int { 3497 return readableDatabase 3498 .select("COUNT(*)") 3499 .from(TABLE_NAME) 3500 .where("$FROM_RECIPIENT_ID = ? AND $TYPE = ?", recipientId, MessageTypes.CHANGE_NUMBER_TYPE) 3501 .run() 3502 .readToSingleInt() 3503 } 3504 3505 fun beginTransaction(): SQLiteDatabase { 3506 writableDatabase.beginTransaction() 3507 return writableDatabase 3508 } 3509 3510 fun setTransactionSuccessful() { 3511 writableDatabase.setTransactionSuccessful() 3512 } 3513 3514 fun endTransaction() { 3515 writableDatabase.endTransaction() 3516 } 3517 3518 @VisibleForTesting 3519 fun collapseJoinRequestEventsIfPossible(threadId: Long, message: IncomingMessage): Optional<InsertResult> { 3520 var result: InsertResult? = null 3521 3522 writableDatabase.withinTransaction { db -> 3523 mmsReaderFor(getConversation(threadId, 0, 2)).use { reader -> 3524 val latestMessage = reader.getNext() 3525 3526 if (latestMessage != null && latestMessage.isGroupV2) { 3527 val changeEditor: Optional<ServiceId> = message.groupContext?.let { GroupV2UpdateMessageUtil.getChangeEditor(it) } ?: Optional.empty() 3528 3529 if (changeEditor.isPresent && latestMessage.isGroupV2JoinRequest(changeEditor.get())) { 3530 val secondLatestMessage = reader.getNext() 3531 3532 val id: Long 3533 val encodedBody: String 3534 val changeRevision: Int = message.groupContext?.let { GroupV2UpdateMessageUtil.getChangeRevision(it) } ?: -1 3535 3536 if (secondLatestMessage != null && secondLatestMessage.isGroupV2JoinRequest(changeEditor.get())) { 3537 id = secondLatestMessage.id 3538 encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(secondLatestMessage, changeRevision, changeEditor.get().toByteString()) 3539 deleteMessage(latestMessage.id) 3540 } else { 3541 id = latestMessage.id 3542 encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(latestMessage, changeRevision, changeEditor.get().toByteString()) 3543 } 3544 3545 db.update(TABLE_NAME) 3546 .values(BODY to encodedBody) 3547 .where("$ID = ?", id) 3548 .run() 3549 3550 result = InsertResult( 3551 messageId = id, 3552 threadId = threadId, 3553 threadWasNewlyCreated = false 3554 ) 3555 } 3556 } 3557 } 3558 } 3559 3560 return result.toOptional() 3561 } 3562 3563 fun getInsecureMessageCount(): Int { 3564 return readableDatabase 3565 .select("COUNT(*)") 3566 .from(TABLE_NAME) 3567 .where(getInsecureMessageClause()) 3568 .run() 3569 .readToSingleInt() 3570 } 3571 3572 fun getSecureMessageCount(threadId: Long): Int { 3573 return readableDatabase 3574 .select("COUNT(*)") 3575 .from(TABLE_NAME) 3576 .where("$secureMessageClause AND $THREAD_ID = ?", threadId) 3577 .run() 3578 .readToSingleInt() 3579 } 3580 3581 fun getOutgoingSecureMessageCount(threadId: Long): Int { 3582 return readableDatabase 3583 .select("COUNT(*)") 3584 .from(TABLE_NAME) 3585 .where("$outgoingSecureMessageClause AND $THREAD_ID = ? AND ($TYPE & ${MessageTypes.GROUP_LEAVE_BIT} = 0 OR $TYPE & ${MessageTypes.GROUP_V2_BIT} = ${MessageTypes.GROUP_V2_BIT})", threadId) 3586 .run() 3587 .readToSingleInt() 3588 } 3589 3590 private fun hasSmsExportMessage(threadId: Long): Boolean { 3591 return readableDatabase 3592 .exists(TABLE_NAME) 3593 .where("$THREAD_ID = ? AND $TYPE = ?", threadId, MessageTypes.SMS_EXPORT_TYPE) 3594 .run() 3595 } 3596 3597 fun hasReportSpamMessage(threadId: Long): Boolean { 3598 return readableDatabase 3599 .exists(TABLE_NAME) 3600 .where("$THREAD_ID = $threadId AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) = ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM}") 3601 .run() 3602 } 3603 3604 private val outgoingInsecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND NOT ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT})" 3605 private val outgoingSecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT})" 3606 3607 private val secureMessageClause: String 3608 get() { 3609 val isSent = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE}" 3610 val isReceived = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE}" 3611 val isSecure = "($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT})" 3612 return "($isSent OR $isReceived) AND $isSecure" 3613 } 3614 3615 private fun getInsecureMessageClause(): String { 3616 return getInsecureMessageClause(-1) 3617 } 3618 3619 private fun getInsecureMessageClause(threadId: Long): String { 3620 val isSent = "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE}" 3621 val isReceived = "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE}" 3622 val isSecure = "($TABLE_NAME.$TYPE & ${MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT})" 3623 val isNotSecure = "($TABLE_NAME.$TYPE <= ${MessageTypes.BASE_TYPE_MASK or MessageTypes.MESSAGE_ATTRIBUTE_MASK})" 3624 3625 var whereClause = "($isSent OR $isReceived) AND NOT $isSecure AND $isNotSecure" 3626 3627 if (threadId != -1L) { 3628 whereClause += " AND $TABLE_NAME.$THREAD_ID = $threadId" 3629 } 3630 3631 return whereClause 3632 } 3633 3634 fun getUnexportedInsecureMessagesCount(): Int { 3635 return getUnexportedInsecureMessagesCount(-1) 3636 } 3637 3638 fun getUnexportedInsecureMessagesCount(threadId: Long): Int { 3639 return writableDatabase 3640 .select("COUNT(*)") 3641 .from(TABLE_NAME) 3642 .where("${getInsecureMessageClause(threadId)} AND $EXPORTED < ?", MessageExportStatus.EXPORTED) 3643 .run() 3644 .readToSingleInt() 3645 } 3646 3647 /** 3648 * Resets the exported state and exported flag so messages can be re-exported. 3649 */ 3650 fun clearExportState() { 3651 writableDatabase.update(TABLE_NAME) 3652 .values( 3653 EXPORT_STATE to null, 3654 EXPORTED to MessageExportStatus.UNEXPORTED.serialize() 3655 ) 3656 .where("$EXPORT_STATE IS NOT NULL OR $EXPORTED != ?", MessageExportStatus.UNEXPORTED) 3657 .run() 3658 } 3659 3660 /** 3661 * Reset the exported status (not state) to the default for clearing errors. 3662 */ 3663 fun clearInsecureMessageExportedErrorStatus() { 3664 writableDatabase.update(TABLE_NAME) 3665 .values(EXPORTED to MessageExportStatus.UNEXPORTED.serialize()) 3666 .where("$EXPORTED < ?", MessageExportStatus.UNEXPORTED) 3667 .run() 3668 } 3669 3670 fun setReactionsSeen(threadId: Long, sinceTimestamp: Long) { 3671 val where = "$THREAD_ID = ? AND $REACTIONS_UNREAD = ?" + if (sinceTimestamp > -1) " AND $DATE_RECEIVED <= $sinceTimestamp" else "" 3672 3673 writableDatabase 3674 .update(TABLE_NAME) 3675 .values( 3676 REACTIONS_UNREAD to 0, 3677 REACTIONS_LAST_SEEN to System.currentTimeMillis() 3678 ) 3679 .where(where, threadId, 1) 3680 .run() 3681 } 3682 3683 fun setAllReactionsSeen() { 3684 writableDatabase 3685 .update(TABLE_NAME) 3686 .values( 3687 REACTIONS_UNREAD to 0, 3688 REACTIONS_LAST_SEEN to System.currentTimeMillis() 3689 ) 3690 .where("$REACTIONS_UNREAD != ?", 0) 3691 .run() 3692 } 3693 3694 fun setNotifiedTimestamp(timestamp: Long, ids: List<Long>) { 3695 if (ids.isEmpty()) { 3696 return 3697 } 3698 3699 val query = buildSingleCollectionQuery(ID, ids) 3700 3701 writableDatabase 3702 .update(TABLE_NAME) 3703 .values(NOTIFIED_TIMESTAMP to timestamp) 3704 .where(query.where, query.whereArgs) 3705 .run() 3706 } 3707 3708 fun addMismatchedIdentity(messageId: Long, recipientId: RecipientId, identityKey: IdentityKey?) { 3709 try { 3710 addToDocument( 3711 messageId = messageId, 3712 column = MISMATCHED_IDENTITIES, 3713 item = IdentityKeyMismatch(recipientId, identityKey), 3714 clazz = IdentityKeyMismatchSet::class.java 3715 ) 3716 } catch (e: IOException) { 3717 Log.w(TAG, e) 3718 } 3719 } 3720 3721 fun removeMismatchedIdentity(messageId: Long, recipientId: RecipientId, identityKey: IdentityKey?) { 3722 try { 3723 removeFromDocument( 3724 messageId = messageId, 3725 column = MISMATCHED_IDENTITIES, 3726 item = IdentityKeyMismatch(recipientId, identityKey), 3727 clazz = IdentityKeyMismatchSet::class.java 3728 ) 3729 } catch (e: IOException) { 3730 Log.w(TAG, e) 3731 } 3732 } 3733 3734 fun setMismatchedIdentities(messageId: Long, mismatches: Set<IdentityKeyMismatch?>) { 3735 try { 3736 setDocument( 3737 database = databaseHelper.signalWritableDatabase, 3738 messageId = messageId, 3739 column = MISMATCHED_IDENTITIES, 3740 document = IdentityKeyMismatchSet(mismatches) 3741 ) 3742 } catch (e: IOException) { 3743 Log.w(TAG, e) 3744 } 3745 } 3746 3747 private fun getReportSpamMessageServerGuids(threadId: Long, timestamp: Long): List<ReportSpamData> { 3748 val data: MutableList<ReportSpamData> = ArrayList() 3749 3750 readableDatabase 3751 .select(FROM_RECIPIENT_ID, SERVER_GUID, DATE_RECEIVED) 3752 .from(TABLE_NAME) 3753 .where("$THREAD_ID = ? AND $DATE_RECEIVED <= ?", threadId, timestamp) 3754 .orderBy("$DATE_RECEIVED DESC") 3755 .limit(3) 3756 .run() 3757 .forEach { cursor -> 3758 val serverGuid: String? = cursor.requireString(SERVER_GUID) 3759 3760 if (serverGuid != null && serverGuid.isNotEmpty()) { 3761 data += ReportSpamData( 3762 recipientId = RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)), 3763 serverGuid = serverGuid, 3764 dateReceived = cursor.requireLong(DATE_RECEIVED) 3765 ) 3766 } 3767 } 3768 3769 return data 3770 } 3771 3772 fun getIncomingPaymentRequestThreads(): List<Long> { 3773 return readableDatabase 3774 .select("DISTINCT $THREAD_ID") 3775 .from(TABLE_NAME) 3776 .where("($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE} AND ($TYPE & ?) != 0", MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST) 3777 .run() 3778 .readToList { it.requireLong(THREAD_ID) } 3779 } 3780 3781 fun getPaymentMessage(paymentUuid: UUID): MessageId? { 3782 val id = readableDatabase 3783 .select(ID) 3784 .from(TABLE_NAME) 3785 .where("$TYPE & ? != 0 AND body = ?", MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION, paymentUuid) 3786 .run() 3787 .readToSingleLong(-1) 3788 3789 return if (id != -1L) { 3790 MessageId(id) 3791 } else { 3792 null 3793 } 3794 } 3795 3796 /** 3797 * @return The user that added you to the group, otherwise null. 3798 */ 3799 fun getGroupAddedBy(threadId: Long): RecipientId? { 3800 var lastQuitChecked = System.currentTimeMillis() 3801 var pair: Pair<RecipientId?, Long> 3802 3803 do { 3804 pair = getGroupAddedBy(threadId, lastQuitChecked) 3805 3806 if (pair.first() != null) { 3807 return pair.first() 3808 } else { 3809 lastQuitChecked = pair.second() 3810 } 3811 } while (pair.second() != -1L) 3812 3813 return null 3814 } 3815 3816 private fun getGroupAddedBy(threadId: Long, lastQuitChecked: Long): Pair<RecipientId?, Long> { 3817 val latestQuit = messages.getLatestGroupQuitTimestamp(threadId, lastQuitChecked) 3818 val id = messages.getOldestGroupUpdateSender(threadId, latestQuit) 3819 return Pair(id, latestQuit) 3820 } 3821 3822 /** 3823 * Whether or not the message has been quoted by another message. 3824 */ 3825 fun isQuoted(messageRecord: MessageRecord): Boolean { 3826 return readableDatabase 3827 .exists(TABLE_NAME) 3828 .where("$QUOTE_ID = ? AND $QUOTE_AUTHOR = ? AND $SCHEDULED_DATE = ?", messageRecord.dateSent, messageRecord.fromRecipient.id, -1) 3829 .run() 3830 } 3831 3832 /** 3833 * Given a collection of MessageRecords, this will return a set of the IDs of the records that have been quoted by another message. 3834 * Does an efficient bulk lookup that makes it faster than [.isQuoted] for multiple records. 3835 */ 3836 fun isQuoted(records: Collection<MessageRecord>): Set<Long> { 3837 if (records.isEmpty()) { 3838 return emptySet() 3839 } 3840 3841 val byQuoteDescriptor: MutableMap<QuoteDescriptor, MessageRecord> = HashMap(records.size) 3842 val args: MutableList<Array<String>> = ArrayList(records.size) 3843 3844 for (record in records) { 3845 val timestamp = record.dateSent 3846 3847 byQuoteDescriptor[QuoteDescriptor(timestamp, record.fromRecipient.id)] = record 3848 args.add(buildArgs(timestamp, record.fromRecipient.id, -1)) 3849 } 3850 3851 val quotedIds: MutableSet<Long> = mutableSetOf() 3852 3853 buildCustomCollectionQuery("$QUOTE_ID = ? AND $QUOTE_AUTHOR = ? AND $SCHEDULED_DATE = ?", args).forEach { query -> 3854 readableDatabase 3855 .select(QUOTE_ID, QUOTE_AUTHOR) 3856 .from(TABLE_NAME) 3857 .where(query.where, query.whereArgs) 3858 .run() 3859 .forEach { cursor -> 3860 val quoteLocator = QuoteDescriptor( 3861 timestamp = cursor.requireLong(QUOTE_ID), 3862 author = RecipientId.from(cursor.requireNonNullString(QUOTE_AUTHOR)) 3863 ) 3864 3865 quotedIds += byQuoteDescriptor[quoteLocator]!!.id 3866 } 3867 } 3868 3869 return quotedIds 3870 } 3871 3872 fun getRootOfQuoteChain(id: MessageId): MessageId { 3873 val targetMessage: MmsMessageRecord = messages.getMessageRecord(id.id) as MmsMessageRecord 3874 3875 if (targetMessage.quote == null) { 3876 return id 3877 } 3878 3879 val query = "$DATE_SENT = ${targetMessage.quote!!.id} AND $FROM_RECIPIENT_ID = '${targetMessage.quote!!.author.serialize()}'" 3880 3881 MmsReader(readableDatabase.query(TABLE_NAME, MMS_PROJECTION, query, null, null, null, "1")).use { reader -> 3882 val record: MessageRecord? = reader.firstOrNull() 3883 if (record != null && !record.isStory()) { 3884 return getRootOfQuoteChain(MessageId(record.id)) 3885 } 3886 } 3887 3888 return id 3889 } 3890 3891 fun getAllMessagesThatQuote(id: MessageId): List<MessageRecord> { 3892 val targetMessage: MessageRecord = getMessageRecord(id.id) 3893 3894 val query = "$QUOTE_ID = ${targetMessage.dateSent} AND $QUOTE_AUTHOR = ${targetMessage.fromRecipient.id.serialize()} AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL" 3895 val order = "$DATE_RECEIVED DESC" 3896 3897 val records: MutableList<MessageRecord> = ArrayList() 3898 MmsReader(readableDatabase.query(TABLE_NAME, MMS_PROJECTION, query, null, null, null, order)).use { reader -> 3899 for (record in reader) { 3900 records += record 3901 records += getAllMessagesThatQuote(MessageId(record.id)) 3902 } 3903 } 3904 3905 return records.sortedByDescending { it.dateReceived } 3906 } 3907 3908 fun getQuotedMessagePosition(threadId: Long, quoteId: Long, authorId: RecipientId): Int { 3909 val targetMessageDateReceived: Long = readableDatabase 3910 .select(DATE_RECEIVED, LATEST_REVISION_ID) 3911 .from(TABLE_NAME) 3912 .where("$DATE_SENT = $quoteId AND $FROM_RECIPIENT_ID = ? AND $REMOTE_DELETED = 0 AND $SCHEDULED_DATE = -1", authorId) 3913 .run() 3914 .readToSingleObject { cursor -> 3915 val latestRevisionId = cursor.requireLongOrNull(LATEST_REVISION_ID) 3916 if (latestRevisionId != null) { 3917 readableDatabase 3918 .select(DATE_RECEIVED) 3919 .from(TABLE_NAME) 3920 .where("$ID = ?", latestRevisionId) 3921 .run() 3922 .readToSingleLong(-1L) 3923 } else { 3924 cursor.requireLong(DATE_RECEIVED) 3925 } 3926 } ?: -1L 3927 3928 if (targetMessageDateReceived == -1L) { 3929 return -1 3930 } 3931 3932 return readableDatabase 3933 .select("COUNT(*)") 3934 .from(TABLE_NAME) 3935 .where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $DATE_RECEIVED > $targetMessageDateReceived") 3936 .run() 3937 .readToSingleInt() 3938 } 3939 3940 fun getMessagePositionInConversation(threadId: Long, receivedTimestamp: Long, authorId: RecipientId): Int { 3941 val validMessageExists: Boolean = readableDatabase 3942 .exists(TABLE_NAME) 3943 .where("$DATE_RECEIVED = $receivedTimestamp AND $FROM_RECIPIENT_ID = ? AND $REMOTE_DELETED = 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL", authorId) 3944 .run() 3945 3946 if (!validMessageExists) { 3947 return -1 3948 } 3949 3950 return readableDatabase 3951 .select("COUNT(*)") 3952 .from(TABLE_NAME) 3953 .where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $DATE_RECEIVED > $receivedTimestamp") 3954 .run() 3955 .readToSingleInt(-1) 3956 } 3957 3958 fun getMessagePositionInConversation(threadId: Long, receivedTimestamp: Long): Int { 3959 return getMessagePositionInConversation(threadId, 0, receivedTimestamp) 3960 } 3961 3962 /** 3963 * Retrieves the position of the message with the provided timestamp in the query results you'd 3964 * get from calling [.getConversation]. 3965 * 3966 * Note: This could give back incorrect results in the situation where multiple messages have the 3967 * same received timestamp. However, because this was designed to determine where to scroll to, 3968 * you'll still wind up in about the right spot. 3969 * 3970 * @param groupStoryId Ignored if passed value is <= 0 3971 */ 3972 fun getMessagePositionInConversation(threadId: Long, groupStoryId: Long, receivedTimestamp: Long): Int { 3973 val order: String 3974 val selection: String 3975 3976 if (groupStoryId > 0) { 3977 order = "$DATE_RECEIVED ASC" 3978 selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL" 3979 } else { 3980 order = "$DATE_RECEIVED DESC" 3981 selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL" 3982 } 3983 3984 return readableDatabase 3985 .select("COUNT(*)") 3986 .from(TABLE_NAME) 3987 .where(selection) 3988 .orderBy(order) 3989 .run() 3990 .readToSingleInt(-1) 3991 } 3992 3993 fun getTimestampForFirstMessageAfterDate(date: Long): Long { 3994 return readableDatabase 3995 .select(DATE_RECEIVED) 3996 .from(TABLE_NAME) 3997 .where("$DATE_RECEIVED > $date AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL") 3998 .orderBy("$DATE_RECEIVED ASC") 3999 .limit(1) 4000 .run() 4001 .readToSingleLong() 4002 } 4003 4004 fun getMessageCountBeforeDate(date: Long): Int { 4005 return readableDatabase 4006 .select("COUNT(*)") 4007 .from(TABLE_NAME) 4008 .where("$DATE_RECEIVED < $date AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL") 4009 .run() 4010 .readToSingleInt() 4011 } 4012 4013 @Throws(NoSuchMessageException::class) 4014 fun getMessagesAfterVoiceNoteInclusive(messageId: Long, limit: Long): List<MessageRecord> { 4015 val origin: MessageRecord = getMessageRecord(messageId) 4016 4017 return getMessagesInThreadAfterInclusive(origin.threadId, origin.dateReceived, limit) 4018 .sortedBy { it.dateReceived } 4019 .take(limit.toInt()) 4020 } 4021 4022 fun getMessagePositionOnOrAfterTimestamp(threadId: Long, timestamp: Long): Int { 4023 return readableDatabase 4024 .select("COUNT(*)") 4025 .from(TABLE_NAME) 4026 .where("$THREAD_ID = $threadId AND $DATE_RECEIVED >= $timestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL") 4027 .run() 4028 .readToSingleInt() 4029 } 4030 4031 @Throws(NoSuchMessageException::class) 4032 fun getConversationSnippetType(threadId: Long): Long { 4033 return readableDatabase 4034 .rawQuery(SNIPPET_QUERY, buildArgs(threadId)) 4035 .readToSingleObject { it.requireLong(TYPE) } ?: throw NoSuchMessageException("no message") 4036 } 4037 4038 @Throws(NoSuchMessageException::class) 4039 fun getConversationSnippet(threadId: Long): MessageRecord { 4040 return getConversationSnippetCursor(threadId) 4041 .readToSingleObject { cursor -> 4042 val id = cursor.requireLong(ID) 4043 messages.getMessageRecord(id) 4044 } ?: throw NoSuchMessageException("no message") 4045 } 4046 4047 @VisibleForTesting 4048 fun getConversationSnippetCursor(threadId: Long): Cursor { 4049 val db = databaseHelper.signalReadableDatabase 4050 return db.rawQuery(SNIPPET_QUERY, buildArgs(threadId)) 4051 } 4052 4053 fun getUnreadCount(threadId: Long): Int { 4054 return readableDatabase 4055 .select("COUNT(*)") 4056 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 4057 .where("$READ = 0 AND $STORY_TYPE = 0 AND $THREAD_ID = $threadId AND $PARENT_STORY_ID <= 0 AND $LATEST_REVISION_ID IS NULL") 4058 .run() 4059 .readToSingleInt() 4060 } 4061 4062 fun messageExists(messageRecord: MessageRecord): Boolean { 4063 return readableDatabase 4064 .exists(TABLE_NAME) 4065 .where("$ID = ?", messageRecord.id) 4066 .run() 4067 } 4068 4069 fun messageExists(sentTimestamp: Long, author: RecipientId): Boolean { 4070 return readableDatabase 4071 .exists("$TABLE_NAME INDEXED BY $INDEX_DATE_SENT_FROM_TO_THREAD") 4072 .where("$DATE_SENT = ? AND $FROM_RECIPIENT_ID = ?", sentTimestamp, author) 4073 .run() 4074 } 4075 4076 fun getReportSpamMessageServerData(threadId: Long, timestamp: Long, limit: Int): List<ReportSpamData> { 4077 return getReportSpamMessageServerGuids(threadId, timestamp) 4078 .sortedBy { it.dateReceived } 4079 .take(limit) 4080 } 4081 4082 fun getGroupReportSpamMessageServerData(threadId: Long, inviter: RecipientId, timestamp: Long, limit: Int): List<ReportSpamData> { 4083 val data: MutableList<ReportSpamData> = ArrayList() 4084 4085 val incomingGroupUpdateClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE} AND ($TYPE & ${MessageTypes.GROUP_UPDATE_BIT}) != 0" 4086 4087 readableDatabase 4088 .select(FROM_RECIPIENT_ID, SERVER_GUID, DATE_RECEIVED) 4089 .from(TABLE_NAME) 4090 .where("$FROM_RECIPIENT_ID = ? AND $THREAD_ID = ? AND $DATE_RECEIVED <= ? AND $incomingGroupUpdateClause", inviter, threadId, timestamp) 4091 .orderBy("$DATE_RECEIVED DESC") 4092 .limit(limit) 4093 .run() 4094 .forEach { cursor -> 4095 val serverGuid: String? = cursor.requireString(SERVER_GUID) 4096 4097 if (serverGuid != null && serverGuid.isNotEmpty()) { 4098 data += ReportSpamData( 4099 recipientId = RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)), 4100 serverGuid = serverGuid, 4101 dateReceived = cursor.requireLong(DATE_RECEIVED) 4102 ) 4103 } 4104 } 4105 4106 return data 4107 } 4108 4109 @Throws(NoSuchMessageException::class) 4110 private fun getMessageExportState(messageId: MessageId): MessageExportState { 4111 return readableDatabase 4112 .select(EXPORT_STATE) 4113 .from(TABLE_NAME) 4114 .where("$ID = ?", messageId.id) 4115 .run() 4116 .readToSingleObject { cursor -> 4117 val bytes: ByteArray? = cursor.requireBlob(EXPORT_STATE) 4118 4119 if (bytes == null) { 4120 MessageExportState() 4121 } else { 4122 try { 4123 MessageExportState.ADAPTER.decode(bytes) 4124 } catch (e: IOException) { 4125 MessageExportState() 4126 } 4127 } 4128 } ?: throw NoSuchMessageException("The requested message does not exist.") 4129 } 4130 4131 @Throws(NoSuchMessageException::class) 4132 fun updateMessageExportState(messageId: MessageId, transform: Function<MessageExportState, MessageExportState>) { 4133 writableDatabase.withinTransaction { db -> 4134 val oldState = getMessageExportState(messageId) 4135 val newState = transform.apply(oldState) 4136 setMessageExportState(messageId, newState) 4137 } 4138 } 4139 4140 fun markMessageExported(messageId: MessageId) { 4141 writableDatabase 4142 .update(TABLE_NAME) 4143 .values(EXPORTED to MessageExportStatus.EXPORTED.serialize()) 4144 .where("$ID = ?", messageId.id) 4145 .run() 4146 } 4147 4148 fun markMessageExportFailed(messageId: MessageId) { 4149 writableDatabase 4150 .update(TABLE_NAME) 4151 .values(EXPORTED to MessageExportStatus.ERROR.serialize()) 4152 .where("$ID = ?", messageId.id) 4153 .run() 4154 } 4155 4156 private fun setMessageExportState(messageId: MessageId, messageExportState: MessageExportState) { 4157 writableDatabase 4158 .update(TABLE_NAME) 4159 .values(EXPORT_STATE to messageExportState.encode()) 4160 .where("$ID = ?", messageId.id) 4161 .run() 4162 } 4163 4164 fun incrementDeliveryReceiptCounts(targetTimestamps: List<Long>, receiptAuthor: RecipientId, receiptSentTimestamp: Long, stopwatch: Stopwatch? = null): Set<Long> { 4165 return incrementReceiptCounts(targetTimestamps, receiptAuthor, receiptSentTimestamp, ReceiptType.DELIVERY, stopwatch = stopwatch) 4166 } 4167 4168 fun incrementDeliveryReceiptCount(targetTimestamps: Long, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Boolean { 4169 return incrementReceiptCount(targetTimestamps, receiptAuthor, receiptSentTimestamp, ReceiptType.DELIVERY) 4170 } 4171 4172 /** 4173 * @return A list of ID's that were not updated. 4174 */ 4175 fun incrementReadReceiptCounts(targetTimestamps: List<Long>, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Set<Long> { 4176 return incrementReceiptCounts(targetTimestamps, receiptAuthor, receiptSentTimestamp, ReceiptType.READ) 4177 } 4178 4179 fun incrementReadReceiptCount(targetTimestamps: Long, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Boolean { 4180 return incrementReceiptCount(targetTimestamps, receiptAuthor, receiptSentTimestamp, ReceiptType.READ) 4181 } 4182 4183 /** 4184 * @return A list of ID's that were not updated. 4185 */ 4186 fun incrementViewedReceiptCounts(targetTimestamps: List<Long>, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Set<Long> { 4187 return incrementReceiptCounts(targetTimestamps, receiptAuthor, receiptSentTimestamp, ReceiptType.VIEWED) 4188 } 4189 4190 fun incrementViewedNonStoryReceiptCounts(targetTimestamps: List<Long>, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Set<Long> { 4191 return incrementReceiptCounts(targetTimestamps, receiptAuthor, receiptSentTimestamp, ReceiptType.VIEWED, MessageQualifier.NORMAL) 4192 } 4193 4194 fun incrementViewedReceiptCount(targetTimestamp: Long, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Boolean { 4195 return incrementReceiptCount(targetTimestamp, receiptAuthor, receiptSentTimestamp, ReceiptType.VIEWED) 4196 } 4197 4198 fun incrementViewedStoryReceiptCounts(targetTimestamps: List<Long>, receiptAuthor: RecipientId, receiptSentTimestamp: Long): Set<Long> { 4199 val messageUpdates: MutableSet<MessageReceiptUpdate> = HashSet() 4200 val unhandled: MutableSet<Long> = HashSet() 4201 4202 writableDatabase.withinTransaction { 4203 for (targetTimestamp in targetTimestamps) { 4204 val updates = incrementReceiptCountInternal(targetTimestamp, receiptAuthor, receiptSentTimestamp, ReceiptType.VIEWED, MessageQualifier.STORY) 4205 4206 if (updates.isNotEmpty()) { 4207 messageUpdates += updates 4208 } else { 4209 unhandled += targetTimestamp 4210 } 4211 } 4212 } 4213 4214 for (update in messageUpdates) { 4215 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(update.messageId) 4216 ApplicationDependencies.getDatabaseObserver().notifyVerboseConversationListeners(setOf(update.threadId)) 4217 } 4218 4219 if (messageUpdates.isNotEmpty()) { 4220 notifyConversationListListeners() 4221 } 4222 4223 return unhandled 4224 } 4225 4226 /** 4227 * Wraps a single receipt update in a transaction and triggers the proper updates. 4228 * 4229 * @return Whether or not some thread was updated. 4230 */ 4231 private fun incrementReceiptCount(targetTimestamp: Long, receiptAuthor: RecipientId, receiptSentTimestamp: Long, receiptType: ReceiptType, messageQualifier: MessageQualifier = MessageQualifier.ALL): Boolean { 4232 var messageUpdates: Set<MessageReceiptUpdate> = HashSet() 4233 4234 writableDatabase.withinTransaction { 4235 messageUpdates = incrementReceiptCountInternal(targetTimestamp, receiptAuthor, receiptSentTimestamp, receiptType, messageQualifier) 4236 4237 for (messageUpdate in messageUpdates) { 4238 threads.updateReceiptStatus(messageUpdate.messageId.id, messageUpdate.threadId) 4239 } 4240 } 4241 4242 for (threadUpdate in messageUpdates) { 4243 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(threadUpdate.messageId) 4244 } 4245 4246 return messageUpdates.isNotEmpty() 4247 } 4248 4249 /** 4250 * Wraps multiple receipt updates in a transaction and triggers the proper updates. 4251 * 4252 * @return All of the target timestamps that couldn't be found in the table. 4253 */ 4254 private fun incrementReceiptCounts(targetTimestamps: List<Long>, receiptAuthor: RecipientId, receiptSentTimestamp: Long, receiptType: ReceiptType, messageQualifier: MessageQualifier = MessageQualifier.ALL, stopwatch: Stopwatch? = null): Set<Long> { 4255 val messageUpdates: MutableSet<MessageReceiptUpdate> = HashSet() 4256 val missingTargetTimestamps: MutableSet<Long> = HashSet() 4257 4258 writableDatabase.withinTransaction { 4259 for (targetTimestamp in targetTimestamps) { 4260 val updates: Set<MessageReceiptUpdate> = incrementReceiptCountInternal(targetTimestamp, receiptAuthor, receiptSentTimestamp, receiptType, messageQualifier, stopwatch) 4261 if (updates.isNotEmpty()) { 4262 messageUpdates += updates 4263 } else { 4264 missingTargetTimestamps += targetTimestamp 4265 } 4266 } 4267 4268 for (update in messageUpdates) { 4269 if (update.shouldUpdateSnippet) { 4270 threads.updateReceiptStatus(update.messageId.id, update.threadId, stopwatch) 4271 } 4272 } 4273 } 4274 4275 for (update in messageUpdates) { 4276 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(update.messageId) 4277 ApplicationDependencies.getDatabaseObserver().notifyVerboseConversationListeners(setOf(update.threadId)) 4278 4279 if (messageQualifier == MessageQualifier.STORY) { 4280 ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(threads.getRecipientIdForThreadId(update.threadId)!!) 4281 } 4282 } 4283 4284 if (messageUpdates.isNotEmpty()) { 4285 notifyConversationListListeners() 4286 } 4287 4288 stopwatch?.split("observers") 4289 4290 return missingTargetTimestamps 4291 } 4292 4293 protected open fun incrementReceiptCountInternal(targetTimestamp: Long, receiptAuthor: RecipientId, receiptSentTimestamp: Long, receiptType: ReceiptType, messageQualifier: MessageQualifier, stopwatch: Stopwatch? = null): Set<MessageReceiptUpdate> {//*TM_SA*/ make fun protected open 4294 val qualifierWhere: String = when (messageQualifier) { 4295 MessageQualifier.NORMAL -> " AND NOT ($IS_STORY_CLAUSE)" 4296 MessageQualifier.STORY -> " AND $IS_STORY_CLAUSE" 4297 MessageQualifier.ALL -> "" 4298 } 4299 4300 // Note: While it is true that multiple messages can have the same (sent, author) pair, this should only happen for stories, which are handled below. 4301 val receiptData: ReceiptData? = readableDatabase 4302 .select(ID, THREAD_ID, STORY_TYPE, receiptType.columnName, TO_RECIPIENT_ID) 4303 .from(TABLE_NAME) 4304 .where( 4305 """ 4306 $DATE_SENT = $targetTimestamp AND 4307 $FROM_RECIPIENT_ID = ? AND 4308 ( 4309 $TO_RECIPIENT_ID = ? OR 4310 EXISTS ( 4311 SELECT 1 4312 FROM ${RecipientTable.TABLE_NAME} 4313 WHERE 4314 ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = $TO_RECIPIENT_ID AND 4315 ${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} != ${RecipientTable.RecipientType.INDIVIDUAL.id} 4316 ) 4317 ) 4318 $qualifierWhere 4319 """, 4320 Recipient.self().id, 4321 receiptAuthor 4322 ) 4323 .limit(1) 4324 .run() 4325 .readToSingleObject { cursor -> 4326 ReceiptData( 4327 messageId = cursor.requireLong(ID), 4328 threadId = cursor.requireLong(THREAD_ID), 4329 storyType = StoryType.fromCode(cursor.requireInt(STORY_TYPE)), 4330 marked = cursor.requireBoolean(receiptType.columnName), 4331 forIndividualChat = cursor.requireLong(TO_RECIPIENT_ID) == receiptAuthor.toLong() 4332 ) 4333 } 4334 4335 stopwatch?.split("receipt-query") 4336 4337 if (receiptData == null) { 4338 if (receiptType == ReceiptType.DELIVERY) { 4339 earlyDeliveryReceiptCache.increment(targetTimestamp, receiptAuthor, receiptSentTimestamp) 4340 } 4341 4342 return emptySet() 4343 } 4344 4345 if (!receiptData.marked) { 4346 // We set the receipt_timestamp to the max of the two values because that single column represents the timestamp of the last receipt of any type. 4347 // That means we want to update it for each new receipt type, but we never want the time to go backwards. 4348 writableDatabase.execSQL( 4349 """ 4350 UPDATE $TABLE_NAME 4351 SET 4352 ${receiptType.columnName} = 1, 4353 $RECEIPT_TIMESTAMP = MAX($RECEIPT_TIMESTAMP, $receiptSentTimestamp) 4354 WHERE 4355 $ID = ${receiptData.messageId} 4356 """ 4357 ) 4358 } 4359 stopwatch?.split("receipt-update") 4360 4361 if (!receiptData.forIndividualChat) { 4362 groupReceipts.update(receiptAuthor, receiptData.messageId, receiptType.groupStatus, receiptSentTimestamp) 4363 } 4364 4365 stopwatch?.split("group-receipt") 4366 4367 return if (receiptData.storyType != StoryType.NONE) { 4368 val storyMessageIds = storySends.getStoryMessagesFor(receiptAuthor, targetTimestamp) 4369 storyMessageIds.forEach { messageId -> groupReceipts.update(receiptAuthor, messageId.id, receiptType.groupStatus, receiptSentTimestamp) } 4370 storyMessageIds.map { messageId -> MessageReceiptUpdate(-1, messageId, false) }.toSet() 4371 } else { 4372 setOf(MessageReceiptUpdate(receiptData.threadId, MessageId(receiptData.messageId), shouldUpdateSnippet = receiptType != ReceiptType.VIEWED && !receiptData.marked)) 4373 }.also { 4374 stopwatch?.split("stories") 4375 } 4376 } 4377 4378 /** 4379 * @return Unhandled ids 4380 */ 4381 fun setTimestampReadFromSyncMessage(readMessages: List<SyncMessage.Read>, proposedExpireStarted: Long, threadToLatestRead: MutableMap<Long, Long>): Collection<SyncMessageId> { 4382 val expiringMessages: MutableList<Pair<Long, Long>> = mutableListOf() 4383 val updatedThreads: MutableSet<Long> = mutableSetOf() 4384 val unhandled: MutableCollection<SyncMessageId> = mutableListOf() 4385 4386 writableDatabase.withinTransaction { 4387 for (readMessage in readMessages) { 4388 val authorId: RecipientId = recipients.getOrInsertFromServiceId(ServiceId.parseOrThrow(readMessage.senderAci!!)) 4389 4390 val result: TimestampReadResult = setTimestampReadFromSyncMessageInternal( 4391 messageId = SyncMessageId(authorId, readMessage.timestamp!!), 4392 proposedExpireStarted = proposedExpireStarted, 4393 threadToLatestRead = threadToLatestRead 4394 ) 4395 4396 expiringMessages += result.expiring 4397 updatedThreads += result.threads 4398 4399 if (result.threads.isEmpty()) { 4400 unhandled += SyncMessageId(authorId, readMessage.timestamp!!) 4401 } 4402 } 4403 4404 for (threadId in updatedThreads) { 4405 threads.updateReadState(threadId) 4406 threads.setLastSeen(threadId) 4407 } 4408 } 4409 4410 for (expiringMessage in expiringMessages) { 4411 ApplicationDependencies.getExpiringMessageManager().scheduleDeletion(expiringMessage.first(), true, proposedExpireStarted, expiringMessage.second()) 4412 } 4413 4414 for (threadId in updatedThreads) { 4415 notifyConversationListeners(threadId) 4416 } 4417 4418 return unhandled 4419 } 4420 4421 /** 4422 * Handles a synchronized read message. 4423 * @param messageId An id representing the author-timestamp pair of the message that was read on a linked device. Note that the author could be self when 4424 * syncing read receipts for reactions. 4425 */ 4426 private fun setTimestampReadFromSyncMessageInternal(messageId: SyncMessageId, proposedExpireStarted: Long, threadToLatestRead: MutableMap<Long, Long>): TimestampReadResult { 4427 val expiring: MutableList<Pair<Long, Long>> = LinkedList() 4428 val threads: MutableList<Long> = LinkedList() 4429 4430 readableDatabase 4431 .select(ID, THREAD_ID, EXPIRES_IN, EXPIRE_STARTED, LATEST_REVISION_ID) 4432 .from(TABLE_NAME) 4433 .where("$DATE_SENT = ? AND ($FROM_RECIPIENT_ID = ? OR ($FROM_RECIPIENT_ID = ? AND $outgoingTypeClause))", messageId.timetamp, messageId.recipientId, Recipient.self().id) 4434 .run() 4435 .forEach { cursor -> 4436 val id = cursor.requireLong(ID) 4437 val threadId = cursor.requireLong(THREAD_ID) 4438 val expiresIn = cursor.requireLong(EXPIRES_IN) 4439 val expireStarted = cursor.requireLong(EXPIRE_STARTED).let { 4440 if (it > 0) { 4441 min(proposedExpireStarted, it) 4442 } else { 4443 proposedExpireStarted 4444 } 4445 } 4446 val latestRevisionId: Long? = cursor.requireLongOrNull(LATEST_REVISION_ID) 4447 4448 val values = contentValuesOf( 4449 READ to 1, 4450 REACTIONS_UNREAD to 0, 4451 REACTIONS_LAST_SEEN to System.currentTimeMillis() 4452 ) 4453 4454 if (expiresIn > 0) { 4455 values.put(EXPIRE_STARTED, expireStarted) 4456 expiring += Pair(id, expiresIn) 4457 } 4458 4459 writableDatabase 4460 .update(TABLE_NAME) 4461 .values(values) 4462 .where("$ID = ?", latestRevisionId ?: id) 4463 .run() 4464 4465 threads += threadId 4466 4467 val latest: Long? = threadToLatestRead[threadId] 4468 4469 threadToLatestRead[threadId] = if (latest != null) { 4470 max(latest, messageId.timetamp) 4471 } else { 4472 messageId.timetamp 4473 } 4474 } 4475 4476 return TimestampReadResult(expiring, threads) 4477 } 4478 4479 /** 4480 * Finds a message by timestamp+author. 4481 * Does *not* include attachments. 4482 */ 4483 fun getMessageFor(timestamp: Long, authorId: RecipientId): MessageRecord? { 4484 val cursor = readableDatabase 4485 .select(*MMS_PROJECTION) 4486 .from(TABLE_NAME) 4487 .where("$DATE_SENT = ? AND $FROM_RECIPIENT_ID = ?", timestamp, authorId) 4488 .run() 4489 4490 return mmsReaderFor(cursor).use { reader -> 4491 reader.firstOrNull() 4492 } 4493 } 4494 4495 /** 4496 * A cursor containing all of the messages in a given thread, in the proper order. 4497 * This does *not* have attachments in it. 4498 */ 4499 fun getConversation(threadId: Long): Cursor { 4500 return getConversation(threadId, 0, 0) 4501 } 4502 4503 /** 4504 * A cursor containing all of the messages in a given thread, in the proper order, respecting offset/limit. 4505 * This does *not* have attachments in it. 4506 */ 4507 fun getConversation(threadId: Long, offset: Long, limit: Long): Cursor { 4508 val limitStr: String = if (limit > 0 || offset > 0) "$offset, $limit" else "" 4509 4510 return readableDatabase 4511 .select(*MMS_PROJECTION) 4512 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 4513 .where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ? AND $LATEST_REVISION_ID IS NULL", threadId, 0, 0, -1) 4514 .orderBy("$DATE_RECEIVED DESC") 4515 .limit(limitStr) 4516 .run() 4517 } 4518 4519 /** 4520 * Returns messages ordered for display in a reverse list (newest first). 4521 */ 4522 fun getScheduledMessagesInThread(threadId: Long): List<MessageRecord> { 4523 val cursor = readableDatabase 4524 .select(*MMS_PROJECTION) 4525 .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID") 4526 .where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ?", threadId, 0, 0, -1) 4527 .orderBy("$SCHEDULED_DATE DESC, $ID DESC") 4528 .run() 4529 4530 return mmsReaderFor(cursor).use { reader -> 4531 reader.filterNotNull() 4532 } 4533 } 4534 4535 /** 4536 * Returns messages order for sending (oldest first). 4537 */ 4538 fun getScheduledMessagesBefore(time: Long): List<MessageRecord> { 4539 val cursor = readableDatabase 4540 .select(*MMS_PROJECTION) 4541 .from(TABLE_NAME) 4542 .where("$STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ? AND $SCHEDULED_DATE <= ?", 0, 0, -1, time) 4543 .orderBy("$SCHEDULED_DATE ASC, $ID ASC") 4544 .run() 4545 4546 return mmsReaderFor(cursor).use { reader -> 4547 reader.filterNotNull() 4548 } 4549 } 4550 4551 fun getOldestScheduledSendTimestamp(): MessageRecord? { 4552 val cursor = readableDatabase 4553 .select(*MMS_PROJECTION) 4554 .from(TABLE_NAME) 4555 .where("$STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ?", 0, 0, -1) 4556 .orderBy("$SCHEDULED_DATE ASC, $ID ASC") 4557 .limit(1) 4558 .run() 4559 4560 return mmsReaderFor(cursor).use { reader -> 4561 reader.firstOrNull() 4562 } 4563 } 4564 4565 fun getMessagesForNotificationState(stickyThreads: Collection<StickyThread>): Cursor { 4566 val stickyQuery = StringBuilder() 4567 4568 for ((conversationId, _, earliestTimestamp) in stickyThreads) { 4569 if (stickyQuery.isNotEmpty()) { 4570 stickyQuery.append(" OR ") 4571 } 4572 4573 stickyQuery.append("(") 4574 .append("$THREAD_ID = ") 4575 .append(conversationId.threadId) 4576 .append(" AND ") 4577 .append(DATE_RECEIVED) 4578 .append(" >= ") 4579 .append(earliestTimestamp) 4580 .append(getStickyWherePartForParentStoryId(conversationId.groupStoryId)) 4581 .append(")") 4582 } 4583 4584 return readableDatabase 4585 .select(*MMS_PROJECTION) 4586 .from(TABLE_NAME) 4587 .where("$NOTIFIED = 0 AND $STORY_TYPE = 0 AND $LATEST_REVISION_ID IS NULL AND ($READ = 0 OR $REACTIONS_UNREAD = 1 ${if (stickyQuery.isNotEmpty()) "OR ($stickyQuery)" else ""})") 4588 .orderBy("$DATE_RECEIVED ASC") 4589 .run() 4590 } 4591 4592 fun updatePendingSelfData(placeholder: RecipientId, self: RecipientId) { 4593 val fromUpdates = writableDatabase 4594 .update(TABLE_NAME) 4595 .values(FROM_RECIPIENT_ID to self.serialize()) 4596 .where("$FROM_RECIPIENT_ID = ?", placeholder) 4597 .run() 4598 4599 val toUpdates = writableDatabase 4600 .update(TABLE_NAME) 4601 .values(TO_RECIPIENT_ID to self.serialize()) 4602 .where("$TO_RECIPIENT_ID = ?", placeholder) 4603 .run() 4604 4605 Log.i(TAG, "Updated $fromUpdates FROM_RECIPIENT_ID rows and $toUpdates TO_RECIPIENT_ID rows.") 4606 } 4607 4608 private fun getStickyWherePartForParentStoryId(parentStoryId: Long?): String { 4609 return if (parentStoryId == null) { 4610 " AND $PARENT_STORY_ID <= 0" 4611 } else { 4612 " AND $PARENT_STORY_ID = $parentStoryId" 4613 } 4614 } 4615 4616 override fun remapRecipient(fromId: RecipientId, toId: RecipientId) { 4617 writableDatabase 4618 .update(TABLE_NAME) 4619 .values(FROM_RECIPIENT_ID to toId.serialize()) 4620 .where("$FROM_RECIPIENT_ID = ?", fromId) 4621 .run() 4622 4623 writableDatabase 4624 .update(TABLE_NAME) 4625 .values(TO_RECIPIENT_ID to toId.serialize()) 4626 .where("$TO_RECIPIENT_ID = ?", fromId) 4627 .run() 4628 } 4629 4630 override fun remapThread(fromId: Long, toId: Long) { 4631 writableDatabase 4632 .update(TABLE_NAME) 4633 .values(THREAD_ID to toId) 4634 .where("$THREAD_ID = ?", fromId) 4635 .run() 4636 } 4637 4638 /** 4639 * Returns the next ID that would be generated if an insert was done on this table. 4640 * You should *not* use this for actually generating an ID to use. That will happen automatically! 4641 * This was added for a very narrow usecase, and you probably don't need to use it. 4642 */ 4643 fun getNextId(): Long { 4644 return getNextAutoIncrementId(writableDatabase, TABLE_NAME) 4645 } 4646 4647 fun updateReactionsUnread(db: SQLiteDatabase, messageId: Long, hasReactions: Boolean, isRemoval: Boolean) { 4648 try { 4649 val isOutgoing = getMessageRecord(messageId).isOutgoing 4650 val values = ContentValues() 4651 4652 if (!hasReactions) { 4653 values.put(REACTIONS_UNREAD, 0) 4654 } else if (!isRemoval) { 4655 values.put(REACTIONS_UNREAD, 1) 4656 } 4657 4658 if (isOutgoing && hasReactions) { 4659 values.put(NOTIFIED, 0) 4660 } 4661 4662 if (values.size() > 0) { 4663 db.update(TABLE_NAME) 4664 .values(values) 4665 .where("$ID = ?", messageId) 4666 .run() 4667 } 4668 } catch (e: NoSuchMessageException) { 4669 Log.w(TAG, "Failed to find message $messageId") 4670 } 4671 } 4672 4673 @Throws(IOException::class) 4674 protected fun <D : Document<I>?, I> removeFromDocument(messageId: Long, column: String, item: I, clazz: Class<D>) { 4675 writableDatabase.withinTransaction { db -> 4676 val document: D = getDocument(db, messageId, column, clazz) 4677 val iterator = document!!.items.iterator() 4678 4679 while (iterator.hasNext()) { 4680 val found = iterator.next() 4681 if (found == item) { 4682 iterator.remove() 4683 break 4684 } 4685 } 4686 4687 setDocument(db, messageId, column, document) 4688 } 4689 } 4690 4691 @Throws(IOException::class) 4692 protected fun <T : Document<I>?, I> addToDocument(messageId: Long, column: String, item: I, clazz: Class<T>) { 4693 addToDocument(messageId, column, listOf(item), clazz) 4694 } 4695 4696 @Throws(IOException::class) 4697 protected fun <T : Document<I>?, I> addToDocument(messageId: Long, column: String, objects: List<I>?, clazz: Class<T>) { 4698 writableDatabase.withinTransaction { db -> 4699 val document: T = getDocument(db, messageId, column, clazz) 4700 document!!.items.addAll(objects!!) 4701 setDocument(db, messageId, column, document) 4702 } 4703 } 4704 4705 @Throws(IOException::class) 4706 protected fun setDocument(database: SQLiteDatabase, messageId: Long, column: String?, document: Document<*>?) { 4707 val contentValues = ContentValues() 4708 4709 if (document == null || document.size() == 0) { 4710 contentValues.put(column, null as String?) 4711 } else { 4712 contentValues.put(column, JsonUtils.toJson(document)) 4713 } 4714 4715 database 4716 .update(TABLE_NAME) 4717 .values(contentValues) 4718 .where("$ID = ?", messageId) 4719 .run() 4720 } 4721 4722 private fun <D : Document<*>?> getDocument( 4723 database: SQLiteDatabase, 4724 messageId: Long, 4725 column: String, 4726 clazz: Class<D> 4727 ): D { 4728 return database 4729 .select(column) 4730 .from(TABLE_NAME) 4731 .where("$ID = ?", messageId) 4732 .run() 4733 .readToSingleObject { cursor -> 4734 val document: String? = cursor.requireString(column) 4735 4736 if (!document.isNullOrEmpty()) { 4737 try { 4738 JsonUtils.fromJson(document, clazz) 4739 } catch (e: IOException) { 4740 Log.w(TAG, e) 4741 clazz.newInstance() 4742 } 4743 } else { 4744 clazz.newInstance() 4745 } 4746 }!! 4747 } 4748 4749 fun getBodyRangesForMessages(messageIds: List<Long?>): Map<Long, BodyRangeList> { 4750 val bodyRanges: MutableMap<Long, BodyRangeList> = HashMap() 4751 4752 SqlUtil.buildCollectionQuery(ID, messageIds).forEach { query -> 4753 readableDatabase 4754 .select(ID, MESSAGE_RANGES) 4755 .from(TABLE_NAME) 4756 .where(query.where, query.whereArgs) 4757 .run() 4758 .forEach { cursor -> 4759 val data: ByteArray? = cursor.requireBlob(MESSAGE_RANGES) 4760 4761 if (data != null) { 4762 try { 4763 bodyRanges[CursorUtil.requireLong(cursor, ID)] = BodyRangeList.ADAPTER.decode(data) 4764 } catch (e: IOException) { 4765 Log.w(TAG, "Unable to parse body ranges for search", e) 4766 } 4767 } 4768 } 4769 } 4770 4771 return bodyRanges 4772 } 4773 4774 private fun generatePduCompatTimestamp(time: Long): Long { 4775 return time - time % 1000 4776 } 4777 4778 private fun getReleaseChannelThreadId(hasSeenReleaseChannelStories: Boolean): Long { 4779 if (hasSeenReleaseChannelStories) { 4780 return -1L 4781 } 4782 4783 val releaseChannelRecipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId ?: return -1L 4784 return threads.getThreadIdFor(releaseChannelRecipientId) ?: return -1L 4785 } 4786 4787 private fun Cursor.toMarkedMessageInfo(outgoing: Boolean): MarkedMessageInfo { 4788 val recipientColumn = if (outgoing) TO_RECIPIENT_ID else FROM_RECIPIENT_ID 4789 return MarkedMessageInfo( 4790 messageId = MessageId(this.requireLong(ID)), 4791 threadId = this.requireLong(THREAD_ID), 4792 syncMessageId = SyncMessageId( 4793 recipientId = RecipientId.from(this.requireLong(recipientColumn)), 4794 timetamp = this.requireLong(DATE_SENT) 4795 ), 4796 expirationInfo = null, 4797 storyType = StoryType.fromCode(this.requireInt(STORY_TYPE)) 4798 ) 4799 } 4800 4801 private fun MessageRecord.getOriginalOrOwnMessageId(): MessageId { 4802 return this.originalMessageId ?: MessageId(this.id) 4803 } 4804 4805 /** 4806 * Determines the database type bitmask for the inbound message. 4807 */ 4808 @Throws(MmsException::class) 4809 private fun IncomingMessage.toMessageType(): Long { 4810 var type = MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT 4811 4812 if (this.giftBadge != null) { 4813 type = type or MessageTypes.SPECIAL_TYPE_GIFT_BADGE 4814 } 4815 4816 type = type or when (this.type) { 4817 MessageType.NORMAL -> MessageTypes.BASE_INBOX_TYPE 4818 MessageType.EXPIRATION_UPDATE -> MessageTypes.EXPIRATION_TIMER_UPDATE_BIT or MessageTypes.BASE_INBOX_TYPE 4819 MessageType.STORY_REACTION -> MessageTypes.SPECIAL_TYPE_STORY_REACTION or MessageTypes.BASE_INBOX_TYPE 4820 MessageType.PAYMENTS_NOTIFICATION -> MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION or MessageTypes.BASE_INBOX_TYPE 4821 MessageType.ACTIVATE_PAYMENTS_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or MessageTypes.BASE_INBOX_TYPE 4822 MessageType.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED or MessageTypes.BASE_INBOX_TYPE 4823 MessageType.CONTACT_JOINED -> MessageTypes.JOINED_TYPE 4824 MessageType.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT or MessageTypes.BASE_INBOX_TYPE 4825 MessageType.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT or MessageTypes.BASE_INBOX_TYPE 4826 MessageType.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT or MessageTypes.BASE_INBOX_TYPE 4827 MessageType.END_SESSION -> MessageTypes.END_SESSION_BIT or MessageTypes.BASE_INBOX_TYPE 4828 MessageType.GROUP_UPDATE -> { 4829 val isOnlyGroupLeave = this.groupContext?.let { GroupV2UpdateMessageUtil.isJustAGroupLeave(it) } ?: false 4830 4831 if (isOnlyGroupLeave) { 4832 MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT or MessageTypes.GROUP_LEAVE_BIT or MessageTypes.BASE_INBOX_TYPE 4833 } else { 4834 MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT or MessageTypes.BASE_INBOX_TYPE 4835 } 4836 } 4837 } 4838 4839 return type 4840 } 4841 4842 fun threadContainsSms(threadId: Long): Boolean { 4843 return readableDatabase 4844 .exists(TABLE_NAME) 4845 .where(getInsecureMessageClause(threadId)) 4846 .run() 4847 } 4848 4849 protected enum class ReceiptType(val columnName: String, val groupStatus: Int) { 4850 READ(HAS_READ_RECEIPT, GroupReceiptTable.STATUS_READ), 4851 DELIVERY(HAS_DELIVERY_RECEIPT, GroupReceiptTable.STATUS_DELIVERED), 4852 VIEWED(VIEWED_COLUMN, GroupReceiptTable.STATUS_VIEWED) 4853 } 4854 4855 data class ReceiptData( 4856 val messageId: Long, 4857 val threadId: Long, 4858 val storyType: StoryType, 4859 val marked: Boolean, 4860 val forIndividualChat: Boolean 4861 ) 4862 4863 data class MessageReceiptStatus( 4864 val hasReadReceipt: Boolean, 4865 val hasDeliveryReceipt: Boolean, 4866 val type: Long 4867 ) 4868 4869 enum class MessageStatus { 4870 PENDING, SENT, DELIVERED, READ, VIEWED, FAILED 4871 } 4872 4873 data class SyncMessageId( 4874 val recipientId: RecipientId, 4875 val timetamp: Long 4876 ) 4877 4878 data class ExpirationInfo( 4879 val id: Long, 4880 val expiresIn: Long, 4881 val expireStarted: Long, 4882 val isMms: Boolean 4883 ) 4884 4885 data class MarkedMessageInfo( 4886 val threadId: Long, 4887 val syncMessageId: SyncMessageId, 4888 val messageId: MessageId, 4889 val expirationInfo: ExpirationInfo?, 4890 val storyType: StoryType 4891 ) 4892 4893 data class InsertResult( 4894 val messageId: Long, 4895 val threadId: Long, 4896 val threadWasNewlyCreated: Boolean, 4897 val insertedAttachments: Map<Attachment, AttachmentId>? = null 4898 ) 4899 4900 data class MessageReceiptUpdate( 4901 val threadId: Long, 4902 val messageId: MessageId, 4903 val shouldUpdateSnippet: Boolean 4904 ) 4905 4906 data class ReportSpamData( 4907 val recipientId: RecipientId, 4908 val serverGuid: String, 4909 val dateReceived: Long 4910 ) 4911 4912 private data class QuoteDescriptor( 4913 private val timestamp: Long, 4914 private val author: RecipientId 4915 ) 4916 4917 private class TimestampReadResult( 4918 val expiring: List<Pair<Long, Long>>, 4919 val threads: List<Long> 4920 ) 4921 4922 /** 4923 * Describes which messages to act on. This is used when incrementing receipts. 4924 * Specifically, this was added to support stories having separate viewed receipt settings. 4925 */ 4926 enum class MessageQualifier { 4927 /** A normal database message (i.e. not a story) */ 4928 NORMAL, 4929 4930 /** A story message */ 4931 STORY, 4932 4933 /** Both normal and story message */ 4934 ALL 4935 } 4936 4937 object MmsStatus { 4938 const val DOWNLOAD_INITIALIZED = 1 4939 const val DOWNLOAD_NO_CONNECTIVITY = 2 4940 const val DOWNLOAD_CONNECTING = 3 4941 const val DOWNLOAD_SOFT_FAILURE = 4 4942 const val DOWNLOAD_HARD_FAILURE = 5 4943 const val DOWNLOAD_APN_UNAVAILABLE = 6 4944 } 4945 4946 object Status { 4947 const val STATUS_NONE = -1 4948 const val STATUS_COMPLETE = 0 4949 const val STATUS_PENDING = 0x20 4950 const val STATUS_FAILED = 0x40 4951 } 4952 4953 fun interface InsertListener { 4954 fun onComplete() 4955 } 4956 4957 /** 4958 * Allows the developer to safely iterate over and close a cursor containing 4959 * data for MessageRecord objects. Supports for-each loops as well as try-with-resources 4960 * blocks. 4961 * 4962 * Readers are considered "one-shot" and it's on the caller to decide what needs 4963 * to be done with the data. Once read, a reader cannot be read from again. This 4964 * is by design, since reading data out of a cursor involves object creations and 4965 * lookups, so it is in the best interest of app performance to only read out the 4966 * data once. If you need to parse the list multiple times, it is recommended that 4967 * you copy the iterable out into a normal List, or use extension methods such as 4968 * partition. 4969 * 4970 * This reader does not support removal, since this would be considered a destructive 4971 * database call. 4972 */ 4973 interface Reader : Closeable, Iterable<MessageRecord> { 4974 4975 @Deprecated("Use the Iterable interface instead.") 4976 fun getNext(): MessageRecord? 4977 4978 @Deprecated("Use the Iterable interface instead.") 4979 fun getCurrent(): MessageRecord 4980 4981 /** 4982 * Pulls the export state out of the query, if it is present. 4983 */ 4984 fun getMessageExportStateForCurrentRecord(): MessageExportState 4985 4986 /** 4987 * From the [Closeable] interface, removing the IOException requirement. 4988 */ 4989 override fun close() 4990 } 4991 4992 /** 4993 * MessageRecord reader which implements the Iterable interface. This allows it to 4994 * be used with many Kotlin Extension Functions as well as with for-each loops. 4995 * 4996 * Note that it's the responsibility of the developer using the reader to ensure that: 4997 * 4998 * 1. They only utilize one of the two interfaces (legacy or iterator) 4999 * 1. They close this reader after use, preferably via try-with-resources or a use block. 5000 */ 5001 class MmsReader(val cursor: Cursor) : Reader { 5002 private val context: Context 5003 5004 init { 5005 context = ApplicationDependencies.getApplication() 5006 } 5007 5008 override fun getNext(): MessageRecord? { 5009 return if (!cursor.moveToNext()) { 5010 null 5011 } else { 5012 getCurrent() 5013 } 5014 } 5015 5016 override fun getCurrent(): MessageRecord { 5017 return getMediaMmsMessageRecord(cursor) 5018 } 5019 5020 override fun getMessageExportStateForCurrentRecord(): MessageExportState { 5021 val messageExportState = CursorUtil.requireBlob(cursor, EXPORT_STATE) ?: return MessageExportState() 5022 return try { 5023 MessageExportState.ADAPTER.decode(messageExportState) 5024 } catch (e: IOException) { 5025 MessageExportState() 5026 } 5027 } 5028 5029 override fun close() { 5030 cursor.close() 5031 } 5032 5033 override fun iterator(): Iterator<MessageRecord> { 5034 return ReaderIterator() 5035 } 5036 5037 fun getCount(): Int { 5038 return cursor.count 5039 } 5040 5041 fun getCurrentId(): MessageId { 5042 return MessageId(cursor.requireLong(ID)) 5043 } 5044 5045 private fun getMediaMmsMessageRecord(cursor: Cursor): MmsMessageRecord { 5046 val id = cursor.requireLong(ID) 5047 val dateSent = cursor.requireLong(DATE_SENT) 5048 val dateReceived = cursor.requireLong(DATE_RECEIVED) 5049 val dateServer = cursor.requireLong(DATE_SERVER) 5050 val box = cursor.requireLong(TYPE) 5051 val threadId = cursor.requireLong(THREAD_ID) 5052 val fromRecipientId = cursor.requireLong(FROM_RECIPIENT_ID) 5053 val fromDeviceId = cursor.requireInt(FROM_DEVICE_ID) 5054 val toRecipientId = cursor.requireLong(TO_RECIPIENT_ID) 5055 val hasDeliveryReceipt = cursor.requireBoolean(HAS_DELIVERY_RECEIPT) 5056 var hasReadReceipt = cursor.requireBoolean(HAS_READ_RECEIPT) 5057 val body = cursor.requireString(BODY) 5058 val mismatchDocument = cursor.requireString(MISMATCHED_IDENTITIES) 5059 val networkDocument = cursor.requireString(NETWORK_FAILURES) 5060 val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID) 5061 val expiresIn = cursor.requireLong(EXPIRES_IN) 5062 val expireStarted = cursor.requireLong(EXPIRE_STARTED) 5063 val unidentified = cursor.requireBoolean(UNIDENTIFIED) 5064 val isViewOnce = cursor.requireBoolean(VIEW_ONCE) 5065 val remoteDelete = cursor.requireBoolean(REMOTE_DELETED) 5066 val mentionsSelf = cursor.requireBoolean(MENTIONS_SELF) 5067 val notifiedTimestamp = cursor.requireLong(NOTIFIED_TIMESTAMP) 5068 var isViewed = cursor.requireBoolean(VIEWED_COLUMN) 5069 val receiptTimestamp = cursor.requireLong(RECEIPT_TIMESTAMP) 5070 val messageRangesData = cursor.requireBlob(MESSAGE_RANGES) 5071 val storyType = StoryType.fromCode(cursor.requireInt(STORY_TYPE)) 5072 val parentStoryId = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID)) 5073 val scheduledDate = cursor.requireLong(SCHEDULED_DATE) 5074 val latestRevisionId: MessageId? = cursor.requireLong(LATEST_REVISION_ID).let { if (it == 0L) null else MessageId(it) } 5075 val originalMessageId: MessageId? = cursor.requireLong(ORIGINAL_MESSAGE_ID).let { if (it == 0L) null else MessageId(it) } 5076 val editCount = cursor.requireInt(REVISION_NUMBER) 5077 val isRead = cursor.requireBoolean(READ) 5078 val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS) 5079 val messageExtras = messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) } 5080 5081 if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { 5082 hasReadReceipt = false 5083 if (MessageTypes.isOutgoingMessageType(box) && !storyType.isStory) { 5084 isViewed = false 5085 } 5086 } 5087 5088 val fromRecipient = Recipient.live(RecipientId.from(fromRecipientId)).get() 5089 val toRecipient = Recipient.live(RecipientId.from(toRecipientId)).get() 5090 val mismatches = getMismatchedIdentities(mismatchDocument) 5091 val networkFailures = getFailures(networkDocument) 5092 5093 val attachments = attachments.getAttachments(cursor) 5094 5095 val contacts = getSharedContacts(cursor, attachments) 5096 val contactAttachments = contacts.mapNotNull { it.avatarAttachment }.toSet() 5097 5098 val previews = getLinkPreviews(cursor, attachments) 5099 val previewAttachments = previews.mapNotNull { it.thumbnail.orElse(null) }.toSet() 5100 5101 val slideDeck = buildSlideDeck(attachments.filterNot { contactAttachments.contains(it) }.filterNot { previewAttachments.contains(it) }) 5102 5103 val quote = getQuote(cursor) 5104 5105 val messageRanges: BodyRangeList? = if (messageRangesData != null) { 5106 try { 5107 BodyRangeList.ADAPTER.decode(messageRangesData) 5108 } catch (e: IOException) { 5109 Log.w(TAG, "Error parsing message ranges", e) 5110 null 5111 } 5112 } else { 5113 null 5114 } 5115 5116 val giftBadge: GiftBadge? = if (body != null && MessageTypes.isGiftBadge(box)) { 5117 try { 5118 GiftBadge.ADAPTER.decode(Base64.decode(body)) 5119 } catch (e: IOException) { 5120 Log.w(TAG, "Error parsing gift badge", e) 5121 null 5122 } 5123 } else { 5124 null 5125 } 5126 5127 return MmsMessageRecord( 5128 id, 5129 fromRecipient, 5130 fromDeviceId, 5131 toRecipient, 5132 dateSent, 5133 dateReceived, 5134 dateServer, 5135 hasDeliveryReceipt, 5136 threadId, 5137 body, 5138 slideDeck, 5139 box, 5140 mismatches, 5141 networkFailures, 5142 subscriptionId, 5143 expiresIn, 5144 expireStarted, 5145 isViewOnce, 5146 hasReadReceipt, 5147 quote, 5148 contacts, 5149 previews, 5150 unidentified, 5151 emptyList(), 5152 remoteDelete, 5153 mentionsSelf, 5154 notifiedTimestamp, 5155 isViewed, 5156 receiptTimestamp, 5157 messageRanges, 5158 storyType, 5159 parentStoryId, 5160 giftBadge, 5161 null, 5162 null, 5163 scheduledDate, 5164 latestRevisionId, 5165 originalMessageId, 5166 editCount, 5167 isRead, 5168 messageExtras 5169 ) 5170 } 5171 5172 private fun getMismatchedIdentities(document: String?): Set<IdentityKeyMismatch> { 5173 if (!TextUtils.isEmpty(document)) { 5174 try { 5175 return JsonUtils.fromJson(document, IdentityKeyMismatchSet::class.java).items 5176 } catch (e: IOException) { 5177 Log.w(TAG, e) 5178 } 5179 } 5180 5181 return emptySet() 5182 } 5183 5184 private fun getFailures(document: String?): Set<NetworkFailure> { 5185 if (!TextUtils.isEmpty(document)) { 5186 try { 5187 return JsonUtils.fromJson(document, NetworkFailureSet::class.java).items 5188 } catch (ioe: IOException) { 5189 Log.w(TAG, ioe) 5190 } 5191 } 5192 5193 return emptySet() 5194 } 5195 5196 private fun getQuote(cursor: Cursor): Quote? { 5197 val quoteId = cursor.requireLong(QUOTE_ID) 5198 val quoteAuthor = cursor.requireLong(QUOTE_AUTHOR) 5199 var quoteText: CharSequence? = cursor.requireString(QUOTE_BODY) 5200 val quoteType = cursor.requireInt(QUOTE_TYPE) 5201 val quoteMissing = cursor.requireBoolean(QUOTE_MISSING) 5202 var quoteMentions = parseQuoteMentions(cursor) 5203 val bodyRanges = parseQuoteBodyRanges(cursor) 5204 5205 val attachments = attachments.getAttachments(cursor) 5206 val quoteAttachments: List<Attachment> = attachments.filter { it.quote } 5207 val quoteDeck = SlideDeck(quoteAttachments) 5208 5209 return if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0) { 5210 if (quoteText != null && (quoteMentions.isNotEmpty() || bodyRanges != null)) { 5211 val updated: UpdatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions) 5212 val styledText = SpannableString(updated.body) 5213 5214 MessageStyler.style(id = "${MessageStyler.QUOTE_ID}$quoteId", messageRanges = bodyRanges.adjustBodyRanges(updated.bodyAdjustments), span = styledText) 5215 5216 quoteText = styledText 5217 quoteMentions = updated.mentions 5218 } 5219 5220 Quote( 5221 quoteId, 5222 RecipientId.from(quoteAuthor), 5223 quoteText, 5224 quoteMissing, 5225 quoteDeck, 5226 quoteMentions, 5227 QuoteModel.Type.fromCode(quoteType) 5228 ) 5229 } else { 5230 null 5231 } 5232 } 5233 5234 private fun String?.toIsoBytes(): ByteArray? { 5235 return if (this != null && this.isNotEmpty()) { 5236 Util.toIsoBytes(this) 5237 } else { 5238 null 5239 } 5240 } 5241 5242 private inner class ReaderIterator : Iterator<MessageRecord> { 5243 override fun hasNext(): Boolean { 5244 return cursor.count != 0 && !cursor.isLast 5245 } 5246 5247 override fun next(): MessageRecord { 5248 return getNext() ?: throw NoSuchElementException() 5249 } 5250 } 5251 5252 companion object { 5253 5254 @JvmStatic 5255 fun buildSlideDeck(attachments: List<DatabaseAttachment>): SlideDeck { 5256 val messageAttachments = attachments 5257 .filterNot { it.quote } 5258 .sortedWith(DisplayOrderComparator()) 5259 5260 return SlideDeck(messageAttachments) 5261 } 5262 } 5263 } 5264}