mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
1545 lines
70 KiB
Kotlin
1545 lines
70 KiB
Kotlin
/**
|
|
* Copyright (C) 2011 Whisper Systems
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http:></http:>//www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms.database
|
|
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.database.Cursor
|
|
import com.annimon.stream.Stream
|
|
import com.google.android.mms.pdu_alt.PduHeaders
|
|
import org.json.JSONArray
|
|
import org.json.JSONException
|
|
import org.json.JSONObject
|
|
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
|
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
|
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage
|
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
|
import org.session.libsession.snode.SnodeAPI
|
|
import org.session.libsession.utilities.Address
|
|
import org.session.libsession.utilities.Address.Companion.UNKNOWN
|
|
import org.session.libsession.utilities.Address.Companion.fromExternal
|
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
|
import org.session.libsession.utilities.Contact
|
|
import org.session.libsession.utilities.IdentityKeyMismatch
|
|
import org.session.libsession.utilities.IdentityKeyMismatchList
|
|
import org.session.libsession.utilities.NetworkFailure
|
|
import org.session.libsession.utilities.NetworkFailureList
|
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
|
|
import org.session.libsession.utilities.Util.toIsoBytes
|
|
import org.session.libsession.utilities.recipients.Recipient
|
|
import org.session.libsignal.utilities.JsonUtil
|
|
import org.session.libsignal.utilities.Log
|
|
import org.session.libsignal.utilities.ThreadUtils.queue
|
|
import org.session.libsignal.utilities.guava.Optional
|
|
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
|
|
import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener
|
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
|
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord
|
|
import org.thoughtcrime.securesms.database.model.Quote
|
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
|
import org.thoughtcrime.securesms.mms.MmsException
|
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
|
import org.thoughtcrime.securesms.util.asSequence
|
|
import java.io.Closeable
|
|
import java.io.IOException
|
|
import java.security.SecureRandom
|
|
import java.util.LinkedList
|
|
|
|
class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : MessagingDatabase(context, databaseHelper) {
|
|
private val earlyDeliveryReceiptCache = EarlyReceiptCache()
|
|
private val earlyReadReceiptCache = EarlyReceiptCache()
|
|
override fun getTableName() = TABLE_NAME
|
|
|
|
fun getMessageCountForThread(threadId: Long): Int {
|
|
val db = databaseHelper.readableDatabase
|
|
db.query(
|
|
TABLE_NAME,
|
|
arrayOf("COUNT(*)"),
|
|
"$THREAD_ID = ?",
|
|
arrayOf(threadId.toString()),
|
|
null,
|
|
null,
|
|
null
|
|
).use { cursor ->
|
|
if (cursor.moveToFirst()) return cursor.getInt(0)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fun isOutgoingMessage(timestamp: Long): Boolean =
|
|
databaseHelper.writableDatabase.query(
|
|
TABLE_NAME,
|
|
arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS),
|
|
DATE_SENT + " = ?",
|
|
arrayOf(timestamp.toString()),
|
|
null,
|
|
null,
|
|
null,
|
|
null
|
|
).use { cursor ->
|
|
cursor.asSequence()
|
|
.map { cursor.getColumnIndexOrThrow(MESSAGE_BOX) }
|
|
.map(cursor::getLong)
|
|
.any { MmsSmsColumns.Types.isOutgoingMessageType(it) }
|
|
}
|
|
|
|
fun incrementReceiptCount(
|
|
messageId: SyncMessageId,
|
|
timestamp: Long,
|
|
deliveryReceipt: Boolean,
|
|
readReceipt: Boolean
|
|
) {
|
|
val database = databaseHelper.writableDatabase
|
|
var cursor: Cursor? = null
|
|
var found = false
|
|
try {
|
|
cursor = database.query(
|
|
TABLE_NAME,
|
|
arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS),
|
|
"$DATE_SENT = ?",
|
|
arrayOf(messageId.timetamp.toString()),
|
|
null,
|
|
null,
|
|
null,
|
|
null
|
|
)
|
|
while (cursor.moveToNext()) {
|
|
if (MmsSmsColumns.Types.isOutgoingMessageType(
|
|
cursor.getLong(
|
|
cursor.getColumnIndexOrThrow(
|
|
MESSAGE_BOX
|
|
)
|
|
)
|
|
)
|
|
) {
|
|
val theirAddress = fromSerialized(
|
|
cursor.getString(
|
|
cursor.getColumnIndexOrThrow(
|
|
ADDRESS
|
|
)
|
|
)
|
|
)
|
|
val ourAddress = messageId.address
|
|
val columnName =
|
|
if (deliveryReceipt) DELIVERY_RECEIPT_COUNT else READ_RECEIPT_COUNT
|
|
if (ourAddress.equals(theirAddress) || theirAddress.isGroup) {
|
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
|
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
|
val status =
|
|
if (deliveryReceipt) GroupReceiptDatabase.STATUS_DELIVERED else GroupReceiptDatabase.STATUS_READ
|
|
found = true
|
|
database.execSQL(
|
|
"UPDATE " + TABLE_NAME + " SET " +
|
|
columnName + " = " + columnName + " + 1 WHERE " + ID + " = ?",
|
|
arrayOf(id.toString())
|
|
)
|
|
get(context).groupReceiptDatabase()
|
|
.update(ourAddress, id, status, timestamp)
|
|
get(context).threadDatabase().update(threadId, false, true)
|
|
notifyConversationListeners(threadId)
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
if (deliveryReceipt) earlyDeliveryReceiptCache.increment(
|
|
messageId.timetamp,
|
|
messageId.address
|
|
)
|
|
if (readReceipt) earlyReadReceiptCache.increment(
|
|
messageId.timetamp,
|
|
messageId.address
|
|
)
|
|
}
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
fun updateSentTimestamp(messageId: Long, newTimestamp: Long, threadId: Long) {
|
|
val db = databaseHelper.writableDatabase
|
|
db.execSQL(
|
|
"UPDATE $TABLE_NAME SET $DATE_SENT = ? WHERE $ID = ?",
|
|
arrayOf(newTimestamp.toString(), messageId.toString())
|
|
)
|
|
notifyConversationListeners(threadId)
|
|
notifyConversationListListeners()
|
|
}
|
|
|
|
fun getThreadIdForMessage(id: Long): Long {
|
|
val sql = "SELECT $THREAD_ID FROM $TABLE_NAME WHERE $ID = ?"
|
|
val sqlArgs = arrayOf(id.toString())
|
|
val db = databaseHelper.readableDatabase
|
|
var cursor: Cursor? = null
|
|
return try {
|
|
cursor = db.rawQuery(sql, sqlArgs)
|
|
if (cursor != null && cursor.moveToFirst()) cursor.getLong(0) else -1
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
private fun rawQuery(where: String, arguments: Array<String>?): Cursor {
|
|
val database = databaseHelper.readableDatabase
|
|
return database.rawQuery(
|
|
"SELECT " + MMS_PROJECTION.joinToString(",") + " FROM " + TABLE_NAME +
|
|
" LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
|
|
" LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 1)" +
|
|
" WHERE " + where + " GROUP BY " + TABLE_NAME + "." + ID, arguments
|
|
)
|
|
}
|
|
|
|
fun getMessage(messageId: Long): Cursor {
|
|
val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
|
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId))
|
|
return cursor
|
|
}
|
|
|
|
val expireStartedMessages: Reader
|
|
get() {
|
|
val where = "$EXPIRE_STARTED > 0"
|
|
return readerFor(rawQuery(where, null))!!
|
|
}
|
|
|
|
private fun updateMailboxBitmask(
|
|
id: Long,
|
|
maskOff: Long,
|
|
maskOn: Long,
|
|
threadId: Optional<Long>
|
|
) {
|
|
val db = databaseHelper.writableDatabase
|
|
db.execSQL(
|
|
"UPDATE " + TABLE_NAME +
|
|
" SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (MmsSmsColumns.Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
|
|
" WHERE " + ID + " = ?", arrayOf(id.toString() + "")
|
|
)
|
|
if (threadId.isPresent) {
|
|
get(context).threadDatabase().update(threadId.get(), false, true)
|
|
}
|
|
}
|
|
|
|
private fun markAs(
|
|
messageId: Long,
|
|
baseType: Long,
|
|
threadId: Long = getThreadIdForMessage(messageId)
|
|
) {
|
|
updateMailboxBitmask(
|
|
messageId,
|
|
MmsSmsColumns.Types.BASE_TYPE_MASK,
|
|
baseType,
|
|
Optional.of(threadId)
|
|
)
|
|
notifyConversationListeners(threadId)
|
|
}
|
|
|
|
override fun markAsSyncing(messageId: Long) {
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_SYNCING_TYPE)
|
|
}
|
|
override fun markAsResyncing(messageId: Long) {
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_RESYNCING_TYPE)
|
|
}
|
|
override fun markAsSyncFailed(messageId: Long) {
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_SYNC_FAILED_TYPE)
|
|
}
|
|
|
|
fun markAsSending(messageId: Long) {
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_SENDING_TYPE)
|
|
}
|
|
|
|
fun markAsSentFailed(messageId: Long) {
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE)
|
|
}
|
|
|
|
override fun markAsSent(messageId: Long, secure: Boolean) {
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_SENT_TYPE or if (secure) MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.SECURE_MESSAGE_BIT else 0)
|
|
}
|
|
|
|
override fun markUnidentified(messageId: Long, unidentified: Boolean) {
|
|
val contentValues = ContentValues()
|
|
contentValues.put(UNIDENTIFIED, if (unidentified) 1 else 0)
|
|
val db = databaseHelper.writableDatabase
|
|
db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
|
|
}
|
|
|
|
override fun markAsDeleted(messageId: Long, read: Boolean, hasMention: Boolean) {
|
|
val database = databaseHelper.writableDatabase
|
|
val contentValues = ContentValues()
|
|
contentValues.put(READ, 1)
|
|
contentValues.put(BODY, "")
|
|
contentValues.put(HAS_MENTION, 0)
|
|
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
|
|
val attachmentDatabase = get(context).attachmentDatabase()
|
|
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
|
|
val threadId = getThreadIdForMessage(messageId)
|
|
|
|
markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId)
|
|
}
|
|
|
|
override fun markExpireStarted(messageId: Long) {
|
|
markExpireStarted(messageId, SnodeAPI.nowWithOffset)
|
|
}
|
|
|
|
override fun markExpireStarted(messageId: Long, startedTimestamp: Long) {
|
|
val contentValues = ContentValues()
|
|
contentValues.put(EXPIRE_STARTED, startedTimestamp)
|
|
val db = databaseHelper.writableDatabase
|
|
db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
|
|
val threadId = getThreadIdForMessage(messageId)
|
|
notifyConversationListeners(threadId)
|
|
}
|
|
|
|
fun markAsNotified(id: Long) {
|
|
val database = databaseHelper.writableDatabase
|
|
val contentValues = ContentValues()
|
|
contentValues.put(NOTIFIED, 1)
|
|
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString()))
|
|
}
|
|
|
|
fun setMessagesRead(threadId: Long, beforeTime: Long): List<MarkedMessageInfo> {
|
|
return setMessagesRead(
|
|
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?",
|
|
arrayOf(threadId.toString(), beforeTime.toString())
|
|
)
|
|
}
|
|
|
|
fun setMessagesRead(threadId: Long): List<MarkedMessageInfo> {
|
|
return setMessagesRead(
|
|
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)",
|
|
arrayOf(threadId.toString())
|
|
)
|
|
}
|
|
|
|
private fun setMessagesRead(where: String, arguments: Array<String>?): List<MarkedMessageInfo> {
|
|
val database = databaseHelper.writableDatabase
|
|
val result: MutableList<MarkedMessageInfo> = LinkedList()
|
|
var cursor: Cursor? = null
|
|
database.beginTransaction()
|
|
try {
|
|
cursor = database.query(
|
|
TABLE_NAME,
|
|
arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED),
|
|
where,
|
|
arguments,
|
|
null,
|
|
null,
|
|
null
|
|
)
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
if (MmsSmsColumns.Types.isSecureType(cursor.getLong(3))) {
|
|
val syncMessageId =
|
|
SyncMessageId(fromSerialized(cursor.getString(1)), cursor.getLong(2))
|
|
val expirationInfo = ExpirationInfo(
|
|
cursor.getLong(0),
|
|
cursor.getLong(4),
|
|
cursor.getLong(5),
|
|
true
|
|
)
|
|
result.add(MarkedMessageInfo(syncMessageId, expirationInfo))
|
|
}
|
|
}
|
|
val contentValues = ContentValues()
|
|
contentValues.put(READ, 1)
|
|
contentValues.put(REACTIONS_UNREAD, 0)
|
|
database.update(TABLE_NAME, contentValues, where, arguments)
|
|
database.setTransactionSuccessful()
|
|
} finally {
|
|
cursor?.close()
|
|
database.endTransaction()
|
|
}
|
|
return result
|
|
}
|
|
|
|
@Throws(MmsException::class, NoSuchMessageException::class)
|
|
fun getOutgoingMessage(messageId: Long): OutgoingMediaMessage {
|
|
val attachmentDatabase = get(context).attachmentDatabase()
|
|
var cursor: Cursor? = null
|
|
try {
|
|
cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString()))
|
|
if (cursor.moveToNext()) {
|
|
val associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId)
|
|
val outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
|
val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY))
|
|
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
|
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
|
|
val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN))
|
|
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
|
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
|
val distributionType = get(context).threadDatabase().getDistributionType(threadId)
|
|
val mismatchDocument = cursor.getString(
|
|
cursor.getColumnIndexOrThrow(
|
|
MISMATCHED_IDENTITIES
|
|
)
|
|
)
|
|
val networkDocument = cursor.getString(
|
|
cursor.getColumnIndexOrThrow(
|
|
NETWORK_FAILURE
|
|
)
|
|
)
|
|
val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID))
|
|
val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR))
|
|
val quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)) // TODO: this should be the referenced quote
|
|
val quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1
|
|
val quoteAttachments = associatedAttachments
|
|
.filter { obj: DatabaseAttachment -> obj.isQuote }
|
|
val contacts = getSharedContacts(cursor, associatedAttachments)
|
|
val contactAttachments: Set<Attachment> =
|
|
contacts.mapNotNull { obj: Contact -> obj.avatarAttachment }.toSet()
|
|
val previews = getLinkPreviews(cursor, associatedAttachments)
|
|
val previewAttachments =
|
|
previews.filter { lp: LinkPreview -> lp.getThumbnail().isPresent }
|
|
.map { lp: LinkPreview -> lp.getThumbnail().get() }
|
|
val attachments = associatedAttachments
|
|
.asSequence()
|
|
.filterNot { obj: DatabaseAttachment -> obj.isQuote || contactAttachments.contains(obj) || previewAttachments.contains(obj) }
|
|
.toList()
|
|
val recipient = Recipient.from(context, fromSerialized(address), false)
|
|
var networkFailures: List<NetworkFailure?>? = LinkedList()
|
|
var mismatches: List<IdentityKeyMismatch?>? = LinkedList()
|
|
var quote: QuoteModel? = null
|
|
if (quoteId > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty())) {
|
|
quote = QuoteModel(
|
|
quoteId,
|
|
fromSerialized(quoteAuthor),
|
|
quoteText, // TODO: refactor this to use referenced quote
|
|
quoteMissing,
|
|
quoteAttachments
|
|
)
|
|
}
|
|
if (!mismatchDocument.isNullOrEmpty()) {
|
|
try {
|
|
mismatches = JsonUtil.fromJson(
|
|
mismatchDocument,
|
|
IdentityKeyMismatchList::class.java
|
|
).list
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, e)
|
|
}
|
|
}
|
|
if (!networkDocument.isNullOrEmpty()) {
|
|
try {
|
|
networkFailures =
|
|
JsonUtil.fromJson(networkDocument, NetworkFailureList::class.java).list
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, e)
|
|
}
|
|
}
|
|
val message = OutgoingMediaMessage(
|
|
recipient,
|
|
body,
|
|
attachments,
|
|
timestamp,
|
|
subscriptionId,
|
|
expiresIn,
|
|
distributionType,
|
|
quote,
|
|
contacts,
|
|
previews,
|
|
networkFailures!!,
|
|
mismatches!!
|
|
)
|
|
return if (MmsSmsColumns.Types.isSecureType(outboxType)) {
|
|
OutgoingSecureMediaMessage(message)
|
|
} else message
|
|
}
|
|
throw NoSuchMessageException("No record found for id: $messageId")
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
private fun getSharedContacts(
|
|
cursor: Cursor,
|
|
attachments: List<DatabaseAttachment>
|
|
): List<Contact> {
|
|
val serializedContacts = cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS))
|
|
if (serializedContacts.isNullOrEmpty()) {
|
|
return emptyList()
|
|
}
|
|
val attachmentIdMap: MutableMap<AttachmentId?, DatabaseAttachment> = HashMap()
|
|
for (attachment in attachments) {
|
|
attachmentIdMap[attachment.attachmentId] = attachment
|
|
}
|
|
try {
|
|
val contacts: MutableList<Contact> = LinkedList()
|
|
val jsonContacts = JSONArray(serializedContacts)
|
|
for (i in 0 until jsonContacts.length()) {
|
|
val contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString())
|
|
if (contact.avatar != null && contact.avatar!!.attachmentId != null) {
|
|
val attachment = attachmentIdMap[contact.avatar!!.attachmentId]
|
|
val updatedAvatar = Contact.Avatar(
|
|
contact.avatar!!.attachmentId,
|
|
attachment,
|
|
contact.avatar!!.isProfile
|
|
)
|
|
contacts.add(Contact(contact, updatedAvatar))
|
|
} else {
|
|
contacts.add(contact)
|
|
}
|
|
}
|
|
return contacts
|
|
} catch (e: JSONException) {
|
|
Log.w(TAG, "Failed to parse shared contacts.", e)
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, "Failed to parse shared contacts.", e)
|
|
}
|
|
return emptyList()
|
|
}
|
|
|
|
private fun getLinkPreviews(
|
|
cursor: Cursor,
|
|
attachments: List<DatabaseAttachment>
|
|
): List<LinkPreview> {
|
|
val serializedPreviews = cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS))
|
|
if (serializedPreviews.isNullOrEmpty()) {
|
|
return emptyList()
|
|
}
|
|
val attachmentIdMap: MutableMap<AttachmentId?, DatabaseAttachment> = HashMap()
|
|
for (attachment in attachments) {
|
|
attachmentIdMap[attachment.attachmentId] = attachment
|
|
}
|
|
try {
|
|
val previews: MutableList<LinkPreview> = LinkedList()
|
|
val jsonPreviews = JSONArray(serializedPreviews)
|
|
for (i in 0 until jsonPreviews.length()) {
|
|
val preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString())
|
|
if (preview.attachmentId != null) {
|
|
val attachment = attachmentIdMap[preview.attachmentId]
|
|
if (attachment != null) {
|
|
previews.add(LinkPreview(preview.url, preview.title, attachment))
|
|
}
|
|
} else {
|
|
previews.add(preview)
|
|
}
|
|
}
|
|
return previews
|
|
} catch (e: JSONException) {
|
|
Log.w(TAG, "Failed to parse shared contacts.", e)
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, "Failed to parse shared contacts.", e)
|
|
}
|
|
return emptyList()
|
|
}
|
|
|
|
@Throws(MmsException::class)
|
|
private fun insertMessageInbox(
|
|
retrieved: IncomingMediaMessage,
|
|
contentLocation: String,
|
|
threadId: Long, mailbox: Long,
|
|
serverTimestamp: Long,
|
|
runThreadUpdate: Boolean
|
|
): Optional<InsertResult> {
|
|
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
|
|
val contentValues = ContentValues()
|
|
contentValues.put(DATE_SENT, retrieved.sentTimeMillis)
|
|
contentValues.put(ADDRESS, retrieved.from.serialize())
|
|
contentValues.put(MESSAGE_BOX, mailbox)
|
|
contentValues.put(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)
|
|
contentValues.put(THREAD_ID, threadId)
|
|
contentValues.put(CONTENT_LOCATION, contentLocation)
|
|
contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED)
|
|
// In open groups messages should be sorted by their server timestamp
|
|
var receivedTimestamp = serverTimestamp
|
|
if (serverTimestamp == 0L) {
|
|
receivedTimestamp = retrieved.sentTimeMillis
|
|
}
|
|
contentValues.put(
|
|
DATE_RECEIVED,
|
|
receivedTimestamp
|
|
) // Loki - This is important due to how we handle GIFs
|
|
contentValues.put(PART_COUNT, retrieved.attachments.size)
|
|
contentValues.put(SUBSCRIPTION_ID, retrieved.subscriptionId)
|
|
contentValues.put(EXPIRES_IN, retrieved.expiresIn)
|
|
contentValues.put(READ, if (retrieved.isExpirationUpdate) 1 else 0)
|
|
contentValues.put(UNIDENTIFIED, retrieved.isUnidentified)
|
|
contentValues.put(HAS_MENTION, retrieved.hasMention())
|
|
contentValues.put(MESSAGE_REQUEST_RESPONSE, retrieved.isMessageRequestResponse)
|
|
if (!contentValues.containsKey(DATE_SENT)) {
|
|
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED))
|
|
}
|
|
var quoteAttachments: List<Attachment?>? = LinkedList()
|
|
if (retrieved.quote != null) {
|
|
contentValues.put(QUOTE_ID, retrieved.quote.id)
|
|
contentValues.put(QUOTE_AUTHOR, retrieved.quote.author.serialize())
|
|
contentValues.put(QUOTE_MISSING, if (retrieved.quote.missing) 1 else 0)
|
|
quoteAttachments = retrieved.quote.attachments
|
|
}
|
|
if (retrieved.isPushMessage && isDuplicate(retrieved, threadId) ||
|
|
retrieved.isMessageRequestResponse && isDuplicateMessageRequestResponse(
|
|
retrieved,
|
|
threadId
|
|
)
|
|
) {
|
|
Log.w(TAG, "Ignoring duplicate media message (" + retrieved.sentTimeMillis + ")")
|
|
return Optional.absent()
|
|
}
|
|
val messageId = insertMediaMessage(
|
|
retrieved.body,
|
|
retrieved.attachments,
|
|
quoteAttachments!!,
|
|
retrieved.sharedContacts,
|
|
retrieved.linkPreviews,
|
|
contentValues,
|
|
null,
|
|
)
|
|
if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) {
|
|
if (runThreadUpdate) {
|
|
get(context).threadDatabase().update(threadId, true, true)
|
|
}
|
|
}
|
|
notifyConversationListeners(threadId)
|
|
return Optional.of(InsertResult(messageId, threadId))
|
|
}
|
|
|
|
@Throws(MmsException::class)
|
|
fun insertSecureDecryptedMessageOutbox(
|
|
retrieved: OutgoingMediaMessage,
|
|
threadId: Long,
|
|
serverTimestamp: Long,
|
|
runThreadUpdate: Boolean
|
|
): Optional<InsertResult> {
|
|
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
|
|
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
|
|
if (messageId == -1L) {
|
|
return Optional.absent()
|
|
}
|
|
markAsSent(messageId, true)
|
|
return Optional.fromNullable(InsertResult(messageId, threadId))
|
|
}
|
|
|
|
@JvmOverloads
|
|
@Throws(MmsException::class)
|
|
fun insertSecureDecryptedMessageInbox(
|
|
retrieved: IncomingMediaMessage,
|
|
threadId: Long,
|
|
serverTimestamp: Long = 0,
|
|
runThreadUpdate: Boolean
|
|
): Optional<InsertResult> {
|
|
var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT
|
|
if (retrieved.isPushMessage) {
|
|
type = type or MmsSmsColumns.Types.PUSH_MESSAGE_BIT
|
|
}
|
|
if (retrieved.isExpirationUpdate) {
|
|
type = type or MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT
|
|
}
|
|
if (retrieved.isScreenshotDataExtraction) {
|
|
type = type or MmsSmsColumns.Types.SCREENSHOT_EXTRACTION_BIT
|
|
}
|
|
if (retrieved.isMediaSavedDataExtraction) {
|
|
type = type or MmsSmsColumns.Types.MEDIA_SAVED_EXTRACTION_BIT
|
|
}
|
|
if (retrieved.isMessageRequestResponse) {
|
|
type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT
|
|
}
|
|
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate)
|
|
}
|
|
|
|
@JvmOverloads
|
|
@Throws(MmsException::class)
|
|
fun insertMessageOutbox(
|
|
message: OutgoingMediaMessage,
|
|
threadId: Long, forceSms: Boolean,
|
|
insertListener: InsertListener?,
|
|
serverTimestamp: Long = 0,
|
|
runThreadUpdate: Boolean
|
|
): Long {
|
|
var type = MmsSmsColumns.Types.BASE_SENDING_TYPE
|
|
if (message.isSecure) type =
|
|
type or (MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.PUSH_MESSAGE_BIT)
|
|
if (forceSms) type = type or MmsSmsColumns.Types.MESSAGE_FORCE_SMS_BIT
|
|
if (message.isGroup && message is OutgoingGroupMediaMessage) {
|
|
if (message.isUpdateMessage) type = type or MmsSmsColumns.Types.GROUP_UPDATE_MESSAGE_BIT
|
|
}
|
|
if (message.isExpirationUpdate) {
|
|
type = type or MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT
|
|
}
|
|
val earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.sentTimeMillis)
|
|
val earlyReadReceipts = earlyReadReceiptCache.remove(message.sentTimeMillis)
|
|
val contentValues = ContentValues()
|
|
contentValues.put(DATE_SENT, message.sentTimeMillis)
|
|
contentValues.put(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ)
|
|
contentValues.put(MESSAGE_BOX, type)
|
|
contentValues.put(THREAD_ID, threadId)
|
|
contentValues.put(READ, 1)
|
|
// In open groups messages should be sorted by their server timestamp
|
|
var receivedTimestamp = serverTimestamp
|
|
if (serverTimestamp == 0L) {
|
|
receivedTimestamp = SnodeAPI.nowWithOffset
|
|
}
|
|
contentValues.put(DATE_RECEIVED, receivedTimestamp)
|
|
contentValues.put(SUBSCRIPTION_ID, message.subscriptionId)
|
|
contentValues.put(EXPIRES_IN, message.expiresIn)
|
|
contentValues.put(ADDRESS, message.recipient.address.serialize())
|
|
contentValues.put(
|
|
DELIVERY_RECEIPT_COUNT,
|
|
Stream.of(earlyDeliveryReceipts.values).mapToLong { obj: Long -> obj }
|
|
.sum())
|
|
contentValues.put(
|
|
READ_RECEIPT_COUNT,
|
|
Stream.of(earlyReadReceipts.values).mapToLong { obj: Long -> obj }
|
|
.sum())
|
|
val quoteAttachments: MutableList<Attachment?> = LinkedList()
|
|
if (message.outgoingQuote != null) {
|
|
contentValues.put(QUOTE_ID, message.outgoingQuote!!.id)
|
|
contentValues.put(QUOTE_AUTHOR, message.outgoingQuote!!.author.serialize())
|
|
contentValues.put(QUOTE_MISSING, if (message.outgoingQuote!!.missing) 1 else 0)
|
|
quoteAttachments.addAll(message.outgoingQuote!!.attachments!!)
|
|
}
|
|
if (isDuplicate(message, threadId)) {
|
|
Log.w(TAG, "Ignoring duplicate media message (" + message.sentTimeMillis + ")")
|
|
return -1
|
|
}
|
|
val messageId = insertMediaMessage(
|
|
message.body,
|
|
message.attachments,
|
|
quoteAttachments,
|
|
message.sharedContacts,
|
|
message.linkPreviews,
|
|
contentValues,
|
|
insertListener,
|
|
)
|
|
if (message.recipient.address.isGroup) {
|
|
val members = get(context).groupDatabase()
|
|
.getGroupMembers(message.recipient.address.toGroupString(), false)
|
|
val receiptDatabase = get(context).groupReceiptDatabase()
|
|
receiptDatabase.insert(Stream.of(members).map { obj: Recipient -> obj.address }
|
|
.toList(),
|
|
messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.sentTimeMillis
|
|
)
|
|
for (address in earlyDeliveryReceipts.keys) receiptDatabase.update(
|
|
address,
|
|
messageId,
|
|
GroupReceiptDatabase.STATUS_DELIVERED,
|
|
-1
|
|
)
|
|
for (address in earlyReadReceipts.keys) receiptDatabase.update(
|
|
address,
|
|
messageId,
|
|
GroupReceiptDatabase.STATUS_READ,
|
|
-1
|
|
)
|
|
}
|
|
with (get(context).threadDatabase()) {
|
|
val lastSeen = getLastSeenAndHasSent(threadId).first()
|
|
if (lastSeen < message.sentTimeMillis) {
|
|
setLastSeen(threadId, message.sentTimeMillis)
|
|
}
|
|
setHasSent(threadId, true)
|
|
if (runThreadUpdate) {
|
|
update(threadId, true, true)
|
|
}
|
|
}
|
|
return messageId
|
|
}
|
|
|
|
@Throws(MmsException::class)
|
|
private fun insertMediaMessage(
|
|
body: String?,
|
|
attachments: List<Attachment?>,
|
|
quoteAttachments: List<Attachment?>,
|
|
sharedContacts: List<Contact>,
|
|
linkPreviews: List<LinkPreview>,
|
|
contentValues: ContentValues,
|
|
insertListener: InsertListener?,
|
|
): Long {
|
|
val db = databaseHelper.writableDatabase
|
|
val partsDatabase = get(context).attachmentDatabase()
|
|
val allAttachments: MutableList<Attachment?> = LinkedList()
|
|
val contactAttachments =
|
|
Stream.of(sharedContacts).map { obj: Contact -> obj.avatarAttachment }
|
|
.filter { a: Attachment? -> a != null }
|
|
.toList()
|
|
val previewAttachments =
|
|
Stream.of(linkPreviews).filter { lp: LinkPreview -> lp.getThumbnail().isPresent }
|
|
.map { lp: LinkPreview -> lp.getThumbnail().get() }
|
|
.toList()
|
|
allAttachments.addAll(attachments)
|
|
allAttachments.addAll(contactAttachments)
|
|
allAttachments.addAll(previewAttachments)
|
|
contentValues.put(BODY, body)
|
|
contentValues.put(PART_COUNT, allAttachments.size)
|
|
db.beginTransaction()
|
|
return try {
|
|
val messageId = db.insert(TABLE_NAME, null, contentValues)
|
|
val insertedAttachments = partsDatabase.insertAttachmentsForMessage(
|
|
messageId,
|
|
allAttachments,
|
|
quoteAttachments
|
|
)
|
|
val serializedContacts =
|
|
getSerializedSharedContacts(insertedAttachments, sharedContacts)
|
|
val serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews)
|
|
if (!serializedContacts.isNullOrEmpty()) {
|
|
val contactValues = ContentValues()
|
|
contactValues.put(SHARED_CONTACTS, serializedContacts)
|
|
val database = databaseHelper.readableDatabase
|
|
val rows = database.update(
|
|
TABLE_NAME,
|
|
contactValues,
|
|
"$ID = ?",
|
|
arrayOf(messageId.toString())
|
|
)
|
|
if (rows <= 0) {
|
|
Log.w(TAG, "Failed to update message with shared contact data.")
|
|
}
|
|
}
|
|
if (!serializedPreviews.isNullOrEmpty()) {
|
|
val contactValues = ContentValues()
|
|
contactValues.put(LINK_PREVIEWS, serializedPreviews)
|
|
val database = databaseHelper.readableDatabase
|
|
val rows = database.update(
|
|
TABLE_NAME,
|
|
contactValues,
|
|
"$ID = ?",
|
|
arrayOf(messageId.toString())
|
|
)
|
|
if (rows <= 0) {
|
|
Log.w(TAG, "Failed to update message with link preview data.")
|
|
}
|
|
}
|
|
db.setTransactionSuccessful()
|
|
messageId
|
|
} finally {
|
|
db.endTransaction()
|
|
insertListener?.onComplete()
|
|
notifyConversationListeners(contentValues.getAsLong(THREAD_ID))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete all the messages in single queries where possible
|
|
* @param messageIds a String array representation of regularly Long types representing message IDs
|
|
*/
|
|
private fun deleteMessages(messageIds: Array<String?>) {
|
|
if (messageIds.isEmpty()) {
|
|
return
|
|
}
|
|
// don't need thread IDs
|
|
val queryBuilder = StringBuilder()
|
|
for (i in messageIds.indices) {
|
|
queryBuilder.append("$TABLE_NAME.$ID").append(" = ").append(
|
|
messageIds[i]
|
|
)
|
|
if (i + 1 < messageIds.size) {
|
|
queryBuilder.append(" OR ")
|
|
}
|
|
}
|
|
val idsAsString = queryBuilder.toString()
|
|
val attachmentDatabase = get(context).attachmentDatabase()
|
|
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
|
|
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
|
groupReceiptDatabase.deleteRowsForMessages(messageIds)
|
|
val database = databaseHelper.writableDatabase
|
|
database.delete(TABLE_NAME, idsAsString, null)
|
|
notifyConversationListListeners()
|
|
notifyStickerListeners()
|
|
notifyStickerPackListeners()
|
|
}
|
|
|
|
override fun deleteMessage(messageId: Long): Boolean {
|
|
val threadId = getThreadIdForMessage(messageId)
|
|
val attachmentDatabase = get(context).attachmentDatabase()
|
|
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
|
|
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
|
groupReceiptDatabase.deleteRowsForMessage(messageId)
|
|
val database = databaseHelper.writableDatabase
|
|
database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString()))
|
|
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
|
notifyConversationListeners(threadId)
|
|
notifyStickerListeners()
|
|
notifyStickerPackListeners()
|
|
return threadDeleted
|
|
}
|
|
|
|
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
|
val attachmentDatabase = get(context).attachmentDatabase()
|
|
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
|
|
|
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
|
|
groupReceiptDatabase.deleteRowsForMessages(messageIds)
|
|
|
|
val database = databaseHelper.writableDatabase
|
|
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
|
|
|
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
|
|
notifyConversationListeners(threadId)
|
|
notifyStickerListeners()
|
|
notifyStickerPackListeners()
|
|
return threadDeleted
|
|
}
|
|
|
|
override fun updateThreadId(fromId: Long, toId: Long) {
|
|
val contentValues = ContentValues(1)
|
|
contentValues.put(THREAD_ID, toId)
|
|
|
|
val db = databaseHelper.writableDatabase
|
|
db.update(SmsDatabase.TABLE_NAME, contentValues, "$THREAD_ID = ?", arrayOf("$fromId"))
|
|
notifyConversationListeners(toId)
|
|
notifyConversationListListeners()
|
|
}
|
|
|
|
@Throws(NoSuchMessageException::class)
|
|
override fun getMessageRecord(messageId: Long): MessageRecord {
|
|
rawQuery(RAW_ID_WHERE, arrayOf("$messageId")).use { cursor ->
|
|
return Reader(cursor).next ?: throw NoSuchMessageException("No message for ID: $messageId")
|
|
}
|
|
}
|
|
|
|
fun deleteThread(threadId: Long) {
|
|
deleteThreads(setOf(threadId))
|
|
}
|
|
|
|
fun deleteMediaFor(threadId: Long, fromUser: String? = null) {
|
|
val db = databaseHelper.writableDatabase
|
|
val whereString =
|
|
if (fromUser == null) "$THREAD_ID = ? AND $LINK_PREVIEWS IS NULL"
|
|
else "$THREAD_ID = ? AND $ADDRESS = ? AND $LINK_PREVIEWS IS NULL"
|
|
val whereArgs = if (fromUser == null) arrayOf(threadId.toString()) else arrayOf(threadId.toString(), fromUser)
|
|
var cursor: Cursor? = null
|
|
try {
|
|
cursor = db.query(TABLE_NAME, arrayOf(ID), whereString, whereArgs, null, null, null, null)
|
|
val toDeleteStringMessageIds = mutableListOf<String>()
|
|
while (cursor.moveToNext()) {
|
|
toDeleteStringMessageIds += cursor.getLong(0).toString() // get the ID as a string
|
|
}
|
|
// TODO: this can probably be optimized out,
|
|
// currently attachmentDB uses MmsID not threadID which makes it difficult to delete
|
|
// and clean up on threadID alone
|
|
toDeleteStringMessageIds.toList().chunked(50).forEach { sublist ->
|
|
deleteMessages(sublist.toTypedArray())
|
|
}
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
val threadDb = get(context).threadDatabase()
|
|
threadDb.update(threadId, false, false)
|
|
notifyConversationListeners(threadId)
|
|
notifyStickerListeners()
|
|
notifyStickerPackListeners()
|
|
}
|
|
|
|
fun deleteMessagesFrom(threadId: Long, fromUser: String) { // copied from deleteThreads implementation
|
|
val db = databaseHelper.writableDatabase
|
|
var cursor: Cursor? = null
|
|
val whereString = "$THREAD_ID = ? AND $ADDRESS = ?"
|
|
try {
|
|
cursor =
|
|
db!!.query(TABLE_NAME, arrayOf<String?>(ID), whereString, arrayOf(threadId.toString(), fromUser), null, null, null)
|
|
val toDeleteStringMessageIds = mutableListOf<String>()
|
|
while (cursor.moveToNext()) {
|
|
toDeleteStringMessageIds += cursor.getLong(0).toString() // get the ID as a string
|
|
}
|
|
// TODO: this can probably be optimized out,
|
|
// currently attachmentDB uses MmsID not threadID which makes it difficult to delete
|
|
// and clean up on threadID alone
|
|
toDeleteStringMessageIds.toList().chunked(50).forEach { sublist ->
|
|
deleteMessages(sublist.toTypedArray())
|
|
}
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
val threadDb = get(context).threadDatabase()
|
|
threadDb.update(threadId, false, true)
|
|
notifyConversationListeners(threadId)
|
|
notifyStickerListeners()
|
|
notifyStickerPackListeners()
|
|
}
|
|
|
|
private fun getSerializedSharedContacts(
|
|
insertedAttachmentIds: Map<Attachment?, AttachmentId?>,
|
|
contacts: List<Contact?>
|
|
): String? {
|
|
if (contacts.isEmpty()) return null
|
|
val sharedContactJson = JSONArray()
|
|
for (contact in contacts) {
|
|
try {
|
|
var attachmentId: AttachmentId? = null
|
|
if (contact!!.avatarAttachment != null) {
|
|
attachmentId = insertedAttachmentIds[contact.avatarAttachment]
|
|
}
|
|
val updatedAvatar = Contact.Avatar(
|
|
attachmentId,
|
|
contact.avatarAttachment,
|
|
contact.avatar != null && contact.avatar!!
|
|
.isProfile
|
|
)
|
|
val updatedContact = Contact(
|
|
contact, updatedAvatar
|
|
)
|
|
sharedContactJson.put(JSONObject(updatedContact.serialize()))
|
|
} catch (e: JSONException) {
|
|
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e)
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e)
|
|
}
|
|
}
|
|
return sharedContactJson.toString()
|
|
}
|
|
|
|
private fun getSerializedLinkPreviews(
|
|
insertedAttachmentIds: Map<Attachment?, AttachmentId?>,
|
|
previews: List<LinkPreview?>
|
|
): String? {
|
|
if (previews.isEmpty()) return null
|
|
val linkPreviewJson = JSONArray()
|
|
for (preview in previews) {
|
|
try {
|
|
var attachmentId: AttachmentId? = null
|
|
if (preview!!.getThumbnail().isPresent) {
|
|
attachmentId = insertedAttachmentIds[preview.getThumbnail().get()]
|
|
}
|
|
val updatedPreview = LinkPreview(
|
|
preview.url, preview.title, attachmentId
|
|
)
|
|
linkPreviewJson.put(JSONObject(updatedPreview.serialize()))
|
|
} catch (e: JSONException) {
|
|
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e)
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e)
|
|
}
|
|
}
|
|
return linkPreviewJson.toString()
|
|
}
|
|
|
|
private fun isDuplicateMessageRequestResponse(
|
|
message: IncomingMediaMessage?,
|
|
threadId: Long
|
|
): Boolean {
|
|
val database = databaseHelper.readableDatabase
|
|
val cursor: Cursor? = database!!.query(
|
|
TABLE_NAME,
|
|
null,
|
|
MESSAGE_REQUEST_RESPONSE + " = 1 AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
|
arrayOf<String?>(
|
|
message!!.from.serialize(), threadId.toString()
|
|
),
|
|
null,
|
|
null,
|
|
null,
|
|
"1"
|
|
)
|
|
return try {
|
|
cursor != null && cursor.moveToFirst()
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
private fun isDuplicate(message: IncomingMediaMessage?, threadId: Long): Boolean {
|
|
val database = databaseHelper.readableDatabase
|
|
val cursor: Cursor? = database!!.query(
|
|
TABLE_NAME,
|
|
null,
|
|
DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
|
arrayOf<String?>(
|
|
message!!.sentTimeMillis.toString(), message.from.serialize(), threadId.toString()
|
|
),
|
|
null,
|
|
null,
|
|
null,
|
|
"1"
|
|
)
|
|
return try {
|
|
cursor != null && cursor.moveToFirst()
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
private fun isDuplicate(message: OutgoingMediaMessage?, threadId: Long): Boolean {
|
|
val database = databaseHelper.readableDatabase
|
|
val cursor: Cursor? = database!!.query(
|
|
TABLE_NAME,
|
|
null,
|
|
DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
|
arrayOf<String?>(
|
|
message!!.sentTimeMillis.toString(),
|
|
message.recipient.address.serialize(),
|
|
threadId.toString()
|
|
),
|
|
null,
|
|
null,
|
|
null,
|
|
"1"
|
|
)
|
|
return try {
|
|
cursor != null && cursor.moveToFirst()
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
fun isSent(messageId: Long): Boolean {
|
|
val database = databaseHelper.readableDatabase
|
|
database!!.query(
|
|
TABLE_NAME,
|
|
arrayOf(MESSAGE_BOX),
|
|
"$ID = ?",
|
|
arrayOf<String?>(messageId.toString()),
|
|
null,
|
|
null,
|
|
null
|
|
).use { cursor ->
|
|
if (cursor != null && cursor.moveToNext()) {
|
|
val type = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
|
return MmsSmsColumns.Types.isSentType(type)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun deleteThreads(threadIds: Set<Long>) {
|
|
val db = databaseHelper.writableDatabase
|
|
val where = StringBuilder()
|
|
var cursor: Cursor? = null
|
|
for (threadId in threadIds) {
|
|
where.append(THREAD_ID).append(" = '").append(threadId).append("' OR ")
|
|
}
|
|
val whereString = where.substring(0, where.length - 4)
|
|
try {
|
|
cursor =
|
|
db!!.query(TABLE_NAME, arrayOf<String?>(ID), whereString, null, null, null, null)
|
|
val toDeleteStringMessageIds = mutableListOf<String>()
|
|
while (cursor.moveToNext()) {
|
|
toDeleteStringMessageIds += cursor.getLong(0).toString()
|
|
}
|
|
// TODO: this can probably be optimized out,
|
|
// currently attachmentDB uses MmsID not threadID which makes it difficult to delete
|
|
// and clean up on threadID alone
|
|
toDeleteStringMessageIds.toList().chunked(50).forEach { sublist ->
|
|
deleteMessages(sublist.toTypedArray())
|
|
}
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
val threadDb = get(context).threadDatabase()
|
|
for (threadId in threadIds) {
|
|
val threadDeleted = threadDb.update(threadId, false, true)
|
|
notifyConversationListeners(threadId)
|
|
}
|
|
notifyStickerListeners()
|
|
notifyStickerPackListeners()
|
|
}
|
|
|
|
/*package*/
|
|
fun deleteMessagesInThreadBeforeDate(threadId: Long, date: Long) {
|
|
var cursor: Cursor? = null
|
|
try {
|
|
val db = databaseHelper.readableDatabase
|
|
var where =
|
|
THREAD_ID + " = ? AND (CASE (" + MESSAGE_BOX + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") "
|
|
for (outgoingType in MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES) {
|
|
where += " WHEN $outgoingType THEN $DATE_SENT < $date"
|
|
}
|
|
where += " ELSE $DATE_RECEIVED < $date END)"
|
|
cursor = db!!.query(
|
|
TABLE_NAME,
|
|
arrayOf<String?>(ID),
|
|
where,
|
|
arrayOf<String?>(threadId.toString() + ""),
|
|
null,
|
|
null,
|
|
null
|
|
)
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
Log.i("MmsDatabase", "Trimming: " + cursor.getLong(0))
|
|
deleteMessage(cursor.getLong(0))
|
|
}
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
fun readerFor(cursor: Cursor?): Reader {
|
|
return Reader(cursor)
|
|
}
|
|
|
|
fun readerFor(message: OutgoingMediaMessage?, threadId: Long): OutgoingMessageReader {
|
|
return OutgoingMessageReader(message, threadId)
|
|
}
|
|
|
|
fun setQuoteMissing(messageId: Long): Int {
|
|
val contentValues = ContentValues()
|
|
contentValues.put(QUOTE_MISSING, 1)
|
|
val database = databaseHelper.writableDatabase
|
|
return database!!.update(
|
|
TABLE_NAME,
|
|
contentValues,
|
|
"$ID = ?",
|
|
arrayOf<String?>(messageId.toString())
|
|
)
|
|
}
|
|
|
|
object Status {
|
|
const val DOWNLOAD_INITIALIZED = 1
|
|
const val DOWNLOAD_NO_CONNECTIVITY = 2
|
|
const val DOWNLOAD_CONNECTING = 3
|
|
}
|
|
|
|
inner class OutgoingMessageReader(private val message: OutgoingMediaMessage?,
|
|
private val threadId: Long) {
|
|
private val id = SecureRandom().nextLong()
|
|
val current: MessageRecord
|
|
get() {
|
|
val slideDeck = SlideDeck(context, message!!.attachments)
|
|
return MediaMmsMessageRecord(
|
|
id, message.recipient, message.recipient,
|
|
1, SnodeAPI.nowWithOffset, SnodeAPI.nowWithOffset,
|
|
0, threadId, message.body,
|
|
slideDeck, slideDeck.slides.size,
|
|
if (message.isSecure) MmsSmsColumns.Types.getOutgoingEncryptedMessageType() else MmsSmsColumns.Types.getOutgoingSmsMessageType(),
|
|
LinkedList(),
|
|
LinkedList(),
|
|
message.subscriptionId,
|
|
message.expiresIn,
|
|
SnodeAPI.nowWithOffset, 0,
|
|
if (message.outgoingQuote != null) Quote(
|
|
message.outgoingQuote!!.id,
|
|
message.outgoingQuote!!.author,
|
|
message.outgoingQuote!!.text, // TODO: use the referenced message's content
|
|
message.outgoingQuote!!.missing,
|
|
SlideDeck(context, message.outgoingQuote!!.attachments!!)
|
|
) else null,
|
|
message.sharedContacts, message.linkPreviews, listOf(), false, false
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
inner class Reader(private val cursor: Cursor?) : Closeable {
|
|
val next: MessageRecord?
|
|
get() = if (cursor == null || !cursor.moveToNext()) null else current
|
|
val current: MessageRecord
|
|
get() {
|
|
val mmsType = cursor!!.getLong(cursor.getColumnIndexOrThrow(MESSAGE_TYPE))
|
|
return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) {
|
|
getNotificationMmsMessageRecord(cursor)
|
|
} else {
|
|
getMediaMmsMessageRecord(cursor)
|
|
}
|
|
}
|
|
|
|
private fun getNotificationMmsMessageRecord(cursor: Cursor): NotificationMmsMessageRecord {
|
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
|
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
|
val dateReceived = cursor.getLong(
|
|
cursor.getColumnIndexOrThrow(
|
|
NORMALIZED_DATE_RECEIVED
|
|
)
|
|
)
|
|
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
|
val mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
|
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
|
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID))
|
|
val recipient = getRecipientFor(address)
|
|
val contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION))
|
|
val transactionId = cursor.getString(cursor.getColumnIndexOrThrow(TRANSACTION_ID))
|
|
val messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_SIZE))
|
|
val expiry = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRY))
|
|
val status = cursor.getInt(cursor.getColumnIndexOrThrow(STATUS))
|
|
val deliveryReceiptCount = cursor.getInt(
|
|
cursor.getColumnIndexOrThrow(
|
|
DELIVERY_RECEIPT_COUNT
|
|
)
|
|
)
|
|
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
|
|
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
|
|
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
|
|
if (!isReadReceiptsEnabled(context)) {
|
|
readReceiptCount = 0
|
|
}
|
|
var contentLocationBytes: ByteArray? = null
|
|
var transactionIdBytes: ByteArray? = null
|
|
if (!contentLocation.isNullOrEmpty()) contentLocationBytes = toIsoBytes(
|
|
contentLocation
|
|
)
|
|
if (!transactionId.isNullOrEmpty()) transactionIdBytes = toIsoBytes(
|
|
transactionId
|
|
)
|
|
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
|
|
return NotificationMmsMessageRecord(
|
|
id, recipient, recipient,
|
|
dateSent, dateReceived, deliveryReceiptCount, threadId,
|
|
contentLocationBytes, messageSize, expiry, status,
|
|
transactionIdBytes, mailbox, slideDeck,
|
|
readReceiptCount, hasMention
|
|
)
|
|
}
|
|
|
|
private fun getMediaMmsMessageRecord(cursor: Cursor): MediaMmsMessageRecord {
|
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
|
|
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
|
val dateReceived = cursor.getLong(
|
|
cursor.getColumnIndexOrThrow(
|
|
NORMALIZED_DATE_RECEIVED
|
|
)
|
|
)
|
|
val box = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX))
|
|
val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID))
|
|
val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))
|
|
val addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(ADDRESS_DEVICE_ID))
|
|
val deliveryReceiptCount = cursor.getInt(
|
|
cursor.getColumnIndexOrThrow(
|
|
DELIVERY_RECEIPT_COUNT
|
|
)
|
|
)
|
|
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
|
|
val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY))
|
|
val partCount = cursor.getInt(cursor.getColumnIndexOrThrow(PART_COUNT))
|
|
val mismatchDocument = cursor.getString(
|
|
cursor.getColumnIndexOrThrow(
|
|
MISMATCHED_IDENTITIES
|
|
)
|
|
)
|
|
val networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(NETWORK_FAILURE))
|
|
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
|
|
val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN))
|
|
val expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED))
|
|
val unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1
|
|
val hasMention = cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1
|
|
if (!isReadReceiptsEnabled(context)) {
|
|
readReceiptCount = 0
|
|
}
|
|
val recipient = getRecipientFor(address)
|
|
val mismatches = getMismatchedIdentities(mismatchDocument)
|
|
val networkFailures = getFailures(networkDocument)
|
|
val attachments = get(context).attachmentDatabase().getAttachment(
|
|
cursor
|
|
)
|
|
val contacts: List<Contact?> = getSharedContacts(cursor, attachments)
|
|
val contactAttachments: Set<Attachment?> =
|
|
contacts.mapNotNull { it?.avatarAttachment }.toSet()
|
|
val previews: List<LinkPreview?> = getLinkPreviews(cursor, attachments)
|
|
val previewAttachments: Set<Attachment?> =
|
|
previews.mapNotNull { it?.getThumbnail()?.orNull() }.toSet()
|
|
val slideDeck = getSlideDeck(
|
|
attachments
|
|
.filterNot { o: DatabaseAttachment? -> o in contactAttachments }
|
|
.filterNot { o: DatabaseAttachment? -> o in previewAttachments }
|
|
)
|
|
val quote = getQuote(cursor)
|
|
val reactions = get(context).reactionDatabase().getReactions(cursor)
|
|
return MediaMmsMessageRecord(
|
|
id, recipient, recipient,
|
|
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
|
|
threadId, body, slideDeck!!, partCount, box, mismatches,
|
|
networkFailures, subscriptionId, expiresIn, expireStarted,
|
|
readReceiptCount, quote, contacts, previews, reactions, unidentified, hasMention
|
|
)
|
|
}
|
|
|
|
private fun getRecipientFor(serialized: String?): Recipient {
|
|
val address: Address = if (serialized.isNullOrEmpty() || "insert-address-token" == serialized) {
|
|
UNKNOWN
|
|
} else {
|
|
fromSerialized(serialized)
|
|
}
|
|
return Recipient.from(context, address, true)
|
|
}
|
|
|
|
private fun getMismatchedIdentities(document: String?): List<IdentityKeyMismatch?>? {
|
|
if (!document.isNullOrEmpty()) {
|
|
try {
|
|
return JsonUtil.fromJson(document, IdentityKeyMismatchList::class.java).list
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, e)
|
|
}
|
|
}
|
|
return LinkedList()
|
|
}
|
|
|
|
private fun getFailures(document: String?): List<NetworkFailure?>? {
|
|
if (!document.isNullOrEmpty()) {
|
|
try {
|
|
return JsonUtil.fromJson(document, NetworkFailureList::class.java).list
|
|
} catch (ioe: IOException) {
|
|
Log.w(TAG, ioe)
|
|
}
|
|
}
|
|
return LinkedList()
|
|
}
|
|
|
|
private fun getSlideDeck(attachments: List<DatabaseAttachment?>): SlideDeck? {
|
|
val messageAttachments: List<Attachment?>? = Stream.of(attachments)
|
|
.filterNot { obj: DatabaseAttachment? -> obj!!.isQuote }
|
|
.toList()
|
|
return SlideDeck(context, messageAttachments!!)
|
|
}
|
|
|
|
private fun getQuote(cursor: Cursor): Quote? {
|
|
val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID))
|
|
val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR))
|
|
if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null
|
|
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor)
|
|
val quoteText = retrievedQuote?.body
|
|
val quoteMissing = retrievedQuote == null
|
|
val quoteDeck = (
|
|
(retrievedQuote as? MmsMessageRecord)?.slideDeck ?:
|
|
Stream.of(get(context).attachmentDatabase().getAttachment(cursor))
|
|
.filter { obj: DatabaseAttachment? -> obj!!.isQuote }
|
|
.toList()
|
|
.let { SlideDeck(context, it) }
|
|
)
|
|
return Quote(
|
|
quoteId,
|
|
fromExternal(context, quoteAuthor),
|
|
quoteText,
|
|
quoteMissing,
|
|
quoteDeck
|
|
)
|
|
}
|
|
|
|
override fun close() {
|
|
cursor?.close()
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private val TAG = MmsDatabase::class.java.simpleName
|
|
const val TABLE_NAME: String = "mms"
|
|
const val DATE_SENT: String = "date"
|
|
const val DATE_RECEIVED: String = "date_received"
|
|
const val MESSAGE_BOX: String = "msg_box"
|
|
const val CONTENT_LOCATION: String = "ct_l"
|
|
const val EXPIRY: String = "exp"
|
|
const val MESSAGE_TYPE: String = "m_type"
|
|
const val MESSAGE_SIZE: String = "m_size"
|
|
const val STATUS: String = "st"
|
|
const val TRANSACTION_ID: String = "tr_id"
|
|
const val PART_COUNT: String = "part_count"
|
|
const val NETWORK_FAILURE: String = "network_failures"
|
|
const val QUOTE_ID: String = "quote_id"
|
|
const val QUOTE_AUTHOR: String = "quote_author"
|
|
const val QUOTE_BODY: String = "quote_body"
|
|
const val QUOTE_ATTACHMENT: String = "quote_attachment"
|
|
const val QUOTE_MISSING: String = "quote_missing"
|
|
const val SHARED_CONTACTS: String = "shared_contacts"
|
|
const val LINK_PREVIEWS: String = "previews"
|
|
const val CREATE_TABLE: String =
|
|
"CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
|
THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
|
|
READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " +
|
|
"sub_cs" + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " +
|
|
"ct_t" + " TEXT, " + CONTENT_LOCATION + " TEXT, " + ADDRESS + " TEXT, " +
|
|
ADDRESS_DEVICE_ID + " INTEGER, " +
|
|
EXPIRY + " INTEGER, " + "m_cls" + " TEXT, " + MESSAGE_TYPE + " INTEGER, " +
|
|
"v" + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + "pri" + " INTEGER, " +
|
|
"rr" + " INTEGER, " + "rpt_a" + " INTEGER, " + "resp_st" + " INTEGER, " +
|
|
STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + "retr_st" + " INTEGER, " +
|
|
"retr_txt" + " TEXT, " + "retr_txt_cs" + " INTEGER, " + "read_status" + " INTEGER, " +
|
|
"ct_cls" + " INTEGER, " + "resp_txt" + " TEXT, " + "d_tm" + " INTEGER, " +
|
|
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
|
|
NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER, " +
|
|
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
|
|
EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " INTEGER DEFAULT 0, " +
|
|
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + QUOTE_ID + " INTEGER DEFAULT 0, " +
|
|
QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
|
|
QUOTE_MISSING + " INTEGER DEFAULT 0, " + SHARED_CONTACTS + " TEXT, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
|
LINK_PREVIEWS + " TEXT);"
|
|
|
|
@JvmField
|
|
val CREATE_INDEXS: Array<String> = arrayOf(
|
|
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON $TABLE_NAME ($THREAD_ID);",
|
|
"CREATE INDEX IF NOT EXISTS mms_read_index ON $TABLE_NAME ($READ);",
|
|
"CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON $TABLE_NAME($READ,$NOTIFIED,$THREAD_ID);",
|
|
"CREATE INDEX IF NOT EXISTS mms_message_box_index ON $TABLE_NAME ($MESSAGE_BOX);",
|
|
"CREATE INDEX IF NOT EXISTS mms_date_sent_index ON $TABLE_NAME ($DATE_SENT);",
|
|
"CREATE INDEX IF NOT EXISTS mms_thread_date_index ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED);"
|
|
)
|
|
private val MMS_PROJECTION: Array<String> = arrayOf(
|
|
"$TABLE_NAME.$ID AS $ID",
|
|
THREAD_ID,
|
|
"$DATE_SENT AS $NORMALIZED_DATE_SENT",
|
|
"$DATE_RECEIVED AS $NORMALIZED_DATE_RECEIVED",
|
|
MESSAGE_BOX,
|
|
READ,
|
|
CONTENT_LOCATION,
|
|
EXPIRY,
|
|
MESSAGE_TYPE,
|
|
MESSAGE_SIZE,
|
|
STATUS,
|
|
TRANSACTION_ID,
|
|
BODY,
|
|
PART_COUNT,
|
|
ADDRESS,
|
|
ADDRESS_DEVICE_ID,
|
|
DELIVERY_RECEIPT_COUNT,
|
|
READ_RECEIPT_COUNT,
|
|
MISMATCHED_IDENTITIES,
|
|
NETWORK_FAILURE,
|
|
SUBSCRIPTION_ID,
|
|
EXPIRES_IN,
|
|
EXPIRE_STARTED,
|
|
NOTIFIED,
|
|
QUOTE_ID,
|
|
QUOTE_AUTHOR,
|
|
QUOTE_BODY,
|
|
QUOTE_ATTACHMENT,
|
|
QUOTE_MISSING,
|
|
SHARED_CONTACTS,
|
|
LINK_PREVIEWS,
|
|
UNIDENTIFIED,
|
|
HAS_MENTION,
|
|
"json_group_array(json_object(" +
|
|
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
|
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
|
"'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " +
|
|
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
|
|
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
|
|
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
|
|
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
|
|
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
|
|
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
|
|
"'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," +
|
|
"'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," +
|
|
"'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + "," +
|
|
"'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + "," +
|
|
"'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " +
|
|
"'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " +
|
|
"'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " +
|
|
"'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " +
|
|
"'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " +
|
|
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " +
|
|
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
|
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID +
|
|
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
|
|
"json_group_array(json_object(" +
|
|
"'" + ReactionDatabase.ROW_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.ROW_ID + ", " +
|
|
"'" + ReactionDatabase.MESSAGE_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + ", " +
|
|
"'" + ReactionDatabase.IS_MMS + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + ", " +
|
|
"'" + ReactionDatabase.AUTHOR_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.AUTHOR_ID + ", " +
|
|
"'" + ReactionDatabase.EMOJI + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.EMOJI + ", " +
|
|
"'" + ReactionDatabase.SERVER_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SERVER_ID + ", " +
|
|
"'" + ReactionDatabase.COUNT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.COUNT + ", " +
|
|
"'" + ReactionDatabase.SORT_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SORT_ID + ", " +
|
|
"'" + ReactionDatabase.DATE_SENT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_SENT + ", " +
|
|
"'" + ReactionDatabase.DATE_RECEIVED + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_RECEIVED +
|
|
")) AS " + ReactionDatabase.REACTION_JSON_ALIAS
|
|
)
|
|
private const val RAW_ID_WHERE: String = "$TABLE_NAME._id = ?"
|
|
const val CREATE_MESSAGE_REQUEST_RESPONSE_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $MESSAGE_REQUEST_RESPONSE INTEGER DEFAULT 0;"
|
|
const val CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_UNREAD INTEGER DEFAULT 0;"
|
|
const val CREATE_REACTIONS_LAST_SEEN_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_LAST_SEEN INTEGER DEFAULT 0;"
|
|
const val CREATE_HAS_MENTION_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $HAS_MENTION INTEGER DEFAULT 0;"
|
|
}
|
|
}
|