Skip to content

Commit

Permalink
Issue 2137 preview attachments for all (#2314)
Browse files Browse the repository at this point in the history
* Added using Content app if 'RESTRICT_ANDROID_ATTACHMENT_HANDLING' is present. Refactored code.| #2137

* Enabled previewing attachments for all users.| #2137

* Refactored code.| #2137

* Modified MessageDetailsEkmFlowTest.| #2137

* Modified MessageDetailsFlowTest.testStandardMsgPlaintextWithOneAttachment().| #2137

* Fixed a few bugs.| #2137
  • Loading branch information
DenBond7 authored May 8, 2023
1 parent c6256ac commit bcbdd10
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package com.flowcrypt.email.ui

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
Expand All @@ -16,23 +17,38 @@ 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.ClientConfiguration
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.database.entity.KeyEntity
import com.flowcrypt.email.extensions.kotlin.toHex
import com.flowcrypt.email.model.KeyImportDetails
import com.flowcrypt.email.rules.AddAccountToDatabaseRule
import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule
import com.flowcrypt.email.rules.ClearAppSettingsRule
import com.flowcrypt.email.rules.FlowCryptMockWebServerRule
import com.flowcrypt.email.rules.GrantPermissionRuleChooser
import com.flowcrypt.email.rules.RetryRule
import com.flowcrypt.email.rules.ScreenshotTestRule
import com.flowcrypt.email.ui.base.BaseMessageDetailsFlowTest
import com.flowcrypt.email.util.AccountDaoManager
import com.flowcrypt.email.util.TestGeneralUtil
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.gmail.model.Message
import com.google.api.services.gmail.model.MessagePart
import com.google.api.services.gmail.model.MessagePartBody
import com.google.api.services.gmail.model.MessagePartHeader
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.RecordedRequest
import org.hamcrest.Matchers.not
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import java.math.BigInteger
import java.net.HttpURLConnection
import java.util.UUID
import kotlin.random.Random

/**
* @author Denys Bondarenko
Expand All @@ -44,8 +60,8 @@ class MessageDetailsEkmFlowTest : BaseMessageDetailsFlowTest() {
"messages/attachments/simple_att.json",
AttachmentInfo::class.java
)
private val userWithClientConfiguration = AccountDaoManager.getUserWithClientConfiguration(
ClientConfiguration(
private val userWithClientConfiguration = AccountDaoManager.getDefaultAccountDao().copy(
clientConfiguration = ClientConfiguration(
flags = listOf(
ClientConfiguration.ConfigurationProperty.NO_PRV_CREATE,
ClientConfiguration.ConfigurationProperty.NO_PRV_BACKUP,
Expand All @@ -56,8 +72,12 @@ class MessageDetailsEkmFlowTest : BaseMessageDetailsFlowTest() {
disallowAttesterSearchForDomains = null,
enforceKeygenAlgo = null,
enforceKeygenExpireMonths = null
)
),
accountType = AccountEntity.ACCOUNT_TYPE_GOOGLE,
useCustomerFesUrl = true,
useAPI = true
)

override val addAccountToDatabaseRule = AddAccountToDatabaseRule(userWithClientConfiguration)
private val addPrivateKeyToDatabaseRule = AddPrivateKeyToDatabaseRule(
accountEntity = addAccountToDatabaseRule.account,
Expand All @@ -67,11 +87,72 @@ class MessageDetailsEkmFlowTest : BaseMessageDetailsFlowTest() {
passphraseType = KeyEntity.PassphraseType.DATABASE
)

private val mockWebServerRule = FlowCryptMockWebServerRule(
TestConstants.MOCK_WEB_SERVER_PORT,
object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
when (request.path) {
"/gmail/v1/users/me/messages/${simpleAttInfo?.uid?.toHex()}?fields=" +
"id,threadId,labelIds,snippet,sizeEstimate,historyId,internalDate,payload/partId," +
"payload/mimeType,payload/filename,payload/headers,payload/body,payload/" +
"parts(partId,mimeType,filename,headers,body/size,body/attachmentId)&format=full" -> {
return MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(
Message().apply {
factory = GsonFactory.getDefaultInstance()
id = "5555555555555555"
threadId = "1111111111111111"
payload = MessagePart().apply {
partId = ""
mimeType = "multipart/mixed"
filename = ""
parts = listOf(
MessagePart().apply {
partId = "0"
mimeType = "image/png"
filename = "android.png"
headers = listOf(
MessagePartHeader().apply {
name = "Content-Type"
value = "image/png; name=\"android.png\""
}, MessagePartHeader().apply {
name = "Content-Disposition"
value = "attachment; filename=\"android.png\""
})
body = MessagePartBody().apply {
attachmentId = ATTACHMENT_ID
}
}
)
}
historyId = BigInteger.valueOf(Random.nextLong())
}.toString()
)
}

"/gmail/v1/users/me/messages/${simpleAttInfo?.uid?.toHex()}/attachments/$ATTACHMENT_ID?fields=data&prettyPrint=false" -> {
return MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(
MessagePartBody().apply {
factory = GsonFactory.getDefaultInstance()
data = "we don't care about this content"
}.toString()
)
}

else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
}
}
})

@get:Rule
var ruleChain: TestRule = RuleChain
.outerRule(RetryRule.DEFAULT)
.around(ClearAppSettingsRule())
.around(GrantPermissionRuleChooser.grant(android.Manifest.permission.POST_NOTIFICATIONS))
.around(mockWebServerRule)
.around(addAccountToDatabaseRule)
.around(addPrivateKeyToDatabaseRule)
.around(activeActivityRule)
Expand All @@ -94,14 +175,25 @@ class MessageDetailsEkmFlowTest : BaseMessageDetailsFlowTest() {
}

@Test
fun testVisibilityOfPreviewAttachmentButton() {
fun testPreviewAttachmentButton() {
baseCheckWithAtt(
getMsgInfo(
"messages/info/standard_msg_info_plaintext_with_one_att.json",
"messages/mime/standard_msg_info_plaintext_with_one_att.txt", simpleAttInfo
), simpleAttInfo
)

unregisterCountingIdlingResource()

onView(withId(R.id.imageButtonPreviewAtt))
.check(matches(isDisplayed()))
.perform(click())
Thread.sleep(2000)
//as the Content app is not installed on a device the app should show the warning
isDialogWithTextDisplayed(decorView, getResString(R.string.warning_don_not_have_content_app))
}

companion object {
val ATTACHMENT_ID = UUID.randomUUID().toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ class MessageDetailsFlowTest : BaseMessageDetailsFlowTest() {
"messages/mime/standard_msg_info_plaintext_with_one_att.txt", simpleAttInfo
), simpleAttInfo
)

onView(withId(R.id.imageButtonPreviewAtt))
.check(matches(isDisplayed()))
}

@Test
Expand Down
2 changes: 2 additions & 0 deletions FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,7 @@ class Constants {
*/
const val REQUEST_KEY_BUTTON_CLICK = "REQUEST_KEY_BUTTON_CLICK"
const val REQUEST_KEY_INFO_BUTTON_CLICK = "REQUEST_KEY_INFO_BUTTON_CLICK"

const val APP_PACKAGE_CONTENT_LOCKER = "com.airwatch.contentlocker"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ data class AccountEntity constructor(
return clientConfiguration?.hasProperty(configurationProperty) ?: false
}

fun isHandlingAttachmentRestricted(): Boolean {
return hasClientConfigurationProperty(
ClientConfiguration.ConfigurationProperty.RESTRICT_ANDROID_ATTACHMENT_HANDLING
)
}

companion object {
const val TABLE_NAME = "accounts"
const val ACCOUNT_TYPE_GOOGLE = "com.google"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ 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.ClientConfiguration
import com.flowcrypt.email.database.FlowCryptRoomDatabase
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.extensions.android.content.getParcelableExtraViaExt
Expand Down Expand Up @@ -161,7 +160,7 @@ class AttachmentDownloadManagerService : Service() {
}

private interface OnDownloadAttachmentListener {
fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri, canBeOpened: Boolean = true)
fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri, useContentApp: Boolean = false)

fun onCanceled(attInfo: AttachmentInfo)

Expand All @@ -186,7 +185,7 @@ class AttachmentDownloadManagerService : Service() {
val attDownloadManagerService = weakRef.get()
val notificationManager = attDownloadManagerService?.attsNotificationManager

val (attInfo, exception, uri, progressInPercentage, timeLeft, isLast, canBeOpened)
val (attInfo, exception, uri, progressInPercentage, timeLeft, isLast, useContentApp)
= message.obj as DownloadAttachmentTaskResult

when (message.what) {
Expand All @@ -208,7 +207,7 @@ class AttachmentDownloadManagerService : Service() {
context = attDownloadManagerService,
attInfo = attInfo!!,
uri = uri!!,
canBeOpened = canBeOpened
useContentApp = useContentApp
)
LogsUtil.d(TAG, attInfo?.getSafeName() + " is downloaded")
}
Expand Down Expand Up @@ -373,15 +372,15 @@ class AttachmentDownloadManagerService : Service() {
}
}

override fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri, canBeOpened: Boolean) {
override fun onAttDownloaded(attInfo: AttachmentInfo, uri: Uri, useContentApp: Boolean) {
attsInfoMap.remove(attInfo.id)
futureMap.remove(attInfo.uniqueStringId)
try {
val result = DownloadAttachmentTaskResult(
attInfo = attInfo,
uri = uri,
isLast = isLast,
canBeOpened = canBeOpened
useContentApp = useContentApp
)
messenger.send(Message.obtain(null, ReplyHandler.MESSAGE_ATTACHMENT_DOWNLOAD, result))
} catch (remoteException: RemoteException) {
Expand Down Expand Up @@ -463,9 +462,7 @@ class AttachmentDownloadManagerService : Service() {
listener?.onAttDownloaded(
attInfo = att.copy(name = finalFileName),
uri = uri,
canBeOpened = !account.hasClientConfigurationProperty(
ClientConfiguration.ConfigurationProperty.RESTRICT_ANDROID_ATTACHMENT_HANDLING
)
useContentApp = account.isHandlingAttachmentRestricted()
)
}

Expand All @@ -488,9 +485,7 @@ class AttachmentDownloadManagerService : Service() {
).use { inputStream ->
handleAttachmentInputStream(
inputStream = inputStream,
canBeOpened = !account.hasClientConfigurationProperty(
ClientConfiguration.ConfigurationProperty.RESTRICT_ANDROID_ATTACHMENT_HANDLING
)
useContentApp = account.isHandlingAttachmentRestricted()
)
}
}
Expand All @@ -517,9 +512,7 @@ class AttachmentDownloadManagerService : Service() {
)?.inputStream?.let { inputStream ->
handleAttachmentInputStream(
inputStream = inputStream,
canBeOpened = !account.hasClientConfigurationProperty(
ClientConfiguration.ConfigurationProperty.RESTRICT_ANDROID_ATTACHMENT_HANDLING
)
useContentApp = account.isHandlingAttachmentRestricted()
)
} ?: throw ManualHandledException(context.getString(R.string.attachment_not_found))
}
Expand All @@ -534,7 +527,10 @@ class AttachmentDownloadManagerService : Service() {
}
}

