That fuck shit the fascists are using
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}