Extracted EmbedeedImage mapper to AttachmentsHelper.

MAILAND-1337
This commit is contained in:
Tomasz Giszczak 2021-02-02 14:20:03 +01:00
parent ab4f5a5de4
commit 6df695a9a9
11 changed files with 144 additions and 143 deletions

View File

@ -37,6 +37,7 @@ import me.proton.core.util.kotlin.DispatcherProvider
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
@ -128,7 +129,7 @@ internal class MessageRenderer(
if (!file.exists() || file.length() == 0L) continue
val size = (MAX_IMAGES_TOTAL_SIZE / embeddedImages.size)
.coerceAtMost(MAX_IMAGE_SINGLE_SIZE)
.coerceAtMost(MAX_IMAGE_SINGLE_SIZE)
val compressed = try {
ByteArrayOutputStream().also {
@ -139,6 +140,7 @@ internal class MessageRenderer(
bitmap.compress(Bitmap.CompressFormat.WEBP, 80, it)
}
} catch (t: Throwable) {
Timber.i(t, "Skip the image")
// Skip the image
continue
}
@ -183,7 +185,7 @@ internal class MessageRenderer(
val contentType = embeddedImage.contentType.formatContentType()
document.findImageElements(contentId)
?.attr("src", "data:$contentType;$encoding,$image64")
?.attr("src", "data:$contentType;$encoding,$image64")
}
documentStringifier.send(Unit) // Deliver after all the elements for now
}
@ -244,7 +246,7 @@ private const val ID_PLACEHOLDER = "%id"
/** [Array] of html attributes that could contain an image */
private val IMAGE_ATTRIBUTES =
arrayOf("img[src=$ID_PLACEHOLDER]", "img[src=cid:$ID_PLACEHOLDER]", "img[rel=$ID_PLACEHOLDER]")
arrayOf("img[src=$ID_PLACEHOLDER]", "img[src=cid:$ID_PLACEHOLDER]", "img[rel=$ID_PLACEHOLDER]")
// endregion
// region typealiases
@ -256,8 +258,8 @@ private typealias ImageString = Pair<EmbeddedImage, String>
private fun String.formatEncoding() = toLowerCase()
private fun String.formatContentId() = trimStart('<').trimEnd('>')
private fun String.formatContentType() = toLowerCase()
.replace("\r", "").replace("\n", "")
.replaceFirst(";.*$".toRegex(), "")
.replace("\r", "").replace("\n", "")
.replaceFirst(";.*$".toRegex(), "")
/**
* Flatten the receiver [Document] by removing the indentation and disabling prettyPrint.
@ -268,12 +270,12 @@ private fun Document.flatten() = apply { outputSettings().indentAmount(0).pretty
/** @return [Elements] matching the image attribute for the given [id] */
private fun Document.findImageElements(id: String): Elements? {
return IMAGE_ATTRIBUTES
.map { attr -> attr.replace(ID_PLACEHOLDER, id) }
// with `asSequence` iteration will stop when the first usable element
// is found and so avoid to make too many calls to document.select
.asSequence()
.map { select(it) }
.find { it.isNotEmpty() }
.map { attr -> attr.replace(ID_PLACEHOLDER, id) }
// with `asSequence` iteration will stop when the first usable element
// is found and so avoid to make too many calls to document.select
.asSequence()
.map { select(it) }
.find { it.isNotEmpty() }
}
// endregion

View File

@ -43,6 +43,7 @@ import ch.protonmail.android.api.models.room.messages.Attachment
import ch.protonmail.android.api.models.room.messages.Label
import ch.protonmail.android.api.models.room.messages.Message
import ch.protonmail.android.api.models.room.pendingActions.PendingSend
import ch.protonmail.android.attachments.AttachmentsHelper
import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker
import ch.protonmail.android.core.BigContentHolder
import ch.protonmail.android.core.Constants
@ -87,6 +88,7 @@ internal class MessageDetailsViewModel @ViewModelInject constructor(
private val fetchVerificationKeys: FetchVerificationKeys,
private val attachmentsWorker: DownloadEmbeddedAttachmentsWorker.Enqueuer,
private val dispatchers: DispatcherProvider,
private val attachmentsHelper: AttachmentsHelper,
messageRendererFactory: MessageRenderer.Factory,
verifyConnection: VerifyConnection,
networkConfigurator: NetworkConfigurator
@ -253,20 +255,22 @@ internal class MessageDetailsViewModel @ViewModelInject constructor(
val attachmentMetadataList = attachmentMetadataDatabase.getAllAttachmentsForMessage(messageId)
val embeddedImages = _embeddedImagesAttachments.mapNotNull {
EmbeddedImage.fromAttachment(
attachmentsHelper.fromAttachmentToEmbededImage(
it, decryptedMessageData.value!!.embeddedImagesArray.toList()
)
}
val embeddedImagesWithLocalFiles = mutableListOf<EmbeddedImage>()
embeddedImages.forEach { embeddedImage ->
attachmentMetadataList.find { it.id == embeddedImage.attachmentId }?.let {
embeddedImage.localFileName = it.localLocation.substringAfterLast("/")
embeddedImagesWithLocalFiles.add(
embeddedImage.copy(localFileName = it.localLocation.substringAfterLast("/"))
)
}
}
// don't download embedded images, if we already have them in local storage
if (embeddedImages.all { it.localFileName != null }) {
AppUtil.postEventOnUi(DownloadEmbeddedImagesEvent(Status.SUCCESS, embeddedImages))
if (embeddedImagesWithLocalFiles.all { it.localFileName != null }) {
AppUtil.postEventOnUi(DownloadEmbeddedImagesEvent(Status.SUCCESS, embeddedImagesWithLocalFiles))
} else {
messageDetailsRepository.startDownloadEmbeddedImages(messageId, userManager.username)
}
@ -510,8 +514,8 @@ internal class MessageDetailsViewModel @ViewModelInject constructor(
val embeddedImagesToFetch = ArrayList<EmbeddedImage>()
val embeddedImagesAttachments = ArrayList<Attachment>()
for (attachment in attachments) {
val embeddedImage = EmbeddedImage
.fromAttachment(attachment, message.embeddedImagesArray.toList()) ?: continue
val embeddedImage = attachmentsHelper
.fromAttachmentToEmbededImage(attachment, message.embeddedImagesArray.toList()) ?: continue
embeddedImagesToFetch.add(embeddedImage)
embeddedImagesAttachments.add(attachment)
}

View File

@ -77,6 +77,7 @@ public class AttachmentHeaders implements Serializable {
return contentDisposition;
}
@Nullable
public String getContentId() {
if (contentId != null && !contentId.isEmpty()) {
return contentId.get(0);

View File

@ -33,22 +33,19 @@ const val COLUMN_ATTACHMENT_FOLDER_LOCATION = "folder_location"
const val COLUMN_ATTACHMENT_DOWNLOAD_TIMESTAMP = "download_timestamp"
// endregion
/**
* Created by dino on 4/20/18.
*/
@Entity(tableName = TABLE_ATTACHMENT_METADATA)
class AttachmentMetadata constructor(
@ColumnInfo(name = COLUMN_ATTACHMENT_ID)
@PrimaryKey
val id: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_NAME)
val name: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_FILE_SIZE)
val size: Long,
@ColumnInfo(name = COLUMN_ATTACHMENT_LOCAL_LOCATION)
val localLocation: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_FOLDER_LOCATION)
val folderLocation: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_DOWNLOAD_TIMESTAMP)
val downloadTimestamp: Long): Serializable
@ColumnInfo(name = COLUMN_ATTACHMENT_ID)
@PrimaryKey
val id: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_NAME)
val name: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_FILE_SIZE)
val size: Long,
@ColumnInfo(name = COLUMN_ATTACHMENT_LOCAL_LOCATION)
val localLocation: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_FOLDER_LOCATION)
val folderLocation: String,
@ColumnInfo(name = COLUMN_ATTACHMENT_DOWNLOAD_TIMESTAMP)
val downloadTimestamp: Long
) : Serializable