private fun handleAttachmentInputStream(inputStream: InputStream, canBeOpened: Boolean = true) {
private fun handleAttachmentInputStream(
inputStream: InputStream,
useContentApp: Boolean = false
) {
downloadFile(attTempFile, inputStream)

if (Thread.currentThread().isInterrupted) {
Expand All @@ -548,7 +544,7 @@ class AttachmentDownloadManagerService : Service() {
listener?.onAttDownloaded(
attInfo = att.copy(name = finalFileName),
uri = uri,
canBeOpened = canBeOpened
useContentApp = useContentApp
)
}
}
Expand All @@ -565,7 +561,7 @@ class AttachmentDownloadManagerService : Service() {
@RequiresApi(Build.VERSION_CODES.Q)
private fun storeFileUsingScopedStorage(context: Context, attFile: File): Uri {
val resolver = context.contentResolver
val fileExtension = FilenameUtils.getExtension(att.name).lowercase()
val fileExtension = FilenameUtils.getExtension(finalFileName).lowercase()
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension)

val contentValues = ContentValues().apply {
Expand Down Expand Up @@ -692,7 +688,7 @@ class AttachmentDownloadManagerService : Service() {
throw NullPointerException("Error. The file is missing")
}

if (!SecurityUtils.isPossiblyEncryptedData(att.name)) {
if (!SecurityUtils.isPossiblyEncryptedData(finalFileName)) {
return file
}

Expand All @@ -710,10 +706,8 @@ class AttachmentDownloadManagerService : Service() {
protector = protector
)

finalFileName = FilenameUtils.getBaseName(att.getSafeName())
val fileNameFromMessageMetadata = messageMetadata.filename
if (att.name == null && fileNameFromMessageMetadata != null) {
finalFileName = fileNameFromMessageMetadata
finalFileName = FilenameUtils.getBaseName(att.getSafeName()).ifEmpty {
messageMetadata.filename ?: ""
}

return decryptedFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.text.TextUtils
import android.text.format.DateUtils
import androidx.core.app.NotificationCompat
import com.flowcrypt.email.BuildConfig
import com.flowcrypt.email.Constants
import com.flowcrypt.email.R
import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify
Expand Down Expand Up @@ -88,18 +89,19 @@ 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.
* @param useContentApp A flag that indicates should we use the Content app as a viewer.
*/
fun downloadCompleted(
context: Context,
attInfo: AttachmentInfo,
uri: Uri,
canBeOpened: Boolean = true
useContentApp: Boolean = false
) {
val intent = if (canBeOpened) {
val intent = if (useContentApp) {
GeneralUtil.genViewAttachmentIntent(uri, attInfo)
.setPackage(Constants.APP_PACKAGE_CONTENT_LOCKER)
} else {
Intent()
GeneralUtil.genViewAttachmentIntent(uri, attInfo)
}
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = genDefBuilder(context, attInfo)
Expand Down Expand Up @@ -198,7 +200,7 @@ class AttachmentNotificationManager(context: Context) : CustomNotificationManage
* @param attachmentInfo The [AttachmentInfo] object.
* @return The prepared title.
*/
private fun prepareContentTitle(attachmentInfo: AttachmentInfo): String? {
private fun prepareContentTitle(attachmentInfo: AttachmentInfo): String {
var contentTitle = attachmentInfo.getSafeName()
if (!TextUtils.isEmpty(contentTitle) && contentTitle.length > MAX_CONTENT_TITLE_LENGTH) {
contentTitle = contentTitle.substring(0, MAX_CONTENT_TITLE_LENGTH) + "..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ data class DownloadAttachmentTaskResult constructor(
val progressInPercentage: Int = 0,
val timeLeft: Long = 0,
val isLast: Boolean = false,
val canBeOpened: Boolean = true
val useContentApp: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,8 @@ class CreateMessageFragment : BaseFragment<FragmentCreateMessageBinding>(),

if (hasAbilityToAddAtt(attachmentInfo)) {

if (attachmentInfo.name.isNullOrEmpty()) {
val msg = "attachmentInfo.getName() == null, uri = " + attachmentInfo.uri!!
if (attachmentInfo.getSafeName().isEmpty()) {
val msg = "attachmentInfo.getName() is empty, uri = " + attachmentInfo.uri!!
ExceptionUtil.handleError(NullPointerException(msg))
return
}
Expand Down
Loading

0 comments on commit bcbdd10

Please sign in to comment.