diff --git a/.idea/runConfigurations/Run_all_UI_tests.xml b/.idea/runConfigurations/Run_all_UI_tests.xml
new file mode 100644
index 0000000000..63063cb9ea
--- /dev/null
+++ b/.idea/runConfigurations/Run_all_UI_tests.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
diff --git a/.idea/runConfigurations/Run_depend_on_email_server_UI_tests.xml b/.idea/runConfigurations/Run_depend_on_email_server_UI_tests.xml
new file mode 100644
index 0000000000..6e93181a43
--- /dev/null
+++ b/.idea/runConfigurations/Run_depend_on_email_server_UI_tests.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
diff --git a/.idea/runConfigurations/Run_depend_on_email_server_tests.xml b/.idea/runConfigurations/Run_depend_on_email_server_tests.xml
deleted file mode 100644
index c68e81b26c..0000000000
--- a/.idea/runConfigurations/Run_depend_on_email_server_tests.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run_no_email_server_UI_tests.xml b/.idea/runConfigurations/Run_no_email_server_UI_tests.xml
new file mode 100644
index 0000000000..a387983ded
--- /dev/null
+++ b/.idea/runConfigurations/Run_no_email_server_UI_tests.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
diff --git a/.idea/runConfigurations/Run_no_email_server_tests.xml b/.idea/runConfigurations/Run_no_email_server_tests.xml
deleted file mode 100644
index 9485734102..0000000000
--- a/.idea/runConfigurations/Run_no_email_server_tests.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityEkmTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityEkmTest.kt
index 68fdb70ea0..70ad960f32 100644
--- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityEkmTest.kt
+++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityEkmTest.kt
@@ -14,6 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.flowcrypt.email.R
import com.flowcrypt.email.TestConstants
+import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.api.retrofit.response.model.OrgRules
import com.flowcrypt.email.database.entity.KeyEntity
import com.flowcrypt.email.model.KeyImportDetails
@@ -24,6 +25,7 @@ import com.flowcrypt.email.rules.RetryRule
import com.flowcrypt.email.rules.ScreenshotTestRule
import com.flowcrypt.email.ui.activity.base.BaseMessageDetailsActivityTest
import com.flowcrypt.email.util.AccountDaoManager
+import com.flowcrypt.email.util.TestGeneralUtil
import org.hamcrest.Matchers.not
import org.junit.Rule
import org.junit.Test
@@ -40,11 +42,16 @@ import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
class MessageDetailsActivityEkmTest : BaseMessageDetailsActivityTest() {
+ private val simpleAttInfo = TestGeneralUtil.getObjectFromJson(
+ "messages/attachments/simple_att.json",
+ AttachmentInfo::class.java
+ )
private val userWithOrgRules = AccountDaoManager.getUserWithOrgRules(
OrgRules(
flags = listOf(
OrgRules.DomainRule.NO_PRV_CREATE,
- OrgRules.DomainRule.NO_PRV_BACKUP
+ OrgRules.DomainRule.NO_PRV_BACKUP,
+ OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING
),
customKeyserverUrl = null,
keyManagerUrl = "https://keymanagerurl.test",
@@ -86,4 +93,16 @@ class MessageDetailsActivityEkmTest : BaseMessageDetailsActivityTest() {
onView(withId(R.id.buttonSendOwnPublicKey))
.check(matches(withText(R.string.inform_sender)))
}
+
+ @Test
+ fun testVisibilityOfPreviewAttachmentButton() {
+ baseCheckWithAtt(
+ getMsgInfo(
+ "messages/info/standard_msg_info_plaintext_with_one_att.json",
+ "messages/mime/standard_msg_info_plaintext_with_one_att.txt", simpleAttInfo
+ ), simpleAttInfo
+ )
+ onView(withId(R.id.imageButtonPreviewAtt))
+ .check(matches(isDisplayed()))
+ }
}
diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt
index 920b96d0d6..3a9b50a99c 100644
--- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt
+++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt
@@ -9,7 +9,6 @@ import android.app.Activity
import android.app.Instrumentation
import android.content.ComponentName
import android.text.format.DateUtils
-import android.text.format.Formatter
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
@@ -33,16 +32,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.flowcrypt.email.R
import com.flowcrypt.email.TestConstants
-import com.flowcrypt.email.api.email.EmailUtil
import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.api.email.model.IncomingMessageInfo
import com.flowcrypt.email.api.retrofit.response.model.DecryptErrorMsgBlock
import com.flowcrypt.email.api.retrofit.response.model.GenericMsgBlock
import com.flowcrypt.email.api.retrofit.response.model.PublicKeyMsgBlock
-import com.flowcrypt.email.database.entity.MessageEntity
import com.flowcrypt.email.junit.annotations.NotReadyForCI
import com.flowcrypt.email.matchers.CustomMatchers
-import com.flowcrypt.email.matchers.CustomMatchers.Companion.withDrawable
import com.flowcrypt.email.matchers.CustomMatchers.Companion.withEmptyRecyclerView
import com.flowcrypt.email.matchers.CustomMatchers.Companion.withPgpBadge
import com.flowcrypt.email.matchers.CustomMatchers.Companion.withRecyclerViewItemCount
@@ -54,14 +50,12 @@ import com.flowcrypt.email.rules.ScreenshotTestRule
import com.flowcrypt.email.ui.activity.base.BaseMessageDetailsActivityTest
import com.flowcrypt.email.ui.adapter.MsgDetailsRecyclerViewAdapter
import com.flowcrypt.email.ui.adapter.PgpBadgeListAdapter
-import com.flowcrypt.email.util.DateTimeUtil
import com.flowcrypt.email.util.GeneralUtil
import com.flowcrypt.email.util.PrivateKeysManager
import com.flowcrypt.email.util.TestGeneralUtil
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
-import org.hamcrest.Matchers.anyOf
import org.hamcrest.Matchers.anything
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.not
@@ -890,91 +884,6 @@ class MessageDetailsActivityTest : BaseMessageDetailsActivityTest() {
matchReplyButtons(details)
}
- private fun baseCheckWithAtt(incomingMsgInfo: IncomingMessageInfo?, att: AttachmentInfo?) {
- assertThat(incomingMsgInfo, notNullValue())
-
- val msgEntity = incomingMsgInfo!!.msgEntity
- launchActivity(msgEntity)
- matchHeader(incomingMsgInfo)
-
- checkWebViewText(incomingMsgInfo.text)
- onView(withId(R.id.layoutAtt))
- .check(matches(isDisplayed()))
- matchAtt(att)
- matchReplyButtons(msgEntity)
- }
-
- private fun matchHeader(incomingMsgInfo: IncomingMessageInfo?) {
- val msgEntity = incomingMsgInfo?.msgEntity
- requireNotNull(msgEntity)
-
- onView(withId(R.id.textViewSenderAddress))
- .check(matches(withText(EmailUtil.getFirstAddressString(msgEntity.from))))
- onView(withId(R.id.textViewDate))
- .check(
- matches(
- withText(
- DateTimeUtil.formatSameDayTime(
- getTargetContext(),
- msgEntity.receivedDate
- )
- )
- )
- )
- onView(withId(R.id.textViewSubject))
- .check(matches(anyOf(withText(msgEntity.subject), withText(incomingMsgInfo.inlineSubject))))
- }
-
- private fun matchAtt(att: AttachmentInfo?) {
- requireNotNull(att)
- onView(withId(R.id.textViewAttachmentName))
- .check(matches(withText(att.name)))
- onView(withId(R.id.textViewAttSize))
- .check(matches(withText(Formatter.formatFileSize(getContext(), att.encodedSize))))
- }
-
- private fun matchReplyButtons(msgEntity: MessageEntity) {
- onView(withId(R.id.imageButtonReplyAll))
- .check(matches(isDisplayed()))
- onView(withId(R.id.layoutReplyButton))
- .perform(scrollTo())
- .check(matches(isDisplayed()))
- onView(withId(R.id.layoutReplyAllButton))
- .check(matches(isDisplayed()))
- onView(withId(R.id.layoutFwdButton))
- .check(matches(isDisplayed()))
-
- if (msgEntity.isEncrypted == true) {
- onView(withId(R.id.textViewReply))
- .check(matches(withText(getResString(R.string.reply_encrypted))))
- onView(withId(R.id.textViewReplyAll))
- .check(matches(withText(getResString(R.string.reply_all_encrypted))))
- onView(withId(R.id.textViewFwd))
- .check(matches(withText(getResString(R.string.forward_encrypted))))
-
- onView(withId(R.id.imageViewReply))
- .check(matches(withDrawable(R.mipmap.ic_reply_green)))
- onView(withId(R.id.imageViewReplyAll))
- .check(matches(withDrawable(R.mipmap.ic_reply_all_green)))
- onView(withId(R.id.imageViewFwd))
- .check(matches(withDrawable(R.mipmap.ic_forward_green)))
- } else {
- onView(withId(R.id.textViewReply))
- .check(matches(withText(getResString(R.string.reply))))
- onView(withId(R.id.textViewReplyAll))
- .check(matches(withText(getResString(R.string.reply_all))))
- onView(withId(R.id.textViewFwd))
- .check(matches(withText(getResString(R.string.forward))))
-
- onView(withId(R.id.imageViewReply))
- .check(matches(withDrawable(R.mipmap.ic_reply_red)))
- onView(withId(R.id.imageViewReplyAll))
- .check(matches(withDrawable(R.mipmap.ic_reply_all_red)))
- onView(withId(R.id.imageViewFwd))
- .check(matches(withDrawable(R.mipmap.ic_forward_red)))
- }
- }
-
private fun testTopReplyAction(title: String) {
testStandardMsgPlaintext()
diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/base/BaseMessageDetailsActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/base/BaseMessageDetailsActivityTest.kt
index 215474188c..c6b3cbaa26 100644
--- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/base/BaseMessageDetailsActivityTest.kt
+++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/base/BaseMessageDetailsActivityTest.kt
@@ -5,23 +5,36 @@
package com.flowcrypt.email.ui.activity.base
+import android.text.format.Formatter
import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.Locator
import com.flowcrypt.email.R
+import com.flowcrypt.email.api.email.EmailUtil
+import com.flowcrypt.email.api.email.model.AttachmentInfo
+import com.flowcrypt.email.api.email.model.IncomingMessageInfo
import com.flowcrypt.email.api.email.model.LocalFolder
import com.flowcrypt.email.base.BaseTest
import com.flowcrypt.email.database.entity.MessageEntity
+import com.flowcrypt.email.matchers.CustomMatchers.Companion.withDrawable
import com.flowcrypt.email.rules.AddAccountToDatabaseRule
import com.flowcrypt.email.rules.lazyActivityScenarioRule
import com.flowcrypt.email.ui.activity.MessageDetailsActivity
+import com.flowcrypt.email.util.DateTimeUtil
import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.Matchers
import org.junit.After
/**
@@ -71,6 +84,93 @@ abstract class BaseMessageDetailsActivityTest : BaseTest() {
}
}
+ protected fun matchReplyButtons(msgEntity: MessageEntity) {
+ onView(withId(R.id.imageButtonReplyAll))
+ .check(matches(isDisplayed()))
+ onView(withId(R.id.layoutReplyButton))
+ .perform(ViewActions.scrollTo())
+ .check(matches(isDisplayed()))
+ onView(withId(R.id.layoutReplyAllButton))
+ .check(matches(isDisplayed()))
+ onView(withId(R.id.layoutFwdButton))
+ .check(matches(isDisplayed()))
+
+ if (msgEntity.isEncrypted == true) {
+ onView(withId(R.id.textViewReply))
+ .check(matches(withText(getResString(R.string.reply_encrypted))))
+ onView(withId(R.id.textViewReplyAll))
+ .check(matches(withText(getResString(R.string.reply_all_encrypted))))
+ onView(withId(R.id.textViewFwd))
+ .check(matches(withText(getResString(R.string.forward_encrypted))))
+
+ onView(withId(R.id.imageViewReply))
+ .check(matches(withDrawable(R.mipmap.ic_reply_green)))
+ onView(withId(R.id.imageViewReplyAll))
+ .check(matches(withDrawable(R.mipmap.ic_reply_all_green)))
+ onView(withId(R.id.imageViewFwd))
+ .check(matches(withDrawable(R.mipmap.ic_forward_green)))
+ } else {
+ onView(withId(R.id.textViewReply))
+ .check(matches(withText(getResString(R.string.reply))))
+ onView(withId(R.id.textViewReplyAll))
+ .check(matches(withText(getResString(R.string.reply_all))))
+ onView(withId(R.id.textViewFwd))
+ .check(matches(withText(getResString(R.string.forward))))
+
+ onView(withId(R.id.imageViewReply))
+ .check(matches(withDrawable(R.mipmap.ic_reply_red)))
+ onView(withId(R.id.imageViewReplyAll))
+ .check(matches(withDrawable(R.mipmap.ic_reply_all_red)))
+ onView(withId(R.id.imageViewFwd))
+ .check(matches(withDrawable(R.mipmap.ic_forward_red)))
+ }
+ }
+
+ protected fun baseCheckWithAtt(incomingMsgInfo: IncomingMessageInfo?, att: AttachmentInfo?) {
+ ViewMatchers.assertThat(incomingMsgInfo, Matchers.notNullValue())
+
+ val msgEntity = incomingMsgInfo!!.msgEntity
+ launchActivity(msgEntity)
+ matchHeader(incomingMsgInfo)
+
+ checkWebViewText(incomingMsgInfo.text)
+ onView(withId(R.id.layoutAtt))
+ .check(matches(isDisplayed()))
+ matchAtt(att)
+ matchReplyButtons(msgEntity)
+ }
+
+ protected fun matchHeader(incomingMsgInfo: IncomingMessageInfo?) {
+ val msgEntity = incomingMsgInfo?.msgEntity
+ requireNotNull(msgEntity)
+
+ onView(withId(R.id.textViewSenderAddress))
+ .check(matches(withText(EmailUtil.getFirstAddressString(msgEntity.from))))
+ onView(withId(R.id.textViewDate))
+ .check(
+ matches(
+ withText(DateTimeUtil.formatSameDayTime(getTargetContext(), msgEntity.receivedDate))
+ )
+ )
+ onView(withId(R.id.textViewSubject))
+ .check(
+ matches(
+ Matchers.anyOf(
+ withText(msgEntity.subject),
+ withText(incomingMsgInfo.inlineSubject)
+ )
+ )
+ )
+ }
+
+ private fun matchAtt(att: AttachmentInfo?) {
+ requireNotNull(att)
+ onView(withId(R.id.textViewAttachmentName))
+ .check(matches(withText(att.name)))
+ onView(withId(R.id.textViewAttSize))
+ .check(matches(withText(Formatter.formatFileSize(getContext(), att.encodedSize))))
+ }
+
protected fun checkWebViewText(text: String?) {
onWebView(withId(R.id.emailWebView)).forceJavascriptEnabled()
onWebView(withId(R.id.emailWebView))
diff --git a/FlowCrypt/src/devTest/res/navigation/msg_details_graph.xml b/FlowCrypt/src/devTest/res/navigation/msg_details_graph.xml
new file mode 100644
index 0000000000..d990bbe05c
--- /dev/null
+++ b/FlowCrypt/src/devTest/res/navigation/msg_details_graph.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FlowCrypt/src/devTest/res/navigation/nav_graph.xml b/FlowCrypt/src/devTest/res/navigation/nav_graph.xml
index 44cd09fc36..807eea6de5 100644
--- a/FlowCrypt/src/devTest/res/navigation/nav_graph.xml
+++ b/FlowCrypt/src/devTest/res/navigation/nav_graph.xml
@@ -263,6 +263,9 @@
+
+
+
diff --git a/FlowCrypt/src/main/AndroidManifest.xml b/FlowCrypt/src/main/AndroidManifest.xml
index 7b3495a09d..8f9b9bd5c2 100644
--- a/FlowCrypt/src/main/AndroidManifest.xml
+++ b/FlowCrypt/src/main/AndroidManifest.xml
@@ -53,8 +53,8 @@
@@ -79,23 +79,26 @@
+ android:screenOrientation="portrait">
+
+
+
@@ -129,69 +132,69 @@
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt
index 856d07cd4f..71791daaf8 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt
@@ -238,7 +238,8 @@ data class OrgRules constructor(
USE_LEGACY_ATTESTER_SUBMIT,
DEFAULT_REMEMBER_PASS_PHRASE,
HIDE_ARMOR_META,
- FORBID_STORING_PASS_PHRASE;
+ FORBID_STORING_PASS_PHRASE,
+ RESTRICT_ANDROID_ATTACHMENT_HANDLING;
companion object CREATOR : Parcelable.Creator {
override fun createFromParcel(parcel: Parcel): DomainRule = values()[parcel.readInt()]
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt
new file mode 100644
index 0000000000..d8824185c2
--- /dev/null
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt
@@ -0,0 +1,232 @@
+/*
+ * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
+ * Contributors: DenBond7
+ */
+
+package com.flowcrypt.email.jetpack.viewmodel
+
+import android.app.Application
+import android.content.Context
+import androidx.lifecycle.viewModelScope
+import com.flowcrypt.email.R
+import com.flowcrypt.email.api.email.gmail.GmailApiHelper
+import com.flowcrypt.email.api.email.model.AttachmentInfo
+import com.flowcrypt.email.api.email.protocol.ImapProtocolUtil
+import com.flowcrypt.email.api.email.protocol.OpenStoreHelper
+import com.flowcrypt.email.api.retrofit.response.base.Result
+import com.flowcrypt.email.database.entity.AccountEntity
+import com.flowcrypt.email.extensions.kotlin.toHex
+import com.flowcrypt.email.security.KeysStorageImpl
+import com.flowcrypt.email.security.SecurityUtils
+import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify
+import com.flowcrypt.email.util.coroutines.runners.ControlledRunner
+import com.flowcrypt.email.util.exception.ExceptionUtil
+import com.flowcrypt.email.util.exception.ManualHandledException
+import com.sun.mail.imap.IMAPFolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.commons.io.IOUtils
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import javax.mail.Folder
+
+/**
+ * @author Denis Bondarenko
+ * Date: 2/21/22
+ * Time: 11:39 AM
+ * E-mail: DenBond7@gmail.com
+ */
+class DownloadAttachmentViewModel(val attachmentInfo: AttachmentInfo, application: Application) :
+ AccountViewModel(application) {
+ private val controlledRunnerForDownloading = ControlledRunner>()
+ private val downloadAttachmentMutableStateFlow: MutableStateFlow> =
+ MutableStateFlow(Result.loading())
+ val downloadAttachmentStateFlow: StateFlow> =
+ downloadAttachmentMutableStateFlow.asStateFlow()
+
+ fun download() {
+ viewModelScope.launch {
+ val context: Context = getApplication()
+ downloadAttachmentMutableStateFlow.value =
+ Result.loading(progressMsg = context.getString(R.string.downloading))
+ downloadAttachmentMutableStateFlow.value =
+ controlledRunnerForDownloading.cancelPreviousThenRun {
+ return@cancelPreviousThenRun downloadInternal(context)
+ }
+ }
+ }
+
+ private suspend fun downloadInternal(context: Context): Result =
+ withContext(Dispatchers.IO) {
+ try {
+ attachmentInfo.uri?.let { uri ->
+ val inputStream = context.contentResolver.openInputStream(uri)
+ if (inputStream != null) {
+ return@withContext Result.success(
+ decryptDataIfNeeded(context, inputStream)
+ )
+ }
+ }
+
+ val email =
+ attachmentInfo.email ?: return@withContext Result.success(byteArrayOf())
+ val account = getAccountEntityWithDecryptedInfoSuspend(
+ roomDatabase.accountDao().getAccount(email)
+ ) ?: return@withContext Result.success(byteArrayOf())
+
+ if (account.useAPI) {
+ when (account.accountType) {
+ AccountEntity.ACCOUNT_TYPE_GOOGLE -> {
+ return@withContext Result.success(downloadAttachmentViaGmailAPI(context, account))
+ }
+
+ else -> throw ManualHandledException("Unsupported provider")
+ }
+ } else {
+ return@withContext Result.success(downloadAttachmentViaIMAP(context, account, email))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ExceptionUtil.handleError(e)
+ return@withContext Result.exception(e)
+ }
+ }
+
+ private suspend fun downloadAttachmentViaGmailAPI(
+ context: Context,
+ account: AccountEntity
+ ): ByteArray = withContext(Dispatchers.IO) {
+ val msg = GmailApiHelper.loadMsgFullInfo(context, account, attachmentInfo.uid.toHex())
+ val attPart = GmailApiHelper.getAttPartByPath(msg.payload, neededPath = attachmentInfo.path)
+ ?: throw ManualHandledException(context.getString(R.string.attachment_not_found))
+
+ GmailApiHelper.getAttInputStream(
+ context = context,
+ accountEntity = account,
+ msgId = attachmentInfo.uid.toHex(),
+ attId = attPart.body.attachmentId
+ ).use { inputStream ->
+ return@withContext decryptDataIfNeeded(
+ context = context,
+ inputStream = downloadFile(inputStream).inputStream()
+ )
+ }
+ }
+
+ private suspend fun downloadAttachmentViaIMAP(
+ context: Context,
+ account: AccountEntity,
+ email: String
+ ): ByteArray = withContext(Dispatchers.IO) {
+ val session = OpenStoreHelper.getAttsSess(context, account)
+ OpenStoreHelper.openStore(context, account, session).use { store ->
+ val label = roomDatabase.labelDao()
+ .getLabel(email, account.accountType, requireNotNull(attachmentInfo.folder))
+ ?: if (roomDatabase.accountDao().getAccount(email) == null) {
+ return@withContext byteArrayOf()
+ } else throw ManualHandledException("Folder \"" + attachmentInfo.folder + "\" not found in the local cache")
+
+ store.getFolder(label.name).use { folder ->
+ val remoteFolder = (folder as IMAPFolder).apply { open(Folder.READ_ONLY) }
+ val msg = remoteFolder.getMessageByUID(attachmentInfo.uid)
+ ?: throw ManualHandledException(context.getString(R.string.no_message_with_this_attachment))
+
+ ImapProtocolUtil.getAttPartByPath(
+ part = msg,
+ neededPath = attachmentInfo.path
+ )?.inputStream?.let { inputStream ->
+ return@withContext decryptDataIfNeeded(
+ context = context,
+ inputStream = downloadFile(inputStream).inputStream()
+ )
+ } ?: throw ManualHandledException(context.getString(R.string.attachment_not_found))
+ }
+ }
+ }
+
+ private suspend fun decryptDataIfNeeded(context: Context, inputStream: InputStream): ByteArray =
+ withContext(Dispatchers.IO) {
+ if (!SecurityUtils.isPossiblyEncryptedData(attachmentInfo.name)) {
+ return@withContext inputStream.readBytes()
+ }
+
+ downloadAttachmentMutableStateFlow.value =
+ Result.loading(progressMsg = context.getString(R.string.decrypting))
+
+ inputStream.use {
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ val pgpSecretKeyRings = KeysStorageImpl.getInstance(context).getPGPSecretKeyRings()
+ val pgpSecretKeyRingCollection = PGPSecretKeyRingCollection(pgpSecretKeyRings)
+ val protector = KeysStorageImpl.getInstance(context).getSecretKeyRingProtector()
+
+ PgpDecryptAndOrVerify.decrypt(
+ srcInputStream = inputStream,
+ destOutputStream = byteArrayOutputStream,
+ secretKeys = pgpSecretKeyRingCollection,
+ protector = protector
+ )
+
+ return@withContext byteArrayOutputStream.toByteArray()
+ }
+ }
+
+ private suspend fun downloadFile(inputStream: InputStream): ByteArray =
+ withContext(Dispatchers.IO) {
+ val outputStream = ByteArrayOutputStream()
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ var count = 0.0
+ val size = attachmentInfo.encodedSize.toDouble()
+ var numberOfReadBytes: Int
+ var lastPercentage = 0
+ var currentPercentage = 0
+ var elapsedTime: Long
+ val startTime: Long = System.currentTimeMillis()
+ var lastUpdateTime = startTime
+ updateProgress(currentPercentage, 0)
+ while (true) {
+ numberOfReadBytes = inputStream.read(buffer)
+
+ if (IOUtils.EOF == numberOfReadBytes) {
+ break
+ }
+
+ if (isActive) {
+ outputStream.write(buffer, 0, numberOfReadBytes)
+ count += numberOfReadBytes.toDouble()
+ currentPercentage = (count / size * 100f).toInt()
+ val isUpdateNeeded =
+ System.currentTimeMillis() - lastUpdateTime >= MIN_UPDATE_PROGRESS_INTERVAL
+ if (currentPercentage - lastPercentage >= 1 && isUpdateNeeded) {
+ lastPercentage = currentPercentage
+ lastUpdateTime = System.currentTimeMillis()
+ elapsedTime = lastUpdateTime - startTime
+ val predictLoadingTime = (elapsedTime * size / count).toLong()
+ updateProgress(currentPercentage, predictLoadingTime - elapsedTime)
+ }
+ } else {
+ break
+ }
+ }
+
+ updateProgress(100, 0)
+ return@withContext outputStream.toByteArray()
+ }
+
+ private fun updateProgress(currentPercentage: Int, timeLeft: Long) {
+ downloadAttachmentMutableStateFlow.value = Result.loading(
+ progressMsg = timeLeft.toString(),
+ progress = currentPercentage.toDouble()
+ )
+ }
+
+ companion object {
+ private const val MIN_UPDATE_PROGRESS_INTERVAL = 500
+ private const val DEFAULT_BUFFER_SIZE = 1024 * 16
+ }
+}
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt
index e3ff70857b..2ebb651a98 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt
@@ -32,6 +32,7 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper
import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.api.email.protocol.ImapProtocolUtil
import com.flowcrypt.email.api.email.protocol.OpenStoreHelper
+import com.flowcrypt.email.api.retrofit.response.model.OrgRules
import com.flowcrypt.email.database.FlowCryptRoomDatabase
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.extensions.kotlin.toHex
@@ -54,7 +55,6 @@ import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.lang.ref.WeakReference
-import java.util.HashMap
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -164,7 +164,7 @@ class AttachmentDownloadManagerService : Service() {
}
private interface OnDownloadAttachmentListener {
- fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri)
+ fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri, canBeOpened: Boolean = true)
fun onCanceled(attInfo: AttachmentInfo)
@@ -190,7 +190,7 @@ class AttachmentDownloadManagerService : Service() {
val attDownloadManagerService = weakRef.get()
val notificationManager = attDownloadManagerService?.attsNotificationManager
- val (attInfo, exception, uri, progressInPercentage, timeLeft, isLast)
+ val (attInfo, exception, uri, progressInPercentage, timeLeft, isLast, canBeOpened)
= message.obj as DownloadAttachmentTaskResult
when (message.what) {
@@ -208,7 +208,12 @@ class AttachmentDownloadManagerService : Service() {
}
MESSAGE_ATTACHMENT_DOWNLOAD -> {
- notificationManager?.downloadCompleted(attDownloadManagerService, attInfo!!, uri!!)
+ notificationManager?.downloadCompleted(
+ context = attDownloadManagerService,
+ attInfo = attInfo!!,
+ uri = uri!!,
+ canBeOpened = canBeOpened
+ )
LogsUtil.d(TAG, attInfo?.getSafeName() + " is downloaded")
}
@@ -372,11 +377,16 @@ class AttachmentDownloadManagerService : Service() {
}
}
- override fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri) {
+ override fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri, canBeOpened: Boolean) {
attsInfoMap.remove(attInfo.id)
futureMap.remove(attInfo.uniqueStringId)
try {
- val result = DownloadAttachmentTaskResult(attInfo, uri = uri, isLast = isLast)
+ val result = DownloadAttachmentTaskResult(
+ attInfo = attInfo,
+ uri = uri,
+ isLast = isLast,
+ canBeOpened = canBeOpened
+ )
messenger.send(Message.obtain(null, ReplyHandler.MESSAGE_ATTACHMENT_DOWNLOAD, result))
} catch (remoteException: RemoteException) {
remoteException.printStackTrace()
@@ -432,6 +442,20 @@ class AttachmentDownloadManagerService : Service() {
val roomDatabase = FlowCryptRoomDatabase.getDatabase(context)
try {
+ val email = att.email
+ if (email == null) {
+ listener?.onCanceled(att)
+ return
+ }
+
+ val account = AccountViewModel.getAccountEntityWithDecryptedInfo(
+ roomDatabase.accountDao().getAccount(email)
+ )
+ if (account == null) {
+ listener?.onCanceled(att)
+ return
+ }
+
if (att.uri != null) {
val inputStream = context.contentResolver.openInputStream(att.uri!!)
if (inputStream != null) {
@@ -439,23 +463,17 @@ class AttachmentDownloadManagerService : Service() {
attTempFile = decryptFileIfNeeded(context, attTempFile)
if (!Thread.currentThread().isInterrupted) {
val uri = storeFileToSharedFolder(context, attTempFile)
- listener?.onAttDownloaded(att, uri)
+ listener?.onAttDownloaded(
+ attInfo = att,
+ uri = uri,
+ canBeOpened = !account.isRuleExist(OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING)
+ )
}
return
}
}
- val email = att.email ?: return
- val account = AccountViewModel.getAccountEntityWithDecryptedInfo(
- roomDatabase.accountDao().getAccount(email)
- )
-
- if (account == null) {
- listener?.onCanceled(this.att)
- return
- }
-
if (account.useAPI) {
when (account.accountType) {
AccountEntity.ACCOUNT_TYPE_GOOGLE -> {
@@ -469,7 +487,10 @@ class AttachmentDownloadManagerService : Service() {
att.uid.toHex(),
attPart.body.attachmentId
).use { inputStream ->
- handleAttachmentInputStream(inputStream)
+ handleAttachmentInputStream(
+ inputStream = inputStream,
+ canBeOpened = !account.isRuleExist(OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING)
+ )
}
}
@@ -493,7 +514,10 @@ class AttachmentDownloadManagerService : Service() {
ImapProtocolUtil.getAttPartByPath(
msg, neededPath = this.att.path
)?.inputStream?.let { inputStream ->
- handleAttachmentInputStream(inputStream)
+ handleAttachmentInputStream(
+ inputStream = inputStream,
+ canBeOpened = !account.isRuleExist(OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING)
+ )
} ?: throw ManualHandledException(context.getString(R.string.attachment_not_found))
}
}
@@ -507,7 +531,7 @@ class AttachmentDownloadManagerService : Service() {
}
}
- private fun handleAttachmentInputStream(inputStream: InputStream) {
+ private fun handleAttachmentInputStream(inputStream: InputStream, canBeOpened: Boolean = true) {
downloadFile(attTempFile, inputStream)
if (Thread.currentThread().isInterrupted) {
@@ -518,7 +542,7 @@ class AttachmentDownloadManagerService : Service() {
listener?.onCanceled(this.att)
} else {
val uri = storeFileToSharedFolder(context, attTempFile)
- listener?.onAttDownloaded(this.att, uri)
+ listener?.onAttDownloaded(this.att, uri, canBeOpened)
}
}
}
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentNotificationManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentNotificationManager.kt
index 4be2863203..35dfa2aa24 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentNotificationManager.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentNotificationManager.kt
@@ -93,15 +93,20 @@ class AttachmentNotificationManager(context: Context) : CustomNotificationManage
* @param context Interface to global information about an application environment.
* @param attInfo [AttachmentInfo] object which contains a detail information about an attachment.
* @param uri The [Uri] of the downloaded attachment.
+ * @param canBeOpened A flag that indicates can we open an attachment after downloading.
*/
- fun downloadCompleted(context: Context, attInfo: AttachmentInfo, uri: Uri) {
- val intent = Intent(Intent.ACTION_VIEW, uri)
- intent.action = Intent.ACTION_VIEW
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
-
+ fun downloadCompleted(
+ context: Context,
+ attInfo: AttachmentInfo,
+ uri: Uri,
+ canBeOpened: Boolean = true
+ ) {
+ val intent = if (canBeOpened) {
+ GeneralUtil.genViewAttachmentIntent(uri, attInfo)
+ } else {
+ Intent()
+ }
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
-
val builder = genDefBuilder(context, attInfo)
builder.setProgress(0, 0, false)
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/DownloadAttachmentTaskResult.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/DownloadAttachmentTaskResult.kt
index 56b10035ff..c0e38e9547 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/DownloadAttachmentTaskResult.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/DownloadAttachmentTaskResult.kt
@@ -22,5 +22,6 @@ data class DownloadAttachmentTaskResult constructor(
val uri: Uri? = null,
val progressInPercentage: Int = 0,
val timeLeft: Long = 0,
- val isLast: Boolean = false
+ val isLast: Boolean = false,
+ val canBeOpened: Boolean = true
)
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/LauncherActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/LauncherActivity.kt
index 40e78fe376..80f033af30 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/LauncherActivity.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/LauncherActivity.kt
@@ -21,6 +21,8 @@ import com.flowcrypt.email.service.FeedbackJobIntentService
import com.flowcrypt.email.service.IdleService
import com.flowcrypt.email.service.actionqueue.actions.EncryptPrivateKeysIfNeededAction
import com.flowcrypt.email.ui.activity.base.BaseActivity
+import com.flowcrypt.email.util.CacheManager
+import com.flowcrypt.email.util.FileAndDirectoryUtils
import com.flowcrypt.email.util.SharedPreferencesHelper
/**
@@ -50,6 +52,7 @@ class LauncherActivity : BaseActivity() {
ForwardedAttachmentsDownloaderWorker.enqueue(applicationContext)
MessagesSenderWorker.enqueue(applicationContext)
FeedbackJobIntentService.enqueueWork(this)
+ FileAndDirectoryUtils.cleanDir(CacheManager.getCurrentMsgTempDir())
}
override fun onAccountInfoRefreshed(accountEntity: AccountEntity?) {
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MessageDetailsActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MessageDetailsActivity.kt
index 1e1f882c64..deb64e842b 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MessageDetailsActivity.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MessageDetailsActivity.kt
@@ -9,11 +9,11 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
+import androidx.navigation.fragment.NavHostFragment
import com.flowcrypt.email.R
import com.flowcrypt.email.api.email.model.LocalFolder
import com.flowcrypt.email.database.entity.MessageEntity
import com.flowcrypt.email.ui.activity.base.BaseBackStackSyncActivity
-import com.flowcrypt.email.ui.activity.fragment.MessageDetailsFragment
import com.flowcrypt.email.util.idling.SingleIdlingResources
/**
@@ -35,14 +35,8 @@ class MessageDetailsActivity : BaseBackStackSyncActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- if (savedInstanceState == null) {
- val fragment = MessageDetailsFragment().apply {
- arguments = intent.extras
- }
- supportFragmentManager.beginTransaction()
- .add(R.id.fragmentContainerView, fragment)
- .commit()
- }
+ (supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as? NavHostFragment)
+ ?.navController?.setGraph(R.navigation.msg_details_graph, intent.extras)
}
companion object {
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt
index f6654b7512..ff6d01681f 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt
@@ -1405,6 +1405,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener,
}
val imageButtonDownloadAtt = rootView.findViewById(R.id.imageButtonDownloadAtt)
+ rootView.findViewById(R.id.imageButtonPreviewAtt)?.visibility = View.GONE
if (!att.isProtected) {
imageButtonDownloadAtt.visibility = View.GONE
diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt
index 0201e65f00..35bb750db1 100644
--- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt
+++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt
@@ -8,8 +8,10 @@ package com.flowcrypt.email.ui.activity.fragment
import android.Manifest
import android.accounts.AuthenticatorException
import android.app.Activity
+import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.SpannableStringBuilder
@@ -24,10 +26,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
-import android.widget.ImageButton
-import android.widget.ImageView
import android.widget.ListView
-import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
@@ -35,11 +34,13 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
+import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.flowcrypt.email.Constants
+import com.flowcrypt.email.MsgDetailsGraphDirections
import com.flowcrypt.email.R
import com.flowcrypt.email.api.email.EmailUtil
import com.flowcrypt.email.api.email.FoldersManager
@@ -52,16 +53,19 @@ import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.api.retrofit.response.model.DecryptErrorMsgBlock
import com.flowcrypt.email.api.retrofit.response.model.DecryptedAttMsgBlock
import com.flowcrypt.email.api.retrofit.response.model.MsgBlock
+import com.flowcrypt.email.api.retrofit.response.model.OrgRules
import com.flowcrypt.email.api.retrofit.response.model.PublicKeyMsgBlock
import com.flowcrypt.email.database.MessageState
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.database.entity.MessageEntity
import com.flowcrypt.email.database.entity.PublicKeyEntity
+import com.flowcrypt.email.databinding.FragmentMessageDetailsBinding
import com.flowcrypt.email.extensions.decrementSafely
import com.flowcrypt.email.extensions.gone
import com.flowcrypt.email.extensions.incrementSafely
import com.flowcrypt.email.extensions.javax.mail.internet.getFormattedString
import com.flowcrypt.email.extensions.javax.mail.internet.personalOrEmail
+import com.flowcrypt.email.extensions.navController
import com.flowcrypt.email.extensions.showNeedPassphraseDialog
import com.flowcrypt.email.extensions.showTwoWayDialog
import com.flowcrypt.email.extensions.toast
@@ -85,6 +89,7 @@ import com.flowcrypt.email.ui.activity.base.BaseSyncActivity
import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment
import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour
import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePublicKeyDialogFragment
+import com.flowcrypt.email.ui.activity.fragment.dialog.DownloadAttachmentDialogFragment
import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment
import com.flowcrypt.email.ui.adapter.AttachmentsRecyclerViewAdapter
import com.flowcrypt.email.ui.adapter.MsgDetailsRecyclerViewAdapter
@@ -120,20 +125,21 @@ import javax.mail.internet.InternetAddress
*/
class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickListener {
override val progressView: View?
- get() = view?.findViewById(R.id.progress)
+ get() = binding?.progress?.root
override val contentView: View?
- get() = view?.findViewById(R.id.layoutContent)
+ get() = binding?.layoutContent
override val statusView: View?
- get() = view?.findViewById(R.id.status)
+ get() = binding?.status?.root
private val args by navArgs()
private val msgDetailsViewModel: MsgDetailsViewModel by viewModels {
MsgDetailsViewModelFactory(args.localFolder, args.messageEntity, requireActivity().application)
}
private val pgpSignatureHandlerViewModel: PgpSignatureHandlerViewModel by viewModels()
+ private var binding: FragmentMessageDetailsBinding? = null
private val attachmentsRecyclerViewAdapter = AttachmentsRecyclerViewAdapter(
- object : AttachmentsRecyclerViewAdapter.Listener {
+ object : AttachmentsRecyclerViewAdapter.AttachmentActionListener {
override fun onDownloadClick(attachmentInfo: AttachmentInfo) {
lastClickedAtt = attachmentInfo
lastClickedAtt?.orderNumber = GeneralUtil.genAttOrderId(requireContext())
@@ -169,28 +175,36 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
)
}
}
- })
- private var textViewSenderAddress: TextView? = null
- private var textViewDate: TextView? = null
- private var textViewSubject: TextView? = null
- private var tVTo: TextView? = null
- private var viewFooterOfHeader: View? = null
- private var layoutMsgParts: ViewGroup? = null
- private var layoutContent: View? = null
- private var imageBtnReplyAll: ImageButton? = null
- private var imageBtnMoreOptions: View? = null
- private var iBShowDetails: View? = null
- private var layoutReplyButton: View? = null
- private var layoutFwdButton: View? = null
- private var layoutReplyBtns: View? = null
- private var emailWebView: EmailWebView? = null
- private var layoutActionProgress: View? = null
- private var textViewActionProgress: TextView? = null
- private var progressBarActionProgress: ProgressBar? = null
- private var rVAttachments: RecyclerView? = null
- private var rVMsgDetails: RecyclerView? = null
- private var rVPgpBadges: RecyclerView? = null
+ override fun onAttachmentClick(attachmentInfo: AttachmentInfo) {
+ if (account?.isRuleExist(OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING) == true) {
+ if (attachmentInfo.uri != null || attachmentInfo.rawData?.isNotEmpty() == true) {
+ previewAttachment(
+ attachmentInfo = attachmentInfo,
+ useEnterpriseBehaviour = true
+ )
+ }
+ }
+ }
+
+ override fun onAttachmentPreviewClick(attachmentInfo: AttachmentInfo) {
+ if (account?.isRuleExist(OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING) == true) {
+ if (attachmentInfo.uri != null || attachmentInfo.rawData?.isNotEmpty() == true) {
+ previewAttachment(
+ attachmentInfo = attachmentInfo,
+ useEnterpriseBehaviour = true
+ )
+ } else {
+ navController?.navigate(
+ MessageDetailsFragmentDirections
+ .actionMessageDetailsFragmentToDownloadAttachmentDialogFragment(
+ REQUEST_CODE_PREVIEW_ATTACHMENT, attachmentInfo
+ )
+ )
+ }
+ }
+ }
+ })
private var msgInfo: IncomingMessageInfo? = null
private var folderType: FoldersManager.FolderType? = null
@@ -211,18 +225,30 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
+ subscribeToDownloadAttachmentViaDialog()
updateActionsVisibility(args.localFolder, null)
}
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View? {
+ binding = FragmentMessageDetailsBinding.inflate(inflater, container, false)
+ return binding?.root
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- initViews(view)
updateViews()
setupLabelsViewModel()
setupMsgDetailsViewModel()
}
+ override fun onDestroy() {
+ super.onDestroy()
+ FileAndDirectoryUtils.cleanDir(CacheManager.getCurrentMsgTempDir())
+ }
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CODE_START_IMPORT_KEY_ACTIVITY -> when (resultCode) {
@@ -367,12 +393,12 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menuActionReply -> {
- layoutReplyButton?.let { view -> onClick(view) }
+ binding?.layoutReplyButtons?.layoutReplyButton?.let { view -> onClick(view) }
true
}
R.id.menuActionForward -> {
- layoutFwdButton?.let { view -> onClick(view) }
+ binding?.layoutReplyButtons?.layoutFwdButton?.let { view -> onClick(view) }
true
}
else -> {
@@ -431,6 +457,12 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
}
}
+ override fun onAccountInfoRefreshed(accountEntity: AccountEntity?) {
+ super.onAccountInfoRefreshed(accountEntity)
+ attachmentsRecyclerViewAdapter.isPreviewEnabled =
+ account?.isRuleExist(OrgRules.DomainRule.RESTRICT_ANDROID_ATTACHMENT_HANDLING) == true
+ }
+
/**
* Show an incoming message info.
*
@@ -439,13 +471,13 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
private fun showIncomingMsgInfo(msgInfo: IncomingMessageInfo) {
this.msgInfo = msgInfo
this.msgEncryptType = msgInfo.encryptionType
- imageBtnReplyAll?.visibility = View.VISIBLE
- imageBtnMoreOptions?.visibility = View.VISIBLE
+ binding?.imageButtonReplyAll?.visibility = View.VISIBLE
+ binding?.imageButtonMoreOptions?.visibility = View.VISIBLE
isAdditionalActionEnabled = true
activity?.invalidateOptionsMenu()
msgInfo.localFolder = args.localFolder
- msgInfo.inlineSubject?.let { textViewSubject?.text = it }
+ msgInfo.inlineSubject?.let { binding?.textViewSubject?.text = it }
updateMsgBody()
showContent()
@@ -453,15 +485,16 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
private fun setActionProgress(progress: Int, message: String? = null) {
if (progress > 0) {
- progressBarActionProgress?.progress = progress
+ binding?.progressBarActionProgress?.progress = progress
}
if (progress != 100) {
- textViewActionProgress?.text = getString(R.string.progress_message, progress, message)
- textViewActionProgress?.visibility = View.VISIBLE
+ binding?.textViewActionProgress?.text =
+ getString(R.string.progress_message, progress, message)
+ binding?.textViewActionProgress?.visibility = View.VISIBLE
} else {
- textViewActionProgress?.text = null
- layoutActionProgress?.visibility = View.GONE
+ binding?.textViewActionProgress?.text = null
+ binding?.layoutActionProgress?.visibility = View.GONE
}
}
@@ -673,41 +706,18 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
supportActionBar?.subtitle = actionBarSubTitle
}
- private fun initViews(view: View) {
- layoutActionProgress = view.findViewById(R.id.layoutActionProgress)
- textViewActionProgress = view.findViewById(R.id.textViewActionProgress)
- progressBarActionProgress = view.findViewById(R.id.progressBarActionProgress)
-
- textViewSenderAddress = view.findViewById(R.id.textViewSenderAddress)
- textViewDate = view.findViewById(R.id.textViewDate)
- textViewSubject = view.findViewById(R.id.textViewSubject)
- tVTo = view.findViewById(R.id.tVTo)
- viewFooterOfHeader = view.findViewById(R.id.layoutFooterOfHeader)
- layoutMsgParts = view.findViewById(R.id.layoutMessageParts)
- layoutReplyBtns = view.findViewById(R.id.layoutReplyButtons)
- emailWebView = view.findViewById(R.id.emailWebView)
-
- layoutContent = view.findViewById(R.id.layoutContent)
- imageBtnReplyAll = view.findViewById(R.id.imageButtonReplyAll)
- imageBtnReplyAll?.setOnClickListener(this)
- imageBtnMoreOptions = view.findViewById(R.id.imageButtonMoreOptions)
- imageBtnMoreOptions?.setOnClickListener(this)
- iBShowDetails = view.findViewById(R.id.iBShowDetails)
-
- rVAttachments = view.findViewById(R.id.rVAttachments)
- rVMsgDetails = view.findViewById(R.id.rVMsgDetails)
- rVPgpBadges = view.findViewById(R.id.rVPgpBadges)
- }
-
private fun updateViews() {
- iBShowDetails?.setOnClickListener {
- rVMsgDetails?.visibleOrGone(!(rVMsgDetails?.isVisible ?: false))
- textViewDate?.visibleOrGone(!(rVMsgDetails?.isVisible ?: false))
+ binding?.imageButtonReplyAll?.setOnClickListener(this)
+ binding?.imageButtonMoreOptions?.setOnClickListener(this)
+
+ binding?.iBShowDetails?.setOnClickListener {
+ binding?.rVMsgDetails?.visibleOrGone(!(binding?.rVMsgDetails?.isVisible ?: false))
+ binding?.textViewDate?.visibleOrGone(!(binding?.rVMsgDetails?.isVisible ?: false))
}
updateMsgDetails()
- rVAttachments?.apply {
+ binding?.rVAttachments?.apply {
layoutManager = LinearLayoutManager(context)
addItemDecoration(
MarginItemDecoration(
@@ -722,16 +732,17 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
args.messageEntity.subject
if (folderType === FoldersManager.FolderType.SENT) {
- textViewSenderAddress?.text = EmailUtil.getFirstAddressString(args.messageEntity.to)
+ binding?.textViewSenderAddress?.text = EmailUtil.getFirstAddressString(args.messageEntity.to)
} else {
- textViewSenderAddress?.text = EmailUtil.getFirstAddressString(args.messageEntity.from)
+ binding?.textViewSenderAddress?.text =
+ EmailUtil.getFirstAddressString(args.messageEntity.from)
}
- textViewSubject?.text = subject
+ binding?.textViewSubject?.text = subject
if (JavaEmailConstants.FOLDER_OUTBOX.equals(args.messageEntity.folder, ignoreCase = true)) {
- textViewDate?.text =
+ binding?.textViewDate?.text =
DateTimeUtil.formatSameDayTime(context, args.messageEntity.sentDate ?: 0)
} else {
- textViewDate?.text =
+ binding?.textViewDate?.text =
DateTimeUtil.formatSameDayTime(context, args.messageEntity.receivedDate ?: 0)
}
@@ -739,7 +750,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
}
private fun updateMsgDetails() {
- rVMsgDetails?.apply {
+ binding?.rVMsgDetails?.apply {
layoutManager = LinearLayoutManager(context)
addItemDecoration(
VerticalSpaceMarginItemDecoration(
@@ -751,7 +762,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
adapter = msgDetailsAdapter
}
- rVPgpBadges?.apply {
+ binding?.rVPgpBadges?.apply {
layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
addItemDecoration(
MarginItemDecoration(
@@ -761,7 +772,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
adapter = pgpBadgeListAdapter
}
- tVTo?.text = prepareToText()
+ binding?.tVTo?.text = prepareToText()
val headers = mutableListOf().apply {
add(
@@ -862,8 +873,8 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
private fun updateMsgView() {
val inlineEncryptedAtts = mutableListOf()
- emailWebView?.loadUrl("about:blank")
- layoutMsgParts?.removeAllViews()
+ binding?.emailWebView?.loadUrl("about:blank")
+ binding?.layoutMessageParts?.removeAllViews()
var isFirstMsgPartText = true
var isHtmlDisplayed = false
@@ -880,22 +891,27 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
MsgBlock.Type.DECRYPTED_TEXT -> {
msgEncryptType = MessageEncryptionType.ENCRYPTED
- layoutMsgParts?.addView(genDecryptedTextPart(block, layoutInflater))
+ binding?.layoutMessageParts?.addView(genDecryptedTextPart(block, layoutInflater))
}
MsgBlock.Type.PLAIN_TEXT -> {
- layoutMsgParts?.addView(genTextPart(block, layoutInflater))
+ binding?.layoutMessageParts?.addView(genTextPart(block, layoutInflater))
if (isFirstMsgPartText) {
- viewFooterOfHeader?.visibility = View.VISIBLE
+ binding?.layoutFooterOfHeader?.visibility = View.VISIBLE
}
}
MsgBlock.Type.PUBLIC_KEY ->
- layoutMsgParts?.addView(genPublicKeyPart(block as PublicKeyMsgBlock, layoutInflater))
+ binding?.layoutMessageParts?.addView(
+ genPublicKeyPart(
+ block as PublicKeyMsgBlock,
+ layoutInflater
+ )
+ )
MsgBlock.Type.DECRYPT_ERROR -> {
msgEncryptType = MessageEncryptionType.ENCRYPTED
- layoutMsgParts?.addView(
+ binding?.layoutMessageParts?.addView(
genDecryptErrorPart(
block as DecryptErrorMsgBlock,
layoutInflater
@@ -931,27 +947,27 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
block: @JvmSuppressWildcards MsgBlock,
layoutInflater: LayoutInflater
) {
- layoutMsgParts?.addView(
+ binding?.layoutMessageParts?.addView(
genDefPart(
block, layoutInflater,
- R.layout.message_part_other, layoutMsgParts
+ R.layout.message_part_other, binding?.layoutMessageParts
)
)
}
private fun setupWebView(block: MsgBlock) {
- emailWebView?.configure()
+ binding?.emailWebView?.configure()
val text = clipLargeText(block.content) ?: ""
- emailWebView?.loadDataWithBaseURL(
+ binding?.emailWebView?.loadDataWithBaseURL(
null,
text,
"text/html",
StandardCharsets.UTF_8.displayName(),
null
)
- emailWebView?.setOnPageFinishedListener(object : EmailWebView.OnPageFinishedListener {
+ binding?.emailWebView?.setOnPageFinishedListener(object : EmailWebView.OnPageFinishedListener {
override fun onPageFinished() {
setActionProgress(100, null)
updateReplyButtons()
@@ -974,42 +990,39 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
* Update the reply buttons layout depending on the [MessageEncryptionType]
*/
private fun updateReplyButtons() {
- if (layoutReplyBtns != null) {
- val imageViewReply = layoutReplyBtns!!.findViewById(R.id.imageViewReply)
- val imageViewReplyAll = layoutReplyBtns!!.findViewById(R.id.imageViewReplyAll)
- val imageViewFwd = layoutReplyBtns!!.findViewById(R.id.imageViewFwd)
+ if (binding?.layoutReplyButtons != null) {
+ val imageViewReply = binding?.layoutReplyButtons?.imageViewReply
+ val imageViewReplyAll = binding?.layoutReplyButtons?.imageViewReplyAll
+ val imageViewFwd = binding?.layoutReplyButtons?.imageViewFwd
- val textViewReply = layoutReplyBtns!!.findViewById(R.id.textViewReply)
- val textViewReplyAll = layoutReplyBtns!!.findViewById(R.id.textViewReplyAll)
- val textViewFwd = layoutReplyBtns!!.findViewById(R.id.textViewFwd)
+ val textViewReply = binding?.layoutReplyButtons?.textViewReply
+ val textViewReplyAll = binding?.layoutReplyButtons?.textViewReplyAll
+ val textViewFwd = binding?.layoutReplyButtons?.textViewFwd
if (msgEncryptType === MessageEncryptionType.ENCRYPTED) {
- imageViewReply.setImageResource(R.mipmap.ic_reply_green)
- imageViewReplyAll.setImageResource(R.mipmap.ic_reply_all_green)
- imageBtnReplyAll?.setImageResource(R.mipmap.ic_reply_all_green)
- imageViewFwd.setImageResource(R.mipmap.ic_forward_green)
-
- textViewReply.setText(R.string.reply_encrypted)
- textViewReplyAll.setText(R.string.reply_all_encrypted)
- textViewFwd.setText(R.string.forward_encrypted)
+ imageViewReply?.setImageResource(R.mipmap.ic_reply_green)
+ imageViewReplyAll?.setImageResource(R.mipmap.ic_reply_all_green)
+ binding?.imageButtonReplyAll?.setImageResource(R.mipmap.ic_reply_all_green)
+ imageViewFwd?.setImageResource(R.mipmap.ic_forward_green)
+
+ textViewReply?.setText(R.string.reply_encrypted)
+ textViewReplyAll?.setText(R.string.reply_all_encrypted)
+ textViewFwd?.setText(R.string.forward_encrypted)
} else {
- imageViewReply.setImageResource(R.mipmap.ic_reply_red)
- imageViewReplyAll.setImageResource(R.mipmap.ic_reply_all_red)
- imageBtnReplyAll?.setImageResource(R.mipmap.ic_reply_all_red)
- imageViewFwd.setImageResource(R.mipmap.ic_forward_red)
-
- textViewReply.setText(R.string.reply)
- textViewReplyAll.setText(R.string.reply_all)
- textViewFwd.setText(R.string.forward)
+ imageViewReply?.setImageResource(R.mipmap.ic_reply_red)
+ imageViewReplyAll?.setImageResource(R.mipmap.ic_reply_all_red)
+ binding?.imageButtonReplyAll?.setImageResource(R.mipmap.ic_reply_all_red)
+ imageViewFwd?.setImageResource(R.mipmap.ic_forward_red)
+
+ textViewReply?.setText(R.string.reply)
+ textViewReplyAll?.setText(R.string.reply_all)
+ textViewFwd?.setText(R.string.forward)
}
- layoutReplyButton = layoutReplyBtns?.findViewById(R.id.layoutReplyButton)
- layoutReplyButton?.setOnClickListener(this)
- layoutFwdButton = layoutReplyBtns?.findViewById(R.id.layoutFwdButton)
- layoutFwdButton?.setOnClickListener(this)
- layoutReplyBtns?.findViewById(R.id.layoutReplyAllButton)?.setOnClickListener(this)
-
- layoutReplyBtns?.visibility = View.VISIBLE
+ binding?.layoutReplyButtons?.layoutReplyButton?.setOnClickListener(this)
+ binding?.layoutReplyButtons?.layoutFwdButton?.setOnClickListener(this)
+ binding?.layoutReplyButtons?.layoutReplyAllButton?.setOnClickListener(this)
+ binding?.layoutReplyButtons?.root?.visibility = View.VISIBLE
}
}
@@ -1030,8 +1043,11 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
)
}
- val pubKeyView =
- inflater.inflate(R.layout.message_part_public_key, layoutMsgParts, false) as ViewGroup
+ val pubKeyView = inflater.inflate(
+ R.layout.message_part_public_key,
+ binding?.layoutMessageParts,
+ false
+ ) as ViewGroup
val textViewPgpPublicKey = pubKeyView.findViewById(R.id.textViewPgpPublicKey)
val switchShowPublicKey = pubKeyView.findViewById(R.id.switchShowPublicKey)
@@ -1149,11 +1165,21 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
}
private fun genTextPart(block: MsgBlock, layoutInflater: LayoutInflater): View {
- return genDefPart(block, layoutInflater, R.layout.message_part_text, layoutMsgParts)
+ return genDefPart(
+ block,
+ layoutInflater,
+ R.layout.message_part_text,
+ binding?.layoutMessageParts
+ )
}
private fun genDecryptedTextPart(block: MsgBlock, layoutInflater: LayoutInflater): View {
- return genDefPart(block, layoutInflater, R.layout.message_part_pgp_message, layoutMsgParts)
+ return genDefPart(
+ block,
+ layoutInflater,
+ R.layout.message_part_pgp_message,
+ binding?.layoutMessageParts
+ )
}
private fun genDecryptErrorPart(
@@ -1229,7 +1255,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
): View {
val viewGroup = layoutInflater.inflate(
R.layout.message_part_pgp_message_error,
- layoutMsgParts, false
+ binding?.layoutMessageParts, false
) as ViewGroup
val textViewErrorMsg = viewGroup.findViewById(R.id.textViewErrorMessage)
ExceptionUtil.handleError(ManualHandledException(errorMsg))
@@ -1255,7 +1281,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
*/
private fun generateMissingPrivateKeyLayout(pgpMsg: String?, inflater: LayoutInflater): View {
val viewGroup = inflater.inflate(
- R.layout.message_part_pgp_message_missing_private_key, layoutMsgParts, false
+ R.layout.message_part_pgp_message_missing_private_key, binding?.layoutMessageParts, false
) as ViewGroup
val buttonImportPrivateKey = viewGroup.findViewById