proton-mail-android/app/src/main/java/ch/protonmail/android/data/local/MessageDao.kt

407 lines
16 KiB
Kotlin

/*
* Copyright (c) 2022 Proton AG
*
* This file is part of Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.data.local
import android.provider.BaseColumns
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import ch.protonmail.android.data.local.model.Attachment
import ch.protonmail.android.data.local.model.COLUMN_ATTACHMENT_ID
import ch.protonmail.android.data.local.model.COLUMN_ATTACHMENT_MESSAGE_ID
import ch.protonmail.android.data.local.model.COLUMN_CONVERSATION_ID
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_ACCESS_TIME
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_EXPIRATION_TIME
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_ID
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_LABELS
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_LOCATION
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_PREFIX_SENDER
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_SENDER_EMAIL
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_SENDER_NAME
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_SUBJECT
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_TIME
import ch.protonmail.android.data.local.model.COLUMN_MESSAGE_UNREAD
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.TABLE_ATTACHMENTS
import ch.protonmail.android.data.local.model.TABLE_MESSAGES
import io.reactivex.Flowable
import io.reactivex.Single
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import me.proton.core.data.room.db.BaseDao
import timber.log.Timber
@Dao
abstract class MessageDao : BaseDao<Message>() {
fun searchMessages(keyword: String): Flow<List<Message>> =
searchMessages(keyword, keyword, keyword)
@Query(
"""
SELECT *
FROM $TABLE_MESSAGES
WHERE $COLUMN_MESSAGE_SUBJECT LIKE '%'||:subject||'%'
OR ${COLUMN_MESSAGE_PREFIX_SENDER + COLUMN_MESSAGE_SENDER_NAME} LIKE '%'||:senderName||'%'
OR ${COLUMN_MESSAGE_PREFIX_SENDER + COLUMN_MESSAGE_SENDER_EMAIL} LIKE '%'||:senderEmail||'%'
ORDER BY $COLUMN_MESSAGE_TIME DESC
"""
)
@Transaction
abstract fun searchMessages(subject: String, senderName: String, senderEmail: String): Flow<List<Message>>
@Query("SELECT COUNT($COLUMN_MESSAGE_ID) FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_LOCATION = :location ")
abstract fun getMessagesCountByLocation(location: Int): Int
@Query(
"""
SELECT $COLUMN_MESSAGE_ID
FROM $TABLE_MESSAGES
WHERE $COLUMN_MESSAGE_LABELS LIKE '%' || :label || '%'
ORDER BY $COLUMN_MESSAGE_TIME DESC
"""
)
abstract suspend fun getMessageIdsByLabelId(label: String): List<String>
fun observeMessages(label: String, unread: Boolean? = null, newestFirst: Boolean = true): Flow<List<Message>> =
if (unread == null) observeMessages(label, newestFirst)
else observeMessagesWithUnreadStatus(label, unread, newestFirst)
/**
* Since we have decided to use this query to also retrieve messages that are SENT now the query looks for the
* :label at the beginning, middle or end of the $COLUMN_MESSAGE_LABELS string.
* The $COLUMN_MESSAGE_LABELS string uses semicolon(;) as separator ex.
* `0;5;7;N2ttCeO9GZ7kNTfW5MUfZ8nP6pUOEnNiWVVlOIPgeIFGBKqrBowMR4wefbeIelXsgDLiYZ5YFRDiFZ-VPC0YUA==` so the label
* that we are looking for can be preceded and followed by zero or one semicolon(;)
**/
@Query(
"""
SELECT *
FROM $TABLE_MESSAGES
WHERE
($COLUMN_MESSAGE_LABELS LIKE :label
OR $COLUMN_MESSAGE_LABELS LIKE :label || ';%'
OR $COLUMN_MESSAGE_LABELS LIKE '%;' || :label
OR $COLUMN_MESSAGE_LABELS LIKE '%;' || :label || ';%')
ORDER BY
CASE WHEN :newestFirst THEN $COLUMN_MESSAGE_TIME END DESC,
CASE WHEN NOT :newestFirst THEN $COLUMN_MESSAGE_TIME END ASC
"""
)
@Transaction
protected abstract fun observeMessages(label: String, newestFirst: Boolean = true): Flow<List<Message>>
/**
* Since we have decided to use this query to also retrieve messages that are SENT now the query looks for the
* :label at the beginning, middle or end of the $COLUMN_MESSAGE_LABELS string.
* The $COLUMN_MESSAGE_LABELS string uses semicolon(;) as separator ex.
* `0;5;7;N2ttCeO9GZ7kNTfW5MUfZ8nP6pUOEnNiWVVlOIPgeIFGBKqrBowMR4wefbeIelXsgDLiYZ5YFRDiFZ-VPC0YUA==` so the label
* that we are looking for can be preceded and followed by zero or one semicolon(;)
**/
@Query(
"""
SELECT *
FROM $TABLE_MESSAGES
WHERE
($COLUMN_MESSAGE_LABELS LIKE :label
OR $COLUMN_MESSAGE_LABELS LIKE :label || ';%'
OR $COLUMN_MESSAGE_LABELS LIKE '%;' || :label
OR $COLUMN_MESSAGE_LABELS LIKE '%;' || :label || ';%')
AND
$COLUMN_MESSAGE_UNREAD = :unread
ORDER BY
CASE WHEN :newestFirst THEN $COLUMN_MESSAGE_TIME END DESC,
CASE WHEN NOT :newestFirst THEN $COLUMN_MESSAGE_TIME END ASC
"""
)
@Transaction
protected abstract fun observeMessagesWithUnreadStatus(
label: String, unread: Boolean, newestFirst: Boolean = true
): Flow<List<Message>>
@Query(
"""SELECT COUNT($COLUMN_MESSAGE_ID)
FROM $TABLE_MESSAGES
WHERE ($COLUMN_MESSAGE_LABELS LIKE :location
OR $COLUMN_MESSAGE_LABELS LIKE :location || ';%'
OR $COLUMN_MESSAGE_LABELS LIKE '%;' || :location
OR $COLUMN_MESSAGE_LABELS LIKE '%;' || :location || ';%')"""
)
abstract fun observeMessagesCountByLocation(location: String): Flow<Int>
fun findMessageById(messageId: String): Flow<Message?> = findMessageInfoById(messageId)
.onEach { message ->
message ?: return@onEach
message.attachments = message.attachments(this)
}
suspend fun findMessageByIdOnce(messageId: String): Message? = findMessageInfoByIdOnce(messageId)?.also { message ->
message.attachments = findAttachmentByMessageId(messageId)
}
@Deprecated("Use Flow variant", ReplaceWith("findMessageById(messageId).first()"))
fun findMessageByIdBlocking(messageId: String): Message? = findMessageInfoByIdBlocking(messageId)
?.also { message ->
message.attachments = message.attachmentsBlocking(this)
}
fun findMessageByIdSingle(messageId: String) = findMessageInfoByIdSingle(messageId)
fun findMessageByIdObservable(messageId: String) = findMessageInfoByIdObservable(messageId)
fun findMessageByDatabaseId(messageDbId: Long): Flow<Message?> = findMessageInfoByDbId(messageDbId)
.onEach { message ->
message ?: return@onEach
message.attachments = message.attachmentsBlocking(this)
}
fun findAllMessageByLastMessageAccessTime(laterThan: Long = 0): Flow<List<Message>> =
observeAllMessagesInfoByLastMessageAccessTime(laterThan)
.map { messages ->
messages.onEach { message ->
message.attachments = message.attachments(this)
}
}
@Query("SELECT * FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_ID = :messageId")
abstract fun findMessageInfoById(messageId: String): Flow<Message?>
@Query("SELECT * FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_ID = :messageId")
abstract suspend fun findMessageInfoByIdOnce(messageId: String): Message?
@Deprecated("Use Flow variant", ReplaceWith("findMessageInfoById(messageId).first()"))
@Query("SELECT * FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_ID = :messageId")
abstract fun findMessageInfoByIdBlocking(messageId: String): Message?
@Query("SELECT * FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_ID = :messageId")
abstract fun findMessageInfoByIdSingle(messageId: String): Single<Message>
@Query("SELECT * FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_ID = :messageId")
abstract fun findMessageInfoByIdObservable(messageId: String): Flowable<Message>
@Query("SELECT * FROM $TABLE_MESSAGES WHERE ${BaseColumns._ID}=:messageDbId")
abstract fun findMessageInfoByDbId(messageDbId: Long): Flow<Message?>
@Query(
"""
SELECT *
FROM $TABLE_MESSAGES
WHERE $COLUMN_MESSAGE_ACCESS_TIME > :laterThan
ORDER BY $COLUMN_MESSAGE_ACCESS_TIME
"""
)
@Transaction
abstract fun observeAllMessagesInfoByLastMessageAccessTime(laterThan: Long = 0): Flow<List<Message>>
@Query(
"""
SELECT *
FROM $TABLE_MESSAGES
WHERE $COLUMN_CONVERSATION_ID = :conversationId
ORDER BY $COLUMN_MESSAGE_TIME DESC
"""
)
abstract fun observeAllMessagesInfoFromConversation(conversationId: String): Flow<List<Message>>
@Query(
"""
SELECT *
FROM $TABLE_MESSAGES
WHERE $COLUMN_CONVERSATION_ID = :conversationId
ORDER BY $COLUMN_MESSAGE_TIME DESC
"""
)
abstract suspend fun findAllConversationMessagesSortedByNewest(conversationId: String): List<Message>
suspend fun saveMessage(message: Message): Long {
Timber.d(
"saveMessage %s, location: %s, labels: %s, isRead: %s",
message.messageId, message.location, message.allLabelIDs, message.isRead
)
processMessageAttachments(message)
return saveMessageInfo(message)
}
private suspend fun processMessageAttachments(message: Message) {
val messageId = message.messageId
var localAttachments: List<Attachment> = ArrayList()
if (messageId != null) {
localAttachments = findAttachmentsByMessageId(messageId).first()
}
var preservedAttachments = message.attachments.map { attachment ->
Attachment(
fileName = attachment.fileName,
mimeType = attachment.mimeType,
fileSize = attachment.fileSize,
keyPackets = attachment.keyPackets,
messageId = message.messageId ?: "",
isUploaded = attachment.isUploaded,
isUploading = attachment.isUploading,
isNew = attachment.isNew,
filePath = attachment.filePath,
attachmentId = attachment.attachmentId,
headers = attachment.headers,
inline = attachment.inline
)
}
val hasAnyAttachment =
message.embeddedImageIds.isNotEmpty() && preservedAttachments.isEmpty() && localAttachments.isNotEmpty()
if (hasAnyAttachment) {
localAttachments.forEach { localAttachment ->
localAttachment.setMessage(message)
preservedAttachments = localAttachments
}
} else {
preservedAttachments.forEach { preservedAtt ->
if (localAttachments.isNotEmpty()) {
localAttachments.find { it.attachmentId == preservedAtt.attachmentId }?.let { attachment ->
if (attachment.inline) {
preservedAtt.inline = attachment.inline
} else {
if (message.embeddedImageIds.isNotEmpty()) {
preservedAtt.setMessage(message)
}
}
preservedAtt.isUploaded = attachment.isUploaded
preservedAtt.isUploading = attachment.isUploading
}
} else {
if (message.embeddedImageIds.isNotEmpty()) {
preservedAtt.setMessage(message)
}
}
}
}
val attachmentsToDelete = message.attachments(this) // .filter { it.messageId != message.messageId }
if (attachmentsToDelete.isNotEmpty() && preservedAttachments.isEmpty()) {
preservedAttachments = localAttachments
}
if (attachmentsToDelete.isNotEmpty()) {
deleteAllAttachments(attachmentsToDelete)
}
if (preservedAttachments.isNotEmpty()) {
val attachmentsToSave = (
preservedAttachments +
localAttachments.filter { !it.isUploaded }
).filter { (!it.isUploaded && it.filePath == null).not() } //don't save local attachments without a path
saveAllAttachments(attachmentsToSave)
}
message.attachments = preservedAttachments
}
suspend fun saveMessages(messages: List<Message>) {
Timber.d("saveMessages ${messages.map { it.messageId }}")
messages.forEach {
processMessageAttachments(it)
}
return saveMessagesInfo(messages)
}
@Query("DELETE FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_LOCATION = :location")
abstract fun deleteMessagesByLocation(location: Int)
@Query("DELETE FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_LABELS LIKE '%'||:labelId||'%'")
abstract fun deleteMessagesByLabelBlocking(labelId: String)
@Query("DELETE FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_LABELS LIKE '%'||:labelId||'%'")
abstract suspend fun deleteMessagesByLabel(labelId: String)
@Query(
"""
DELETE
FROM $TABLE_MESSAGES
WHERE $COLUMN_MESSAGE_EXPIRATION_TIME <> 0
AND $COLUMN_MESSAGE_EXPIRATION_TIME < :currentTime
"""
)
abstract fun deleteExpiredMessages(currentTime: Long)
@Query(
"""
DELETE
FROM $TABLE_MESSAGES
WHERE $COLUMN_MESSAGE_ID IN (:ids)
"""
)
abstract suspend fun deleteMessagesByIds(ids: List<String>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveMessageInfo(message: Message): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveMessagesInfo(messages: List<Message>)
@Query("DELETE FROM $TABLE_MESSAGES")
abstract fun clearMessagesCache()
@Delete
abstract fun deleteMessage(message: Message)
@Query("SELECT * FROM $TABLE_ATTACHMENTS WHERE $COLUMN_ATTACHMENT_MESSAGE_ID = :messageId")
abstract fun findAttachmentsByMessageIdAsync(messageId: String): LiveData<List<Attachment>>
@Query("SELECT * FROM $TABLE_ATTACHMENTS WHERE $COLUMN_ATTACHMENT_MESSAGE_ID = :messageId")
abstract fun findAttachmentsByMessageId(messageId: String): Flow<List<Attachment>>
@Query("SELECT * FROM $TABLE_ATTACHMENTS WHERE $COLUMN_ATTACHMENT_MESSAGE_ID = :messageId")
abstract suspend fun findAttachmentByMessageId(messageId: String): List<Attachment>
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveAttachment(attachment: Attachment): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveAllAttachments(attachments: List<Attachment>): List<Long>
@Delete
abstract suspend fun deleteAllAttachments(attachments: List<Attachment>)
@Query("DELETE FROM $TABLE_ATTACHMENTS WHERE $COLUMN_ATTACHMENT_MESSAGE_ID IN (:messageIds)")
abstract suspend fun deleteAttachmentsByMessageIds(messageIds: List<String>)
@Delete
abstract fun deleteAttachment(vararg attachment: Attachment)
@Query("SELECT * FROM $TABLE_ATTACHMENTS WHERE $COLUMN_ATTACHMENT_ID=:correctId ")
abstract fun findAttachmentByIdCorrectId(correctId: String): Attachment?
fun findAttachmentById(attachmentId: String): Attachment? {
if (attachmentId.startsWith("PGPAttachment")) {
val parts = attachmentId.split("_".toRegex()).dropLastWhile { it.isEmpty() }
if (parts.size != 4) {
return null
}
findMessageInfoById(parts[1])
}
return findAttachmentByIdCorrectId(attachmentId)
}
@Query("DELETE FROM $TABLE_ATTACHMENTS")
abstract fun clearAttachmentsCache()
}