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