2020-04-16 15:44:53 +00:00
|
|
|
/*
|
2022-02-28 15:15:59 +00:00
|
|
|
* Copyright (c) 2022 Proton AG
|
2020-09-17 10:44:37 +00:00
|
|
|
*
|
2022-02-28 15:15:59 +00:00
|
|
|
* This file is part of Proton Mail.
|
2020-09-17 10:44:37 +00:00
|
|
|
*
|
2022-02-28 15:15:59 +00:00
|
|
|
* Proton Mail is free software: you can redistribute it and/or modify
|
2020-04-16 15:44:53 +00:00
|
|
|
* 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
|
|
|
*
|
2022-02-28 15:15:59 +00:00
|
|
|
* Proton Mail is distributed in the hope that it will be useful,
|
2020-04-16 15:44:53 +00:00
|
|
|
* 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
|
2022-02-28 15:15:59 +00:00
|
|
|
* along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
2020-04-16 15:44:53 +00:00
|
|
|
*/
|
|
|
|
package ch.protonmail.android.activities.messageDetails.viewmodel
|
|
|
|
|
2021-02-08 14:39:26 +00:00
|
|
|
import android.annotation.TargetApi
|
2022-01-21 15:07:56 +00:00
|
|
|
import android.content.ActivityNotFoundException
|
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
|
2021-09-10 15:40:04 +00:00
|
|
|
import androidx.lifecycle.Lifecycle
|
|
|
|
import androidx.lifecycle.LifecycleObserver
|
2020-09-17 10:44:37 +00:00
|
|
|
import androidx.lifecycle.LiveData
|
|
|
|
import androidx.lifecycle.MutableLiveData
|
2021-09-10 15:40:04 +00:00
|
|
|
import androidx.lifecycle.OnLifecycleEvent
|
2020-10-07 13:14:55 +00:00
|
|
|
import androidx.lifecycle.SavedStateHandle
|
2020-09-17 10:44:37 +00:00
|
|
|
import androidx.lifecycle.viewModelScope
|
2022-01-21 15:07:56 +00:00
|
|
|
import ch.protonmail.android.R
|
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.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-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.core.UserManager
|
|
|
|
import ch.protonmail.android.data.ContactsRepository
|
2021-02-18 16:10:43 +00:00
|
|
|
import ch.protonmail.android.data.local.AttachmentMetadataDao
|
2021-05-17 13:00:26 +00:00
|
|
|
import ch.protonmail.android.data.local.model.Attachment
|
|
|
|
import ch.protonmail.android.data.local.model.Message
|
2021-05-27 14:04:52 +00:00
|
|
|
import ch.protonmail.android.details.data.toConversationUiModel
|
2022-03-09 14:03:59 +00:00
|
|
|
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
|
2021-05-17 16:13:51 +00:00
|
|
|
import ch.protonmail.android.details.presentation.model.ConversationUiModel
|
2021-08-17 13:26:34 +00:00
|
|
|
import ch.protonmail.android.details.presentation.model.MessageBodyState
|
2022-03-10 09:43:31 +00:00
|
|
|
import ch.protonmail.android.details.presentation.ui.MessageDetailsActivity
|
2022-01-21 15:07:56 +00:00
|
|
|
import ch.protonmail.android.domain.entity.EmailAddress
|
2021-05-13 13:18:53 +00:00
|
|
|
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
|
2021-02-15 10:15:18 +00:00
|
|
|
import ch.protonmail.android.jobs.ReportPhishingJob
|
2020-04-16 15:44:53 +00:00
|
|
|
import ch.protonmail.android.jobs.helper.EmbeddedImage
|
2021-09-07 14:02:45 +00:00
|
|
|
import ch.protonmail.android.labels.domain.LabelRepository
|
2021-09-14 14:50:57 +00:00
|
|
|
import ch.protonmail.android.labels.domain.model.Label
|
2021-09-07 14:02:45 +00:00
|
|
|
import ch.protonmail.android.labels.domain.model.LabelId
|
2021-09-08 07:22:31 +00:00
|
|
|
import ch.protonmail.android.labels.domain.model.LabelType
|
2021-07-06 14:56:39 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.ChangeConversationsReadStatus
|
2021-07-19 13:30:20 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.ChangeConversationsStarredStatus
|
2021-05-18 16:47:25 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.ConversationsRepository
|
2021-08-18 19:21:04 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.DeleteConversations
|
2021-07-16 12:48:43 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.MoveConversationsToFolder
|
2021-08-04 15:03:52 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.model.Conversation
|
2021-09-14 14:50:57 +00:00
|
|
|
import ch.protonmail.android.mailbox.domain.usecase.MoveMessagesToFolder
|
2022-03-31 15:44:46 +00:00
|
|
|
import ch.protonmail.android.mailbox.presentation.util.ConversationModeEnabled
|
2021-05-13 14:28:15 +00:00
|
|
|
import ch.protonmail.android.repository.MessageRepository
|
2021-08-19 09:15:56 +00:00
|
|
|
import ch.protonmail.android.ui.model.LabelChipUiModel
|
2022-01-25 18:34:19 +00:00
|
|
|
import ch.protonmail.android.usecase.IsAppInDarkMode
|
2020-10-12 09:19:45 +00:00
|
|
|
import ch.protonmail.android.usecase.VerifyConnection
|
2021-08-18 19:21:04 +00:00
|
|
|
import ch.protonmail.android.usecase.delete.DeleteMessage
|
2020-10-29 15:08:37 +00:00
|
|
|
import ch.protonmail.android.usecase.fetch.FetchVerificationKeys
|
2021-10-22 11:18:10 +00:00
|
|
|
import ch.protonmail.android.usecase.message.ChangeMessagesReadStatus
|
2021-10-25 11:23:39 +00:00
|
|
|
import ch.protonmail.android.usecase.message.ChangeMessagesStarredStatus
|
2022-01-21 15:07:56 +00:00
|
|
|
import ch.protonmail.android.util.ProtonCalendarUtil
|
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
|
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-02-15 10:15:18 +00:00
|
|
|
import com.birbit.android.jobqueue.JobManager
|
2021-04-09 06:50:35 +00:00
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
2021-10-01 15:27:18 +00:00
|
|
|
import kotlinx.coroutines.CancellationException
|
2021-12-23 14:42:44 +00:00
|
|
|
import kotlinx.coroutines.Job
|
2021-05-13 13:18:53 +00:00
|
|
|
import kotlinx.coroutines.flow.Flow
|
2021-07-08 16:00:09 +00:00
|
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
2021-07-13 19:57:42 +00:00
|
|
|
import kotlinx.coroutines.flow.SharedFlow
|
2021-07-08 16:00:09 +00:00
|
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
2021-09-02 16:36:56 +00:00
|
|
|
import kotlinx.coroutines.flow.filterNot
|
2021-05-20 07:35:04 +00:00
|
|
|
import kotlinx.coroutines.flow.filterNotNull
|
2021-07-14 15:51:20 +00:00
|
|
|
import kotlinx.coroutines.flow.first
|
2021-07-13 19:57:42 +00:00
|
|
|
import kotlinx.coroutines.flow.firstOrNull
|
2021-05-13 13:18:53 +00:00
|
|
|
import kotlinx.coroutines.flow.flatMapLatest
|
2021-06-23 15:01:55 +00:00
|
|
|
import kotlinx.coroutines.flow.flow
|
2021-07-14 15:51:20 +00:00
|
|
|
import kotlinx.coroutines.flow.flowOf
|
2021-06-23 15:01:55 +00:00
|
|
|
import kotlinx.coroutines.flow.flowOn
|
2021-07-08 16:00:09 +00:00
|
|
|
import kotlinx.coroutines.flow.launchIn
|
2021-05-13 13:18:53 +00:00
|
|
|
import kotlinx.coroutines.flow.map
|
2021-07-08 16:00:09 +00:00
|
|
|
import kotlinx.coroutines.flow.onEach
|
2020-04-16 15:44:53 +00:00
|
|
|
import kotlinx.coroutines.launch
|
2021-08-06 17:25:55 +00:00
|
|
|
import kotlinx.coroutines.runBlocking
|
2021-05-18 16:47:25 +00:00
|
|
|
import me.proton.core.domain.arch.DataResult
|
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-08-25 13:34:08 +00:00
|
|
|
import me.proton.core.util.kotlin.mapSecond
|
2021-05-14 08:15:28 +00:00
|
|
|
import me.proton.core.util.kotlin.takeIfNotBlank
|
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
|
2021-04-09 06:50:35 +00:00
|
|
|
import javax.inject.Inject
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-10-21 16:45:42 +00:00
|
|
|
@Suppress("LongParameterList") // Every new parameter adds a new issue and breaks the build
|
2021-04-09 06:50:35 +00:00
|
|
|
@HiltViewModel
|
|
|
|
internal class MessageDetailsViewModel @Inject constructor(
|
2022-01-25 18:34:19 +00:00
|
|
|
private val isAppInDarkMode: IsAppInDarkMode,
|
2022-02-04 16:39:21 +00:00
|
|
|
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference,
|
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-07-16 12:48:43 +00:00
|
|
|
private val moveConversationsToFolder: MoveConversationsToFolder,
|
2021-07-06 14:56:39 +00:00
|
|
|
private val conversationModeEnabled: ConversationModeEnabled,
|
2021-05-18 16:47:25 +00:00
|
|
|
private val conversationRepository: ConversationsRepository,
|
2021-10-22 11:18:10 +00:00
|
|
|
private val changeMessagesReadStatus: ChangeMessagesReadStatus,
|
2021-07-06 14:56:39 +00:00
|
|
|
private val changeConversationsReadStatus: ChangeConversationsReadStatus,
|
2021-10-25 11:23:39 +00:00
|
|
|
private val changeMessagesStarredStatus: ChangeMessagesStarredStatus,
|
2021-07-19 13:30:20 +00:00
|
|
|
private val changeConversationsStarredStatus: ChangeConversationsStarredStatus,
|
2021-08-18 19:21:04 +00:00
|
|
|
private val deleteMessage: DeleteMessage,
|
|
|
|
private val deleteConversations: DeleteConversations,
|
2021-08-20 08:25:03 +00:00
|
|
|
private val savedStateHandle: SavedStateHandle,
|
2021-02-02 09:28:53 +00:00
|
|
|
messageRendererFactory: MessageRenderer.Factory,
|
2020-10-13 12:48:55 +00:00
|
|
|
verifyConnection: VerifyConnection,
|
2022-01-21 15:07:56 +00:00
|
|
|
networkConfigurator: NetworkConfigurator,
|
|
|
|
private val protonCalendarUtil: ProtonCalendarUtil
|
2021-09-10 15:40:04 +00:00
|
|
|
) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator), LifecycleObserver {
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-05-19 12:56:12 +00:00
|
|
|
private val messageOrConversationId: String =
|
|
|
|
savedStateHandle.get<String>(MessageDetailsActivity.EXTRA_MESSAGE_OR_CONVERSATION_ID)
|
|
|
|
?: throw IllegalStateException("messageId in MessageDetails is Empty!")
|
2020-10-07 13:14:55 +00:00
|
|
|
|
2021-08-20 08:25:03 +00:00
|
|
|
private val location: Constants.MessageLocationType
|
|
|
|
get() = Constants.MessageLocationType.fromInt(
|
2021-05-18 16:47:25 +00:00
|
|
|
savedStateHandle.get<Int>(MessageDetailsActivity.EXTRA_MESSAGE_LOCATION_ID)
|
|
|
|
?: Constants.MessageLocationType.INVALID.messageLocationTypeValue
|
|
|
|
)
|
|
|
|
|
2021-07-23 17:10:44 +00:00
|
|
|
private val mailboxLocationId: String? by lazy {
|
|
|
|
savedStateHandle.get<String>(MessageDetailsActivity.EXTRA_MAILBOX_LABEL_ID)
|
|
|
|
}
|
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
private val messageRenderer
|
2021-05-28 15:45:23 +00:00
|
|
|
by lazy { messageRendererFactory.create(viewModelScope) }
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
var renderingPassed = false
|
|
|
|
var hasEmbeddedImages: Boolean = false
|
2021-05-28 14:43:02 +00:00
|
|
|
private var embeddedImagesAttachments: ArrayList<Attachment> = ArrayList()
|
|
|
|
private var embeddedImagesToFetch: ArrayList<EmbeddedImage> = ArrayList()
|
2020-04-16 15:44:53 +00:00
|
|
|
private var remoteContentDisplayed: Boolean = false
|
2022-01-21 15:07:56 +00:00
|
|
|
private var calendarAttachmentId: String? = null
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
private val _prepareEditMessageIntentResult: MutableLiveData<Event<IntentExtrasData>> = MutableLiveData()
|
2021-06-02 13:54:37 +00:00
|
|
|
private val _decryptedConversationUiModel: MutableLiveData<ConversationUiModel> = MutableLiveData()
|
2021-06-17 13:05:07 +00:00
|
|
|
private val _messageRenderedWithImages: MutableLiveData<Message> = MutableLiveData()
|
2020-04-16 15:44:53 +00:00
|
|
|
private val _checkStoragePermission: MutableLiveData<Event<Boolean>> = MutableLiveData()
|
2020-07-14 20:46:07 +00:00
|
|
|
private val _messageDetailsError: MutableLiveData<Event<String>> = MutableLiveData()
|
2021-08-12 08:47:24 +00:00
|
|
|
private val _showPermissionMissingDialog: MutableLiveData<Unit> = MutableLiveData()
|
2021-09-10 15:40:04 +00:00
|
|
|
private val _conversationUiFlow = MutableSharedFlow<ConversationUiModel>(replay = 1)
|
2022-02-07 20:07:26 +00:00
|
|
|
private val _reloadMessageFlow = MutableSharedFlow<String>(replay = 1)
|
2021-07-08 16:00:09 +00:00
|
|
|
|
2021-07-13 19:57:42 +00:00
|
|
|
val conversationUiModel: SharedFlow<ConversationUiModel>
|
2021-09-10 15:40:04 +00:00
|
|
|
get() = _conversationUiFlow
|
2020-04-16 15:44:53 +00:00
|
|
|
|
|
|
|
val checkStoragePermission: LiveData<Event<Boolean>>
|
|
|
|
get() = _checkStoragePermission
|
|
|
|
|
2020-07-14 20:46:07 +00:00
|
|
|
val messageDetailsError: LiveData<Event<String>>
|
2020-04-16 15:44:53 +00:00
|
|
|
get() = _messageDetailsError
|
|
|
|
|
2021-08-12 08:47:24 +00:00
|
|
|
val showPermissionMissingDialog: LiveData<Unit>
|
|
|
|
get() = _showPermissionMissingDialog
|
|
|
|
|
2020-10-05 13:59:03 +00:00
|
|
|
val prepareEditMessageIntent: LiveData<Event<IntentExtrasData>>
|
2020-04-16 15:44:53 +00:00
|
|
|
get() = _prepareEditMessageIntentResult
|
|
|
|
|
2021-06-02 13:54:37 +00:00
|
|
|
val decryptedConversationUiModel: LiveData<ConversationUiModel>
|
|
|
|
get() = _decryptedConversationUiModel
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-06-17 13:05:07 +00:00
|
|
|
val messageRenderedWithImages: LiveData<Message>
|
|
|
|
get() = _messageRenderedWithImages
|
|
|
|
|
2022-02-07 20:07:26 +00:00
|
|
|
val reloadMessageFlow: SharedFlow<String>
|
|
|
|
get() = _reloadMessageFlow
|
|
|
|
|
2021-02-09 13:18:01 +00:00
|
|
|
private var areImagesDisplayed: Boolean = false
|
2021-02-08 10:16:40 +00:00
|
|
|
|
2021-09-15 13:09:37 +00:00
|
|
|
private var visibleToTheUser = true
|
2021-09-10 15:40:04 +00:00
|
|
|
|
2021-12-23 14:42:44 +00:00
|
|
|
private var conversationFlowJob: Job? = null
|
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
init {
|
2021-07-08 16:00:09 +00:00
|
|
|
// message render flow
|
2021-12-23 14:42:44 +00:00
|
|
|
conversationFlowJob = userManager.primaryUserId
|
2021-07-13 19:57:42 +00:00
|
|
|
.filterNotNull()
|
|
|
|
.flatMapLatest { userId ->
|
2021-07-16 08:21:41 +00:00
|
|
|
if (isConversationEnabled()) {
|
2021-07-13 19:57:42 +00:00
|
|
|
getConversationFlow(userId)
|
|
|
|
} else {
|
|
|
|
getMessageFlow(userId)
|
|
|
|
}
|
|
|
|
}
|
2021-07-14 19:09:38 +00:00
|
|
|
.filterNotNull()
|
2021-07-08 16:00:09 +00:00
|
|
|
.distinctUntilChanged()
|
2021-08-25 13:34:08 +00:00
|
|
|
.combineWithLabels()
|
2021-07-08 16:00:09 +00:00
|
|
|
.onEach {
|
|
|
|
Timber.i("Emit conversation Ui model subject ${it.subject}")
|
2021-07-14 19:09:38 +00:00
|
|
|
emitConversationUiItem(it)
|
2021-07-08 16:00:09 +00:00
|
|
|
}
|
|
|
|
.launchIn(viewModelScope)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 19:09:38 +00:00
|
|
|
private fun getMessageFlow(userId: UserId): Flow<ConversationUiModel?> =
|
2021-07-13 19:57:42 +00:00
|
|
|
messageRepository.observeMessage(userId, messageOrConversationId)
|
|
|
|
.distinctUntilChanged()
|
|
|
|
.map {
|
|
|
|
loadMessageDetails(it)
|
2021-07-08 16:00:09 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 19:09:38 +00:00
|
|
|
private fun getConversationFlow(userId: UserId): Flow<ConversationUiModel?> =
|
2021-08-17 13:51:00 +00:00
|
|
|
conversationRepository.getConversation(userId, messageOrConversationId)
|
2021-07-13 19:57:42 +00:00
|
|
|
.distinctUntilChanged()
|
2021-09-02 16:36:56 +00:00
|
|
|
.filterOutIncompleteConversations()
|
2021-07-13 19:57:42 +00:00
|
|
|
.map {
|
2021-08-19 08:42:47 +00:00
|
|
|
loadConversationDetails(it, userId)
|
2021-07-13 19:57:42 +00:00
|
|
|
}
|
2021-07-08 16:00:09 +00:00
|
|
|
|
2021-09-03 07:46:16 +00:00
|
|
|
private fun Flow<DataResult<Conversation>>.filterOutIncompleteConversations() = filterNot { result ->
|
|
|
|
result is DataResult.Success && !result.value.isComplete()
|
2021-09-02 16:36:56 +00:00
|
|
|
}
|
|
|
|
|
2021-08-25 13:34:08 +00:00
|
|
|
private fun Flow<ConversationUiModel>.combineWithLabels() = flatMapLatest { conversation ->
|
|
|
|
val nonExclusiveLabelsHashMap = hashMapOf<String, List<LabelChipUiModel>>()
|
2021-09-14 14:50:57 +00:00
|
|
|
val exclusiveLabelsHashMap = hashMapOf<String, List<Label>>()
|
2021-08-25 13:34:08 +00:00
|
|
|
conversation.messages.filter { it.allLabelIDs.isNotEmpty() }.forEach { message ->
|
|
|
|
val messageId = requireNotNull(message.messageId)
|
2021-09-15 10:15:41 +00:00
|
|
|
getAllLabelsFor(message)?.let { (exclusiveLabels, nonExclusiveLabels) ->
|
2021-08-25 13:34:08 +00:00
|
|
|
exclusiveLabelsHashMap[messageId] = exclusiveLabels.toList()
|
|
|
|
nonExclusiveLabelsHashMap[messageId] = nonExclusiveLabels
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return@flatMapLatest flowOf(
|
|
|
|
conversation.copy(
|
|
|
|
nonExclusiveLabels = nonExclusiveLabelsHashMap,
|
|
|
|
exclusiveLabels = exclusiveLabelsHashMap
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun getAllLabelsFor(
|
|
|
|
message: Message
|
2021-09-14 14:50:57 +00:00
|
|
|
): Pair<Collection<Label>, List<LabelChipUiModel>>? {
|
2021-08-25 13:34:08 +00:00
|
|
|
val allLabelIds = message.allLabelIDs.map { labelId -> LabelId(labelId) }
|
2021-09-15 10:15:41 +00:00
|
|
|
return labelRepository.observeLabels(allLabelIds)
|
2021-08-25 13:34:08 +00:00
|
|
|
.firstOrNull()
|
2021-08-26 07:26:24 +00:00
|
|
|
?.partition { it.type == LabelType.FOLDER }
|
2021-08-25 13:34:08 +00:00
|
|
|
?.mapSecond { it.toNonExclusiveLabelModel() }
|
|
|
|
}
|
|
|
|
|
2021-09-14 14:50:57 +00:00
|
|
|
private fun Label.toNonExclusiveLabelModel(): LabelChipUiModel {
|
2021-08-25 13:34:08 +00:00
|
|
|
val labelColor = color.takeIfNotBlank()
|
|
|
|
?.let { Color.parseColor(UiUtil.normalizeColor(it)) }
|
2021-08-26 07:26:24 +00:00
|
|
|
return LabelChipUiModel(id, Name(name), labelColor)
|
2021-08-25 13:34:08 +00:00
|
|
|
}
|
|
|
|
|
2021-12-23 14:42:44 +00:00
|
|
|
fun cancelConversationFlowJob() = conversationFlowJob?.cancel()
|
|
|
|
|
2021-05-17 09:36:54 +00:00
|
|
|
fun markUnread() {
|
2021-08-06 17:25:55 +00:00
|
|
|
viewModelScope.launch {
|
2021-09-23 16:04:29 +00:00
|
|
|
if (isConversationEnabled()) {
|
2021-07-06 14:56:39 +00:00
|
|
|
changeConversationsReadStatus(
|
|
|
|
listOf(messageOrConversationId),
|
|
|
|
ChangeConversationsReadStatus.Action.ACTION_MARK_UNREAD,
|
2021-08-17 13:51:00 +00:00
|
|
|
userManager.requireCurrentUserId(),
|
2021-07-23 17:10:44 +00:00
|
|
|
mailboxLocationId ?: location.messageLocationTypeValue.toString()
|
2021-07-06 14:56:39 +00:00
|
|
|
)
|
2021-08-06 17:25:55 +00:00
|
|
|
} else {
|
2021-10-22 11:18:10 +00:00
|
|
|
changeMessagesReadStatus(
|
|
|
|
listOf(messageOrConversationId),
|
|
|
|
ChangeMessagesReadStatus.Action.ACTION_MARK_UNREAD,
|
|
|
|
userManager.requireCurrentUserId()
|
|
|
|
)
|
2021-07-06 14:56:39 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 15:01:55 +00:00
|
|
|
fun loadMessageBody(message: Message) = flow {
|
|
|
|
Timber.v("loadMessageBody ${message.messageId} isNotDecrypted: ${message.decryptedHTML.isNullOrEmpty()}")
|
2021-05-26 12:54:34 +00:00
|
|
|
|
2021-06-23 15:01:55 +00:00
|
|
|
if (!message.decryptedHTML.isNullOrEmpty()) {
|
2021-08-17 13:26:34 +00:00
|
|
|
emit(MessageBodyState.Success(message))
|
2021-06-23 15:01:55 +00:00
|
|
|
} else {
|
2021-05-26 12:54:34 +00:00
|
|
|
val userId = userManager.requireCurrentUserId()
|
2021-06-23 15:01:55 +00:00
|
|
|
val messageId = requireNotNull(message.messageId)
|
2021-08-17 13:26:34 +00:00
|
|
|
val fetchedMessage = messageRepository.getMessage(userId, messageId, true) ?: return@flow
|
2022-03-16 12:13:47 +00:00
|
|
|
val verificationKeys = runCatching {
|
|
|
|
fetchVerificationKeys.invoke(message.senderEmail)
|
|
|
|
}.getOrNull()
|
|
|
|
val isDecrypted = fetchedMessage.tryDecrypt(verificationKeys)
|
2021-08-17 13:26:34 +00:00
|
|
|
Timber.v("message $messageId isDecrypted, isRead: ${fetchedMessage.isRead}")
|
2021-09-15 13:09:37 +00:00
|
|
|
if (!fetchedMessage.isRead && visibleToTheUser) {
|
2021-08-17 13:26:34 +00:00
|
|
|
messageRepository.markRead(listOf(messageId))
|
|
|
|
}
|
|
|
|
|
2021-05-26 12:54:34 +00:00
|
|
|
if (isDecrypted == true) {
|
2021-08-17 13:26:34 +00:00
|
|
|
emit(MessageBodyState.Success(fetchedMessage))
|
|
|
|
} else {
|
|
|
|
emit(MessageBodyState.Error.DecryptionError(fetchedMessage))
|
2021-05-26 12:54:34 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-25 16:02:44 +00:00
|
|
|
}.flowOn(dispatchers.Io)
|
2021-05-26 12:54:34 +00:00
|
|
|
|
2021-09-10 15:40:04 +00:00
|
|
|
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
|
|
|
fun pause() {
|
2021-09-15 13:09:37 +00:00
|
|
|
visibleToTheUser = false
|
2021-09-10 15:40:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
|
|
|
fun resume() {
|
2021-09-15 13:09:37 +00:00
|
|
|
visibleToTheUser = true
|
2021-09-10 15:40:04 +00:00
|
|
|
}
|
|
|
|
|
2021-08-06 14:59:25 +00:00
|
|
|
private suspend fun loadMessageDetails(message: Message?): ConversationUiModel? {
|
2022-03-24 16:53:26 +00:00
|
|
|
val userId = userManager.currentUserId
|
|
|
|
?: return null
|
2021-08-06 14:59:25 +00:00
|
|
|
|
|
|
|
val messageWithDetails = if (message == null || !message.isDownloaded) {
|
2021-07-13 19:57:42 +00:00
|
|
|
Timber.v("Message is not downloaded, trying to fetch it")
|
2022-02-09 17:04:40 +00:00
|
|
|
userManager.currentUserId
|
|
|
|
?.let { messageRepository.getMessage(it, messageOrConversationId, true) }
|
2021-07-08 16:00:09 +00:00
|
|
|
} else {
|
2021-07-13 19:57:42 +00:00
|
|
|
message
|
|
|
|
}
|
|
|
|
|
|
|
|
if (messageWithDetails == null || !messageWithDetails.isDownloaded) {
|
|
|
|
Timber.i("Failed fetching Message Details for message $messageOrConversationId")
|
2021-07-08 16:00:09 +00:00
|
|
|
_messageDetailsError.postValue(Event("Failed getting message details"))
|
2021-07-13 19:57:42 +00:00
|
|
|
return null
|
2021-05-18 16:47:25 +00:00
|
|
|
}
|
2021-07-13 19:57:42 +00:00
|
|
|
|
2022-03-24 16:53:26 +00:00
|
|
|
val contact = contactsRepository.findContactEmailByEmail(userId, messageWithDetails.senderEmail)
|
2021-08-04 15:03:52 +00:00
|
|
|
val contactName = contact?.name?.takeIfNotBlank()
|
|
|
|
if (contactName != null && contactName != contact.email) {
|
|
|
|
messageWithDetails.senderDisplayName = contact.name
|
|
|
|
}
|
2021-07-13 19:57:42 +00:00
|
|
|
return messageWithDetails.toConversationUiModel()
|
2021-05-18 16:47:25 +00:00
|
|
|
}
|
2021-05-17 13:00:26 +00:00
|
|
|
|
2021-07-07 11:55:06 +00:00
|
|
|
fun isConversationEnabled() = conversationModeEnabled(location)
|
|
|
|
|
2021-08-06 17:25:55 +00:00
|
|
|
fun doesConversationHaveMoreThanOneMessage() = runBlocking {
|
2021-09-16 08:46:45 +00:00
|
|
|
val messagesCount = conversationUiModel.first().messagesCount
|
|
|
|
if (messagesCount != null) messagesCount > 1 else false
|
2021-08-06 17:25:55 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 10:15:18 +00:00
|
|
|
private suspend fun loadConversationDetails(
|
2021-09-07 07:41:18 +00:00
|
|
|
result: DataResult<Conversation>,
|
|
|
|
userId: UserId
|
2021-02-15 10:15:18 +00:00
|
|
|
): ConversationUiModel? {
|
2021-07-14 19:09:38 +00:00
|
|
|
return when (result) {
|
|
|
|
is DataResult.Success -> {
|
|
|
|
Timber.v("loadConversationDetails Success")
|
|
|
|
val conversation = result.value
|
|
|
|
if (conversation.messages?.isEmpty() == true) {
|
2021-07-21 18:56:31 +00:00
|
|
|
Timber.i("Failed getting conversation details, empty messages")
|
2021-07-14 19:09:38 +00:00
|
|
|
null
|
|
|
|
} else {
|
|
|
|
onConversationLoaded(conversation, userId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
is DataResult.Error -> {
|
|
|
|
Timber.d("loadConversationDetails $messageOrConversationId Error - cause: ${result.cause}")
|
|
|
|
_messageDetailsError.postValue(Event("Failed getting conversation details"))
|
|
|
|
null
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
Timber.v("loadConversationDetails result ${result.javaClass.canonicalName}")
|
|
|
|
null
|
2021-05-21 12:18:05 +00:00
|
|
|
}
|
2021-07-08 16:00:09 +00:00
|
|
|
}
|
2021-05-21 12:18:05 +00:00
|
|
|
}
|
|
|
|
|
2021-05-21 10:42:25 +00:00
|
|
|
private suspend fun onConversationLoaded(
|
2021-06-23 15:01:55 +00:00
|
|
|
conversation: Conversation,
|
2021-07-27 14:19:07 +00:00
|
|
|
userId: UserId
|
2021-07-14 19:09:38 +00:00
|
|
|
): ConversationUiModel? {
|
2021-05-21 10:42:25 +00:00
|
|
|
val messages = conversation.messages?.mapNotNull { message ->
|
2021-07-13 20:28:41 +00:00
|
|
|
messageRepository.findMessage(userId, message.id)?.let { localMessage ->
|
2022-03-24 16:53:26 +00:00
|
|
|
val contact = contactsRepository.findContactEmailByEmail(userId, localMessage.senderEmail)
|
2021-08-04 15:03:52 +00:00
|
|
|
val contactName = contact?.name?.takeIfNotBlank()
|
|
|
|
if (contactName != null && contactName != contact.email) {
|
|
|
|
localMessage.senderDisplayName = contact.name
|
|
|
|
}
|
2021-05-21 10:42:25 +00:00
|
|
|
localMessage
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (messages.isNullOrEmpty()) {
|
2021-05-19 12:56:12 +00:00
|
|
|
Timber.d("Failed fetching Message Details for message $messageOrConversationId")
|
2021-05-21 10:42:25 +00:00
|
|
|
_messageDetailsError.postValue(Event("Failed getting conversation's messages"))
|
2021-07-14 19:09:38 +00:00
|
|
|
return null
|
2021-05-17 13:00:26 +00:00
|
|
|
}
|
2021-05-21 10:42:25 +00:00
|
|
|
|
2021-07-14 19:09:38 +00:00
|
|
|
return conversation.toConversationUiModel().copy(
|
2021-05-27 07:46:28 +00:00
|
|
|
messages = messages.sortedBy { it.time }
|
2021-05-27 14:04:52 +00:00
|
|
|
)
|
2021-05-21 10:42:25 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 19:09:38 +00:00
|
|
|
private suspend fun emitConversationUiItem(conversationUiModel: ConversationUiModel) {
|
2021-09-15 13:09:37 +00:00
|
|
|
_decryptedConversationUiModel.postValue(conversationUiModel)
|
|
|
|
_conversationUiFlow.emit(conversationUiModel)
|
2021-05-17 13:00:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun Message.tryDecrypt(verificationKeys: List<KeyInformation>?): Boolean? {
|
|
|
|
return try {
|
|
|
|
decrypt(userManager, userManager.requireCurrentUserId(), verificationKeys)
|
2021-07-08 16:00:09 +00:00
|
|
|
Timber.d("decrypted verificationKeys size: ${verificationKeys?.size}, body size: ${messageBody?.length}")
|
2021-05-17 13:00:26 +00:00
|
|
|
true
|
|
|
|
} catch (exception: Exception) {
|
|
|
|
// signature verification failed with special case, try to decrypt again without verification
|
|
|
|
// and hardcode verification error
|
2022-03-16 12:13:47 +00:00
|
|
|
if (verificationKeys != null &&
|
|
|
|
verificationKeys.isNotEmpty() &&
|
|
|
|
exception.isSignatureError()
|
2021-05-17 13:00:26 +00:00
|
|
|
) {
|
2021-07-08 16:00:09 +00:00
|
|
|
Timber.i(exception, "Decrypting message again without verkeys")
|
2021-05-17 13:00:26 +00:00
|
|
|
decrypt(userManager, userManager.requireCurrentUserId())
|
|
|
|
this.hasValidSignature = false
|
2022-03-16 12:13:47 +00:00
|
|
|
this.hasInvalidSignature = !exception.isMessageNotSignedError()
|
2021-05-17 13:00:26 +00:00
|
|
|
true
|
|
|
|
} else {
|
2021-07-08 16:00:09 +00:00
|
|
|
Timber.w(exception, "Cannot decrypt message")
|
2021-05-17 13:00:26 +00:00
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 12:13:47 +00:00
|
|
|
private fun Exception.isSignatureError() =
|
|
|
|
message?.matches("Signature Verification Error: .+".toRegex()) == true
|
|
|
|
|
|
|
|
private fun Exception.isMessageNotSignedError() =
|
|
|
|
message?.equals("Signature Verification Error: Missing signature") == true
|
|
|
|
|
2021-09-02 08:12:02 +00:00
|
|
|
fun startDownloadEmbeddedImagesJob(message: Message, embeddedImageIds: List<String>) {
|
2020-04-16 15:44:53 +00:00
|
|
|
hasEmbeddedImages = false
|
|
|
|
|
2020-11-30 12:44:44 +00:00
|
|
|
viewModelScope.launch(dispatchers.Io) {
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-06-16 16:07:03 +00:00
|
|
|
val messageId = message.messageId ?: return@launch
|
|
|
|
val attachmentMetadataList = attachmentMetadataDao.getAllAttachmentsForMessage(messageId)
|
|
|
|
val embeddedImages = embeddedImagesAttachments.mapNotNull { embeddedImage ->
|
2021-09-02 08:12:02 +00:00
|
|
|
attachmentsHelper.fromAttachmentToEmbeddedImage(embeddedImage, embeddedImageIds)
|
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-05-19 12:56:12 +00:00
|
|
|
messageDetailsRepository.startDownloadEmbeddedImages(
|
2021-06-16 16:07:03 +00:00
|
|
|
messageId, userManager.requireCurrentUserId()
|
2021-05-19 12:56:12 +00:00
|
|
|
)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onEmbeddedImagesDownloaded(event: DownloadEmbeddedImagesEvent) {
|
2021-09-29 16:27:38 +00:00
|
|
|
viewModelScope.launch {
|
|
|
|
Timber.v("onEmbeddedImagesDownloaded status: ${event.status} images size: ${event.images.size}")
|
|
|
|
val messageId = event.images.first().messageId
|
2021-10-01 15:27:18 +00:00
|
|
|
val renderedMessage = try {
|
|
|
|
messageRenderer.setImagesAndProcess(messageId, event.images)
|
|
|
|
} catch (e: IllegalStateException) {
|
|
|
|
if (e is CancellationException) throw e
|
|
|
|
Timber.e(e)
|
|
|
|
return@launch
|
|
|
|
}
|
2021-09-29 16:27:38 +00:00
|
|
|
|
|
|
|
val updatedMessage = updateUiModelMessageWithFormattedHtml(
|
|
|
|
renderedMessage.messageId,
|
|
|
|
renderedMessage.renderedHtmlBody
|
2021-10-01 15:27:18 +00:00
|
|
|
) ?: run {
|
|
|
|
Timber.e("Cannot update message with formatted html. Message id: $messageId")
|
|
|
|
return@launch
|
|
|
|
}
|
|
|
|
|
|
|
|
Timber.v("Update rendered HTML message id: ${updatedMessage.messageId}")
|
2021-09-29 16:27:38 +00:00
|
|
|
_messageRenderedWithImages.value = updatedMessage
|
|
|
|
areImagesDisplayed = true
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2021-05-21 12:18:05 +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,
|
2021-05-28 14:43:02 +00:00
|
|
|
embeddedImagesAttachments,
|
2021-05-05 16:17:24 +00:00
|
|
|
dispatchers.Io
|
2020-10-05 13:59:03 +00:00
|
|
|
)
|
2020-04-16 15:44:53 +00:00
|
|
|
_prepareEditMessageIntentResult.value = Event(intent)
|
|
|
|
}
|
|
|
|
}
|
2021-05-20 07:35:04 +00:00
|
|
|
|
2021-06-15 14:00:11 +00:00
|
|
|
fun viewOrDownloadAttachment(context: Context, attachment: Attachment) {
|
2020-11-30 12:44:44 +00:00
|
|
|
viewModelScope.launch(dispatchers.Io) {
|
2021-06-15 14:00:11 +00:00
|
|
|
val attachmentId = requireNotNull(attachment.attachmentId)
|
|
|
|
val messageId = attachment.messageId
|
|
|
|
val metadata = attachmentMetadataDao.getAttachmentMetadataForMessageAndAttachmentId(messageId, attachmentId)
|
|
|
|
Timber.v("viewOrDownloadAttachment Id: $attachmentId metadataId: ${metadata?.id}")
|
2021-05-14 07:05:20 +00:00
|
|
|
val uri = metadata?.uri
|
|
|
|
// extra check if user has not deleted the file
|
2022-01-21 15:07:56 +00:00
|
|
|
if (uri != null && attachmentsHelper.isFileAvailable(uri)) {
|
|
|
|
val path = checkNotNull(uri.path)
|
|
|
|
if (DIR_EMB_ATTACHMENT_DOWNLOADS in path) {
|
|
|
|
copyAttachmentToDownloadsAndDisplay(context, attachmentId, metadata.name, uri)
|
2021-03-18 11:52:04 +00:00
|
|
|
} else {
|
2022-01-21 15:07:56 +00:00
|
|
|
viewAttachment(attachmentId, metadata.name, uri)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-01-21 15:07:56 +00:00
|
|
|
|
2021-06-15 14:00:11 +00:00
|
|
|
Timber.d("Attachment id: $attachmentId file not available, uri: $uri ")
|
|
|
|
attachmentsWorker.enqueue(messageId, userManager.requireCurrentUserId(), attachmentId)
|
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,
|
2022-01-21 15:07:56 +00:00
|
|
|
attachmentId: String,
|
2021-02-08 10:16:40 +00:00
|
|
|
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}")
|
2022-01-21 15:07:56 +00:00
|
|
|
viewAttachment(attachmentId, filename, newUri)
|
2021-02-08 14:39:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-21 15:07:56 +00:00
|
|
|
fun viewAttachment(attachmentId: String, filename: String?, uri: Uri?) {
|
|
|
|
uri ?: return
|
|
|
|
|
|
|
|
val shouldBeOpenedInProtonCalendar = protonCalendarUtil.isProtonCalendarInstalled() &&
|
|
|
|
isCalendarAttachment(attachmentId)
|
|
|
|
if (shouldBeOpenedInProtonCalendar) {
|
|
|
|
viewAttachmentInProtonCalendar(attachmentId, uri, filename)
|
|
|
|
} else {
|
|
|
|
downloadUtils.viewAttachment(filename, uri)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun isCalendarAttachment(attachmentId: String): Boolean =
|
|
|
|
(attachmentId == calendarAttachmentId).also {
|
|
|
|
calendarAttachmentId = null
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun viewAttachmentInProtonCalendar(attachmentId: String, uri: Uri, filename: String?) {
|
|
|
|
viewModelScope.launch(dispatchers.Io) {
|
|
|
|
val conversation = conversationUiModel.first()
|
|
|
|
val message = conversation.messages
|
|
|
|
.find { message -> attachmentId in message.attachments.map { it.attachmentId } }
|
|
|
|
?: return@launch
|
|
|
|
val senderEmail = EmailAddress(message.senderEmail)
|
2021-08-30 15:22:27 +00:00
|
|
|
val header = message.header
|
|
|
|
val recipientEmail = if (header != null) {
|
|
|
|
protonCalendarUtil.extractRecipientEmailOrNull(header)
|
|
|
|
?: EmailAddress(message.toList.first().emailAddress)
|
|
|
|
} else {
|
|
|
|
val messageToEmails = message.toList.map { EmailAddress(it.emailAddress) }
|
|
|
|
val currentUserAddresses = userManager.requireCurrentUser().addresses.addresses.values
|
|
|
|
currentUserAddresses.first { it.email in messageToEmails }.email
|
|
|
|
}
|
2022-01-21 15:07:56 +00:00
|
|
|
|
|
|
|
val mimeType = downloadUtils.getMimeType(uri, filename)
|
|
|
|
Timber.d("viewAttachment mimeType: $mimeType uri: $uri uriScheme: ${uri.scheme}")
|
|
|
|
|
|
|
|
try {
|
|
|
|
protonCalendarUtil.openIcsInProtonCalendar(uri, senderEmail, recipientEmail)
|
|
|
|
} catch (notFoundException: ActivityNotFoundException) {
|
|
|
|
Timber.i(notFoundException, "Unable to view attachment with ProtonCalendar")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun openInProtonCalendar(context: Context, message: Message) {
|
|
|
|
if (protonCalendarUtil.isProtonCalendarInstalled()) {
|
|
|
|
val attachment = protonCalendarUtil.requireCalendarAttachment(message)
|
|
|
|
calendarAttachmentId = requireNotNull(attachment.attachmentId)
|
|
|
|
viewOrDownloadAttachment(context, attachment)
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
protonCalendarUtil.openProtonCalendarOnPlayStore()
|
|
|
|
} catch (e: ActivityNotFoundException) {
|
|
|
|
Timber.e(e)
|
|
|
|
_messageDetailsError.value = Event(context.getString(R.string.details_play_store_error))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-08 08:58:18 +00:00
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
fun remoteContentDisplayed() {
|
|
|
|
remoteContentDisplayed = true
|
|
|
|
}
|
|
|
|
|
2021-05-31 10:05:12 +00:00
|
|
|
fun displayRemoteContent(message: Message) {
|
2020-04-16 15:44:53 +00:00
|
|
|
remoteContentDisplayed()
|
2021-05-31 10:05:12 +00:00
|
|
|
prepareEmbeddedImages(message)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2021-02-09 13:18:01 +00:00
|
|
|
fun isEmbeddedImagesDisplayed() = areImagesDisplayed
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-06-16 16:07:03 +00:00
|
|
|
fun displayEmbeddedImages(message: Message) {
|
2021-02-09 13:18:01 +00:00
|
|
|
areImagesDisplayed = true // this will be passed to edit intent
|
2021-09-02 08:12:02 +00:00
|
|
|
startDownloadEmbeddedImagesJob(message, message.embeddedImageIds)
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
|
|
|
|
2021-05-26 16:21:47 +00:00
|
|
|
fun isAutoShowEmbeddedImages(): Boolean {
|
|
|
|
val mailSettings = userManager.getCurrentUserMailSettingsBlocking()
|
|
|
|
return mailSettings?.showImagesFrom?.includesEmbedded() ?: false
|
|
|
|
}
|
|
|
|
|
2021-06-04 16:38:41 +00:00
|
|
|
fun prepareEmbeddedImages(message: Message): Boolean {
|
|
|
|
val attachments = message.attachments
|
|
|
|
val embeddedImagesToFetch = ArrayList<EmbeddedImage>()
|
|
|
|
val embeddedImagesAttachments = ArrayList<Attachment>()
|
|
|
|
for (attachment in attachments) {
|
|
|
|
val embeddedImage = attachmentsHelper
|
|
|
|
.fromAttachmentToEmbeddedImage(attachment, message.embeddedImageIds) ?: continue
|
|
|
|
embeddedImagesToFetch.add(embeddedImage)
|
|
|
|
embeddedImagesAttachments.add(attachment)
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-05-28 14:43:02 +00:00
|
|
|
this.embeddedImagesToFetch = embeddedImagesToFetch
|
|
|
|
this.embeddedImagesAttachments = embeddedImagesAttachments
|
2020-04-16 15:44:53 +00:00
|
|
|
|
2021-05-28 14:43:02 +00:00
|
|
|
if (embeddedImagesToFetch.isNotEmpty()) {
|
|
|
|
hasEmbeddedImages = true
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|
2021-05-28 14:43:02 +00:00
|
|
|
|
2020-04-16 15:44:53 +00:00
|
|
|
return hasEmbeddedImages
|
|
|
|
}
|
|
|
|
|
2021-07-14 08:05:02 +00:00
|
|
|
fun printMessage(messageId: String, activityContext: Context) {
|
|
|
|
_decryptedConversationUiModel.value?.messages?.find { it.messageId == messageId }?.let {
|
|
|
|
MessagePrinter(
|
|
|
|
activityContext,
|
|
|
|
activityContext.resources,
|
|
|
|
activityContext.getSystemService(Context.PRINT_SERVICE) as PrintManager,
|
|
|
|
remoteContentDisplayed
|
|
|
|
).printMessage(it, it.decryptedHTML ?: "")
|
2020-10-29 06:51:34 +00:00
|
|
|
}
|
|
|
|
}
|
2020-12-30 14:20:18 +00:00
|
|
|
|
2021-07-13 08:50:00 +00:00
|
|
|
fun formatMessageHtmlBody(
|
|
|
|
message: Message,
|
2020-12-30 14:20:18 +00:00
|
|
|
windowWidth: Int,
|
|
|
|
css: String,
|
2021-05-20 08:05:50 +00:00
|
|
|
darkModeCss: String,
|
2020-12-30 14:20:18 +00:00
|
|
|
defaultErrorMessage: String
|
2021-07-14 08:05:02 +00:00
|
|
|
): String {
|
2021-09-24 08:29:35 +00:00
|
|
|
val messageId = requireNotNull(message.messageId) { "message id is null" }
|
2021-07-14 08:05:02 +00:00
|
|
|
val formattedHtml = try {
|
2020-12-30 14:20:18 +00:00
|
|
|
val contentTransformer = DefaultTransformer()
|
2021-05-20 08:05:50 +00:00
|
|
|
.pipe(ViewportTransformer(windowWidth, css, darkModeCss))
|
2020-12-30 14:20:18 +00:00
|
|
|
|
2021-07-13 08:50:00 +00:00
|
|
|
contentTransformer.transform(Jsoup.parse(message.decryptedHTML)).toString()
|
2020-12-30 14:20:18 +00:00
|
|
|
} catch (ioException: IOException) {
|
|
|
|
Timber.e(ioException, "Jsoup is unable to parse HTML message details")
|
|
|
|
defaultErrorMessage
|
|
|
|
}
|
|
|
|
|
2021-07-14 15:15:05 +00:00
|
|
|
updateUiModelMessageWithFormattedHtml(message.messageId, formattedHtml, message.decryptedBody)
|
2021-07-14 08:05:02 +00:00
|
|
|
// Set the body of the message currently being displayed in messageRenderer to allow embedded images loading
|
2021-09-24 08:29:35 +00:00
|
|
|
messageRenderer.setMessageBody(messageId, formattedHtml)
|
2021-07-14 08:05:02 +00:00
|
|
|
return formattedHtml
|
2020-12-30 14:20:18 +00:00
|
|
|
}
|
2021-04-16 07:42:29 +00:00
|
|
|
|
2021-07-14 15:15:05 +00:00
|
|
|
private fun updateUiModelMessageWithFormattedHtml(
|
|
|
|
messageId: String?,
|
|
|
|
formattedHtml: String?,
|
|
|
|
decryptedBody: String? = null
|
|
|
|
): Message? {
|
2021-07-14 11:14:01 +00:00
|
|
|
// Needed to ensure the `decryptedHTML` is available on a message when an action is executed on it,
|
|
|
|
// since most of the click listeners in `MessageDetailsActivity` that trigger actions (such as
|
|
|
|
// reply or printMessage) hold a reference to the message in the `conversationUiModel object.
|
|
|
|
// This is considered tech debt and detailed in MAILAND-2119
|
2021-07-13 08:50:00 +00:00
|
|
|
val currentUiModel = _decryptedConversationUiModel.value
|
|
|
|
val message = currentUiModel?.messages?.find { it.messageId == messageId }
|
2021-07-14 15:15:05 +00:00
|
|
|
decryptedBody?.let {
|
|
|
|
message?.decryptedBody = it
|
|
|
|
}
|
2021-07-13 08:50:00 +00:00
|
|
|
message?.decryptedHTML = formattedHtml
|
|
|
|
return message
|
|
|
|
}
|
|
|
|
|
2021-10-19 15:28:30 +00:00
|
|
|
fun moveToTrash() {
|
2021-07-13 19:57:42 +00:00
|
|
|
viewModelScope.launch {
|
2021-09-03 15:33:41 +00:00
|
|
|
val primaryUserId = userManager.requireCurrentUserId()
|
2021-09-23 16:04:29 +00:00
|
|
|
if (isConversationEnabled()) {
|
2021-07-16 12:48:43 +00:00
|
|
|
moveConversationsToFolder(
|
2021-07-13 19:57:42 +00:00
|
|
|
listOf(messageOrConversationId),
|
2021-07-16 12:48:43 +00:00
|
|
|
primaryUserId,
|
|
|
|
Constants.MessageLocationType.TRASH.messageLocationTypeValue.toString()
|
2021-07-13 19:57:42 +00:00
|
|
|
)
|
2021-07-16 12:48:43 +00:00
|
|
|
} else {
|
2021-09-23 16:04:29 +00:00
|
|
|
moveMessagesToFolder(
|
|
|
|
listOf(messageOrConversationId),
|
|
|
|
Constants.MessageLocationType.TRASH.messageLocationTypeValue.toString(),
|
|
|
|
location.messageLocationTypeValue.toString(),
|
|
|
|
primaryUserId
|
|
|
|
)
|
2021-07-13 19:57:42 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-16 10:51:16 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 16:07:10 +00:00
|
|
|
fun moveDraftToTrash(messageId: String) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
moveMessagesToFolder(
|
|
|
|
listOf(messageId),
|
|
|
|
Constants.MessageLocationType.TRASH.asLabelIdString(),
|
|
|
|
Constants.MessageLocationType.DRAFT.asLabelIdString(),
|
|
|
|
userManager.requireCurrentUserId()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-18 19:21:04 +00:00
|
|
|
fun delete() {
|
|
|
|
viewModelScope.launch {
|
2021-10-21 16:45:42 +00:00
|
|
|
val primaryUserId = userManager.requireCurrentUserId()
|
2021-09-23 16:04:29 +00:00
|
|
|
if (isConversationEnabled()) {
|
2021-08-18 19:21:04 +00:00
|
|
|
deleteConversations(
|
|
|
|
listOf(messageOrConversationId),
|
|
|
|
primaryUserId,
|
|
|
|
location.messageLocationTypeValue.toString()
|
|
|
|
)
|
|
|
|
} else {
|
2021-09-23 16:04:29 +00:00
|
|
|
deleteMessage(
|
|
|
|
listOf(messageOrConversationId),
|
2021-10-21 16:45:42 +00:00
|
|
|
location.messageLocationTypeValue.toString(),
|
|
|
|
primaryUserId
|
2021-09-23 16:04:29 +00:00
|
|
|
)
|
2021-08-18 19:21:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-19 13:30:20 +00:00
|
|
|
fun handleStarUnStar(messageOrConversationId: String, isChecked: Boolean) {
|
|
|
|
val ids = listOf(messageOrConversationId)
|
2021-10-25 11:23:39 +00:00
|
|
|
val primaryUserId = userManager.requireCurrentUserId()
|
2021-07-19 13:30:20 +00:00
|
|
|
|
2021-10-25 11:23:39 +00:00
|
|
|
viewModelScope.launch {
|
|
|
|
if (isConversationEnabled()) {
|
2021-07-19 13:30:20 +00:00
|
|
|
val starAction = if (isChecked) {
|
|
|
|
ChangeConversationsStarredStatus.Action.ACTION_STAR
|
|
|
|
} else {
|
|
|
|
ChangeConversationsStarredStatus.Action.ACTION_UNSTAR
|
|
|
|
}
|
|
|
|
changeConversationsStarredStatus(
|
|
|
|
ids,
|
|
|
|
primaryUserId,
|
|
|
|
starAction
|
|
|
|
)
|
|
|
|
} else {
|
2021-10-25 11:23:39 +00:00
|
|
|
if (isChecked) {
|
|
|
|
changeMessagesStarredStatus(
|
2021-10-27 14:41:34 +00:00
|
|
|
primaryUserId,
|
2021-10-25 11:23:39 +00:00
|
|
|
ids,
|
2021-10-27 14:41:34 +00:00
|
|
|
ChangeMessagesStarredStatus.Action.ACTION_STAR
|
2021-10-25 11:23:39 +00:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
changeMessagesStarredStatus(
|
2021-10-27 14:41:34 +00:00
|
|
|
primaryUserId,
|
2021-10-25 11:23:39 +00:00
|
|
|
ids,
|
2021-10-27 14:41:34 +00:00
|
|
|
ChangeMessagesStarredStatus.Action.ACTION_UNSTAR
|
2021-10-25 11:23:39 +00:00
|
|
|
)
|
|
|
|
}
|
2021-07-19 13:30:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-15 10:15:18 +00:00
|
|
|
fun sendPhishingReport(message: Message, jobManager: JobManager) {
|
|
|
|
jobManager.addJobInBackground(
|
|
|
|
ReportPhishingJob(
|
|
|
|
requireNotNull(message.messageId),
|
|
|
|
requireNotNull(message.decryptedBody),
|
|
|
|
requireNotNull(message.mimeType)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-08-12 08:47:24 +00:00
|
|
|
fun storagePermissionDenied() {
|
|
|
|
_showPermissionMissingDialog.value = Unit
|
|
|
|
}
|
2021-08-18 19:21:04 +00:00
|
|
|
|
|
|
|
fun shouldShowDeleteActionInBottomActionBar(): Boolean {
|
|
|
|
return if (isConversationEnabled() && doesConversationHaveMoreThanOneMessage()) {
|
|
|
|
location == Constants.MessageLocationType.TRASH
|
|
|
|
} else {
|
|
|
|
location in arrayOf(
|
|
|
|
Constants.MessageLocationType.DRAFT,
|
2022-03-09 14:03:59 +00:00
|
|
|
Constants.MessageLocationType.ALL_DRAFT,
|
2021-08-18 19:21:04 +00:00
|
|
|
Constants.MessageLocationType.SENT,
|
2022-03-09 14:03:59 +00:00
|
|
|
Constants.MessageLocationType.ALL_SENT,
|
|
|
|
Constants.MessageLocationType.TRASH,
|
2021-08-18 19:21:04 +00:00
|
|
|
Constants.MessageLocationType.SPAM
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2022-01-25 18:34:19 +00:00
|
|
|
|
|
|
|
fun isAppInDarkMode(context: Context) = isAppInDarkMode.invoke(context)
|
2022-02-04 16:39:21 +00:00
|
|
|
|
|
|
|
fun isWebViewInDarkModeBlocking(context: Context, messageId: String) = runBlocking {
|
|
|
|
getViewInDarkModeMessagePreference(context, userManager.requireCurrentUserId(), messageId)
|
|
|
|
}
|
2022-02-07 20:07:26 +00:00
|
|
|
|
|
|
|
fun reloadMessage(messageId: String) {
|
|
|
|
_reloadMessageFlow.tryEmit(messageId)
|
|
|
|
}
|
2020-04-16 15:44:53 +00:00
|
|
|
}
|