2020-04-16 15:44:53 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2020 Proton Technologies AG
|
2020-09-17 10:44:37 +00:00
|
|
|
*
|
2020-04-16 15:44:53 +00:00
|
|
|
* This file is part of ProtonMail.
|
2020-09-17 10:44:37 +00:00
|
|
|
*
|
2020-04-16 15:44:53 +00:00
|
|
|
* ProtonMail 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.
|
2020-09-17 10:44:37 +00:00
|
|
|
*
|
2020-04-16 15:44:53 +00:00
|
|
|
* ProtonMail 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.
|
2020-09-17 10:44:37 +00:00
|
|
|
*
|
2020-04-16 15:44:53 +00:00
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
|
|
|
|
*/
|
|
|
|
package ch.protonmail.android.activities.messageDetails.viewmodel
|
|
|
|
|
2021-02-08 14:39:26 +00:00
|
|
|
import android.annotation.TargetApi
|
2020-04-16 15:44:53 +00:00
|
|
|
import android.content.Context
|
2021-05-13 13:18:53 +00:00
|
|
|
import android.graphics.Color
|
2021-02-08 08:58:18 +00:00
|
|
|
import android.net.Uri
|
2021-02-08 10:16:40 +00:00
|
|
|
import android.os.Build
|
|
|
|
import android.os.Environment
|
2020-10-29 06:51:34 +00:00
|
|
|
import android.print.PrintManager
|
2021-02-08 10:16:40 +00:00
|
|
|
import androidx.core.content.FileProvider
|
2020-09-17 10:44:37 +00:00
|
|
|
import androidx.lifecycle.LiveData
|
|
|
|
import androidx.lifecycle.MediatorLiveData
|
|
|
|
import androidx.lifecycle.MutableLiveData
|
2020-10-07 13:14:55 +00:00
|
|
|
import androidx.lifecycle.SavedStateHandle
|
2020-09-17 10:44:37 +00:00
|
|
|
import androidx.lifecycle.ViewModel
|
2021-05-13 14:28:15 +00:00
|
|
|
import androidx.lifecycle.asLiveData
|
2020-09-17 10:44:37 +00:00
|
|
|
import androidx.lifecycle.distinctUntilChanged
|
|
|
|
import androidx.lifecycle.viewModelScope
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.activities.messageDetails.IntentExtrasData
|
2020-10-29 06:51:34 +00:00
|
|
|
import ch.protonmail.android.activities.messageDetails.MessagePrinter
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.activities.messageDetails.MessageRenderer
|
|
|
|
import ch.protonmail.android.activities.messageDetails.RegisterReloadTask
|
|
|
|
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
|
2020-10-13 12:48:55 +00:00
|
|
|
import ch.protonmail.android.api.NetworkConfigurator
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.api.models.User
|
2021-02-02 13:20:03 +00:00
|
|
|
import ch.protonmail.android.attachments.AttachmentsHelper
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker
|
|
|
|
import ch.protonmail.android.core.BigContentHolder
|
|
|
|
import ch.protonmail.android.core.Constants
|
2021-02-08 10:16:40 +00:00
|
|
|
import ch.protonmail.android.core.Constants.DIR_EMB_ATTACHMENT_DOWNLOADS
|
2020-07-14 20:46:07 +00:00
|
|
|
import ch.protonmail.android.core.Constants.RESPONSE_CODE_OK
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.core.UserManager
|
|
|
|
import ch.protonmail.android.data.ContactsRepository
|
2021-05-13 13:18:53 +00:00
|
|
|
import ch.protonmail.android.data.LabelRepository
|
2021-02-18 16:10:43 +00:00
|
|
|
import ch.protonmail.android.data.local.AttachmentMetadataDao
|
2021-03-18 09:46:25 +00:00
|
|
|
import ch.protonmail.android.data.local.model.*
|
2021-05-13 13:18:53 +00:00
|
|
|
import ch.protonmail.android.details.presentation.MessageDetailsActivity
|
|
|
|
import ch.protonmail.android.domain.entity.Id
|
|
|
|
import ch.protonmail.android.domain.entity.Name
|
2020-09-17 10:44:37 +00:00
|
|
|
import ch.protonmail.android.events.DownloadEmbeddedImagesEvent
|
|
|
|
import ch.protonmail.android.events.Status
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.jobs.helper.EmbeddedImage
|
2021-05-06 12:57:28 +00:00
|
|
|
import ch.protonmail.android.labels.domain.usecase.MoveMessagesToFolder
|
2021-05-13 14:28:15 +00:00
|
|
|
import ch.protonmail.android.repository.MessageRepository
|
2021-05-13 13:18:53 +00:00
|
|
|
import ch.protonmail.android.ui.view.LabelChipUiModel
|
2020-10-12 09:19:45 +00:00
|
|
|
import ch.protonmail.android.usecase.VerifyConnection
|
2020-10-29 15:08:37 +00:00
|
|
|
import ch.protonmail.android.usecase.fetch.FetchVerificationKeys
|
2020-09-17 10:44:37 +00:00
|
|
|
import ch.protonmail.android.utils.AppUtil
|
|
|
|
import ch.protonmail.android.utils.DownloadUtils
|
|
|
|
import ch.protonmail.android.utils.Event
|
2020-12-30 14:20:18 +00:00
|
|
|
import ch.protonmail.android.utils.HTMLTransformer.DefaultTransformer
|
|
|
|
import ch.protonmail.android.utils.HTMLTransformer.ViewportTransformer
|
2020-09-17 10:44:37 +00:00
|
|
|
import ch.protonmail.android.utils.ServerTime
|
2021-05-13 13:18:53 +00:00
|
|
|
import ch.protonmail.android.utils.UiUtil
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.utils.crypto.KeyInformation
|
2020-10-13 12:48:55 +00:00
|
|
|
import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel
|
2021-04-09 06:50:35 +00:00
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
2021-05-13 13:18:53 +00:00
|
|
|
import kotlinx.coroutines.flow.Flow
|
2021-05-13 14:28:15 +00:00
|
|
|
import kotlinx.coroutines.flow.emptyFlow
|
2021-03-18 09:48:13 +00:00
|
|
|
import kotlinx.coroutines.flow.first
|
2021-05-13 13:18:53 +00:00
|
|
|
import kotlinx.coroutines.flow.flatMapLatest
|
|
|
|
import kotlinx.coroutines.flow.map
|
2020-04-16 15:44:53 +00:00
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import kotlinx.coroutines.withContext
|
2021-05-13 13:18:53 +00:00
|
|
|
import me.proton.core.domain.entity.UserId
|
2020-11-30 12:44:44 +00:00
|
|
|
import me.proton.core.util.kotlin.DispatcherProvider
|
2021-04-29 15:59:00 +00:00
|
|
|
import me.proton.core.util.kotlin.EMPTY_STRING
|
2021-02-08 10:16:40 +00:00
|
|
|
import okio.buffer
|
|
|
|
import okio.sink
|
|
|
|
import okio.source
|
2020-12-30 14:20:18 +00:00
|
|
|
import org.jsoup.Jsoup
|
2020-04-16 15:44:53 +00:00
|
|
|
import timber.log.Timber
|
2021-02-08 10:16:40 +00:00
|
|
|
import java.io.File
|
2020-12-30 14:20:18 +00:00
|
|
|
import java.io.IOException
|
2020-04-16 15:44:53 +00:00
|
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
2021-04-09 06:50:35 +00:00
|
|
|
import javax.inject.Inject
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [ViewModel] for `MessageDetailsActivity`
|
|
|
|
*
|
|
|
|
* TODO reduce [LiveData]s and keep only a single version of the message
|
|
|
|
*/
|
2021-04-09 06:50:35 +00:00
|
|
|
@HiltViewModel
|
|
|
|
internal class MessageDetailsViewModel @Inject constructor(
|
2021-05-13 13:18:53 +00:00
|
|
|
savedStateHandle: SavedStateHandle,
|
2020-10-07 13:14:55 +00:00
|
|
|
private val messageDetailsRepository: MessageDetailsRepository,
|
2021-05-13 14:28:15 +00:00
|
|
|
private val messageRepository: MessageRepository,
|
2020-09-17 10:44:37 +00:00
|
|
|
private val userManager: UserManager,
|
|
|
|
private val contactsRepository: ContactsRepository,
|
2021-05-13 13:18:53 +00:00
|
|
|
private val labelRepository: LabelRepository,
|
2021-02-18 16:10:43 +00:00
|
|
|
private val attachmentMetadataDao: AttachmentMetadataDao,
|
2020-10-29 15:08:37 +00:00
|
|
|
private val fetchVerificationKeys: FetchVerificationKeys,
|
2021-02-02 09:28:53 +00:00
|
|
|
private val attachmentsWorker: DownloadEmbeddedAttachmentsWorker.Enqueuer,
|
2020-11-30 12:44:44 +00:00
|
|
|
private val dispatchers: DispatcherProvider,
|
2021-02-02 13:20:03 +00:00
|
|
|
private val attachmentsHelper: AttachmentsHelper,
|
2021-02-08 08:58:18 +00:00
|
|
|
private val downloadUtils: DownloadUtils,
|
2021-04-29 15:59:00 +00:00
|
|
|
private val moveMessagesToFolder: MoveMessagesToFolder,
|
2021-02-02 09:28:53 +00:00
|
|
|
messageRendererFactory: MessageRenderer.Factory,
|
2020-10-13 12:48:55 +00:00
|
|
|
verifyConnection: VerifyConnection,
|
|
|
|
networkConfigurator: NetworkConfigurator
|
|
|
|
) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) {
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2020-10-07 13:14:55 +00:00
|
|
|
private val messageId: String = savedStateHandle.get<String>(MessageDetailsActivity.EXTRA_MESSAGE_ID)
|
|
|
|
?: throw IllegalStateException("messageId in MessageDetails is Empty!")
|
|
|
|
private val isTransientMessage = savedStateHandle.get<Boolean>(MessageDetailsActivity.EXTRA_TRANSIENT_MESSAGE)
|
|
|
|
?: false
|
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
private val messageRenderer
|
2021-04-16 07:42:29 +00:00
|
|
|
by lazy { messageRendererFactory.create(viewModelScope, messageId) }
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-05-13 14:28:15 +00:00
|
|
|
val messageFlow: Flow<Message?> =
|
|
|
|
userManager.primaryUserId
|
|
|
|
.flatMapLatest { userId ->
|
|
|
|
if (userId != null) {
|
|
|
|
messageRepository.findMessage(userId, messageId)
|
|
|
|
} else {
|
|
|
|
emptyFlow()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val message: LiveData<Message?> =
|
|
|
|
messageFlow.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
|
|
|
|
2021-04-20 10:14:44 +00:00
|
|
|
lateinit var decryptedMessageData: MediatorLiveData<Message>
|
2020-10-05 13:59:03 +00:00
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
lateinit var addressId: String
|
|
|
|
|
|
|
|
var renderingPassed = false
|
|
|
|
var hasEmbeddedImages: Boolean = false
|
|
|
|
private var fetchingPubKeys: Boolean = false
|
|
|
|
private var _embeddedImagesAttachments: ArrayList<Attachment> = ArrayList()
|
|
|
|
private var _embeddedImagesToFetch: ArrayList<EmbeddedImage> = ArrayList()
|
|
|
|
private var remoteContentDisplayed: Boolean = false
|
|
|
|
|
|
|
|
// region properties and data
|
|
|
|
private val requestPending = AtomicBoolean(false)
|
|
|
|
var renderedFromCache = AtomicBoolean(false)
|
|
|
|
|
|
|
|
var refreshedKeys: Boolean = true
|
|
|
|
|
|
|
|
private val _messageSavedInDBResult: MutableLiveData<Boolean> = MutableLiveData()
|
2020-12-31 08:56:47 +00:00
|
|
|
private val _downloadEmbeddedImagesResult: MutableLiveData<String> = MutableLiveData()
|
2020-04-16 15:44:53 +00:00
|
|
|
private val _prepareEditMessageIntentResult: MutableLiveData<Event<IntentExtrasData>> = MutableLiveData()
|
|
|
|
private val _checkStoragePermission: MutableLiveData<Event<Boolean>> = MutableLiveData()
|
|
|
|
private val _reloadRecipientsEvent: MutableLiveData<Event<Boolean>> = MutableLiveData()
|
2020-07-14 20:46:07 +00:00
|
|
|
private val _messageDetailsError: MutableLiveData<Event<String>> = MutableLiveData()
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2020-12-31 08:56:47 +00:00
|
|
|
private var bodyString: String? = null
|
2020-04-16 15:44:53 +00:00
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
messageRenderer.messageBody = value
|
|
|
|
}
|
|
|
|
|
2021-05-13 13:18:53 +00:00
|
|
|
val labels: Flow<List<Label>> =
|
2021-05-13 14:28:15 +00:00
|
|
|
messageFlow
|
2021-05-13 13:18:53 +00:00
|
|
|
.flatMapLatest { message ->
|
|
|
|
val userId = UserId(userManager.requireCurrentUserId().s)
|
2021-05-13 14:28:15 +00:00
|
|
|
val labelsIds = (message?.labelIDsNotIncludingLocations ?: emptyList()).map(::Id)
|
2021-05-13 13:18:53 +00:00
|
|
|
labelRepository.findLabels(userId, labelsIds)
|
|
|
|
}
|
|
|
|
|
|
|
|
val nonExclusiveLabelsUiModels: Flow<List<LabelChipUiModel>> =
|
|
|
|
labels.map { labelsList ->
|
|
|
|
labelsList.map { label ->
|
|
|
|
val color =
|
|
|
|
if (label.color.isNotBlank()) Color.parseColor(UiUtil.normalizeColor(label.color))
|
|
|
|
else Color.BLACK
|
|
|
|
|
|
|
|
LabelChipUiModel(Id(label.id), Name(label.name), color)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
val messageAttachments: LiveData<List<Attachment>> by lazy {
|
|
|
|
if (!isTransientMessage) {
|
|
|
|
messageDetailsRepository.findAttachments(decryptedMessageData).distinctUntilChanged()
|
|
|
|
} else {
|
|
|
|
messageDetailsRepository.findAttachmentsSearchMessage(decryptedMessageData).distinctUntilChanged()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val pendingSend: LiveData<PendingSend?> by lazy {
|
|
|
|
messageDetailsRepository.findPendingSendByOfflineMessageIdAsync(messageId)
|
|
|
|
}
|
|
|
|
|
|
|
|
val messageSavedInDBResult: LiveData<Boolean>
|
|
|
|
get() = _messageSavedInDBResult
|
|
|
|
|
|
|
|
val checkStoragePermission: LiveData<Event<Boolean>>
|
|
|
|
get() = _checkStoragePermission
|
|
|
|
|
|
|
|
val reloadRecipientsEvent: LiveData<Event<Boolean>>
|
|
|
|
get() = _reloadRecipientsEvent
|
|
|
|
|
2020-07-14 20:46:07 +00:00
|
|
|
val messageDetailsError: LiveData<Event<String>>
|
2020-04-16 15:44:53 +00:00
|
|
|
get() = _messageDetailsError
|
|
|
|
|
2020-12-31 08:56:47 +00:00
|
|
|
val downloadEmbeddedImagesResult: LiveData<String>
|
2020-04-16 15:44:53 +00:00
|
|
|
get() = _downloadEmbeddedImagesResult
|
|
|
|
|
2020-10-05 13:59:03 +00:00
|
|
|
val prepareEditMessageIntent: LiveData<Event<IntentExtrasData>>
|
2020-04-16 15:44:53 +00:00
|
|
|
get() = _prepareEditMessageIntentResult
|
|
|
|
|
|
|
|
val publicKeys = MutableLiveData<List<KeyInformation>>()
|
|
|
|
|
2020-11-30 13:21:11 +00:00
|
|
|
val webViewContentWithoutImages = MutableLiveData<String>()
|
|
|
|
val webViewContentWithImages = MutableLiveData<String>()
|
|
|
|
val webViewContent = object : MediatorLiveData<String>() {
|
|
|
|
var contentWithoutImages: String? = null
|
|
|
|
var contentWithImages: String? = null
|
|
|
|
|
|
|
|
init {
|
|
|
|
addSource(webViewContentWithoutImages) {
|
|
|
|
contentWithoutImages = it
|
|
|
|
emit()
|
|
|
|
}
|
|
|
|
addSource(webViewContentWithImages) {
|
|
|
|
contentWithImages = it
|
|
|
|
emit()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun emit() {
|
|
|
|
value = contentWithImages ?: contentWithoutImages
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-09 13:18:01 +00:00
|
|
|
private var areImagesDisplayed: Boolean = false
|
2021-02-08 10:16:40 +00:00
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
init {
|
2021-05-13 14:28:15 +00:00
|
|
|
observeDecryption()
|
2021-03-16 16:01:40 +00:00
|
|
|
messageDetailsRepository.reloadDependenciesForUser(userManager.requireCurrentUserId())
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
viewModelScope.launch {
|
|
|
|
for (body in messageRenderer.renderedBody) {
|
|
|
|
// TODO Sending twice the same value, perhaps we could improve this
|
2020-12-31 08:56:47 +00:00
|
|
|
_downloadEmbeddedImagesResult.postValue(body)
|
2021-02-09 13:18:01 +00:00
|
|
|
areImagesDisplayed = true
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun saveMessage() {
|
|
|
|
// Return if message is null
|
|
|
|
val message = message.value ?: return
|
2020-11-30 12:44:44 +00:00
|
|
|
viewModelScope.launch(dispatchers.Io) {
|
2020-04-16 15:44:53 +00:00
|
|
|
val result = runCatching {
|
|
|
|
messageDetailsRepository.saveMessageInDB(message, isTransientMessage)
|
|
|
|
}
|
|
|
|
_messageSavedInDBResult.postValue(result.isSuccess)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun markRead(read: Boolean) {
|
|
|
|
val message = message.value
|
|
|
|
message?.let {
|
|
|
|
message.accessTime = ServerTime.currentTimeMillis()
|
|
|
|
message.setIsRead(read)
|
|
|
|
saveMessage()
|
|
|
|
if (read) {
|
2021-04-29 15:59:00 +00:00
|
|
|
messageDetailsRepository.markRead(listOf(messageId))
|
2020-04-16 15:44:53 +00:00
|
|
|
saveMessage()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
|
|
|
|
fun startDownloadEmbeddedImagesJob() {
|
|
|
|
hasEmbeddedImages = false
|
|
|
|
|
2020-11-30 12:44:44 +00:00
|
|
|
viewModelScope.launch(dispatchers.Io) {
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-02-18 16:10:43 +00:00
|
|
|
val attachmentMetadataList = attachmentMetadataDao.getAllAttachmentsForMessage(messageId)
|
2020-10-05 13:59:03 +00:00
|
|
|
val embeddedImages = _embeddedImagesAttachments.mapNotNull {
|
2021-02-08 16:21:39 +00:00
|
|
|
attachmentsHelper.fromAttachmentToEmbeddedImage(
|
2021-02-10 10:56:21 +00:00
|
|
|
it, decryptedMessageData.value!!.embeddedImageIds.toList()
|
2020-04-16 15:44:53 +00:00
|
|
|
)
|
|
|
|
}
|
2021-02-02 13:20:03 +00:00
|
|
|
val embeddedImagesWithLocalFiles = mutableListOf<EmbeddedImage>()
|
2020-04-16 15:44:53 +00:00
|
|
|
embeddedImages.forEach { embeddedImage ->
|
|
|
|
attachmentMetadataList.find { it.id == embeddedImage.attachmentId }?.let {
|
2021-02-02 13:20:03 +00:00
|
|
|
embeddedImagesWithLocalFiles.add(
|
|
|
|
embeddedImage.copy(localFileName = it.localLocation.substringAfterLast("/"))
|
|
|
|
)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't download embedded images, if we already have them in local storage
|
2021-02-04 16:22:43 +00:00
|
|
|
if (
|
|
|
|
embeddedImagesWithLocalFiles.isNotEmpty() &&
|
|
|
|
embeddedImagesWithLocalFiles.all { it.localFileName != null }
|
|
|
|
) {
|
2021-02-02 13:20:03 +00:00
|
|
|
AppUtil.postEventOnUi(DownloadEmbeddedImagesEvent(Status.SUCCESS, embeddedImagesWithLocalFiles))
|
2020-04-16 15:44:53 +00:00
|
|
|
} else {
|
2021-03-16 16:01:40 +00:00
|
|
|
messageDetailsRepository.startDownloadEmbeddedImages(messageId, userManager.requireCurrentUserId())
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onEmbeddedImagesDownloaded(event: DownloadEmbeddedImagesEvent) {
|
2021-02-04 16:22:43 +00:00
|
|
|
Timber.v("onEmbeddedImagesDownloaded status: ${event.status} images size: ${event.images.size}")
|
2020-04-16 15:44:53 +00:00
|
|
|
if (bodyString.isNullOrEmpty()) {
|
2020-12-31 08:56:47 +00:00
|
|
|
_downloadEmbeddedImagesResult.value = bodyString ?: ""
|
2020-04-16 15:44:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-04 16:22:43 +00:00
|
|
|
if (event.status == Status.SUCCESS) {
|
|
|
|
messageRenderer.images.offer(event.images)
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2020-10-05 13:59:03 +00:00
|
|
|
fun prepareEditMessageIntent(
|
|
|
|
messageAction: Constants.MessageActionType,
|
|
|
|
message: Message,
|
|
|
|
newMessageTitle: String?,
|
|
|
|
content: String,
|
|
|
|
mBigContentHolder: BigContentHolder
|
|
|
|
) {
|
2021-04-26 08:41:05 +00:00
|
|
|
val user: User = userManager.requireCurrentLegacyUser()
|
2020-04-16 15:44:53 +00:00
|
|
|
viewModelScope.launch {
|
2020-10-05 13:59:03 +00:00
|
|
|
val intent = messageDetailsRepository.prepareEditMessageIntent(
|
|
|
|
messageAction,
|
|
|
|
message,
|
|
|
|
user,
|
|
|
|
newMessageTitle,
|
|
|
|
content,
|
|
|
|
mBigContentHolder,
|
2021-02-09 13:18:01 +00:00
|
|
|
areImagesDisplayed,
|
2020-10-05 13:59:03 +00:00
|
|
|
remoteContentDisplayed,
|
|
|
|
_embeddedImagesAttachments,
|
2020-11-30 12:44:44 +00:00
|
|
|
dispatchers.Io,
|
2020-10-05 13:59:03 +00:00
|
|
|
isTransientMessage
|
|
|
|
)
|
2020-04-16 15:44:53 +00:00
|
|
|
_prepareEditMessageIntentResult.value = Event(intent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun observeDecryption() {
|
|
|
|
decryptedMessageData = object : MediatorLiveData<Message>() {
|
|
|
|
var message: Message? = null
|
|
|
|
var keys: List<KeyInformation>? = null
|
|
|
|
var contact: ContactEmail? = null
|
2020-11-30 13:21:11 +00:00
|
|
|
var isDecrypted: Boolean = false
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
init {
|
|
|
|
addSource(this@MessageDetailsViewModel.message) {
|
|
|
|
message = it
|
2020-09-21 12:19:32 +00:00
|
|
|
message?.senderEmail?.let { senderEmail ->
|
2020-04-16 15:44:53 +00:00
|
|
|
addSource(contactsRepository.findContactEmailByEmailLiveData(senderEmail)) { contactEmail ->
|
|
|
|
contact = contactEmail ?: ContactEmail("", message?.senderEmail ?: "", message?.senderName)
|
2020-11-30 13:21:11 +00:00
|
|
|
if (!isDecrypted) {
|
2020-04-16 15:44:53 +00:00
|
|
|
refreshedKeys = true
|
2020-11-30 13:21:11 +00:00
|
|
|
tryEmit()
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-11-30 13:21:11 +00:00
|
|
|
if (!isDecrypted) {
|
2020-04-16 15:44:53 +00:00
|
|
|
refreshedKeys = true
|
2020-11-30 13:21:11 +00:00
|
|
|
tryEmit()
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
addSource(publicKeys) {
|
|
|
|
keys = it
|
|
|
|
refreshedKeys = false
|
2020-11-30 13:21:11 +00:00
|
|
|
tryEmit()
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun tryEmit() {
|
|
|
|
val message = message ?: return
|
|
|
|
if (!message.isDownloaded) {
|
|
|
|
return
|
|
|
|
}
|
2020-11-30 13:21:11 +00:00
|
|
|
viewModelScope.launch {
|
|
|
|
if (contact?.name != message.sender?.emailAddress)
|
|
|
|
message.senderDisplayName = contact?.name
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2020-11-30 13:21:11 +00:00
|
|
|
isDecrypted = withContext(dispatchers.Comp) {
|
|
|
|
message.tryDecrypt(keys) ?: false
|
|
|
|
}
|
2021-03-11 15:36:59 +00:00
|
|
|
Timber.v("Message isDecrypted:$isDecrypted, keys size: ${keys?.size}")
|
2020-11-30 13:21:11 +00:00
|
|
|
value = message
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-30 13:21:11 +00:00
|
|
|
private fun Message.tryDecrypt(verificationKeys: List<KeyInformation>?): Boolean? {
|
|
|
|
return try {
|
2021-03-18 09:47:58 +00:00
|
|
|
decrypt(userManager, userManager.requireCurrentUserId(), verificationKeys)
|
2020-11-30 13:21:11 +00:00
|
|
|
true
|
|
|
|
} catch (exception: Exception) {
|
|
|
|
// signature verification failed with special case, try to decrypt again without verification
|
|
|
|
// and hardcode verification error
|
2021-03-11 15:36:59 +00:00
|
|
|
if (verificationKeys != null && verificationKeys.isNotEmpty() &&
|
|
|
|
exception.message == "Signature Verification Error: No matching signature"
|
|
|
|
) {
|
2020-11-30 13:21:11 +00:00
|
|
|
Timber.d(exception, "Decrypting message again without verkeys")
|
2021-03-18 09:47:58 +00:00
|
|
|
decrypt(userManager, userManager.requireCurrentUserId())
|
2020-11-30 13:21:11 +00:00
|
|
|
this.hasValidSignature = false
|
|
|
|
this.hasInvalidSignature = true
|
|
|
|
true
|
|
|
|
} else {
|
2021-03-11 15:36:59 +00:00
|
|
|
Timber.d(exception, "Cannot decrypt message")
|
2020-11-30 13:21:11 +00:00
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun fetchMessageDetails(checkForMessageAttachmentHeaders: Boolean) {
|
|
|
|
if (requestPending.get()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
requestPending.set(true)
|
|
|
|
|
|
|
|
viewModelScope.launch {
|
|
|
|
var shouldExit = false
|
|
|
|
if (checkForMessageAttachmentHeaders) {
|
|
|
|
val attHeadersPresent = message.value?.let {
|
2020-11-30 13:21:11 +00:00
|
|
|
messageDetailsRepository.checkIfAttHeadersArePresent(it, dispatchers.Io)
|
2020-04-16 15:44:53 +00:00
|
|
|
} ?: false
|
|
|
|
shouldExit = checkForMessageAttachmentHeaders && !attHeadersPresent
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!shouldExit) {
|
2020-11-30 12:44:44 +00:00
|
|
|
withContext(dispatchers.Io) {
|
2020-04-16 15:44:53 +00:00
|
|
|
val messageDetailsResult = runCatching {
|
2020-10-05 13:59:03 +00:00
|
|
|
with(messageDetailsRepository) {
|
2020-04-16 15:44:53 +00:00
|
|
|
if (isTransientMessage) fetchSearchMessageDetails(messageId)
|
|
|
|
else fetchMessageDetails(messageId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
messageDetailsResult
|
2020-10-05 13:59:03 +00:00
|
|
|
.onFailure {
|
|
|
|
requestPending.set(false)
|
|
|
|
_messageDetailsError.postValue(Event(""))
|
|
|
|
}
|
|
|
|
.onSuccess { messageResponse ->
|
|
|
|
if (messageResponse.code == RESPONSE_CODE_OK) {
|
|
|
|
with(messageDetailsRepository) {
|
|
|
|
|
|
|
|
if (isTransientMessage) {
|
2021-03-18 09:48:13 +00:00
|
|
|
val savedMessage = findSearchMessageById(messageId).first()
|
2020-10-05 13:59:03 +00:00
|
|
|
if (savedMessage != null) {
|
|
|
|
messageResponse.message.writeTo(savedMessage)
|
|
|
|
saveSearchMessageInDB(savedMessage)
|
|
|
|
} else {
|
|
|
|
prepareMessage(messageResponse.message)
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2020-10-05 13:59:03 +00:00
|
|
|
} else {
|
2021-03-18 09:48:13 +00:00
|
|
|
val savedMessage = findMessageById(messageId).first()
|
2020-10-05 13:59:03 +00:00
|
|
|
if (savedMessage != null) {
|
|
|
|
messageResponse.message.writeTo(savedMessage)
|
|
|
|
saveMessageInDB(savedMessage)
|
2020-04-16 15:44:53 +00:00
|
|
|
} else {
|
2020-10-05 13:59:03 +00:00
|
|
|
prepareMessage(messageResponse.message)
|
|
|
|
setFolderLocation(messageResponse.message)
|
|
|
|
saveMessageInDB(messageResponse.message, isTransientMessage)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-05 13:59:03 +00:00
|
|
|
} else {
|
|
|
|
_messageDetailsError.postValue(Event(messageResponse.error))
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
2020-10-05 13:59:03 +00:00
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun prepareMessage(message: Message) { // TODO: it's not clear why message is assigning values to itself
|
|
|
|
message.toList = message.toList
|
|
|
|
message.ccList = message.ccList
|
|
|
|
message.bccList = message.bccList
|
|
|
|
message.replyTos = message.replyTos
|
|
|
|
message.sender = message.sender
|
|
|
|
message.setLabelIDs(message.getEventLabelIDs())
|
|
|
|
message.header = message.header
|
|
|
|
message.parsedHeaders = message.parsedHeaders
|
|
|
|
var location = Constants.MessageLocationType.INBOX
|
|
|
|
for (labelId in message.allLabelIDs) {
|
|
|
|
if (labelId.length <= 2) {
|
|
|
|
location = Constants.MessageLocationType.fromInt(Integer.valueOf(labelId))
|
2021-03-18 11:52:04 +00:00
|
|
|
if (location != Constants.MessageLocationType.ALL_MAIL &&
|
|
|
|
location != Constants.MessageLocationType.STARRED
|
|
|
|
) {
|
2020-04-16 15:44:53 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
message.location = location.messageLocationTypeValue
|
|
|
|
}
|
|
|
|
|
2021-02-08 10:16:40 +00:00
|
|
|
fun viewOrDownloadAttachment(context: Context, attachmentToDownloadId: String, messageId: String) {
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2020-11-30 12:44:44 +00:00
|
|
|
viewModelScope.launch(dispatchers.Io) {
|
2021-02-18 16:10:43 +00:00
|
|
|
val metadata = attachmentMetadataDao
|
2021-02-05 09:21:04 +00:00
|
|
|
.getAttachmentMetadataForMessageAndAttachmentId(messageId, attachmentToDownloadId)
|
2021-02-08 10:16:40 +00:00
|
|
|
Timber.v("viewOrDownloadAttachment Id: $attachmentToDownloadId metadataId: ${metadata?.id}")
|
2020-04-16 15:44:53 +00:00
|
|
|
if (metadata != null) {
|
2021-02-08 10:16:40 +00:00
|
|
|
val uri = metadata.uri
|
2021-03-18 11:52:04 +00:00
|
|
|
if (uri != null && attachmentsHelper.isFileAvailable(context, uri)) {
|
|
|
|
if (uri.path?.contains(DIR_EMB_ATTACHMENT_DOWNLOADS) == true) {
|
2021-02-11 08:49:40 +00:00
|
|
|
copyAttachmentToDownloadsAndDisplay(context, metadata.name, uri)
|
2021-03-18 11:52:04 +00:00
|
|
|
} else {
|
2021-02-11 08:49:40 +00:00
|
|
|
viewAttachment(context, metadata.name, uri)
|
|
|
|
}
|
2021-03-18 11:52:04 +00:00
|
|
|
} else {
|
|
|
|
Timber.v("No file attachment id: $attachmentToDownloadId downloading again")
|
|
|
|
attachmentsWorker.enqueue(messageId, userManager.requireCurrentUserId(), attachmentToDownloadId)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-02-09 15:18:24 +00:00
|
|
|
Timber.v("No metadata found for attachment id: $attachmentToDownloadId")
|
2021-03-16 16:01:40 +00:00
|
|
|
attachmentsWorker.enqueue(messageId, userManager.requireCurrentUserId(), attachmentToDownloadId)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-08 14:39:26 +00:00
|
|
|
/**
|
|
|
|
* Explicitly make a copy of embedded attachment to downloads and display it (product requirement)
|
|
|
|
*/
|
2021-02-08 10:16:40 +00:00
|
|
|
private fun copyAttachmentToDownloadsAndDisplay(
|
|
|
|
context: Context,
|
|
|
|
filename: String,
|
|
|
|
uri: Uri
|
|
|
|
) {
|
2021-02-08 14:39:26 +00:00
|
|
|
val newUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
getCopiedUriFromQ(filename, uri, context)
|
2021-02-08 10:16:40 +00:00
|
|
|
} else {
|
2021-02-08 14:39:26 +00:00
|
|
|
getCopiedUriBeforeQ(filename, uri, context)
|
|
|
|
}
|
2021-02-08 10:16:40 +00:00
|
|
|
|
2021-02-08 14:39:26 +00:00
|
|
|
Timber.v("Copied attachment file from ${uri.path} to ${newUri?.path}")
|
|
|
|
viewAttachment(context, filename, newUri)
|
|
|
|
}
|
|
|
|
|
|
|
|
@TargetApi(Build.VERSION_CODES.Q)
|
|
|
|
private fun getCopiedUriFromQ(filename: String, uri: Uri, context: Context): Uri? {
|
|
|
|
val contentResolver = context.contentResolver
|
2021-02-08 10:16:40 +00:00
|
|
|
|
2021-02-08 14:39:26 +00:00
|
|
|
return contentResolver.openInputStream(uri)?.let {
|
|
|
|
attachmentsHelper.saveAttachmentInMediaStore(
|
|
|
|
contentResolver, filename, contentResolver.getType(uri), it
|
2021-02-08 10:16:40 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-08 14:39:26 +00:00
|
|
|
private fun getCopiedUriBeforeQ(filename: String, uri: Uri, context: Context): Uri {
|
|
|
|
val fileInDownloads = File(
|
|
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
|
|
filename
|
|
|
|
)
|
|
|
|
|
2021-02-08 15:02:57 +00:00
|
|
|
context.contentResolver.openInputStream(uri)?.use { stream ->
|
|
|
|
fileInDownloads.sink().buffer().use { sink ->
|
|
|
|
sink.writeAll(stream.source())
|
|
|
|
}
|
2021-02-08 14:39:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return FileProvider.getUriForFile(
|
|
|
|
context, context.applicationContext.packageName + ".provider", fileInDownloads
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-02-08 08:58:18 +00:00
|
|
|
fun viewAttachment(context: Context, filename: String?, uri: Uri?) =
|
|
|
|
downloadUtils.viewAttachment(context, filename, uri)
|
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
fun remoteContentDisplayed() {
|
|
|
|
remoteContentDisplayed = true
|
|
|
|
}
|
|
|
|
|
|
|
|
fun displayRemoteContentClicked() {
|
|
|
|
webViewContentWithImages.value = bodyString
|
|
|
|
remoteContentDisplayed()
|
|
|
|
prepareEmbeddedImages()
|
|
|
|
}
|
|
|
|
|
2021-02-09 13:18:01 +00:00
|
|
|
fun isEmbeddedImagesDisplayed() = areImagesDisplayed
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
fun displayEmbeddedImages() {
|
2021-02-09 13:18:01 +00:00
|
|
|
areImagesDisplayed = true // this will be passed to edit intent
|
2020-04-16 15:44:53 +00:00
|
|
|
startDownloadEmbeddedImagesJob()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun prepareEmbeddedImages(): Boolean {
|
|
|
|
val message = decryptedMessageData.value
|
|
|
|
message?.let {
|
|
|
|
val attachments = message.Attachments
|
|
|
|
val embeddedImagesToFetch = ArrayList<EmbeddedImage>()
|
|
|
|
val embeddedImagesAttachments = ArrayList<Attachment>()
|
|
|
|
for (attachment in attachments) {
|
2021-02-02 13:20:03 +00:00
|
|
|
val embeddedImage = attachmentsHelper
|
2021-02-10 10:56:21 +00:00
|
|
|
.fromAttachmentToEmbeddedImage(attachment, message.embeddedImageIds) ?: continue
|
2020-04-16 15:44:53 +00:00
|
|
|
embeddedImagesToFetch.add(embeddedImage)
|
|
|
|
embeddedImagesAttachments.add(attachment)
|
|
|
|
}
|
|
|
|
|
|
|
|
this._embeddedImagesToFetch = embeddedImagesToFetch
|
|
|
|
this._embeddedImagesAttachments = embeddedImagesAttachments
|
|
|
|
|
|
|
|
if (embeddedImagesToFetch.isNotEmpty()) {
|
|
|
|
hasEmbeddedImages = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return hasEmbeddedImages
|
|
|
|
}
|
|
|
|
|
|
|
|
fun triggerVerificationKeyLoading() {
|
|
|
|
if (!fetchingPubKeys && publicKeys.value == null) {
|
|
|
|
val message = message.value
|
|
|
|
message?.let {
|
|
|
|
fetchingPubKeys = true
|
2020-10-29 15:08:37 +00:00
|
|
|
viewModelScope.launch {
|
2020-12-28 16:29:53 +00:00
|
|
|
val result = fetchVerificationKeys(message.senderEmail)
|
2020-10-29 15:08:37 +00:00
|
|
|
onFetchVerificationKeysEvent(result)
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-29 15:08:37 +00:00
|
|
|
private fun onFetchVerificationKeysEvent(pubKeys: List<KeyInformation>) {
|
|
|
|
Timber.v("FetchVerificationKeys received $pubKeys")
|
2020-04-16 15:44:53 +00:00
|
|
|
val message = message.value
|
|
|
|
publicKeys.value = pubKeys
|
|
|
|
fetchingPubKeys = false
|
|
|
|
renderedFromCache = AtomicBoolean(false)
|
|
|
|
_reloadRecipientsEvent.value = Event(true)
|
|
|
|
// render with the new verification keys
|
|
|
|
if (renderingPassed && message != null) {
|
|
|
|
RegisterReloadTask(message, requestPending).execute()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setAttachmentsList(attachments: List<Attachment>) {
|
|
|
|
val message = decryptedMessageData.value
|
|
|
|
message!!.setAttachmentList(attachments)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun isPgpEncrypted(): Boolean = message.value?.messageEncryption?.isPGPEncrypted ?: false
|
|
|
|
|
2021-04-16 13:44:46 +00:00
|
|
|
fun printMessage(activityContext: Context) {
|
2020-10-29 06:51:34 +00:00
|
|
|
val message = message.value
|
|
|
|
message?.let {
|
|
|
|
MessagePrinter(
|
2020-11-12 13:28:29 +00:00
|
|
|
activityContext,
|
|
|
|
activityContext.resources,
|
|
|
|
activityContext.getSystemService(Context.PRINT_SERVICE) as PrintManager,
|
2020-10-29 06:51:34 +00:00
|
|
|
remoteContentDisplayed
|
|
|
|
).printMessage(it, this.bodyString ?: "")
|
|
|
|
}
|
|
|
|
}
|
2020-12-30 14:20:18 +00:00
|
|
|
|
|
|
|
fun getParsedMessage(
|
|
|
|
decryptedMessage: String,
|
|
|
|
windowWidth: Int,
|
|
|
|
css: String,
|
|
|
|
defaultErrorMessage: String
|
|
|
|
): String? {
|
2020-12-31 08:56:47 +00:00
|
|
|
bodyString = try {
|
2020-12-30 14:20:18 +00:00
|
|
|
val contentTransformer = DefaultTransformer()
|
|
|
|
.pipe(ViewportTransformer(windowWidth, css))
|
|
|
|
|
|
|
|
contentTransformer.transform(Jsoup.parse(decryptedMessage)).toString()
|
|
|
|
} catch (ioException: IOException) {
|
|
|
|
Timber.e(ioException, "Jsoup is unable to parse HTML message details")
|
|
|
|
defaultErrorMessage
|
|
|
|
}
|
|
|
|
|
2020-12-31 08:56:47 +00:00
|
|
|
return bodyString
|
2020-12-30 14:20:18 +00:00
|
|
|
}
|
2021-04-16 07:42:29 +00:00
|
|
|
|
2021-04-29 15:59:00 +00:00
|
|
|
fun moveToTrash() {
|
|
|
|
moveMessagesToFolder(
|
|
|
|
listOf(messageId),
|
|
|
|
Constants.MessageLocationType.TRASH.toString(),
|
|
|
|
message.value?.folderLocation ?: EMPTY_STRING
|
|
|
|
)
|
2021-04-16 10:51:16 +00:00
|
|
|
}
|
|
|
|
|
2021-04-29 15:59:00 +00:00
|
|
|
fun markUnread() {
|
|
|
|
messageDetailsRepository.markUnRead(listOf(messageId))
|
2021-05-05 13:25:34 +00:00
|
|
|
}
|
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|