proton-mail-android/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt

355 lines
14 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.activities.messageDetails.repository
import android.content.Context
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.room.Transaction
import ch.protonmail.android.activities.messageDetails.IntentExtrasData
import ch.protonmail.android.api.models.DatabaseProvider
import ch.protonmail.android.api.models.User
import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker
import ch.protonmail.android.core.BigContentHolder
import ch.protonmail.android.core.Constants
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.data.local.MessageDao
import ch.protonmail.android.pendingaction.data.PendingActionDao
import ch.protonmail.android.data.local.model.Attachment
import ch.protonmail.android.data.local.model.LocalAttachment
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.labels.domain.LabelRepository
import ch.protonmail.android.labels.domain.model.Label
import ch.protonmail.android.labels.domain.model.LabelId
import ch.protonmail.android.pendingaction.data.model.PendingSend
import ch.protonmail.android.pendingaction.data.model.PendingUpload
import ch.protonmail.android.utils.MessageUtils
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import io.reactivex.Flowable
import io.reactivex.Single
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import me.proton.core.domain.entity.UserId
import me.proton.core.util.kotlin.equalsNoCase
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.Arrays
import java.util.HashSet
import javax.inject.Inject
private const val MAX_BODY_SIZE_IN_DB = 900 * 1024 // 900 KB
private const val DEPRECATION_MESSAGE =
"We should strive towards moving methods out of this repository and stopping using it."
@Deprecated("Scheduled to be removed, do not add new usages", ReplaceWith("MessageRepository"))
class MessageDetailsRepository @Inject constructor(
private val applicationContext: Context,
private val userManager: UserManager,
private val databaseProvider: DatabaseProvider,
private val attachmentsWorker: DownloadEmbeddedAttachmentsWorker.Enqueuer,
private val labelRepository: LabelRepository
) {
private var requestedUserId: UserId? = null
private val userId: UserId
get() = requestedUserId ?: userManager.requireCurrentUserId()
private val messagesDao: MessageDao
get() = databaseProvider.provideMessageDao(userId)
private val pendingActionDao: PendingActionDao
get() = databaseProvider.providePendingActionDao(userId)
@AssistedInject
constructor(
applicationContext: Context,
userManager: UserManager,
databaseProvider: DatabaseProvider,
attachmentsWorker: DownloadEmbeddedAttachmentsWorker.Enqueuer,
labelRepository: LabelRepository,
@Assisted userId: UserId,
) : this(applicationContext, userManager, databaseProvider, attachmentsWorker, labelRepository) {
requestedUserId = userId
}
fun findMessageByIdBlocking(messageId: String): Message? =
messagesDao.findMessageByIdBlocking(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) }
fun findMessageById(messageId: String): Flow<Message?> =
messagesDao.findMessageById(messageId).map { readMessageBodyFromFileIfNeeded(it) }
fun findMessageByIdSingle(messageId: String): Single<Message> =
messagesDao.findMessageByIdSingle(messageId).map(readMessageBodyFromFileIfNeeded)
fun findMessageByIdObservable(messageId: String): Flowable<Message> =
messagesDao.findMessageByIdObservable(messageId).map(readMessageBodyFromFileIfNeeded)
fun findMessageByDatabaseId(messageDbId: Long): Flow<Message?> =
messagesDao.findMessageByDatabaseId(messageDbId).map { readMessageBodyFromFileIfNeeded(it) }
@Deprecated("Use Flow", ReplaceWith("findMessageByDatabaseId(messageDbId).first()"))
fun findMessageByDatabaseIdBlocking(messageDbId: Long): Message? =
runBlocking {
findMessageByDatabaseId(messageDbId).first()
}
private fun findAllMessageByLastMessageAccessTime(laterThan: Long = 0): Flow<List<Message>> =
messagesDao.findAllMessageByLastMessageAccessTime(laterThan)
.map { list ->
list.mapNotNull { readMessageBodyFromFileIfNeeded(it) }
}
fun findAllMessageByLastMessageAccessTimeBlocking(laterThan: Long = 0) =
runBlocking { findAllMessageByLastMessageAccessTime(laterThan).first() }
/**
* Helper function mapping Message with body saved in file to body in memory.
*/
@Deprecated(
message = DEPRECATION_MESSAGE,
replaceWith = ReplaceWith(
expression = "messageBodyFileManager.readMessageBodyFromFile(message)",
imports = arrayOf("ch.protonmail.android.utils.MessageBodyFileManager")
)
)
private val readMessageBodyFromFileIfNeeded: (Message?) -> Message? = { message ->
message?.apply {
if (true == messageBody?.startsWith("file://")) {
val messageBodyFile = File(
applicationContext.filesDir.toString() + Constants.DIR_MESSAGE_BODY_DOWNLOADS,
messageId?.replace(" ", "_")?.replace("/", ":")
)
messageBody = try {
Timber.d("Reading body from file ${messageBodyFile.name}")
FileInputStream(messageBodyFile).bufferedReader().use { it.readText() }
} catch (e: Exception) {
Timber.e(e, "Error reading file body")
null
}
}
}
}
fun searchMessages(subject: String, senderName: String, senderEmail: String): List<Message> =
runBlocking {
messagesDao.searchMessages(subject, senderName, senderEmail).first()
.mapNotNull { readMessageBodyFromFileIfNeeded(it) }
}
suspend fun findAttachmentsByMessageId(messageId: String): List<Attachment> =
messagesDao.findAttachmentsByMessageId(messageId).first()
suspend fun saveAttachment(attachment: Attachment) = messagesDao.saveAttachment(attachment)
suspend fun deleteAllAttachments(attachments: List<Attachment>) = messagesDao.deleteAllAttachments(attachments)
suspend fun saveMessage(message: Message): Long {
saveFile(message)
return messagesDao.saveMessage(message)
}
@Deprecated("Use suspend function", ReplaceWith("saveMessage(message)"))
fun saveMessageBlocking(message: Message): Long = runBlocking {
saveMessage(message)
}
@Deprecated("Use suspend function", ReplaceWith("saveAllMessages(messages)"))
@Transaction
fun saveAllMessagesBlocking(messages: List<Message>) {
messages.forEach { saveMessageBlocking(it) }
}
@Transaction
suspend fun saveMessagesInOneTransaction(messages: List<Message>) {
if (messages.isEmpty()) {
return
}
messages.forEach { message ->
saveFile(message)
}
messagesDao.saveMessages(messages)
}
private fun saveFile(message: Message) {
val localFilePath = saveBodyToFileIfNeeded(message)
localFilePath?.let {
message.messageBody = it
}
}
/**
* This is a workaround for too large Message bodies saved into database. We offload them to file
* in internal memory, if needed.
*
* @return absolute path to saved File
*/
@Deprecated(
message = DEPRECATION_MESSAGE,
replaceWith = ReplaceWith(
expression = "messageBodyFileManager.saveMessageBodyToFile(message)",
imports = arrayOf("ch.protonmail.android.utils.MessageBodyFileManager")
)
)
@WorkerThread
private fun saveBodyToFileIfNeeded(message: Message, overwrite: Boolean = true): String? {
val messageId = message.messageId
val messageBody = message.messageBody
if (messageId != null && messageBody != null && messageBody.toByteArray().size > MAX_BODY_SIZE_IN_DB) {
val messageBodyDirectory =
File(applicationContext.filesDir.toString() + Constants.DIR_MESSAGE_BODY_DOWNLOADS)
messageBodyDirectory.mkdirs()
val messageBodyFile = File(messageBodyDirectory, messageId.replace(" ", "_").replace("/", ":"))
return try {
if (overwrite || messageBodyFile.length() == 0L /* file doesn't exist */) {
FileOutputStream(messageBodyFile).use {
it.write(messageBody.toByteArray())
}
Log.d("PMTAG", "wrote message body to file")
}
Log.d("PMTAG", "returning path to message body ${messageBodyFile.absolutePath}")
"file://${messageBodyFile.absolutePath}"
} catch (e: Exception) {
Timber.i(e, "saveBodyToFileIfNeeded error")
null
}
}
return null
}
fun deleteMessage(message: Message) = messagesDao.deleteMessage(message)
fun deleteMessagesByLocation(location: Constants.MessageLocationType) =
messagesDao.deleteMessagesByLocation(location.messageLocationTypeValue)
fun deleteMessagesByLabel(labelId: String) =
messagesDao.deleteMessagesByLabelBlocking(labelId)
fun findAttachmentById(attachmentId: String) = messagesDao.findAttachmentById(attachmentId)
suspend fun findLabelsWithIds(labelIds: List<String>): List<Label> =
labelRepository.findLabels(labelIds.map { LabelId(it) })
suspend fun prepareEditMessageIntent(
messageAction: Constants.MessageActionType,
message: Message,
user: User,
newMessageTitle: String?,
content: String,
mBigContentHolder: BigContentHolder,
mImagesDisplayed: Boolean,
remoteContentDisplayed: Boolean,
embeddedImagesAttachments: MutableList<Attachment>?,
dispatcher: CoroutineDispatcher
): IntentExtrasData = withContext(dispatcher) {
var toRecipientListString = ""
var includeCCList = false
val replyToEmailsFiltered = ArrayList<String>()
when (messageAction) {
Constants.MessageActionType.REPLY -> {
val replyToEmails = message.replyToEmails
for (replyToEmail in replyToEmails) {
if (user.addresses!!.none { it.email equalsNoCase replyToEmail }) {
replyToEmailsFiltered.add(replyToEmail)
}
}
if (replyToEmailsFiltered.isEmpty()) {
replyToEmailsFiltered.addAll(listOf(message.toListString))
}
toRecipientListString = MessageUtils.getListOfStringsAsString(replyToEmailsFiltered)
}
Constants.MessageActionType.REPLY_ALL -> {
val emailSet = HashSet(
Arrays.asList(
*message.toListString.split(Constants.EMAIL_DELIMITER.toRegex())
.dropLastWhile { it.isEmpty() }.toTypedArray()
)
)
val senderEmailAddress = if (message.replyToEmails.isNotEmpty())
message.replyToEmails[0] else message.sender!!.emailAddress
toRecipientListString = if (emailSet.contains(senderEmailAddress)) {
message.toListString
} else {
senderEmailAddress + Constants.EMAIL_DELIMITER + message.toListString
}
includeCCList = true
}
else -> {
// NO OP
}
}
val attachments = withContext(dispatcher) {
ArrayList(LocalAttachment.createLocalAttachmentList(message.attachments(messagesDao)))
}
IntentExtrasData.Builder()
.user(user)
.userAddresses()
.message(message)
.toRecipientListString(toRecipientListString)
.messageCcList()
.includeCCList(includeCCList)
.senderEmailAddress()
.messageSenderName()
.newMessageTitle(newMessageTitle)
.content(content)
.mBigContentHolder(mBigContentHolder)
.body()
.messageAction(messageAction)
.imagesDisplayed(mImagesDisplayed)
.remoteContentDisplayed(remoteContentDisplayed)
.isPGPMime()
.timeMs()
.messageId()
.addressID()
.addressEmailAlias()
.attachments(attachments, embeddedImagesAttachments)
.build()
}
fun startDownloadEmbeddedImages(messageId: String, userId: UserId) {
attachmentsWorker.enqueue(messageId, userId, "")
}
fun findAllPendingSendsAsync(): LiveData<List<PendingSend>> =
pendingActionDao.findAllPendingSendsAsync()
fun findAllPendingUploadsAsync(): LiveData<List<PendingUpload>> =
pendingActionDao.findAllPendingUploadsAsync()
@AssistedInject.Factory
interface AssistedFactory {
fun create(userId: UserId): MessageDetailsRepository
}
}