View File

@ -126,13 +126,12 @@ data class Attachment @JvmOverloads constructor(
private fun isInline(embeddedImagesArray: List<String>): Boolean {
val headers = headers ?: return false
val contentDisposition = headers.contentDisposition
var contentId = headers.contentId
if (TextUtils.isEmpty(contentId)) {
contentId = headers.contentLocation
}
if (contentId.isNotEmpty()) {
contentId = contentId.removeSurrounding("<", ">")
var contentId = if (headers.contentId.isNullOrEmpty()) {
headers.contentLocation
} else {
headers.contentId
}
contentId = contentId?.removeSurrounding("<", ">")
val embeddedMimeTypes = listOf("image/gif", "image/jpeg", "image/png", "image/bmp")
var containsInline = false
for (element in contentDisposition) {
@ -143,7 +142,7 @@ data class Attachment @JvmOverloads constructor(
}
return contentDisposition != null &&
(containsInline || embeddedImagesArray.contains(contentId.removeSurrounding("<", ">"))) &&
(containsInline || embeddedImagesArray.contains(contentId?.removeSurrounding("<", ">"))) &&
embeddedMimeTypes.contains(mimeType)
}

View File

@ -28,6 +28,7 @@ import ch.protonmail.android.R
import ch.protonmail.android.api.ProgressListener
import ch.protonmail.android.api.ProtonMailApiManager
import ch.protonmail.android.api.models.room.attachmentMetadata.AttachmentMetadataDatabase
import ch.protonmail.android.api.models.room.messages.Attachment
import ch.protonmail.android.crypto.AddressCrypto
import ch.protonmail.android.crypto.CipherText
import ch.protonmail.android.events.DownloadEmbeddedImagesEvent
@ -39,12 +40,14 @@ import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.Date
import java.util.Locale
import javax.inject.Inject
private const val NOTIFICATION_ID = 213_412
private const val FULL_PROGRESS = 100
private const val BASE_64 = "base64"
class DownloadAttachmentsHelper @Inject constructor(
class AttachmentsHelper @Inject constructor(
private val context: Context,
private val api: ProtonMailApiManager,
private val notificationManager: NotificationManager
@ -63,14 +66,17 @@ class DownloadAttachmentsHelper @Inject constructor(
val attachmentMetadataList = attachmentMetadataDatabase.getAllAttachmentsForMessage(messageId)
attachmentMetadataList.size
val embeddedImagesWithLocalFiles = mutableListOf<EmbeddedImage>()
embeddedImages.forEach { embeddedImage ->
attachmentMetadataList.find { it.id == embeddedImage.attachmentId }?.let {
embeddedImage.localFileName = it.localLocation.substringAfterLast("/")
embeddedImagesWithLocalFiles.add(
embeddedImage.copy(localFileName = it.localLocation.substringAfterLast("/"))
)
}
}
// all embedded images are in the local filestorage already
if (embeddedImages.all { it.localFileName != null }) return true
if (embeddedImagesWithLocalFiles.all { it.localFileName != null }) return true
}
return false
@ -195,4 +201,65 @@ class DownloadAttachmentsHelper @Inject constructor(
}
}
}
fun fromAttachmentToEmbededImage(
attachment: Attachment,
embeddedImagesArray: List<String>
): EmbeddedImage? {
val headers = attachment.headers ?: return null
val contentDisposition = headers.contentDisposition
var contentId = if (headers.contentId.isNullOrEmpty()) {
headers.contentLocation
} else {
headers.contentId
}
contentId = contentId?.removeSurrounding("<", ">")
if (contentDisposition != null) {
if (contentDisposition.isEmpty()) {
return null
} else {
var containsInlineMarker = false
for (element in contentDisposition) {
if (!element.isNullOrEmpty() && element.contains("inline")) {
containsInlineMarker = true
break
}
}
if (!containsInlineMarker && !embeddedImagesArray.contains(contentId)) {
return null
}
}
}
if (attachment.attachmentId.isNullOrEmpty()) {
return null
}
val fileName = attachment.fileName
if (fileName.isNullOrEmpty()) {
return null
}
val encoding = headers.contentTransferEncoding
val contentType = headers.contentType
val mimeData = attachment.mimeData
val embeddedMimeTypes = listOf("image/gif", "image/jpeg", "image/png", "image/bmp")
return if (!embeddedMimeTypes.contains(attachment.mimeTypeFirstValue?.toLowerCase(Locale.ENGLISH))) {
null
} else EmbeddedImage(
attachment.attachmentId ?: "",
fileName,
attachment.keyPackets ?: "",
if (contentType.isEmpty()) {
attachment.mimeType ?: ""
} else {
contentType
},
if (encoding.isEmpty()) BASE_64 else encoding,
contentId ?: headers.contentLocation,
mimeData,
attachment.fileSize,
attachment.messageId,
null
)
}
}

View File

@ -136,7 +136,7 @@ class AttachmentsRepository @Inject constructor(
}
private fun contentIdFormatted(headers: AttachmentHeaders): String {
val contentId = headers.contentId
val contentId = requireNotNull(headers.contentId)
val parts = contentId.split("<").dropLastWhile { it.isEmpty() }.toTypedArray()
if (parts.size > 1) {
return parts[1].replace(">", "")

View File

@ -76,7 +76,7 @@ class DownloadEmbeddedAttachmentsWorker @WorkerInject constructor(
private val userManager: UserManager,
private val messageDetailsRepository: MessageDetailsRepository,
private val attachmentMetadataDatabase: AttachmentMetadataDatabase,
private val downloadHelper: DownloadAttachmentsHelper
private val downloadHelper: AttachmentsHelper
) : Worker(context, params) {
override fun doWork(): Result {
@ -111,7 +111,9 @@ class DownloadEmbeddedAttachmentsWorker @WorkerInject constructor(
attachments = message.Attachments
}
val embeddedImages = attachments.mapNotNull { EmbeddedImage.fromAttachment(it, message.embeddedImagesArray) }
val embeddedImages = attachments.mapNotNull {
downloadHelper.fromAttachmentToEmbededImage(it, message.embeddedImagesArray)
}
val otherAttachments = attachments.filter { attachment ->
embeddedImages.find { attachment.attachmentId == it.attachmentId } == null
}
@ -232,7 +234,7 @@ class DownloadEmbeddedAttachmentsWorker @WorkerInject constructor(
var failure = false
embeddedImages.forEachIndexed { index, embeddedImage ->
val filename = downloadHelper.calculateFilename(embeddedImage.fileName!!, index)
val filename = downloadHelper.calculateFilename(embeddedImage.fileNameFormatted!!, index)
val attachmentFile = File(attachmentsDirectoryFile, filename)
try {
@ -247,12 +249,12 @@ class DownloadEmbeddedAttachmentsWorker @WorkerInject constructor(
it.write(decryptedByteArray)
}
embeddedImage.localFileName = filename
val embeddedImageWithFile = embeddedImage.copy(localFileName = filename)
val attachmentMetadata = AttachmentMetadata(
embeddedImage.attachmentId,
embeddedImage.fileName!!, embeddedImage.size,
embeddedImage.messageId + "/" + filename,
embeddedImage.messageId, System.currentTimeMillis()
embeddedImageWithFile.attachmentId,
embeddedImageWithFile.fileNameFormatted!!, embeddedImageWithFile.size,
embeddedImageWithFile.messageId + "/" + filename,
embeddedImageWithFile.messageId, System.currentTimeMillis()
)
attachmentMetadataDatabase.insertAttachmentMetadata(attachmentMetadata)

View File

@ -18,92 +18,17 @@
*/
package ch.protonmail.android.jobs.helper
import android.text.TextUtils
import ch.protonmail.android.api.models.room.messages.Attachment
import java.util.*
// region constants
private const val BASE_64 = "base64"
// endregion
/**
* Created by dkadrikj on 7/16/16.
*/
class EmbeddedImage private constructor(
val attachmentId: String,
fileName: String,
val key: String,
val contentType: String,
val encoding: String,
val contentId: String,
val mimeData: ByteArray?,
val size: Long,
val messageId: String,
var localFileName: String?) {
var fileName: String? = null
init {
this.fileName = fileName.replace(" ", "_")
}
companion object {
fun fromAttachment(attachment: Attachment, embeddedImagesArray: List<String>): EmbeddedImage? {
val headers = attachment.headers ?: return null
val contentDisposition = headers.contentDisposition
var contentId = headers.contentId
if (TextUtils.isEmpty(contentId)) {
contentId = headers.contentLocation
}
if (!TextUtils.isEmpty(contentId)) {
contentId = contentId.removeSurrounding("<", ">")
}
if (contentDisposition != null) {
if (contentDisposition.isEmpty()) {
return null
} else {
var containsInlineMarker = false
for (element in contentDisposition) {
if (!element.isNullOrEmpty() && element.contains("inline")) {
containsInlineMarker = true
break
}
}
if (!containsInlineMarker && !embeddedImagesArray.contains(contentId)) {
return null
}
}
}
if (TextUtils.isEmpty(attachment.attachmentId)) {
return null
}
val fileName = attachment.fileName
if (TextUtils.isEmpty(fileName)) {
return null
}
val encoding = headers.contentTransferEncoding
val contentType = headers.contentType
val mimeData = attachment.mimeData
val embeddedMimeTypes = Arrays.asList("image/gif", "image/jpeg", "image/png", "image/bmp")
return if (!embeddedMimeTypes.contains(attachment.mimeTypeFirstValue?.toLowerCase())) {
null
} else EmbeddedImage(attachment.attachmentId ?: "",
fileName!!,
attachment.keyPackets ?: "",
if (TextUtils.isEmpty(contentType)) {
attachment.mimeType ?: ""
} else {
contentType
},
if (TextUtils.isEmpty(encoding)) BASE_64 else encoding,
contentId,
mimeData,
attachment.fileSize,
attachment.messageId,
null)
}
}
data class EmbeddedImage constructor(
val attachmentId: String,
val fileName: String,
val key: String,
val contentType: String,
val encoding: String,
val contentId: String,
val mimeData: ByteArray?,
val size: Long,
val messageId: String,
val localFileName: String?
) {
var fileNameFormatted: String? = fileName.replace(" ", "_")
}

View File

@ -25,6 +25,7 @@ import ch.protonmail.android.activities.messageDetails.MessageRenderer
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
import ch.protonmail.android.api.NetworkConfigurator
import ch.protonmail.android.api.models.room.attachmentMetadata.AttachmentMetadataDatabase
import ch.protonmail.android.attachments.AttachmentsHelper
import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.data.ContactsRepository
@ -59,6 +60,9 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
@RelaxedMockK
private lateinit var contactsRepository: ContactsRepository
@RelaxedMockK
private lateinit var attachmentsHelper: AttachmentsHelper
@RelaxedMockK
private lateinit var attachmentMetadataDatabase: AttachmentMetadataDatabase

View File

@ -22,7 +22,7 @@ package ch.protonmail.android.attachments
import org.junit.Before
import org.junit.Test
class DownloadAttachmentsHelperTest {
class AttachmentsHelperTest {
@Before
fun setUp() {