proton-mail-android/app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt

705 lines
28 KiB
Kotlin
Raw Normal View History

2020-04-16 15:44:53 +00:00
/*
* Copyright (c) 2020 Proton Technologies AG
*
2020-04-16 15:44:53 +00:00
* This file is part of ProtonMail.
*
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-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-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
import android.annotation.TargetApi
2020-04-16 15:44:53 +00:00
import android.content.Context
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.print.PrintManager
import androidx.core.content.FileProvider
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewModelScope
2020-04-16 15:44:53 +00:00
import ch.protonmail.android.activities.messageDetails.IntentExtrasData
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
import ch.protonmail.android.api.NetworkConfigurator
2020-04-16 15:44:53 +00:00
import ch.protonmail.android.api.models.User
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
import ch.protonmail.android.core.Constants.DIR_EMB_ATTACHMENT_DOWNLOADS
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
import ch.protonmail.android.data.LabelRepository
import ch.protonmail.android.data.local.AttachmentMetadataDao
Replace imports for import ch.protonmail.android.api.models.room..+; MAILAND-1189 # Conflicts: # app/src/androidTest/java/ch/protonmail/android/api/models/room/ContactGroupsDatabaseTest.kt # app/src/main/java/ch/protonmail/android/api/segments/contact/ContactEmailsManager.kt # app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt # app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModel.kt # app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsFragment.kt # app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsRepository.kt # app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModel.kt # app/src/main/java/ch/protonmail/android/contacts/list/listView/ProtonMailContactsLiveData.kt # app/src/main/java/ch/protonmail/android/jobs/UpdateContactJob.java # app/src/test/java/ch/protonmail/android/contacts/details/ContactDetailsRepositoryTest.kt # app/src/test/java/ch/protonmail/android/contacts/details/ContactGroupsRepositoryTest.kt # app/src/test/java/ch/protonmail/android/contacts/groups/ContactGroupsViewModelTest.kt # app/src/test/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepositoryTest.kt # Conflicts: # app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt # app/src/main/java/ch/protonmail/android/api/models/messages/receive/ServerAttachment.kt # app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt # app/src/main/java/ch/protonmail/android/jobs/helper/EmbeddedImage.kt # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # Conflicts: # app/src/androidTest/java/ch/protonmail/android/api/models/room/contacts/ContactDaoTest.kt # Conflicts: # app/src/main/java/ch/protonmail/android/api/models/factories/PackageFactory.java # app/src/main/java/ch/protonmail/android/api/models/factories/SendPreferencesFactory.java # app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt # app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt # app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt # app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt # app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt # app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt # Conflicts: # app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt # app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java
2021-03-18 09:46:25 +00:00
import ch.protonmail.android.data.local.model.*
import ch.protonmail.android.details.presentation.MessageDetailsActivity
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.Name
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
import ch.protonmail.android.labels.domain.usecase.MoveMessagesToFolder
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.ui.view.LabelChipUiModel
import ch.protonmail.android.usecase.VerifyConnection
import ch.protonmail.android.usecase.fetch.FetchVerificationKeys
import ch.protonmail.android.utils.AppUtil
import ch.protonmail.android.utils.DownloadUtils
import ch.protonmail.android.utils.Event
import ch.protonmail.android.utils.HTMLTransformer.DefaultTransformer
import ch.protonmail.android.utils.HTMLTransformer.ViewportTransformer
import ch.protonmail.android.utils.ServerTime
import ch.protonmail.android.utils.UiUtil
2020-04-16 15:44:53 +00:00
import ch.protonmail.android.utils.crypto.KeyInformation
import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel
2021-04-09 06:50:35 +00:00
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
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
import me.proton.core.domain.entity.UserId
import me.proton.core.util.kotlin.DispatcherProvider
import me.proton.core.util.kotlin.EMPTY_STRING
import okio.buffer
import okio.sink
import okio.source
import org.jsoup.Jsoup
2020-04-16 15:44:53 +00:00
import timber.log.Timber
import java.io.File
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(
savedStateHandle: SavedStateHandle,
private val messageDetailsRepository: MessageDetailsRepository,
private val messageRepository: MessageRepository,
private val userManager: UserManager,
private val contactsRepository: ContactsRepository,
private val labelRepository: LabelRepository,
private val attachmentMetadataDao: AttachmentMetadataDao,
private val fetchVerificationKeys: FetchVerificationKeys,
private val attachmentsWorker: DownloadEmbeddedAttachmentsWorker.Enqueuer,
private val dispatchers: DispatcherProvider,
private val attachmentsHelper: AttachmentsHelper,
private val downloadUtils: DownloadUtils,
private val moveMessagesToFolder: MoveMessagesToFolder,
messageRendererFactory: MessageRenderer.Factory,
verifyConnection: VerifyConnection,
networkConfigurator: NetworkConfigurator
) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) {
2020-04-16 15:44:53 +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
by lazy { messageRendererFactory.create(viewModelScope, messageId) }
2020-04-16 15:44:53 +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)
lateinit var decryptedMessageData: MediatorLiveData<Message>
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()
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()
private val _messageDetailsError: MutableLiveData<Event<String>> = MutableLiveData()
2020-04-16 15:44:53 +00:00
private var bodyString: String? = null
2020-04-16 15:44:53 +00:00
set(value) {
field = value
messageRenderer.messageBody = value
}
val labels: Flow<List<Label>> =
messageFlow
.flatMapLatest { message ->
val userId = UserId(userManager.requireCurrentUserId().s)
val labelsIds = (message?.labelIDsNotIncludingLocations ?: emptyList()).map(::Id)
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
val messageDetailsError: LiveData<Event<String>>
2020-04-16 15:44:53 +00:00
get() = _messageDetailsError
val downloadEmbeddedImagesResult: LiveData<String>
2020-04-16 15:44:53 +00:00
get() = _downloadEmbeddedImagesResult
val prepareEditMessageIntent: LiveData<Event<IntentExtrasData>>
2020-04-16 15:44:53 +00:00
get() = _prepareEditMessageIntentResult
val publicKeys = MutableLiveData<List<KeyInformation>>()
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
}
}
private var areImagesDisplayed: Boolean = false
2020-04-16 15:44:53 +00:00
init {
observeDecryption()
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
_downloadEmbeddedImagesResult.postValue(body)
areImagesDisplayed = true
2020-04-16 15:44:53 +00:00
}
}
}
fun saveMessage() {
// Return if message is null
val message = message.value ?: return
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) {
messageDetailsRepository.markRead(listOf(messageId))
2020-04-16 15:44:53 +00:00
saveMessage()
}
}
}
//endregion
fun startDownloadEmbeddedImagesJob() {
hasEmbeddedImages = false
viewModelScope.launch(dispatchers.Io) {
2020-04-16 15:44:53 +00:00
val attachmentMetadataList = attachmentMetadataDao.getAllAttachmentsForMessage(messageId)
val embeddedImages = _embeddedImagesAttachments.mapNotNull {
attachmentsHelper.fromAttachmentToEmbeddedImage(
it, decryptedMessageData.value!!.embeddedImageIds.toList()
2020-04-16 15:44:53 +00:00
)
}
val embeddedImagesWithLocalFiles = mutableListOf<EmbeddedImage>()
2020-04-16 15:44:53 +00:00
embeddedImages.forEach { embeddedImage ->
attachmentMetadataList.find { it.id == embeddedImage.attachmentId }?.let {
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
if (
embeddedImagesWithLocalFiles.isNotEmpty() &&
embeddedImagesWithLocalFiles.all { it.localFileName != null }
) {
AppUtil.postEventOnUi(DownloadEmbeddedImagesEvent(Status.SUCCESS, embeddedImagesWithLocalFiles))
2020-04-16 15:44:53 +00:00
} else {
messageDetailsRepository.startDownloadEmbeddedImages(messageId, userManager.requireCurrentUserId())
2020-04-16 15:44:53 +00:00
}
}
}
fun onEmbeddedImagesDownloaded(event: DownloadEmbeddedImagesEvent) {
Timber.v("onEmbeddedImagesDownloaded status: ${event.status} images size: ${event.images.size}")
2020-04-16 15:44:53 +00:00
if (bodyString.isNullOrEmpty()) {
_downloadEmbeddedImagesResult.value = bodyString ?: ""
2020-04-16 15:44:53 +00:00
return
}
if (event.status == Status.SUCCESS) {
messageRenderer.images.offer(event.images)
}
2020-04-16 15:44:53 +00:00
}
fun prepareEditMessageIntent(
messageAction: Constants.MessageActionType,
message: Message,
newMessageTitle: String?,
content: String,
mBigContentHolder: BigContentHolder
) {
val user: User = userManager.requireCurrentLegacyUser()
2020-04-16 15:44:53 +00:00
viewModelScope.launch {
val intent = messageDetailsRepository.prepareEditMessageIntent(
messageAction,
message,
user,
newMessageTitle,
content,
mBigContentHolder,
areImagesDisplayed,
remoteContentDisplayed,
_embeddedImagesAttachments,
dispatchers.Io,
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
var isDecrypted: Boolean = false
2020-04-16 15:44:53 +00:00
init {
addSource(this@MessageDetailsViewModel.message) {
message = it
message?.senderEmail?.let { senderEmail ->
2020-04-16 15:44:53 +00:00
addSource(contactsRepository.findContactEmailByEmailLiveData(senderEmail)) { contactEmail ->
contact = contactEmail ?: ContactEmail("", message?.senderEmail ?: "", message?.senderName)
if (!isDecrypted) {
2020-04-16 15:44:53 +00:00
refreshedKeys = true
tryEmit()
2020-04-16 15:44:53 +00:00
}
}
}
if (!isDecrypted) {
2020-04-16 15:44:53 +00:00
refreshedKeys = true
tryEmit()
2020-04-16 15:44:53 +00:00
}
}
addSource(publicKeys) {
keys = it
refreshedKeys = false
tryEmit()
2020-04-16 15:44:53 +00:00
}
}
private fun tryEmit() {
val message = message ?: return
if (!message.isDownloaded) {
return
}
viewModelScope.launch {
if (contact?.name != message.sender?.emailAddress)
message.senderDisplayName = contact?.name
2020-04-16 15:44:53 +00:00
isDecrypted = withContext(dispatchers.Comp) {
message.tryDecrypt(keys) ?: false
}
Timber.v("Message isDecrypted:$isDecrypted, keys size: ${keys?.size}")
value = message
}
2020-04-16 15:44:53 +00:00
}
private fun Message.tryDecrypt(verificationKeys: List<KeyInformation>?): Boolean? {
return try {
Clean up and fixes after rebase MAILAND-1189 # Conflicts: # app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt # app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt # app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt # app/src/main/java/ch/protonmail/android/data/local/model/Message.kt # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/storage/AttachmentClearingService.java # Conflicts: # app/src/main/java/ch/protonmail/android/activities/multiuser/ConnectAccountActivity.kt # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt # app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt # Conflicts: # app/src/main/java/ch/protonmail/android/api/models/factories/SendPreferencesFactory.java # app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt # app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt # app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt # app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt # app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt # app/src/main/java/ch/protonmail/android/utils/notifier/AndroidUserNotifier.kt # app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt # Conflicts: # app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt # app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt
2021-03-18 09:47:58 +00:00
decrypt(userManager, userManager.requireCurrentUserId(), verificationKeys)
true
} catch (exception: Exception) {
// signature verification failed with special case, try to decrypt again without verification
// and hardcode verification error
if (verificationKeys != null && verificationKeys.isNotEmpty() &&
exception.message == "Signature Verification Error: No matching signature"
) {
Timber.d(exception, "Decrypting message again without verkeys")
Clean up and fixes after rebase MAILAND-1189 # Conflicts: # app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt # app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt # app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt # app/src/main/java/ch/protonmail/android/data/local/model/Message.kt # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/storage/AttachmentClearingService.java # Conflicts: # app/src/main/java/ch/protonmail/android/activities/multiuser/ConnectAccountActivity.kt # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt # app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt # Conflicts: # app/src/main/java/ch/protonmail/android/api/models/factories/SendPreferencesFactory.java # app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt # app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt # app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt # app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt # app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt # app/src/main/java/ch/protonmail/android/utils/notifier/AndroidUserNotifier.kt # app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt # Conflicts: # app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt # app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt
2021-03-18 09:47:58 +00:00
decrypt(userManager, userManager.requireCurrentUserId())
this.hasValidSignature = false
this.hasInvalidSignature = true
true
} else {
Timber.d(exception, "Cannot decrypt message")
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 {
messageDetailsRepository.checkIfAttHeadersArePresent(it, dispatchers.Io)
2020-04-16 15:44:53 +00:00
} ?: false
shouldExit = checkForMessageAttachmentHeaders && !attHeadersPresent
}
if (!shouldExit) {
withContext(dispatchers.Io) {
2020-04-16 15:44:53 +00:00
val messageDetailsResult = runCatching {
with(messageDetailsRepository) {
2020-04-16 15:44:53 +00:00
if (isTransientMessage) fetchSearchMessageDetails(messageId)
else fetchMessageDetails(messageId)
}
}
messageDetailsResult
.onFailure {
requestPending.set(false)
_messageDetailsError.postValue(Event(""))
}
.onSuccess { messageResponse ->
if (messageResponse.code == RESPONSE_CODE_OK) {
with(messageDetailsRepository) {
if (isTransientMessage) {
val savedMessage = findSearchMessageById(messageId).first()
if (savedMessage != null) {
messageResponse.message.writeTo(savedMessage)
saveSearchMessageInDB(savedMessage)
} else {
prepareMessage(messageResponse.message)
}
2020-04-16 15:44:53 +00:00
} else {
val savedMessage = findMessageById(messageId).first()
if (savedMessage != null) {
messageResponse.message.writeTo(savedMessage)
saveMessageInDB(savedMessage)
2020-04-16 15:44:53 +00:00
} else {
prepareMessage(messageResponse.message)
setFolderLocation(messageResponse.message)
saveMessageInDB(messageResponse.message, isTransientMessage)
2020-04-16 15:44:53 +00:00
}
}
}
} else {
_messageDetailsError.postValue(Event(messageResponse.error))
2020-04-16 15:44:53 +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))
if (location != Constants.MessageLocationType.ALL_MAIL &&
location != Constants.MessageLocationType.STARRED
) {
2020-04-16 15:44:53 +00:00
break
}
}
}
message.location = location.messageLocationTypeValue
}
fun viewOrDownloadAttachment(context: Context, attachmentToDownloadId: String, messageId: String) {
2020-04-16 15:44:53 +00:00
viewModelScope.launch(dispatchers.Io) {
val metadata = attachmentMetadataDao
.getAttachmentMetadataForMessageAndAttachmentId(messageId, attachmentToDownloadId)
Timber.v("viewOrDownloadAttachment Id: $attachmentToDownloadId metadataId: ${metadata?.id}")
2020-04-16 15:44:53 +00:00
if (metadata != null) {
val uri = metadata.uri
if (uri != null && attachmentsHelper.isFileAvailable(context, uri)) {
if (uri.path?.contains(DIR_EMB_ATTACHMENT_DOWNLOADS) == true) {
copyAttachmentToDownloadsAndDisplay(context, metadata.name, uri)
} else {
viewAttachment(context, metadata.name, uri)
}
} 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 {
Timber.v("No metadata found for attachment id: $attachmentToDownloadId")
attachmentsWorker.enqueue(messageId, userManager.requireCurrentUserId(), attachmentToDownloadId)
2020-04-16 15:44:53 +00:00
}
}
}
/**
* Explicitly make a copy of embedded attachment to downloads and display it (product requirement)
*/
private fun copyAttachmentToDownloadsAndDisplay(
context: Context,
filename: String,
uri: Uri
) {
val newUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
getCopiedUriFromQ(filename, uri, context)
} else {
getCopiedUriBeforeQ(filename, uri, context)
}
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
return contentResolver.openInputStream(uri)?.let {
attachmentsHelper.saveAttachmentInMediaStore(
contentResolver, filename, contentResolver.getType(uri), it
)
}
}
private fun getCopiedUriBeforeQ(filename: String, uri: Uri, context: Context): Uri {
val fileInDownloads = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
filename
)
context.contentResolver.openInputStream(uri)?.use { stream ->
fileInDownloads.sink().buffer().use { sink ->
sink.writeAll(stream.source())
}
}
return FileProvider.getUriForFile(
context, context.applicationContext.packageName + ".provider", fileInDownloads
)
}
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()
}
fun isEmbeddedImagesDisplayed() = areImagesDisplayed
2020-04-16 15:44:53 +00:00
fun displayEmbeddedImages() {
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) {
val embeddedImage = attachmentsHelper
.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
viewModelScope.launch {
val result = fetchVerificationKeys(message.senderEmail)
onFetchVerificationKeysEvent(result)
}
2020-04-16 15:44:53 +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
fun printMessage(activityContext: Context) {
val message = message.value
message?.let {
MessagePrinter(
activityContext,
activityContext.resources,
activityContext.getSystemService(Context.PRINT_SERVICE) as PrintManager,
remoteContentDisplayed
).printMessage(it, this.bodyString ?: "")
}
}
fun getParsedMessage(
decryptedMessage: String,
windowWidth: Int,
css: String,
defaultErrorMessage: String
): String? {
bodyString = try {
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
}
return bodyString
}
fun moveToTrash() {
moveMessagesToFolder(
listOf(messageId),
Constants.MessageLocationType.TRASH.toString(),
message.value?.folderLocation ?: EMPTY_STRING
)
}
fun markUnread() {
messageDetailsRepository.markUnRead(listOf(messageId))
}
2020-04-16 15:44:53 +00:00
}